aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore8
-rw-r--r--.travis.yml16
-rw-r--r--CONTRIBUTING.md10
-rw-r--r--Gemfile83
-rw-r--r--RAILS_VERSION2
-rw-r--r--README.md83
-rw-r--r--README.rdoc78
-rw-r--r--RELEASING_RAILS.rdoc10
-rw-r--r--Rakefile112
-rw-r--r--actionmailer/CHANGELOG.md38
-rw-r--r--actionmailer/MIT-LICENSE2
-rw-r--r--actionmailer/README.rdoc19
-rw-r--r--actionmailer/Rakefile6
-rw-r--r--actionmailer/actionmailer.gemspec18
-rw-r--r--actionmailer/lib/action_mailer.rb3
-rw-r--r--actionmailer/lib/action_mailer/base.rb229
-rw-r--r--actionmailer/lib/action_mailer/collector.rb2
-rw-r--r--actionmailer/lib/action_mailer/delivery_methods.rb3
-rw-r--r--actionmailer/lib/action_mailer/log_subscriber.rb14
-rw-r--r--actionmailer/lib/action_mailer/mail_helper.rb8
-rw-r--r--actionmailer/lib/action_mailer/queued_message.rb37
-rw-r--r--actionmailer/lib/action_mailer/railtie.rb3
-rw-r--r--actionmailer/lib/action_mailer/test_case.rb7
-rw-r--r--actionmailer/lib/action_mailer/test_helper.rb12
-rw-r--r--actionmailer/lib/action_mailer/version.rb13
-rw-r--r--actionmailer/lib/rails/generators/mailer/USAGE5
-rw-r--r--actionmailer/test/abstract_unit.rb13
-rw-r--r--actionmailer/test/asset_host_test.rb1
-rw-r--r--actionmailer/test/base_test.rb104
-rw-r--r--actionmailer/test/delivery_methods_test.rb3
-rw-r--r--actionmailer/test/fixtures/base_mailer/without_mail_call.erb1
-rw-r--r--actionmailer/test/fixtures/base_test/after_action_mailer/welcome.html.erb (renamed from actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb)0
-rw-r--r--actionmailer/test/fixtures/base_test/before_action_mailer/welcome.html.erb (renamed from actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb)0
-rw-r--r--actionmailer/test/fixtures/raw_email1020
-rw-r--r--actionmailer/test/fixtures/raw_email1232
-rw-r--r--actionmailer/test/fixtures/raw_email1329
-rw-r--r--actionmailer/test/fixtures/raw_email2114
-rw-r--r--actionmailer/test/fixtures/raw_email370
-rw-r--r--actionmailer/test/fixtures/raw_email459
-rw-r--r--actionmailer/test/fixtures/raw_email519
-rw-r--r--actionmailer/test/fixtures/raw_email620
-rw-r--r--actionmailer/test/fixtures/raw_email766
-rw-r--r--actionmailer/test/fixtures/raw_email847
-rw-r--r--actionmailer/test/fixtures/raw_email928
-rw-r--r--actionmailer/test/fixtures/raw_email_quoted_with_0d0a14
-rw-r--r--actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type104
-rw-r--r--actionmailer/test/fixtures/raw_email_with_nested_attachment100
-rw-r--r--actionmailer/test/fixtures/raw_email_with_partially_quoted_subject14
-rw-r--r--actionmailer/test/i18n_with_controller_test.rb4
-rw-r--r--actionmailer/test/log_subscriber_test.rb9
-rw-r--r--actionmailer/test/mailers/async_mailer.rb3
-rw-r--r--actionmailer/test/mailers/base_mailer.rb14
-rw-r--r--actionmailer/test/mailers/proc_mailer.rb3
-rw-r--r--actionmailer/test/spec_type_test.rb37
-rw-r--r--actionmailer/test/test_test.rb144
-rw-r--r--actionmailer/test/url_test.rb9
-rw-r--r--actionpack/CHANGELOG.md631
-rw-r--r--actionpack/MIT-LICENSE2
-rw-r--r--actionpack/README.rdoc7
-rw-r--r--actionpack/RUNNING_UNIT_TESTS27
-rw-r--r--actionpack/RUNNING_UNIT_TESTS.rdoc17
-rw-r--r--actionpack/Rakefile37
-rw-r--r--actionpack/actionpack.gemspec26
-rw-r--r--actionpack/lib/abstract_controller.rb3
-rw-r--r--actionpack/lib/abstract_controller/asset_paths.rb2
-rw-r--r--actionpack/lib/abstract_controller/base.rb10
-rw-r--r--actionpack/lib/abstract_controller/callbacks.rb171
-rw-r--r--actionpack/lib/abstract_controller/helpers.rb39
-rw-r--r--actionpack/lib/abstract_controller/layouts.rb410
-rw-r--r--actionpack/lib/abstract_controller/rendering.rb161
-rw-r--r--actionpack/lib/abstract_controller/translation.rb11
-rw-r--r--actionpack/lib/abstract_controller/view_paths.rb96
-rw-r--r--actionpack/lib/action_controller.rb17
-rw-r--r--actionpack/lib/action_controller/base.rb23
-rw-r--r--actionpack/lib/action_controller/caching.rb34
-rw-r--r--actionpack/lib/action_controller/caching/sweeping.rb116
-rw-r--r--actionpack/lib/action_controller/deprecated.rb7
-rw-r--r--actionpack/lib/action_controller/deprecated/integration_test.rb5
-rw-r--r--actionpack/lib/action_controller/deprecated/performance_test.rb3
-rw-r--r--actionpack/lib/action_controller/log_subscriber.rb7
-rw-r--r--actionpack/lib/action_controller/metal.rb8
-rw-r--r--actionpack/lib/action_controller/metal/conditional_get.rb6
-rw-r--r--actionpack/lib/action_controller/metal/data_streaming.rb11
-rw-r--r--actionpack/lib/action_controller/metal/exceptions.rb9
-rw-r--r--actionpack/lib/action_controller/metal/flash.rb19
-rw-r--r--actionpack/lib/action_controller/metal/force_ssl.rb71
-rw-r--r--actionpack/lib/action_controller/metal/head.rb7
-rw-r--r--actionpack/lib/action_controller/metal/helpers.rb16
-rw-r--r--actionpack/lib/action_controller/metal/hide_actions.rb10
-rw-r--r--actionpack/lib/action_controller/metal/http_authentication.rb77
-rw-r--r--actionpack/lib/action_controller/metal/instrumentation.rb2
-rw-r--r--actionpack/lib/action_controller/metal/live.rb108
-rw-r--r--actionpack/lib/action_controller/metal/mime_responds.rb42
-rw-r--r--actionpack/lib/action_controller/metal/params_wrapper.rb166
-rw-r--r--actionpack/lib/action_controller/metal/redirecting.rb67
-rw-r--r--actionpack/lib/action_controller/metal/renderers.rb12
-rw-r--r--actionpack/lib/action_controller/metal/rendering.rb30
-rw-r--r--actionpack/lib/action_controller/metal/request_forgery_protection.rb72
-rw-r--r--actionpack/lib/action_controller/metal/responder.rb36
-rw-r--r--actionpack/lib/action_controller/metal/streaming.rb54
-rw-r--r--actionpack/lib/action_controller/metal/strong_parameters.rb327
-rw-r--r--actionpack/lib/action_controller/metal/url_for.rb5
-rw-r--r--actionpack/lib/action_controller/railtie.rb21
-rw-r--r--actionpack/lib/action_controller/record_identifier.rb20
-rw-r--r--actionpack/lib/action_controller/test_case.rb139
-rw-r--r--actionpack/lib/action_controller/vendor/html-scanner.rb5
-rw-r--r--actionpack/lib/action_dispatch.rb6
-rw-r--r--actionpack/lib/action_dispatch/http/cache.rb4
-rw-r--r--actionpack/lib/action_dispatch/http/filter_parameters.rb24
-rw-r--r--actionpack/lib/action_dispatch/http/filter_redirect.rb37
-rw-r--r--actionpack/lib/action_dispatch/http/headers.rb65
-rw-r--r--actionpack/lib/action_dispatch/http/mime_negotiation.rb8
-rw-r--r--actionpack/lib/action_dispatch/http/mime_type.rb70
-rw-r--r--actionpack/lib/action_dispatch/http/parameter_filter.rb90
-rw-r--r--actionpack/lib/action_dispatch/http/parameters.rb54
-rw-r--r--actionpack/lib/action_dispatch/http/request.rb75
-rw-r--r--actionpack/lib/action_dispatch/http/response.rb61
-rw-r--r--actionpack/lib/action_dispatch/http/upload.rb19
-rw-r--r--actionpack/lib/action_dispatch/http/url.rb98
-rw-r--r--actionpack/lib/action_dispatch/journey.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/backwards.rb5
-rw-r--r--actionpack/lib/action_dispatch/journey/formatter.rb150
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/builder.rb162
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/simulator.rb44
-rw-r--r--actionpack/lib/action_dispatch/journey/gtg/transition_table.rb167
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/builder.rb76
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/dot.rb36
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/simulator.rb47
-rw-r--r--actionpack/lib/action_dispatch/journey/nfa/transition_table.rb163
-rw-r--r--actionpack/lib/action_dispatch/journey/nodes/node.rb124
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.rb206
-rw-r--r--actionpack/lib/action_dispatch/journey/parser.y48
-rw-r--r--actionpack/lib/action_dispatch/journey/parser_extras.rb23
-rw-r--r--actionpack/lib/action_dispatch/journey/path/pattern.rb196
-rw-r--r--actionpack/lib/action_dispatch/journey/route.rb136
-rw-r--r--actionpack/lib/action_dispatch/journey/router.rb172
-rw-r--r--actionpack/lib/action_dispatch/journey/router/strexp.rb24
-rw-r--r--actionpack/lib/action_dispatch/journey/router/utils.rb57
-rw-r--r--actionpack/lib/action_dispatch/journey/routes.rb76
-rw-r--r--actionpack/lib/action_dispatch/journey/scanner.rb61
-rw-r--r--actionpack/lib/action_dispatch/journey/visitors.rb199
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.css34
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/fsm.js134
-rw-r--r--actionpack/lib/action_dispatch/journey/visualizer/index.html.erb52
-rw-r--r--actionpack/lib/action_dispatch/middleware/best_standards_support.rb22
-rw-r--r--actionpack/lib/action_dispatch/middleware/cookies.rb345
-rw-r--r--actionpack/lib/action_dispatch/middleware/debug_exceptions.rb61
-rw-r--r--actionpack/lib/action_dispatch/middleware/exception_wrapper.rb30
-rw-r--r--actionpack/lib/action_dispatch/middleware/flash.rb66
-rw-r--r--actionpack/lib/action_dispatch/middleware/params_parser.rb30
-rw-r--r--actionpack/lib/action_dispatch/middleware/public_exceptions.rb5
-rw-r--r--actionpack/lib/action_dispatch/middleware/remote_ip.rb172
-rw-r--r--actionpack/lib/action_dispatch/middleware/request_id.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/abstract_store.rb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/session/cookie_store.rb104
-rw-r--r--actionpack/lib/action_dispatch/middleware/show_exceptions.rb18
-rw-r--r--actionpack/lib/action_dispatch/middleware/ssl.rb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/static.rb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb31
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb34
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb25
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb26
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb24
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb15
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb24
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb132
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb7
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb23
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb30
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb11
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb17
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb43
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb8
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb2
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb6
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb3
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb16
-rw-r--r--actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb144
-rw-r--r--actionpack/lib/action_dispatch/railtie.rb7
-rw-r--r--actionpack/lib/action_dispatch/request/session.rb16
-rw-r--r--actionpack/lib/action_dispatch/request/utils.rb24
-rw-r--r--actionpack/lib/action_dispatch/routing.rb136
-rw-r--r--actionpack/lib/action_dispatch/routing/inspector.rb163
-rw-r--r--actionpack/lib/action_dispatch/routing/mapper.rb584
-rw-r--r--actionpack/lib/action_dispatch/routing/polymorphic_routes.rb19
-rw-r--r--actionpack/lib/action_dispatch/routing/redirection.rb36
-rw-r--r--actionpack/lib/action_dispatch/routing/route_set.rb264
-rw-r--r--actionpack/lib/action_dispatch/routing/routes_proxy.rb2
-rw-r--r--actionpack/lib/action_dispatch/routing/url_for.rb37
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/dom.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/response.rb26
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/routing.rb56
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/selector.rb8
-rw-r--r--actionpack/lib/action_dispatch/testing/assertions/tag.rb38
-rw-r--r--actionpack/lib/action_dispatch/testing/integration.rb98
-rw-r--r--actionpack/lib/action_dispatch/testing/performance_test.rb10
-rw-r--r--actionpack/lib/action_dispatch/testing/test_process.rb14
-rw-r--r--actionpack/lib/action_dispatch/testing/test_request.rb10
-rw-r--r--actionpack/lib/action_pack.rb2
-rw-r--r--actionpack/lib/action_pack/version.rb13
-rw-r--r--actionpack/lib/action_view.rb94
-rw-r--r--actionpack/lib/action_view/asset_paths.rb143
-rw-r--r--actionpack/lib/action_view/base.rb201
-rw-r--r--actionpack/lib/action_view/buffers.rb43
-rw-r--r--actionpack/lib/action_view/digestor.rb104
-rw-r--r--actionpack/lib/action_view/flows.rb76
-rw-r--r--actionpack/lib/action_view/helpers.rb57
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helper.rb293
-rw-r--r--actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb93
-rw-r--r--actionpack/lib/action_view/helpers/asset_url_helper.rb334
-rw-r--r--actionpack/lib/action_view/helpers/atom_feed_helper.rb203
-rw-r--r--actionpack/lib/action_view/helpers/benchmark_helper.rb13
-rw-r--r--actionpack/lib/action_view/helpers/cache_helper.rb147
-rw-r--r--actionpack/lib/action_view/helpers/capture_helper.rb222
-rw-r--r--actionpack/lib/action_view/helpers/date_helper.rb1045
-rw-r--r--actionpack/lib/action_view/helpers/debug_helper.rb41
-rw-r--r--actionpack/lib/action_view/helpers/form_helper.rb1416
-rw-r--r--actionpack/lib/action_view/helpers/form_options_helper.rb788
-rw-r--r--actionpack/lib/action_view/helpers/form_tag_helper.rb773
-rw-r--r--actionpack/lib/action_view/helpers/javascript_helper.rb112
-rw-r--r--actionpack/lib/action_view/helpers/number_helper.rb442
-rw-r--r--actionpack/lib/action_view/helpers/output_safety_helper.rb37
-rw-r--r--actionpack/lib/action_view/helpers/record_tag_helper.rb102
-rw-r--r--actionpack/lib/action_view/helpers/rendering_helper.rb90
-rw-r--r--actionpack/lib/action_view/helpers/sanitize_helper.rb256
-rw-r--r--actionpack/lib/action_view/helpers/tag_helper.rb173
-rw-r--r--actionpack/lib/action_view/helpers/tags/base.rb150
-rw-r--r--actionpack/lib/action_view/helpers/tags/check_box.rb62
-rw-r--r--actionpack/lib/action_view/helpers/tags/checkable.rb16
-rw-r--r--actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb37
-rw-r--r--actionpack/lib/action_view/helpers/tags/collection_helpers.rb82
-rw-r--r--actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb30
-rw-r--r--actionpack/lib/action_view/helpers/tags/collection_select.rb28
-rw-r--r--actionpack/lib/action_view/helpers/tags/color_field.rb25
-rw-r--r--actionpack/lib/action_view/helpers/tags/date_field.rb13
-rw-r--r--actionpack/lib/action_view/helpers/tags/date_select.rb70
-rw-r--r--actionpack/lib/action_view/helpers/tags/datetime_field.rb22
-rw-r--r--actionpack/lib/action_view/helpers/tags/datetime_local_field.rb19
-rw-r--r--actionpack/lib/action_view/helpers/tags/datetime_select.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/email_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/file_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb29
-rw-r--r--actionpack/lib/action_view/helpers/tags/hidden_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/label.rb65
-rw-r--r--actionpack/lib/action_view/helpers/tags/month_field.rb13
-rw-r--r--actionpack/lib/action_view/helpers/tags/number_field.rb18
-rw-r--r--actionpack/lib/action_view/helpers/tags/password_field.rb12
-rw-r--r--actionpack/lib/action_view/helpers/tags/radio_button.rb31
-rw-r--r--actionpack/lib/action_view/helpers/tags/range_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/search_field.rb24
-rw-r--r--actionpack/lib/action_view/helpers/tags/select.rb41
-rw-r--r--actionpack/lib/action_view/helpers/tags/tel_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/text_area.rb18
-rw-r--r--actionpack/lib/action_view/helpers/tags/text_field.rb29
-rw-r--r--actionpack/lib/action_view/helpers/tags/time_field.rb13
-rw-r--r--actionpack/lib/action_view/helpers/tags/time_select.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/time_zone_select.rb20
-rw-r--r--actionpack/lib/action_view/helpers/tags/url_field.rb8
-rw-r--r--actionpack/lib/action_view/helpers/tags/week_field.rb13
-rw-r--r--actionpack/lib/action_view/helpers/text_helper.rb439
-rw-r--r--actionpack/lib/action_view/helpers/translation_helper.rb107
-rw-r--r--actionpack/lib/action_view/helpers/url_helper.rb654
-rw-r--r--actionpack/lib/action_view/log_subscriber.rb30
-rw-r--r--actionpack/lib/action_view/lookup_context.rb233
-rw-r--r--actionpack/lib/action_view/path_set.rb89
-rw-r--r--actionpack/lib/action_view/railtie.rb47
-rw-r--r--actionpack/lib/action_view/record_identifier.rb84
-rw-r--r--actionpack/lib/action_view/renderer/abstract_renderer.rb32
-rw-r--r--actionpack/lib/action_view/renderer/partial_renderer.rb475
-rw-r--r--actionpack/lib/action_view/renderer/renderer.rb54
-rw-r--r--actionpack/lib/action_view/renderer/template_renderer.rb93
-rw-r--r--actionpack/lib/action_view/routing_url_for.rb107
-rw-r--r--actionpack/lib/action_view/template.rb339
-rw-r--r--actionpack/lib/action_view/template/error.rb130
-rw-r--r--actionpack/lib/action_view/template/handlers/erb.rb104
-rw-r--r--actionpack/lib/action_view/template/resolver.rb321
-rw-r--r--actionpack/lib/action_view/template/types.rb58
-rw-r--r--actionpack/lib/action_view/test_case.rb268
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner.rb20
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner/html/node.rb532
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb188
-rw-r--r--actionpack/lib/action_view/vendor/html-scanner/html/selector.rb830
-rw-r--r--actionpack/test/abstract/abstract_controller_test.rb263
-rw-r--r--actionpack/test/abstract/callbacks_test.rb94
-rw-r--r--actionpack/test/abstract/collector_test.rb2
-rw-r--r--actionpack/test/abstract/helper_test.rb103
-rw-r--r--actionpack/test/abstract/layouts_test.rb363
-rw-r--r--actionpack/test/abstract/render_test.rb102
-rw-r--r--actionpack/test/abstract/translation_test.rb81
-rw-r--r--actionpack/test/abstract_unit.rb40
-rw-r--r--actionpack/test/active_record_unit.rb99
-rw-r--r--actionpack/test/activerecord/controller_runtime_test.rb81
-rw-r--r--actionpack/test/controller/action_pack_assertions_test.rb75
-rw-r--r--actionpack/test/controller/assert_select_test.rb13
-rw-r--r--actionpack/test/controller/base_test.rb42
-rw-r--r--actionpack/test/controller/caching_test.rb40
-rw-r--r--actionpack/test/controller/capture_test.rb79
-rw-r--r--actionpack/test/controller/default_url_options_with_before_action_test.rb29
-rw-r--r--actionpack/test/controller/default_url_options_with_filter_test.rb29
-rw-r--r--actionpack/test/controller/filters_test.rb73
-rw-r--r--actionpack/test/controller/flash_hash_test.rb21
-rw-r--r--actionpack/test/controller/flash_test.rb23
-rw-r--r--actionpack/test/controller/force_ssl_test.rb175
-rw-r--r--actionpack/test/controller/helper_test.rb6
-rw-r--r--actionpack/test/controller/http_basic_authentication_test.rb10
-rw-r--r--actionpack/test/controller/http_digest_authentication_test.rb17
-rw-r--r--actionpack/test/controller/http_token_authentication_test.rb41
-rw-r--r--actionpack/test/controller/integration_test.rb97
-rw-r--r--actionpack/test/controller/layout_test.rb237
-rw-r--r--actionpack/test/controller/live_stream_test.rb183
-rw-r--r--actionpack/test/controller/localized_templates_test.rb17
-rw-r--r--actionpack/test/controller/log_subscriber_test.rb88
-rw-r--r--actionpack/test/controller/mime/accept_format_test.rb94
-rw-r--r--actionpack/test/controller/mime/respond_to_test.rb493
-rw-r--r--actionpack/test/controller/mime/respond_with_test.rb714
-rw-r--r--actionpack/test/controller/mime_responds_test.rb1233
-rw-r--r--actionpack/test/controller/new_base/base_test.rb2
-rw-r--r--actionpack/test/controller/new_base/middleware_test.rb4
-rw-r--r--actionpack/test/controller/new_base/render_context_test.rb2
-rw-r--r--actionpack/test/controller/new_base/render_partial_test.rb24
-rw-r--r--actionpack/test/controller/new_base/render_streaming_test.rb2
-rw-r--r--actionpack/test/controller/new_base/render_template_test.rb10
-rw-r--r--actionpack/test/controller/new_base/render_test.rb24
-rw-r--r--actionpack/test/controller/new_base/render_text_test.rb15
-rw-r--r--actionpack/test/controller/output_escaping_test.rb4
-rw-r--r--actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb50
-rw-r--r--actionpack/test/controller/parameters/multi_parameter_attributes_test.rb23
-rw-r--r--actionpack/test/controller/parameters/nested_parameters_test.rb102
-rw-r--r--actionpack/test/controller/parameters/parameters_permit_test.rb181
-rw-r--r--actionpack/test/controller/parameters/parameters_require_test.rb8
-rw-r--r--actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb33
-rw-r--r--actionpack/test/controller/params_wrapper_test.rb2
-rw-r--r--actionpack/test/controller/render_test.rb1421
-rw-r--r--actionpack/test/controller/request_forgery_protection_test.rb51
-rw-r--r--actionpack/test/controller/required_params_test.rb34
-rw-r--r--actionpack/test/controller/rescue_test.rb4
-rw-r--r--actionpack/test/controller/resources_test.rb1
-rw-r--r--actionpack/test/controller/routing_test.rb61
-rw-r--r--actionpack/test/controller/send_file_test.rb16
-rw-r--r--actionpack/test/controller/show_exceptions_test.rb24
-rw-r--r--actionpack/test/controller/spec_style_test.rb208
-rw-r--r--actionpack/test/controller/spec_type_test.rb37
-rw-r--r--actionpack/test/controller/sweeper_test.rb16
-rw-r--r--actionpack/test/controller/test_case_test.rb79
-rw-r--r--actionpack/test/controller/url_for_test.rb12
-rw-r--r--actionpack/test/controller/view_paths_test.rb174
-rw-r--r--actionpack/test/controller/webservice_test.rb189
-rw-r--r--actionpack/test/dispatch/cookies_test.rb239
-rw-r--r--actionpack/test/dispatch/debug_exceptions_test.rb83
-rw-r--r--actionpack/test/dispatch/header_test.rb128
-rw-r--r--actionpack/test/dispatch/live_response_test.rb2
-rw-r--r--actionpack/test/dispatch/mime_type_test.rb7
-rw-r--r--actionpack/test/dispatch/mount_test.rb7
-rw-r--r--actionpack/test/dispatch/prefix_generation_test.rb105
-rw-r--r--actionpack/test/dispatch/rack_test.rb16
-rw-r--r--actionpack/test/dispatch/request/json_params_parsing_test.rb33
-rw-r--r--actionpack/test/dispatch/request/multipart_params_parsing_test.rb36
-rw-r--r--actionpack/test/dispatch/request/query_string_parsing_test.rb4
-rw-r--r--actionpack/test/dispatch/request/session_test.rb19
-rw-r--r--actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb54
-rw-r--r--actionpack/test/dispatch/request/xml_params_parsing_test.rb165
-rw-r--r--actionpack/test/dispatch/request_test.rb189
-rw-r--r--actionpack/test/dispatch/response_test.rb9
-rw-r--r--actionpack/test/dispatch/routing/inspector_test.rb128
-rw-r--r--actionpack/test/dispatch/routing/route_set_test.rb97
-rw-r--r--actionpack/test/dispatch/routing_test.rb2215
-rw-r--r--actionpack/test/dispatch/session/abstract_store_test.rb2
-rw-r--r--actionpack/test/dispatch/session/cookie_store_test.rb8
-rw-r--r--actionpack/test/dispatch/session/test_session_test.rb16
-rw-r--r--actionpack/test/dispatch/show_exceptions_test.rb12
-rw-r--r--actionpack/test/dispatch/spec_type_test.rb41
-rw-r--r--actionpack/test/dispatch/ssl_test.rb57
-rw-r--r--actionpack/test/dispatch/test_request_test.rb36
-rw-r--r--actionpack/test/dispatch/url_generation_test.rb49
-rw-r--r--actionpack/test/fixtures/developer.rb9
-rw-r--r--actionpack/test/fixtures/digestor/messages/show.html.erb9
-rw-r--r--actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb3
-rw-r--r--actionpack/test/fixtures/helpers/abc_helper.rb2
l---------actionpack/test/fixtures/layout_tests/layouts/symlinked1
-rw-r--r--actionpack/test/fixtures/multipart/bracketed_utf8_param5
-rw-r--r--actionpack/test/fixtures/multipart/single_utf8_param5
-rw-r--r--actionpack/test/fixtures/public/400.html1
-rw-r--r--actionpack/test/fixtures/public/images/rails.pngbin1787 -> 0 bytes
-rw-r--r--actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb1
-rw-r--r--actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb (renamed from actionpack/test/fixtures/digestor/events/_event.html.erb)0
-rw-r--r--actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb1
-rw-r--r--actionpack/test/fixtures/test/_partial_name_local_variable.erb1
-rw-r--r--actionpack/test/fixtures/test/change_priority.html.erb (renamed from actionpack/test/fixtures/test/change_priorty.html.erb)0
-rw-r--r--actionpack/test/fixtures/test/render_partial_inside_directory.html.erb1
-rw-r--r--actionpack/test/journey/gtg/builder_test.rb79
-rw-r--r--actionpack/test/journey/gtg/transition_table_test.rb115
-rw-r--r--actionpack/test/journey/nfa/simulator_test.rb98
-rw-r--r--actionpack/test/journey/nfa/transition_table_test.rb72
-rw-r--r--actionpack/test/journey/nodes/symbol_test.rb17
-rw-r--r--actionpack/test/journey/path/pattern_test.rb288
-rw-r--r--actionpack/test/journey/route/definition/parser_test.rb110
-rw-r--r--actionpack/test/journey/route/definition/scanner_test.rb56
-rw-r--r--actionpack/test/journey/route_test.rb106
-rw-r--r--actionpack/test/journey/router/strexp_test.rb32
-rw-r--r--actionpack/test/journey/router/utils_test.rb29
-rw-r--r--actionpack/test/journey/router_test.rb579
-rw-r--r--actionpack/test/journey/routes_test.rb53
-rw-r--r--actionpack/test/lib/controller/fake_models.rb101
-rw-r--r--actionpack/test/routing/helper_test.rb2
-rw-r--r--actionpack/test/template/asset_tag_helper_test.rb670
-rw-r--r--actionpack/test/template/atom_feed_helper_test.rb349
-rw-r--r--actionpack/test/template/benchmark_helper_test.rb24
-rw-r--r--actionpack/test/template/capture_helper_test.rb238
-rw-r--r--actionpack/test/template/compiled_templates_test.rb63
-rw-r--r--actionpack/test/template/date_helper_i18n_test.rb134
-rw-r--r--actionpack/test/template/date_helper_test.rb3046
-rw-r--r--actionpack/test/template/digestor_test.rb166
-rw-r--r--actionpack/test/template/form_collections_helper_test.rb336
-rw-r--r--actionpack/test/template/form_helper_test.rb2713
-rw-r--r--actionpack/test/template/form_options_helper_test.rb1259
-rw-r--r--actionpack/test/template/form_tag_helper_test.rb642
-rw-r--r--actionpack/test/template/html-scanner/sanitizer_test.rb315
-rw-r--r--actionpack/test/template/javascript_helper_test.rb104
-rw-r--r--actionpack/test/template/lookup_context_test.rb263
-rw-r--r--actionpack/test/template/number_helper_test.rb399
-rw-r--r--actionpack/test/template/record_tag_helper_test.rb103
-rw-r--r--actionpack/test/template/render_test.rb533
-rw-r--r--actionpack/test/template/resolver_patterns_test.rb31
-rw-r--r--actionpack/test/template/spec_type_test.rb39
-rw-r--r--actionpack/test/template/streaming_render_test.rb109
-rw-r--r--actionpack/test/template/tag_helper_test.rb125
-rw-r--r--actionpack/test/template/template_test.rb192
-rw-r--r--actionpack/test/template/test_case_test.rb346
-rw-r--r--actionpack/test/template/test_test.rb136
-rw-r--r--actionpack/test/template/text_helper_test.rb467
-rw-r--r--actionpack/test/template/url_helper_test.rb864
-rw-r--r--actionpack/test/ts_isolated.rb15
-rw-r--r--actionview/CHANGELOG.md210
-rw-r--r--actionview/MIT-LICENSE21
-rw-r--r--actionview/README.rdoc34
-rw-r--r--actionview/RUNNING_UNIT_TESTS.rdoc27
-rw-r--r--actionview/Rakefile80
-rw-r--r--actionview/actionview.gemspec29
-rw-r--r--actionview/lib/action_view.rb96
-rw-r--r--actionview/lib/action_view/base.rb205
-rw-r--r--actionview/lib/action_view/buffers.rb49
-rw-r--r--actionview/lib/action_view/context.rb (renamed from actionpack/lib/action_view/context.rb)0
-rw-r--r--actionview/lib/action_view/dependency_tracker.rb93
-rw-r--r--actionview/lib/action_view/digestor.rb116
-rw-r--r--actionview/lib/action_view/flows.rb76
-rw-r--r--actionview/lib/action_view/helpers.rb58
-rw-r--r--actionview/lib/action_view/helpers/active_model_helper.rb (renamed from actionpack/lib/action_view/helpers/active_model_helper.rb)0
-rw-r--r--actionview/lib/action_view/helpers/asset_tag_helper.rb319
-rw-r--r--actionview/lib/action_view/helpers/asset_url_helper.rb355
-rw-r--r--actionview/lib/action_view/helpers/atom_feed_helper.rb203
-rw-r--r--actionview/lib/action_view/helpers/cache_helper.rb200
-rw-r--r--actionview/lib/action_view/helpers/capture_helper.rb216
-rw-r--r--actionview/lib/action_view/helpers/controller_helper.rb (renamed from actionpack/lib/action_view/helpers/controller_helper.rb)0
-rw-r--r--actionview/lib/action_view/helpers/csrf_helper.rb (renamed from actionpack/lib/action_view/helpers/csrf_helper.rb)0
-rw-r--r--actionview/lib/action_view/helpers/date_helper.rb1075
-rw-r--r--actionview/lib/action_view/helpers/debug_helper.rb39
-rw-r--r--actionview/lib/action_view/helpers/form_helper.rb1877
-rw-r--r--actionview/lib/action_view/helpers/form_options_helper.rb843
-rw-r--r--actionview/lib/action_view/helpers/form_tag_helper.rb746
-rw-r--r--actionview/lib/action_view/helpers/javascript_helper.rb75
-rw-r--r--actionview/lib/action_view/helpers/number_helper.rb441
-rw-r--r--actionview/lib/action_view/helpers/output_safety_helper.rb38
-rw-r--r--actionview/lib/action_view/helpers/record_tag_helper.rb108
-rw-r--r--actionview/lib/action_view/helpers/rendering_helper.rb90
-rw-r--r--actionview/lib/action_view/helpers/sanitize_helper.rb256
-rw-r--r--actionview/lib/action_view/helpers/tag_helper.rb176
-rw-r--r--actionview/lib/action_view/helpers/tags.rb (renamed from actionpack/lib/action_view/helpers/tags.rb)0
-rw-r--r--actionview/lib/action_view/helpers/tags/base.rb148
-rw-r--r--actionview/lib/action_view/helpers/tags/check_box.rb64
-rw-r--r--actionview/lib/action_view/helpers/tags/checkable.rb16
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_check_boxes.rb44
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_helpers.rb85
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb36
-rw-r--r--actionview/lib/action_view/helpers/tags/collection_select.rb28
-rw-r--r--actionview/lib/action_view/helpers/tags/color_field.rb25
-rw-r--r--actionview/lib/action_view/helpers/tags/date_field.rb13
-rw-r--r--actionview/lib/action_view/helpers/tags/date_select.rb72
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_field.rb22
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_local_field.rb19
-rw-r--r--actionview/lib/action_view/helpers/tags/datetime_select.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/email_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/file_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/grouped_collection_select.rb29
-rw-r--r--actionview/lib/action_view/helpers/tags/hidden_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/label.rb66
-rw-r--r--actionview/lib/action_view/helpers/tags/month_field.rb13
-rw-r--r--actionview/lib/action_view/helpers/tags/number_field.rb18
-rw-r--r--actionview/lib/action_view/helpers/tags/password_field.rb12
-rw-r--r--actionview/lib/action_view/helpers/tags/radio_button.rb31
-rw-r--r--actionview/lib/action_view/helpers/tags/range_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/search_field.rb24
-rw-r--r--actionview/lib/action_view/helpers/tags/select.rb41
-rw-r--r--actionview/lib/action_view/helpers/tags/tel_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/text_area.rb18
-rw-r--r--actionview/lib/action_view/helpers/tags/text_field.rb29
-rw-r--r--actionview/lib/action_view/helpers/tags/time_field.rb13
-rw-r--r--actionview/lib/action_view/helpers/tags/time_select.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/time_zone_select.rb20
-rw-r--r--actionview/lib/action_view/helpers/tags/url_field.rb8
-rw-r--r--actionview/lib/action_view/helpers/tags/week_field.rb13
-rw-r--r--actionview/lib/action_view/helpers/text_helper.rb445
-rw-r--r--actionview/lib/action_view/helpers/translation_helper.rb107
-rw-r--r--actionview/lib/action_view/helpers/url_helper.rb625
-rw-r--r--actionview/lib/action_view/layouts.rb424
-rw-r--r--actionview/lib/action_view/locale/en.yml (renamed from actionpack/lib/action_view/locale/en.yml)0
-rw-r--r--actionview/lib/action_view/log_subscriber.rb32
-rw-r--r--actionview/lib/action_view/lookup_context.rb241
-rw-r--r--actionview/lib/action_view/model_naming.rb (renamed from actionpack/lib/action_view/model_naming.rb)0
-rw-r--r--actionview/lib/action_view/path_set.rb77
-rw-r--r--actionview/lib/action_view/railtie.rb56
-rw-r--r--actionview/lib/action_view/record_identifier.rb84
-rw-r--r--actionview/lib/action_view/renderer/abstract_renderer.rb47
-rw-r--r--actionview/lib/action_view/renderer/partial_renderer.rb492
-rw-r--r--actionview/lib/action_view/renderer/renderer.rb50
-rw-r--r--actionview/lib/action_view/renderer/streaming_template_renderer.rb (renamed from actionpack/lib/action_view/renderer/streaming_template_renderer.rb)0
-rw-r--r--actionview/lib/action_view/renderer/template_renderer.rb96
-rw-r--r--actionview/lib/action_view/rendering.rb141
-rw-r--r--actionview/lib/action_view/routing_url_for.rb107
-rw-r--r--actionview/lib/action_view/tasks/dependencies.rake17
-rw-r--r--actionview/lib/action_view/template.rb340
-rw-r--r--actionview/lib/action_view/template/error.rb138
-rw-r--r--actionview/lib/action_view/template/handlers.rb (renamed from actionpack/lib/action_view/template/handlers.rb)0
-rw-r--r--actionview/lib/action_view/template/handlers/builder.rb (renamed from actionpack/lib/action_view/template/handlers/builder.rb)0
-rw-r--r--actionview/lib/action_view/template/handlers/erb.rb145
-rw-r--r--actionview/lib/action_view/template/handlers/raw.rb (renamed from actionpack/lib/action_view/template/handlers/raw.rb)0
-rw-r--r--actionview/lib/action_view/template/resolver.rb326
-rw-r--r--actionview/lib/action_view/template/text.rb (renamed from actionpack/lib/action_view/template/text.rb)0
-rw-r--r--actionview/lib/action_view/template/types.rb57
-rw-r--r--actionview/lib/action_view/test_case.rb272
-rw-r--r--actionview/lib/action_view/testing/resolvers.rb (renamed from actionpack/lib/action_view/testing/resolvers.rb)0
-rw-r--r--actionview/lib/action_view/vendor/html-scanner.rb20
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/document.rb (renamed from actionpack/lib/action_view/vendor/html-scanner/html/document.rb)0
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/node.rb532
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb188
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/selector.rb830
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb (renamed from actionpack/lib/action_view/vendor/html-scanner/html/tokenizer.rb)0
-rw-r--r--actionview/lib/action_view/vendor/html-scanner/html/version.rb (renamed from actionpack/lib/action_view/vendor/html-scanner/html/version.rb)0
-rw-r--r--actionview/lib/action_view/version.rb11
-rw-r--r--actionview/lib/action_view/view_paths.rb96
-rw-r--r--actionview/test/abstract_unit.rb332
-rw-r--r--actionview/test/actionpack/abstract/abstract_controller_test.rb262
-rw-r--r--actionview/test/actionpack/abstract/helper_test.rb127
-rw-r--r--actionview/test/actionpack/abstract/layouts_test.rb384
-rw-r--r--actionview/test/actionpack/abstract/render_test.rb103
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb (renamed from actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/action_with_ivars.erb (renamed from actionpack/test/abstract/views/action_with_ivars.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/helper_test.erb (renamed from actionpack/test/abstract/views/helper_test.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/index.erb (renamed from actionpack/test/abstract/views/index.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb (renamed from actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/layouts/application.erb (renamed from actionpack/test/abstract/views/layouts/application.erb)0
-rw-r--r--actionview/test/actionpack/abstract/views/naked_render.erb (renamed from actionpack/test/abstract/views/naked_render.erb)0
-rw-r--r--actionview/test/actionpack/controller/capture_test.rb81
-rw-r--r--actionview/test/actionpack/controller/layout_test.rb262
-rw-r--r--actionview/test/actionpack/controller/render_test.rb1337
-rw-r--r--actionview/test/actionpack/controller/view_paths_test.rb174
-rw-r--r--actionview/test/active_record_unit.rb91
-rw-r--r--actionview/test/activerecord/controller_runtime_test.rb95
-rw-r--r--actionview/test/activerecord/form_helper_activerecord_test.rb91
-rw-r--r--actionview/test/activerecord/polymorphic_routes_test.rb (renamed from actionpack/test/activerecord/polymorphic_routes_test.rb)0
-rw-r--r--actionview/test/activerecord/render_partial_with_record_identification_test.rb (renamed from actionpack/test/activerecord/render_partial_with_record_identification_test.rb)0
-rw-r--r--actionview/test/fixtures/_top_level_partial.html.erb (renamed from actionpack/test/fixtures/_top_level_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/_top_level_partial_only.erb1
-rw-r--r--actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/customers/_customer.html.erb (renamed from actionpack/test/fixtures/customers/_customer.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/fun/games/_form.erb (renamed from actionpack/test/fixtures/fun/games/_form.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/fun/games/hello_world.erb (renamed from actionpack/test/fixtures/fun/games/hello_world.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb (renamed from actionpack/test/fixtures/good_customers/_good_customer.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/hello.html (renamed from actionpack/test/fixtures/hello.html)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb (renamed from actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/item.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/layout_test.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb (renamed from actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb5
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab (renamed from actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb (renamed from actionpack/test/fixtures/layout_tests/views/goodbye.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layout_tests/views/hello.erb (renamed from actionpack/test/fixtures/layout_tests/views/hello.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_column.html.erb (renamed from actionpack/test/fixtures/layouts/_column.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_customers.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb (renamed from actionpack/test/fixtures/layouts/_partial_and_yield.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_yield_only.erb (renamed from actionpack/test/fixtures/layouts/_yield_only.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb (renamed from actionpack/test/fixtures/layouts/_yield_with_params.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/block_with_layout.erb3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/builder.builder3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb3
-rw-r--r--actionview/test/fixtures/actionpack/layouts/standard.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/streaming.erb (renamed from actionpack/test/fixtures/layouts/streaming.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/talk_from_action.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/layouts/xhr.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield.erb2
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb (renamed from actionpack/test/fixtures/layouts/yield_with_render_inline_inside.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb (renamed from actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb (renamed from actionpack/test/fixtures/quiz/questions/_question.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/shared.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_changing_priority.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_changing_priority.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_counter.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_greeting.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_customer_with_var.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_form.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_greeting.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_hash_object.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_hello.builder1
-rw-r--r--actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb (renamed from actionpack/test/fixtures/digestor/level/below/_header.html.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/_labelling_form.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb3
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial.js.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_only_html.html1
-rw-r--r--actionview/test/fixtures/actionpack/test/_partial_with_partial.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_person.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb13
-rw-r--r--actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/capturing.erb4
-rw-r--r--actionview/test/fixtures/actionpack/test/change_priority.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_concatenated.erb3
-rw-r--r--actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/greeting.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/greeting.xml.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello,world.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/hello/hello.erb (renamed from actionpack/test/fixtures/happy_path/render_action/hello_world.erb)0
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_container.builder3
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder3
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/hello_xml_world.builder11
-rw-r--r--actionview/test/fixtures/actionpack/test/html_template.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/hyphen-ated.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder2
-rw-r--r--actionview/test/fixtures/actionpack/test/list.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder4
-rw-r--r--actionview/test/fixtures/actionpack/test/potential_conflicts.erb4
-rw-r--r--actionview/test/fixtures/actionpack/test/proper_block_detection.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_locals.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_to_string_test.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/render_two_partials.html.erb2
-rw-r--r--actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_html_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.html.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_partial.text.erb1
-rw-r--r--actionview/test/fixtures/actionpack/test/with_xml_template.html.erb1
-rw-r--r--actionview/test/fixtures/blog_public/.gitignore (renamed from actionpack/test/fixtures/blog_public/.gitignore)0
-rw-r--r--actionview/test/fixtures/blog_public/blog.html (renamed from actionpack/test/fixtures/blog_public/blog.html)0
-rw-r--r--actionview/test/fixtures/blog_public/index.html (renamed from actionpack/test/fixtures/blog_public/index.html)0
-rw-r--r--actionview/test/fixtures/blog_public/subdir/index.html (renamed from actionpack/test/fixtures/blog_public/subdir/index.html)0
-rw-r--r--actionview/test/fixtures/comments/empty.de.html.erb (renamed from actionpack/test/fixtures/comments/empty.de.html.erb)0
-rw-r--r--actionview/test/fixtures/comments/empty.html.builder (renamed from actionpack/test/fixtures/comments/empty.html.builder)0
-rw-r--r--actionview/test/fixtures/comments/empty.html.erb (renamed from actionpack/test/fixtures/comments/empty.html.erb)0
-rw-r--r--actionview/test/fixtures/comments/empty.xml.erb (renamed from actionpack/test/fixtures/comments/empty.xml.erb)0
-rw-r--r--actionview/test/fixtures/companies.yml (renamed from actionpack/test/fixtures/companies.yml)0
-rw-r--r--actionview/test/fixtures/company.rb9
-rw-r--r--actionview/test/fixtures/custom_pattern/another.html.erb (renamed from actionpack/test/fixtures/custom_pattern/another.html.erb)0
-rw-r--r--actionview/test/fixtures/custom_pattern/html/another.erb (renamed from actionpack/test/fixtures/custom_pattern/html/another.erb)0
-rw-r--r--actionview/test/fixtures/custom_pattern/html/path.erb (renamed from actionpack/test/fixtures/custom_pattern/html/path.erb)0
-rw-r--r--actionview/test/fixtures/customers/_customer.html.erb1
-rw-r--r--actionview/test/fixtures/db_definitions/sqlite.sql (renamed from actionpack/test/fixtures/db_definitions/sqlite.sql)0
-rw-r--r--actionview/test/fixtures/developer.rb10
-rw-r--r--actionview/test/fixtures/developers.yml (renamed from actionpack/test/fixtures/developers.yml)0
-rw-r--r--actionview/test/fixtures/developers/_developer.erb (renamed from actionpack/test/fixtures/developers/_developer.erb)0
-rw-r--r--actionview/test/fixtures/developers_projects.yml (renamed from actionpack/test/fixtures/developers_projects.yml)0
-rw-r--r--actionview/test/fixtures/digestor/comments/_comment.html.erb (renamed from actionpack/test/fixtures/digestor/comments/_comment.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/comments/_comments.html.erb (renamed from actionpack/test/fixtures/digestor/comments/_comments.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/events/_event.html.erb (renamed from actionpack/test/fixtures/digestor/messages/_form.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/level/_recursion.html.erb1
-rw-r--r--actionview/test/fixtures/digestor/level/below/_header.html.erb (renamed from actionpack/test/fixtures/digestor/messages/_header.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/level/below/index.html.erb (renamed from actionpack/test/fixtures/digestor/level/below/index.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/level/recursion.html.erb1
-rw-r--r--actionview/test/fixtures/digestor/messages/_form.html.erb (renamed from actionpack/test/fixtures/digestor/messages/actions/_move.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/messages/_header.html.erb (renamed from guides/code/getting_started/app/mailers/.gitkeep)0
-rw-r--r--actionview/test/fixtures/digestor/messages/_message.html.erb (renamed from actionpack/test/fixtures/digestor/messages/_message.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/messages/actions/_move.html.erb (renamed from guides/code/getting_started/app/models/.gitkeep)0
-rw-r--r--actionview/test/fixtures/digestor/messages/edit.html.erb (renamed from actionpack/test/fixtures/digestor/messages/edit.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/messages/index.html.erb (renamed from actionpack/test/fixtures/digestor/messages/index.html.erb)0
-rw-r--r--actionview/test/fixtures/digestor/messages/show.html.erb14
-rw-r--r--actionview/test/fixtures/fun/games/_game.erb (renamed from actionpack/test/fixtures/fun/games/_game.erb)0
-rw-r--r--actionview/test/fixtures/fun/games/hello_world.erb1
-rw-r--r--actionview/test/fixtures/fun/serious/games/_game.erb (renamed from actionpack/test/fixtures/fun/serious/games/_game.erb)0
-rw-r--r--actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb3
-rw-r--r--actionview/test/fixtures/games/_game.erb (renamed from actionpack/test/fixtures/games/_game.erb)0
-rw-r--r--actionview/test/fixtures/good_customers/_good_customer.html.erb1
-rw-r--r--actionview/test/fixtures/happy_path/render_action/hello_world.erb1
-rw-r--r--actionview/test/fixtures/helpers/abc_helper.rb3
-rw-r--r--actionview/test/fixtures/helpers/helpery_test_helper.rb (renamed from actionpack/test/fixtures/helpers/helpery_test_helper.rb)0
-rw-r--r--actionview/test/fixtures/helpers_missing/invalid_require_helper.rb5
-rw-r--r--actionview/test/fixtures/layout_tests/alt/hello.erb (renamed from actionpack/test/fixtures/layout_tests/alt/hello.erb)0
-rw-r--r--actionview/test/fixtures/layouts/_column.html.erb2
-rw-r--r--actionview/test/fixtures/layouts/_customers.erb1
-rw-r--r--actionview/test/fixtures/layouts/_partial_and_yield.erb2
-rw-r--r--actionview/test/fixtures/layouts/_yield_only.erb1
-rw-r--r--actionview/test/fixtures/layouts/_yield_with_params.erb1
-rw-r--r--actionview/test/fixtures/layouts/streaming.erb4
-rw-r--r--actionview/test/fixtures/layouts/yield.erb2
-rw-r--r--actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb2
-rw-r--r--actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb2
-rw-r--r--actionview/test/fixtures/mascot.rb (renamed from actionpack/test/fixtures/mascot.rb)0
-rw-r--r--actionview/test/fixtures/mascots.yml (renamed from actionpack/test/fixtures/mascots.yml)0
-rw-r--r--actionview/test/fixtures/mascots/_mascot.html.erb (renamed from actionpack/test/fixtures/mascots/_mascot.html.erb)0
-rw-r--r--actionview/test/fixtures/multipart/bracketed_utf8_param5
-rw-r--r--actionview/test/fixtures/multipart/single_utf8_param5
-rw-r--r--actionview/test/fixtures/override/test/hello_world.erb (renamed from actionpack/test/fixtures/override/test/hello_world.erb)0
-rw-r--r--actionview/test/fixtures/override2/layouts/test/sub.erb (renamed from actionpack/test/fixtures/override2/layouts/test/sub.erb)0
-rw-r--r--actionview/test/fixtures/plain_text.raw (renamed from actionpack/test/fixtures/plain_text.raw)0
-rw-r--r--actionview/test/fixtures/plain_text_with_characters.raw (renamed from actionpack/test/fixtures/plain_text_with_characters.raw)0
-rw-r--r--actionview/test/fixtures/project.rb (renamed from actionpack/test/fixtures/project.rb)0
-rw-r--r--actionview/test/fixtures/projects.yml (renamed from actionpack/test/fixtures/projects.yml)0
-rw-r--r--actionview/test/fixtures/projects/_project.erb (renamed from actionpack/test/fixtures/projects/_project.erb)0
-rw-r--r--actionview/test/fixtures/public/.gitignore (renamed from actionpack/test/fixtures/public/.gitignore)0
-rw-r--r--actionview/test/fixtures/public/elsewhere/cools.js (renamed from actionpack/test/fixtures/public/elsewhere/cools.js)0
-rw-r--r--actionview/test/fixtures/public/elsewhere/file.css (renamed from actionpack/test/fixtures/public/elsewhere/file.css)0
-rw-r--r--actionview/test/fixtures/public/foo/baz.css3
-rw-r--r--actionview/test/fixtures/public/javascripts/application.js (renamed from actionpack/test/fixtures/public/javascripts/application.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/bank.js (renamed from actionpack/test/fixtures/public/javascripts/bank.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/common.javascript (renamed from actionpack/test/fixtures/public/javascripts/common.javascript)0
-rw-r--r--actionview/test/fixtures/public/javascripts/controls.js (renamed from actionpack/test/fixtures/public/javascripts/controls.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/dragdrop.js (renamed from actionpack/test/fixtures/public/javascripts/dragdrop.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/effects.js (renamed from actionpack/test/fixtures/public/javascripts/effects.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/prototype.js (renamed from actionpack/test/fixtures/public/javascripts/prototype.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/robber.js (renamed from actionpack/test/fixtures/public/javascripts/robber.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/subdir/subdir.js (renamed from actionpack/test/fixtures/public/javascripts/subdir/subdir.js)0
-rw-r--r--actionview/test/fixtures/public/javascripts/version.1.0.js (renamed from actionpack/test/fixtures/public/javascripts/version.1.0.js)0
-rw-r--r--actionview/test/fixtures/public/stylesheets/bank.css (renamed from actionpack/test/fixtures/public/stylesheets/bank.css)0
-rw-r--r--actionview/test/fixtures/public/stylesheets/random.styles (renamed from actionpack/test/fixtures/public/stylesheets/random.styles)0
-rw-r--r--actionview/test/fixtures/public/stylesheets/robber.css (renamed from actionpack/test/fixtures/public/stylesheets/robber.css)0
-rw-r--r--actionview/test/fixtures/public/stylesheets/subdir/subdir.css (renamed from actionpack/test/fixtures/public/stylesheets/subdir/subdir.css)0
-rw-r--r--actionview/test/fixtures/public/stylesheets/version.1.0.css (renamed from actionpack/test/fixtures/public/stylesheets/version.1.0.css)0
-rw-r--r--actionview/test/fixtures/replies.yml (renamed from actionpack/test/fixtures/replies.yml)0
-rw-r--r--actionview/test/fixtures/replies/_reply.erb (renamed from actionpack/test/fixtures/replies/_reply.erb)0
-rw-r--r--actionview/test/fixtures/reply.rb (renamed from actionpack/test/fixtures/reply.rb)0
-rw-r--r--actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb1
-rw-r--r--actionview/test/fixtures/ruby_template.ruby2
-rw-r--r--actionview/test/fixtures/scope/test/modgreet.erb (renamed from actionpack/test/fixtures/scope/test/modgreet.erb)0
-rw-r--r--actionview/test/fixtures/shared.html.erb1
-rw-r--r--actionview/test/fixtures/test/_200.html.erb (renamed from actionpack/test/fixtures/test/_200.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_b_layout_for_partial.html.erb (renamed from actionpack/test/fixtures/test/_b_layout_for_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb (renamed from actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb (renamed from actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_changing_priority.html.erb1
-rw-r--r--actionview/test/fixtures/test/_changing_priority.json.erb1
-rw-r--r--actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb (renamed from actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb)0
-rw-r--r--actionview/test/fixtures/test/_counter.html.erb1
-rw-r--r--actionview/test/fixtures/test/_customer.erb1
-rw-r--r--actionview/test/fixtures/test/_customer_greeting.erb1
-rw-r--r--actionview/test/fixtures/test/_customer_with_var.erb1
-rw-r--r--actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb1
-rw-r--r--actionview/test/fixtures/test/_first_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/test/_from_helper.erb (renamed from actionpack/test/fixtures/test/_from_helper.erb)0
-rw-r--r--actionview/test/fixtures/test/_json_change_priority.json.erb (renamed from guides/code/getting_started/lib/assets/.gitkeep)0
-rw-r--r--actionview/test/fixtures/test/_label_with_block.erb (renamed from actionpack/test/fixtures/test/_label_with_block.erb)0
-rw-r--r--actionview/test/fixtures/test/_layout_for_block_with_args.html.erb (renamed from actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_layout_for_partial.html.erb3
-rw-r--r--actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb (renamed from actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_local_inspector.html.erb (renamed from actionpack/test/fixtures/test/_local_inspector.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_object_inspector.erb (renamed from actionpack/test/fixtures/test/_object_inspector.erb)0
-rw-r--r--actionview/test/fixtures/test/_one.html.erb (renamed from actionpack/test/fixtures/test/_one.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_partial.erb1
-rw-r--r--actionview/test/fixtures/test/_partial.html.erb1
-rw-r--r--actionview/test/fixtures/test/_partial.js.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_name_local_variable.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_only.erb1
-rw-r--r--actionview/test/fixtures/test/_partial_with_layout.erb (renamed from actionpack/test/fixtures/test/_partial_with_layout.erb)0
-rw-r--r--actionview/test/fixtures/test/_partial_with_layout_block_content.erb (renamed from actionpack/test/fixtures/test/_partial_with_layout_block_content.erb)0
-rw-r--r--actionview/test/fixtures/test/_partial_with_layout_block_partial.erb (renamed from actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb)0
-rw-r--r--actionview/test/fixtures/test/_partial_with_only_html_version.html.erb (renamed from actionpack/test/fixtures/test/_partial_with_only_html_version.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_partial_with_partial.erb2
-rw-r--r--actionview/test/fixtures/test/_raise.html.erb (renamed from actionpack/test/fixtures/test/_raise.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_raise_indentation.html.erb13
-rw-r--r--actionview/test/fixtures/test/_second_json_partial.json.erb1
-rw-r--r--actionview/test/fixtures/test/_two.html.erb (renamed from actionpack/test/fixtures/test/_two.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_utf8_partial.html.erb (renamed from actionpack/test/fixtures/test/_utf8_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/test/_utf8_partial_magic.html.erb (renamed from actionpack/test/fixtures/test/_utf8_partial_magic.html.erb)0
-rw-r--r--actionview/test/fixtures/test/basic.html.erb (renamed from actionpack/test/fixtures/test/basic.html.erb)0
-rw-r--r--actionview/test/fixtures/test/calling_partial_with_layout.html.erb1
-rw-r--r--actionview/test/fixtures/test/change_priority.html.erb2
-rw-r--r--actionview/test/fixtures/test/dont_pick_me (renamed from actionpack/test/fixtures/test/dont_pick_me)0
-rw-r--r--actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/test/greeting.xml.erb1
-rw-r--r--actionview/test/fixtures/test/hello.builder4
-rw-r--r--actionview/test/fixtures/test/hello/hello.erb1
-rw-r--r--actionview/test/fixtures/test/hello_world.da.html.erb (renamed from actionpack/test/fixtures/test/hello_world.da.html.erb)0
-rw-r--r--actionview/test/fixtures/test/hello_world.erb1
-rw-r--r--actionview/test/fixtures/test/hello_world.erb~ (renamed from actionpack/test/fixtures/test/hello_world.erb~)0
-rw-r--r--actionview/test/fixtures/test/hello_world.pt-BR.html.erb (renamed from actionpack/test/fixtures/test/hello_world.pt-BR.html.erb)0
-rw-r--r--actionview/test/fixtures/test/hello_world_with_partial.html.erb2
-rw-r--r--actionview/test/fixtures/test/html_template.html.erb1
-rw-r--r--actionview/test/fixtures/test/layout_render_file.erb (renamed from actionpack/test/fixtures/test/layout_render_file.erb)0
-rw-r--r--actionview/test/fixtures/test/layout_render_object.erb (renamed from actionpack/test/fixtures/test/layout_render_object.erb)0
-rw-r--r--actionview/test/fixtures/test/list.erb1
-rw-r--r--actionview/test/fixtures/test/malformed/malformed.en.html.erb~ (renamed from actionpack/test/fixtures/test/malformed/malformed.en.html.erb~)0
-rw-r--r--actionview/test/fixtures/test/malformed/malformed.erb~ (renamed from actionpack/test/fixtures/test/malformed/malformed.erb~)0
-rw-r--r--actionview/test/fixtures/test/malformed/malformed.html.erb~ (renamed from actionpack/test/fixtures/test/malformed/malformed.html.erb~)0
-rw-r--r--actionview/test/fixtures/test/malformed/malformed~1
-rw-r--r--actionview/test/fixtures/test/nested_layout.erb (renamed from actionpack/test/fixtures/test/nested_layout.erb)0
-rw-r--r--actionview/test/fixtures/test/nested_streaming.erb (renamed from actionpack/test/fixtures/test/nested_streaming.erb)0
-rw-r--r--actionview/test/fixtures/test/one.html.erb (renamed from actionpack/test/fixtures/test/one.html.erb)0
-rw-r--r--actionview/test/fixtures/test/render_file_with_ivar.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_with_locals.erb1
-rw-r--r--actionview/test/fixtures/test/render_file_with_locals_and_default.erb1
-rw-r--r--actionview/test/fixtures/test/render_partial_inside_directory.html.erb1
-rw-r--r--actionview/test/fixtures/test/render_two_partials.html.erb2
-rw-r--r--actionview/test/fixtures/test/streaming.erb (renamed from actionpack/test/fixtures/test/streaming.erb)0
-rw-r--r--actionview/test/fixtures/test/streaming_buster.erb (renamed from actionpack/test/fixtures/test/streaming_buster.erb)0
-rw-r--r--actionview/test/fixtures/test/sub_template_raise.html.erb (renamed from actionpack/test/fixtures/test/sub_template_raise.html.erb)0
-rw-r--r--actionview/test/fixtures/test/template.erb (renamed from actionpack/test/fixtures/test/template.erb)0
-rw-r--r--actionview/test/fixtures/test/update_element_with_capture.erb (renamed from actionpack/test/fixtures/test/update_element_with_capture.erb)0
-rw-r--r--actionview/test/fixtures/test/utf8.html.erb (renamed from actionpack/test/fixtures/test/utf8.html.erb)0
-rw-r--r--actionview/test/fixtures/test/utf8_magic.html.erb (renamed from actionpack/test/fixtures/test/utf8_magic.html.erb)0
-rw-r--r--actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb (renamed from actionpack/test/fixtures/test/utf8_magic_with_bare_partial.html.erb)0
-rw-r--r--actionview/test/fixtures/topic.rb (renamed from actionpack/test/fixtures/topic.rb)0
-rw-r--r--actionview/test/fixtures/topics.yml (renamed from actionpack/test/fixtures/topics.yml)0
-rw-r--r--actionview/test/fixtures/topics/_topic.html.erb (renamed from actionpack/test/fixtures/topics/_topic.html.erb)0
-rw-r--r--actionview/test/fixtures/translations/templates/array.erb (renamed from actionpack/test/fixtures/translations/templates/array.erb)0
-rw-r--r--actionview/test/fixtures/translations/templates/default.erb (renamed from actionpack/test/fixtures/translations/templates/default.erb)0
-rw-r--r--actionview/test/fixtures/translations/templates/found.erb (renamed from actionpack/test/fixtures/translations/templates/found.erb)0
-rw-r--r--actionview/test/fixtures/translations/templates/missing.erb (renamed from actionpack/test/fixtures/translations/templates/missing.erb)0
-rw-r--r--actionview/test/fixtures/with_format.json.erb (renamed from actionpack/test/fixtures/with_format.json.erb)0
-rw-r--r--actionview/test/lib/controller/fake_models.rb185
-rw-r--r--actionview/test/template/active_model_helper_test.rb (renamed from actionpack/test/template/active_model_helper_test.rb)0
-rw-r--r--actionview/test/template/asset_tag_helper_test.rb762
-rw-r--r--actionview/test/template/atom_feed_helper_test.rb349
-rw-r--r--actionview/test/template/capture_helper_test.rb242
-rw-r--r--actionview/test/template/compiled_templates_test.rb62
-rw-r--r--actionview/test/template/date_helper_i18n_test.rb148
-rw-r--r--actionview/test/template/date_helper_test.rb3201
-rw-r--r--actionview/test/template/debug_helper_test.rb8
-rw-r--r--actionview/test/template/dependency_tracker_test.rb74
-rw-r--r--actionview/test/template/digestor_test.rb284
-rw-r--r--actionview/test/template/erb/form_for_test.rb (renamed from actionpack/test/template/erb/form_for_test.rb)0
-rw-r--r--actionview/test/template/erb/helper.rb (renamed from actionpack/test/template/erb/helper.rb)0
-rw-r--r--actionview/test/template/erb/tag_helper_test.rb (renamed from actionpack/test/template/erb/tag_helper_test.rb)0
-rw-r--r--actionview/test/template/erb_util_test.rb (renamed from actionpack/test/template/erb_util_test.rb)0
-rw-r--r--actionview/test/template/form_collections_helper_test.rb365
-rw-r--r--actionview/test/template/form_helper_test.rb3019
-rw-r--r--actionview/test/template/form_options_helper_i18n_test.rb (renamed from actionpack/test/template/form_options_helper_i18n_test.rb)0
-rw-r--r--actionview/test/template/form_options_helper_test.rb1325
-rw-r--r--actionview/test/template/form_tag_helper_test.rb629
-rw-r--r--actionview/test/template/html-scanner/cdata_node_test.rb (renamed from actionpack/test/template/html-scanner/cdata_node_test.rb)0
-rw-r--r--actionview/test/template/html-scanner/document_test.rb (renamed from actionpack/test/template/html-scanner/document_test.rb)0
-rw-r--r--actionview/test/template/html-scanner/node_test.rb (renamed from actionpack/test/template/html-scanner/node_test.rb)0
-rw-r--r--actionview/test/template/html-scanner/sanitizer_test.rb330
-rw-r--r--actionview/test/template/html-scanner/tag_node_test.rb (renamed from actionpack/test/template/html-scanner/tag_node_test.rb)0
-rw-r--r--actionview/test/template/html-scanner/text_node_test.rb (renamed from actionpack/test/template/html-scanner/text_node_test.rb)0
-rw-r--r--actionview/test/template/html-scanner/tokenizer_test.rb (renamed from actionpack/test/template/html-scanner/tokenizer_test.rb)0
-rw-r--r--actionview/test/template/javascript_helper_test.rb69
-rw-r--r--actionview/test/template/log_subscriber_test.rb (renamed from actionpack/test/template/log_subscriber_test.rb)0
-rw-r--r--actionview/test/template/lookup_context_test.rb263
-rw-r--r--actionview/test/template/number_helper_test.rb151
-rw-r--r--actionview/test/template/output_buffer_test.rb (renamed from actionpack/test/template/output_buffer_test.rb)0
-rw-r--r--actionview/test/template/output_safety_helper_test.rb (renamed from actionpack/test/template/output_safety_helper_test.rb)0
-rw-r--r--actionview/test/template/record_identifier_test.rb (renamed from actionpack/test/template/record_identifier_test.rb)0
-rw-r--r--actionview/test/template/record_tag_helper_test.rb117
-rw-r--r--actionview/test/template/render_test.rb543
-rw-r--r--actionview/test/template/resolver_patterns_test.rb31
-rw-r--r--actionview/test/template/sanitize_helper_test.rb (renamed from actionpack/test/template/sanitize_helper_test.rb)0
-rw-r--r--actionview/test/template/streaming_render_test.rb108
-rw-r--r--actionview/test/template/tag_helper_test.rb134
-rw-r--r--actionview/test/template/template_error_test.rb (renamed from actionpack/test/template/template_error_test.rb)0
-rw-r--r--actionview/test/template/template_test.rb200
-rw-r--r--actionview/test/template/test_case_test.rb366
-rw-r--r--actionview/test/template/test_test.rb80
-rw-r--r--actionview/test/template/testing/fixture_resolver_test.rb (renamed from actionpack/test/template/testing/fixture_resolver_test.rb)0
-rw-r--r--actionview/test/template/testing/null_resolver_test.rb (renamed from actionpack/test/template/testing/null_resolver_test.rb)0
-rw-r--r--actionview/test/template/text_helper_test.rb470
-rw-r--r--actionview/test/template/translation_helper_test.rb (renamed from actionpack/test/template/translation_helper_test.rb)0
-rw-r--r--actionview/test/template/url_helper_test.rb791
-rw-r--r--actionview/test/tmp/.gitkeep (renamed from guides/code/getting_started/lib/tasks/.gitkeep)0
-rw-r--r--activemodel/CHANGELOG.md88
-rw-r--r--activemodel/MIT-LICENSE2
-rw-r--r--activemodel/README.rdoc62
-rw-r--r--activemodel/Rakefile6
-rw-r--r--activemodel/activemodel.gemspec16
-rw-r--r--activemodel/examples/validations.rb11
-rw-r--r--activemodel/lib/active_model.rb5
-rw-r--r--activemodel/lib/active_model/attribute_methods.rb100
-rw-r--r--activemodel/lib/active_model/callbacks.rb45
-rw-r--r--activemodel/lib/active_model/conversion.rb4
-rw-r--r--activemodel/lib/active_model/deprecated_mass_assignment_security.rb19
-rw-r--r--activemodel/lib/active_model/dirty.rb70
-rw-r--r--activemodel/lib/active_model/errors.rb86
-rw-r--r--activemodel/lib/active_model/forbidden_attributes_protection.rb2
-rw-r--r--activemodel/lib/active_model/lint.rb10
-rw-r--r--activemodel/lib/active_model/locale/en.yml1
-rw-r--r--activemodel/lib/active_model/model.rb4
-rw-r--r--activemodel/lib/active_model/naming.rb11
-rw-r--r--activemodel/lib/active_model/observer_array.rb152
-rw-r--r--activemodel/lib/active_model/observing.rb374
-rw-r--r--activemodel/lib/active_model/railtie.rb8
-rw-r--r--activemodel/lib/active_model/secure_password.rb63
-rw-r--r--activemodel/lib/active_model/serialization.rb4
-rw-r--r--activemodel/lib/active_model/serializers/json.rb4
-rw-r--r--[-rwxr-xr-x]activemodel/lib/active_model/serializers/xml.rb32
-rw-r--r--activemodel/lib/active_model/translation.rb4
-rw-r--r--activemodel/lib/active_model/validations.rb75
-rw-r--r--activemodel/lib/active_model/validations/absence.rb31
-rw-r--r--activemodel/lib/active_model/validations/acceptance.rb11
-rw-r--r--activemodel/lib/active_model/validations/callbacks.rb17
-rw-r--r--activemodel/lib/active_model/validations/clusivity.rb33
-rw-r--r--activemodel/lib/active_model/validations/confirmation.rb19
-rw-r--r--activemodel/lib/active_model/validations/exclusion.rb5
-rw-r--r--activemodel/lib/active_model/validations/format.rb5
-rw-r--r--activemodel/lib/active_model/validations/inclusion.rb7
-rw-r--r--activemodel/lib/active_model/validations/length.rb26
-rw-r--r--activemodel/lib/active_model/validations/numericality.rb19
-rw-r--r--activemodel/lib/active_model/validations/presence.rb7
-rw-r--r--activemodel/lib/active_model/validations/validates.rb17
-rw-r--r--activemodel/lib/active_model/validations/with.rb5
-rw-r--r--activemodel/lib/active_model/validator.rb47
-rw-r--r--activemodel/lib/active_model/version.rb13
-rw-r--r--activemodel/test/cases/attribute_methods_test.rb17
-rw-r--r--activemodel/test/cases/callbacks_test.rb8
-rw-r--r--activemodel/test/cases/conversion_test.rb8
-rw-r--r--activemodel/test/cases/deprecated_mass_assignment_security_test.rb16
-rw-r--r--activemodel/test/cases/dirty_test.rb34
-rw-r--r--activemodel/test/cases/errors_test.rb176
-rw-r--r--activemodel/test/cases/helper.rb2
-rw-r--r--activemodel/test/cases/model_test.rb53
-rw-r--r--activemodel/test/cases/naming_test.rb10
-rw-r--r--activemodel/test/cases/observer_array_test.rb220
-rw-r--r--activemodel/test/cases/observing_test.rb181
-rw-r--r--activemodel/test/cases/railtie_test.rb32
-rw-r--r--activemodel/test/cases/secure_password_test.rb51
-rw-r--r--activemodel/test/cases/serialization_test.rb34
-rw-r--r--activemodel/test/cases/serializers/json_serialization_test.rb5
-rw-r--r--[-rwxr-xr-x]activemodel/test/cases/serializers/xml_serialization_test.rb75
-rw-r--r--activemodel/test/cases/translation_test.rb43
-rw-r--r--activemodel/test/cases/validations/absence_validation_test.rb67
-rw-r--r--activemodel/test/cases/validations/acceptance_validation_test.rb4
-rw-r--r--activemodel/test/cases/validations/callbacks_test.rb21
-rw-r--r--activemodel/test/cases/validations/conditional_validation_test.rb48
-rw-r--r--activemodel/test/cases/validations/confirmation_validation_test.rb37
-rw-r--r--activemodel/test/cases/validations/exclusion_validation_test.rb12
-rw-r--r--activemodel/test/cases/validations/format_validation_test.rb28
-rw-r--r--activemodel/test/cases/validations/i18n_generate_message_validation_test.rb74
-rw-r--r--activemodel/test/cases/validations/i18n_validation_test.rb137
-rw-r--r--activemodel/test/cases/validations/inclusion_validation_test.rb49
-rw-r--r--activemodel/test/cases/validations/length_validation_test.rb146
-rw-r--r--activemodel/test/cases/validations/numericality_validation_test.rb46
-rw-r--r--activemodel/test/cases/validations/presence_validation_test.rb36
-rw-r--r--activemodel/test/cases/validations/validates_test.rb38
-rw-r--r--activemodel/test/cases/validations/validations_context_test.rb12
-rw-r--r--activemodel/test/cases/validations/with_validation_test.rb62
-rw-r--r--activemodel/test/cases/validations_test.rb77
-rw-r--r--activemodel/test/models/automobile.rb4
-rw-r--r--activemodel/test/models/contact.rb2
-rw-r--r--activemodel/test/models/helicopter.rb4
-rw-r--r--activemodel/test/models/observers.rb27
-rw-r--r--activemodel/test/models/reply.rb6
-rw-r--r--activemodel/test/models/topic.rb2
-rw-r--r--activerecord/CHANGELOG.md1197
-rw-r--r--activerecord/MIT-LICENSE2
-rw-r--r--activerecord/README.rdoc19
-rw-r--r--activerecord/RUNNING_UNIT_TESTS31
-rw-r--r--activerecord/RUNNING_UNIT_TESTS.rdoc44
-rw-r--r--activerecord/Rakefile22
-rw-r--r--activerecord/activerecord.gemspec20
-rw-r--r--activerecord/examples/associations.pngbin40623 -> 0 bytes
-rw-r--r--activerecord/examples/performance.rb50
-rw-r--r--activerecord/examples/simple.rb8
-rw-r--r--activerecord/lib/active_record.rb13
-rw-r--r--activerecord/lib/active_record/aggregations.rb41
-rw-r--r--activerecord/lib/active_record/association_relation.rb18
-rw-r--r--activerecord/lib/active_record/associations.rb267
-rw-r--r--activerecord/lib/active_record/associations/alias_tracker.rb2
-rw-r--r--activerecord/lib/active_record/associations/association.rb48
-rw-r--r--activerecord/lib/active_record/associations/association_scope.rb42
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_association.rb11
-rw-r--r--activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/builder/association.rb136
-rw-r--r--activerecord/lib/active_record/associations/builder/belongs_to.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/collection_association.rb92
-rw-r--r--activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb132
-rw-r--r--activerecord/lib/active_record/associations/builder/has_many.rb10
-rw-r--r--activerecord/lib/active_record/associations/builder/has_one.rb16
-rw-r--r--activerecord/lib/active_record/associations/builder/singular_association.rb15
-rw-r--r--activerecord/lib/active_record/associations/collection_association.rb206
-rw-r--r--activerecord/lib/active_record/associations/collection_proxy.rb87
-rw-r--r--activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb65
-rw-r--r--activerecord/lib/active_record/associations/has_many_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_many_through_association.rb29
-rw-r--r--activerecord/lib/active_record/associations/has_one_association.rb23
-rw-r--r--activerecord/lib/active_record/associations/has_one_through_association.rb2
-rw-r--r--activerecord/lib/active_record/associations/join_dependency.rb372
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_association.rb144
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_base.rb16
-rw-r--r--activerecord/lib/active_record/associations/join_dependency/join_part.rb66
-rw-r--r--activerecord/lib/active_record/associations/join_helper.rb13
-rw-r--r--activerecord/lib/active_record/associations/preloader.rb127
-rw-r--r--activerecord/lib/active_record/associations/preloader/association.rb68
-rw-r--r--activerecord/lib/active_record/associations/preloader/collection_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb60
-rw-r--r--activerecord/lib/active_record/associations/preloader/has_many_through.rb10
-rw-r--r--activerecord/lib/active_record/associations/preloader/singular_association.rb4
-rw-r--r--activerecord/lib/active_record/associations/preloader/through_association.rb83
-rw-r--r--activerecord/lib/active_record/associations/singular_association.rb3
-rw-r--r--activerecord/lib/active_record/associations/through_association.rb8
-rw-r--r--activerecord/lib/active_record/attribute_assignment.rb24
-rw-r--r--activerecord/lib/active_record/attribute_methods.rb299
-rw-r--r--activerecord/lib/active_record/attribute_methods/before_type_cast.rb46
-rw-r--r--activerecord/lib/active_record/attribute_methods/dirty.rb49
-rw-r--r--activerecord/lib/active_record/attribute_methods/primary_key.rb9
-rw-r--r--activerecord/lib/active_record/attribute_methods/read.rb107
-rw-r--r--activerecord/lib/active_record/attribute_methods/serialization.rb61
-rw-r--r--activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb43
-rw-r--r--activerecord/lib/active_record/attribute_methods/write.rb49
-rw-r--r--activerecord/lib/active_record/autosave_association.rb463
-rw-r--r--activerecord/lib/active_record/base.rb127
-rw-r--r--activerecord/lib/active_record/callbacks.rb47
-rw-r--r--activerecord/lib/active_record/coders/yaml_column.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb97
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb9
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb34
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb25
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/quoting.rb14
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb21
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb231
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb18
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb556
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract/transaction.rb44
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_adapter.rb190
-rw-r--r--activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb217
-rw-r--r--activerecord/lib/active_record/connection_adapters/column.rb106
-rw-r--r--activerecord/lib/active_record/connection_adapters/connection_specification.rb15
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb62
-rw-r--r--activerecord/lib/active_record/connection_adapters/mysql_adapter.rb61
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb37
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/cast.rb54
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb67
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/oid.rb126
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb60
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb16
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb123
-rw-r--r--activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb314
-rw-r--r--activerecord/lib/active_record/connection_adapters/schema_cache.rb36
-rw-r--r--activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb145
-rw-r--r--activerecord/lib/active_record/connection_handling.rb27
-rw-r--r--activerecord/lib/active_record/core.rb294
-rw-r--r--activerecord/lib/active_record/counter_cache.rb27
-rw-r--r--activerecord/lib/active_record/dynamic_matchers.rb2
-rw-r--r--activerecord/lib/active_record/errors.rb43
-rw-r--r--activerecord/lib/active_record/explain.rb68
-rw-r--r--activerecord/lib/active_record/explain_registry.rb30
-rw-r--r--activerecord/lib/active_record/explain_subscriber.rb7
-rw-r--r--activerecord/lib/active_record/fixture_set/file.rb3
-rw-r--r--activerecord/lib/active_record/fixtures.rb226
-rw-r--r--activerecord/lib/active_record/inheritance.rb135
-rw-r--r--activerecord/lib/active_record/integration.rb23
-rw-r--r--activerecord/lib/active_record/locale/en.yml6
-rw-r--r--activerecord/lib/active_record/locking/optimistic.rb18
-rw-r--r--activerecord/lib/active_record/locking/pessimistic.rb10
-rw-r--r--activerecord/lib/active_record/log_subscriber.rb35
-rw-r--r--activerecord/lib/active_record/migration.rb368
-rw-r--r--activerecord/lib/active_record/migration/command_recorder.rb127
-rw-r--r--activerecord/lib/active_record/migration/join_table.rb2
-rw-r--r--activerecord/lib/active_record/model.rb167
-rw-r--r--activerecord/lib/active_record/model_schema.rb113
-rw-r--r--activerecord/lib/active_record/nested_attributes.rb279
-rw-r--r--activerecord/lib/active_record/null_relation.rb18
-rw-r--r--activerecord/lib/active_record/observer.rb126
-rw-r--r--activerecord/lib/active_record/persistence.rb211
-rw-r--r--activerecord/lib/active_record/query_cache.rb14
-rw-r--r--activerecord/lib/active_record/querying.rb53
-rw-r--r--activerecord/lib/active_record/railtie.rb49
-rw-r--r--activerecord/lib/active_record/railties/console_sandbox.rb5
-rw-r--r--activerecord/lib/active_record/railties/controller_runtime.rb3
-rw-r--r--activerecord/lib/active_record/railties/databases.rake160
-rw-r--r--activerecord/lib/active_record/readonly_attributes.rb5
-rw-r--r--activerecord/lib/active_record/reflection.rb322
-rw-r--r--activerecord/lib/active_record/relation.rb303
-rw-r--r--activerecord/lib/active_record/relation/batches.rb81
-rw-r--r--activerecord/lib/active_record/relation/calculations.rb170
-rw-r--r--activerecord/lib/active_record/relation/delegation.rb129
-rw-r--r--activerecord/lib/active_record/relation/finder_methods.rb222
-rw-r--r--activerecord/lib/active_record/relation/merger.rb116
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder.rb95
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/array_handler.rb29
-rw-r--r--activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb13
-rw-r--r--activerecord/lib/active_record/relation/query_methods.rb498
-rw-r--r--activerecord/lib/active_record/relation/spawn_methods.rb34
-rw-r--r--activerecord/lib/active_record/result.rb62
-rw-r--r--activerecord/lib/active_record/runtime_registry.rb17
-rw-r--r--activerecord/lib/active_record/sanitization.rb53
-rw-r--r--activerecord/lib/active_record/schema.rb9
-rw-r--r--activerecord/lib/active_record/schema_dumper.rb46
-rw-r--r--activerecord/lib/active_record/schema_migration.rb41
-rw-r--r--activerecord/lib/active_record/scoping.rb56
-rw-r--r--activerecord/lib/active_record/scoping/default.rb30
-rw-r--r--activerecord/lib/active_record/scoping/named.rb43
-rw-r--r--activerecord/lib/active_record/serialization.rb10
-rw-r--r--activerecord/lib/active_record/serializers/xml_serializer.rb14
-rw-r--r--activerecord/lib/active_record/statement_cache.rb26
-rw-r--r--activerecord/lib/active_record/store.rb100
-rw-r--r--activerecord/lib/active_record/tasks/database_tasks.rb78
-rw-r--r--activerecord/lib/active_record/tasks/mysql_database_tasks.rb48
-rw-r--r--activerecord/lib/active_record/tasks/postgresql_database_tasks.rb9
-rw-r--r--activerecord/lib/active_record/tasks/sqlite_database_tasks.rb8
-rw-r--r--activerecord/lib/active_record/test_case.rb95
-rw-r--r--activerecord/lib/active_record/timestamp.rb25
-rw-r--r--activerecord/lib/active_record/transactions.rb111
-rw-r--r--activerecord/lib/active_record/validations.rb3
-rw-r--r--activerecord/lib/active_record/validations/associated.rb6
-rw-r--r--activerecord/lib/active_record/validations/presence.rb6
-rw-r--r--activerecord/lib/active_record/validations/uniqueness.rb84
-rw-r--r--activerecord/lib/active_record/version.rb13
-rw-r--r--activerecord/lib/rails/generators/active_record.rb14
-rw-r--r--activerecord/lib/rails/generators/active_record/migration.rb18
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/migration_generator.rb21
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb19
-rw-r--r--activerecord/lib/rails/generators/active_record/migration/templates/migration.rb20
-rw-r--r--activerecord/lib/rails/generators/active_record/model/model_generator.rb10
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/migration.rb15
-rw-r--r--activerecord/lib/rails/generators/active_record/model/templates/model.rb5
-rw-r--r--activerecord/lib/rails/generators/active_record/observer/observer_generator.rb15
-rw-r--r--activerecord/lib/rails/generators/active_record/observer/templates/observer.rb4
-rw-r--r--activerecord/test/cases/adapter_test.rb39
-rw-r--r--activerecord/test/cases/adapters/mysql/active_schema_test.rb64
-rw-r--r--activerecord/test/cases/adapters/mysql/connection_test.rb52
-rw-r--r--activerecord/test/cases/adapters/mysql/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb52
-rw-r--r--activerecord/test/cases/adapters/mysql/reserved_word_test.rb7
-rw-r--r--activerecord/test/cases/adapters/mysql/schema_test.rb22
-rw-r--r--activerecord/test/cases/adapters/mysql/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/mysql2/active_schema_test.rb62
-rw-r--r--activerecord/test/cases/adapters/mysql2/connection_test.rb45
-rw-r--r--activerecord/test/cases/adapters/mysql2/enum_test.rb2
-rw-r--r--activerecord/test/cases/adapters/mysql2/reserved_word_test.rb8
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb26
-rw-r--r--activerecord/test/cases/adapters/mysql2/schema_test.rb33
-rw-r--r--activerecord/test/cases/adapters/mysql2/sql_types_test.rb14
-rw-r--r--activerecord/test/cases/adapters/postgresql/active_schema_test.rb27
-rw-r--r--activerecord/test/cases/adapters/postgresql/array_test.rb65
-rw-r--r--activerecord/test/cases/adapters/postgresql/bytea_test.rb104
-rw-r--r--activerecord/test/cases/adapters/postgresql/connection_test.rb105
-rw-r--r--activerecord/test/cases/adapters/postgresql/datatype_test.rb450
-rw-r--r--activerecord/test/cases/adapters/postgresql/explain_test.rb7
-rw-r--r--activerecord/test/cases/adapters/postgresql/hstore_test.rb85
-rw-r--r--activerecord/test/cases/adapters/postgresql/json_test.rb53
-rw-r--r--activerecord/test/cases/adapters/postgresql/ltree_test.rb41
-rw-r--r--activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb50
-rw-r--r--activerecord/test/cases/adapters/postgresql/quoting_test.rb13
-rw-r--r--activerecord/test/cases/adapters/postgresql/schema_test.rb20
-rw-r--r--activerecord/test/cases/adapters/postgresql/sql_types_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/statement_pool_test.rb54
-rw-r--r--activerecord/test/cases/adapters/postgresql/timestamp_test.rb18
-rw-r--r--activerecord/test/cases/adapters/postgresql/uuid_test.rb136
-rw-r--r--activerecord/test/cases/adapters/postgresql/xml_test.rb38
-rw-r--r--activerecord/test/cases/adapters/sqlite3/copy_table_test.rb21
-rw-r--r--activerecord/test/cases/adapters/sqlite3/quoting_test.rb2
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb49
-rw-r--r--activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb21
-rw-r--r--activerecord/test/cases/aggregations_test.rb4
-rw-r--r--activerecord/test/cases/ar_schema_test.rb23
-rw-r--r--activerecord/test/cases/associations/belongs_to_associations_test.rb206
-rw-r--r--activerecord/test/cases/associations/cascaded_eager_loading_test.rb10
-rw-r--r--activerecord/test/cases/associations/eager_test.rb111
-rw-r--r--activerecord/test/cases/associations/extension_test.rb14
-rw-r--r--activerecord/test/cases/associations/habtm_join_table_test.rb35
-rw-r--r--activerecord/test/cases/associations/has_and_belongs_to_many_associations_test.rb132
-rw-r--r--activerecord/test/cases/associations/has_many_associations_test.rb479
-rw-r--r--activerecord/test/cases/associations/has_many_through_associations_test.rb233
-rw-r--r--activerecord/test/cases/associations/has_one_associations_test.rb81
-rw-r--r--activerecord/test/cases/associations/has_one_through_associations_test.rb3
-rw-r--r--activerecord/test/cases/associations/inner_join_association_test.rb23
-rw-r--r--activerecord/test/cases/associations/inverse_associations_test.rb203
-rw-r--r--activerecord/test/cases/associations/join_dependency_test.rb8
-rw-r--r--activerecord/test/cases/associations/join_model_test.rb10
-rw-r--r--activerecord/test/cases/associations/nested_through_associations_test.rb24
-rw-r--r--activerecord/test/cases/associations_test.rb47
-rw-r--r--activerecord/test/cases/attribute_methods/read_test.rb13
-rw-r--r--activerecord/test/cases/attribute_methods/serialization_test.rb29
-rw-r--r--activerecord/test/cases/attribute_methods_test.rb87
-rw-r--r--activerecord/test/cases/autosave_association_test.rb47
-rw-r--r--activerecord/test/cases/base_test.rb478
-rw-r--r--activerecord/test/cases/batches_test.rb54
-rw-r--r--activerecord/test/cases/binary_test.rb2
-rw-r--r--activerecord/test/cases/calculations_test.rb151
-rw-r--r--activerecord/test/cases/callbacks_test.rb4
-rw-r--r--activerecord/test/cases/clone_test.rb7
-rw-r--r--activerecord/test/cases/coders/yaml_column_test.rb14
-rw-r--r--activerecord/test/cases/column_definition_test.rb13
-rw-r--r--activerecord/test/cases/column_test.rb76
-rw-r--r--activerecord/test/cases/connection_adapters/abstract_adapter_test.rb20
-rw-r--r--activerecord/test/cases/connection_adapters/connection_handler_test.rb20
-rw-r--r--activerecord/test/cases/connection_adapters/schema_cache_test.rb41
-rw-r--r--activerecord/test/cases/connection_management_test.rb4
-rw-r--r--activerecord/test/cases/connection_pool_test.rb14
-rw-r--r--activerecord/test/cases/connection_specification/resolver_test.rb55
-rw-r--r--activerecord/test/cases/core_test.rb33
-rw-r--r--activerecord/test/cases/counter_cache_test.rb31
-rw-r--r--activerecord/test/cases/date_time_test.rb8
-rw-r--r--activerecord/test/cases/defaults_test.rb99
-rw-r--r--activerecord/test/cases/deprecated_dynamic_methods_test.rb592
-rw-r--r--activerecord/test/cases/dirty_test.rb137
-rw-r--r--activerecord/test/cases/disconnected_test.rb27
-rw-r--r--activerecord/test/cases/dup_test.rb25
-rw-r--r--activerecord/test/cases/explain_subscriber_test.rb64
-rw-r--r--activerecord/test/cases/explain_test.rb73
-rw-r--r--activerecord/test/cases/finder_respond_to_test.rb41
-rw-r--r--activerecord/test/cases/finder_test.rb110
-rw-r--r--activerecord/test/cases/fixtures_test.rb92
-rw-r--r--activerecord/test/cases/forbidden_attributes_protection_test.rb20
-rw-r--r--activerecord/test/cases/helper.rb104
-rw-r--r--activerecord/test/cases/inclusion_test.rb133
-rw-r--r--activerecord/test/cases/inheritance_test.rb70
-rw-r--r--activerecord/test/cases/integration_test.rb84
-rw-r--r--activerecord/test/cases/invalid_connection_test.rb22
-rw-r--r--activerecord/test/cases/invertible_migration_test.rb159
-rw-r--r--activerecord/test/cases/json_serialization_test.rb88
-rw-r--r--activerecord/test/cases/lifecycle_test.rb256
-rw-r--r--activerecord/test/cases/locking_test.rb33
-rw-r--r--activerecord/test/cases/log_subscriber_test.rb56
-rw-r--r--activerecord/test/cases/migration/change_schema_test.rb75
-rw-r--r--activerecord/test/cases/migration/change_table_test.rb43
-rw-r--r--activerecord/test/cases/migration/column_attributes_test.rb41
-rw-r--r--activerecord/test/cases/migration/columns_test.rb279
-rw-r--r--activerecord/test/cases/migration/command_recorder_test.rb216
-rw-r--r--activerecord/test/cases/migration/create_join_table_test.rb48
-rw-r--r--activerecord/test/cases/migration/helper.rb6
-rw-r--r--activerecord/test/cases/migration/index_test.rb40
-rw-r--r--activerecord/test/cases/migration/logger_test.rb2
-rw-r--r--activerecord/test/cases/migration/references_index_test.rb8
-rw-r--r--activerecord/test/cases/migration/references_statements_test.rb20
-rw-r--r--activerecord/test/cases/migration/rename_column_test.rb194
-rw-r--r--activerecord/test/cases/migration/rename_table_test.rb12
-rw-r--r--activerecord/test/cases/migration_test.rb248
-rw-r--r--activerecord/test/cases/migrator_test.rb17
-rw-r--r--activerecord/test/cases/modules_test.rb1
-rw-r--r--activerecord/test/cases/multiparameter_attributes_test.rb156
-rw-r--r--activerecord/test/cases/multiple_db_test.rb4
-rw-r--r--activerecord/test/cases/named_scope_test.rb449
-rw-r--r--activerecord/test/cases/nested_attributes_test.rb167
-rw-r--r--activerecord/test/cases/nested_attributes_with_callbacks_test.rb144
-rw-r--r--activerecord/test/cases/persistence_test.rb173
-rw-r--r--activerecord/test/cases/pooled_connections_test.rb32
-rw-r--r--activerecord/test/cases/primary_keys_test.rb12
-rw-r--r--activerecord/test/cases/query_cache_test.rb13
-rw-r--r--activerecord/test/cases/quoting_test.rb69
-rw-r--r--activerecord/test/cases/readonly_test.rb18
-rw-r--r--activerecord/test/cases/reflection_test.rb35
-rw-r--r--activerecord/test/cases/relation/delegation_test.rb98
-rw-r--r--activerecord/test/cases/relation/mutation_test.rb148
-rw-r--r--activerecord/test/cases/relation/predicate_builder_test.rb14
-rw-r--r--activerecord/test/cases/relation/where_chain_test.rb80
-rw-r--r--activerecord/test/cases/relation/where_test.rb58
-rw-r--r--activerecord/test/cases/relation_scoping_test.rb518
-rw-r--r--activerecord/test/cases/relation_test.rb160
-rw-r--r--activerecord/test/cases/relations_test.rb321
-rw-r--r--activerecord/test/cases/result_test.rb32
-rw-r--r--activerecord/test/cases/sanitize_test.rb24
-rw-r--r--activerecord/test/cases/schema_dumper_test.rb79
-rw-r--r--activerecord/test/cases/scoping/default_scoping_test.rb384
-rw-r--r--activerecord/test/cases/scoping/named_scoping_test.rb451
-rw-r--r--activerecord/test/cases/scoping/relation_scoping_test.rb331
-rw-r--r--activerecord/test/cases/serialization_test.rb20
-rw-r--r--activerecord/test/cases/serialized_attribute_test.rb63
-rw-r--r--activerecord/test/cases/statement_cache_test.rb64
-rw-r--r--activerecord/test/cases/store_test.rb32
-rw-r--r--activerecord/test/cases/tasks/database_tasks_test.rb27
-rw-r--r--activerecord/test/cases/tasks/mysql_rake_test.rb52
-rw-r--r--activerecord/test/cases/tasks/postgresql_rake_test.rb23
-rw-r--r--activerecord/test/cases/tasks/sqlite_rake_test.rb6
-rw-r--r--activerecord/test/cases/test_case.rb111
-rw-r--r--activerecord/test/cases/timestamp_test.rb112
-rw-r--r--activerecord/test/cases/transaction_callbacks_test.rb155
-rw-r--r--activerecord/test/cases/transaction_isolation_test.rb2
-rw-r--r--activerecord/test/cases/transactions_test.rb144
-rw-r--r--activerecord/test/cases/unconnected_test.rb6
-rw-r--r--activerecord/test/cases/validations/association_validation_test.rb71
-rw-r--r--activerecord/test/cases/validations/i18n_generate_message_validation_test.rb19
-rw-r--r--activerecord/test/cases/validations/presence_validation_test.rb7
-rw-r--r--activerecord/test/cases/validations/uniqueness_validation_test.rb35
-rw-r--r--activerecord/test/cases/validations_repair_helper.rb4
-rw-r--r--activerecord/test/cases/xml_serialization_test.rb20
-rw-r--r--activerecord/test/cases/yaml_serialization_test.rb14
l---------activerecord/test/fixtures/all/admin1
-rw-r--r--activerecord/test/fixtures/dog_lovers.yml3
-rw-r--r--activerecord/test/fixtures/dogs.yml1
-rw-r--r--activerecord/test/fixtures/friendships.yml4
-rw-r--r--activerecord/test/fixtures/people.yml3
-rw-r--r--activerecord/test/fixtures/pets.yml5
-rw-r--r--activerecord/test/fixtures/readers.yml4
-rw-r--r--activerecord/test/fixtures/sponsors.yml2
-rw-r--r--activerecord/test/fixtures/tasks.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/accounts.yml2
-rw-r--r--activerecord/test/fixtures/to_be_linked/users.yml10
-rw-r--r--activerecord/test/fixtures/toys.yml10
-rw-r--r--activerecord/test/fixtures/traffic_lights.yml4
-rw-r--r--activerecord/test/migrations/10_urban/9_add_expressions.rb11
-rw-r--r--activerecord/test/migrations/magic/1_currencies_have_symbols.rb12
-rw-r--r--activerecord/test/models/admin/user.rb27
-rw-r--r--activerecord/test/models/author.rb38
-rw-r--r--activerecord/test/models/auto_id.rb4
-rw-r--r--activerecord/test/models/autoloadable/extra_firm.rb2
-rw-r--r--activerecord/test/models/book.rb2
-rw-r--r--activerecord/test/models/bulb.rb8
-rw-r--r--activerecord/test/models/cake_designer.rb3
-rw-r--r--activerecord/test/models/car.rb4
-rw-r--r--activerecord/test/models/category.rb5
-rw-r--r--activerecord/test/models/chef.rb3
-rw-r--r--activerecord/test/models/citation.rb3
-rw-r--r--activerecord/test/models/club.rb3
-rw-r--r--activerecord/test/models/column_name.rb4
-rw-r--r--activerecord/test/models/comment.rb6
-rw-r--r--activerecord/test/models/company.rb46
-rw-r--r--activerecord/test/models/company_in_module.rb4
-rw-r--r--activerecord/test/models/contract.rb1
-rw-r--r--activerecord/test/models/department.rb4
-rw-r--r--activerecord/test/models/developer.rb32
-rw-r--r--activerecord/test/models/dog.rb5
-rw-r--r--activerecord/test/models/dog_lover.rb5
-rw-r--r--activerecord/test/models/drink_designer.rb3
-rw-r--r--activerecord/test/models/friendship.rb4
-rw-r--r--activerecord/test/models/hotel.rb6
-rw-r--r--activerecord/test/models/liquid.rb3
-rw-r--r--activerecord/test/models/man.rb1
-rw-r--r--activerecord/test/models/member.rb3
-rw-r--r--activerecord/test/models/member_detail.rb2
-rw-r--r--activerecord/test/models/membership.rb5
-rw-r--r--activerecord/test/models/mixed_case_monkey.rb2
-rw-r--r--activerecord/test/models/movie.rb4
-rw-r--r--activerecord/test/models/owner.rb2
-rw-r--r--activerecord/test/models/parrot.rb6
-rw-r--r--activerecord/test/models/person.rb28
-rw-r--r--activerecord/test/models/pet.rb2
-rw-r--r--activerecord/test/models/post.rb52
-rw-r--r--activerecord/test/models/project.rb23
-rw-r--r--activerecord/test/models/reader.rb1
-rw-r--r--activerecord/test/models/reference.rb7
-rw-r--r--activerecord/test/models/reply.rb3
-rw-r--r--activerecord/test/models/speedometer.rb2
-rw-r--r--activerecord/test/models/teapot.rb35
-rw-r--r--activerecord/test/models/topic.rb9
-rw-r--r--activerecord/test/models/traffic_light.rb1
-rw-r--r--activerecord/test/schema/mysql2_specific_schema.rb34
-rw-r--r--activerecord/test/schema/mysql_specific_schema.rb34
-rw-r--r--activerecord/test/schema/postgresql_specific_schema.rb54
-rw-r--r--activerecord/test/schema/schema.rb45
-rw-r--r--activerecord/test/schema/sqlite_specific_schema.rb7
-rw-r--r--activerecord/test/support/connection.rb6
-rw-r--r--activerecord/test/support/mysql.rb11
-rw-r--r--activesupport/CHANGELOG.md300
-rw-r--r--activesupport/MIT-LICENSE2
-rw-r--r--activesupport/README.rdoc4
-rw-r--r--activesupport/Rakefile12
-rw-r--r--activesupport/activesupport.gemspec20
-rwxr-xr-xactivesupport/bin/generate_tables6
-rw-r--r--activesupport/lib/active_support.rb4
-rw-r--r--activesupport/lib/active_support/all.rb1
-rw-r--r--activesupport/lib/active_support/backtrace_cleaner.rb19
-rw-r--r--activesupport/lib/active_support/basic_object.rb13
-rw-r--r--activesupport/lib/active_support/benchmarkable.rb9
-rw-r--r--activesupport/lib/active_support/buffered_logger.rb7
-rw-r--r--activesupport/lib/active_support/cache.rb220
-rw-r--r--activesupport/lib/active_support/cache/file_store.rb17
-rw-r--r--activesupport/lib/active_support/cache/mem_cache_store.rb4
-rw-r--r--activesupport/lib/active_support/cache/memory_store.rb17
-rw-r--r--activesupport/lib/active_support/cache/strategy/local_cache.rb85
-rw-r--r--activesupport/lib/active_support/callbacks.rb671
-rw-r--r--activesupport/lib/active_support/concern.rb28
-rw-r--r--activesupport/lib/active_support/configurable.rb6
-rw-r--r--activesupport/lib/active_support/core_ext.rb5
-rw-r--r--activesupport/lib/active_support/core_ext/array.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/array/access.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/array/conversions.rb39
-rw-r--r--activesupport/lib/active_support/core_ext/array/grouping.rb40
-rw-r--r--activesupport/lib/active_support/core_ext/array/uniq_by.rb19
-rw-r--r--activesupport/lib/active_support/core_ext/array/wrap.rb15
-rw-r--r--activesupport/lib/active_support/core_ext/benchmark.rb7
-rw-r--r--activesupport/lib/active_support/core_ext/big_decimal/conversions.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute.rb62
-rw-r--r--activesupport/lib/active_support/core_ext/class/attribute_accessors.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/class/subclasses.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/date/calculations.rb30
-rw-r--r--activesupport/lib/active_support/core_ext/date/conversions.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/date/zones.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/calculations.rb30
-rw-r--r--activesupport/lib/active_support/core_ext/date_and_time/zones.rb41
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/acts_like.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/calculations.rb56
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/conversions.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/date_time/zones.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/exception.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/file/atomic.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/hash.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/hash/conversions.rb194
-rw-r--r--activesupport/lib/active_support/core_ext/hash/deep_merge.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/hash/diff.rb13
-rw-r--r--activesupport/lib/active_support/core_ext/hash/except.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/indifferent_access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/hash/keys.rb10
-rw-r--r--activesupport/lib/active_support/core_ext/hash/slice.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/integer/time.rb3
-rw-r--r--activesupport/lib/active_support/core_ext/kernel/reporting.rb45
-rw-r--r--activesupport/lib/active_support/core_ext/logger.rb84
-rw-r--r--activesupport/lib/active_support/core_ext/marshal.rb21
-rw-r--r--activesupport/lib/active_support/core_ext/module/delegation.rb99
-rw-r--r--activesupport/lib/active_support/core_ext/module/deprecation.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/module/introspection.rb16
-rw-r--r--activesupport/lib/active_support/core_ext/module/method_transplanting.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/object.rb9
-rw-r--r--activesupport/lib/active_support/core_ext/object/acts_like.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/object/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/object/deep_dup.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/inclusion.rb28
-rw-r--r--activesupport/lib/active_support/core_ext/object/instance_variables.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/json.rb218
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_json.rb30
-rw-r--r--activesupport/lib/active_support/core_ext/object/to_param.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/object/try.rb54
-rw-r--r--activesupport/lib/active_support/core_ext/proc.rb17
-rw-r--r--activesupport/lib/active_support/core_ext/range.rb1
-rw-r--r--activesupport/lib/active_support/core_ext/range/each.rb24
-rw-r--r--activesupport/lib/active_support/core_ext/range/include_range.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/access.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/conversions.rb58
-rw-r--r--activesupport/lib/active_support/core_ext/string/encoding.rb8
-rw-r--r--activesupport/lib/active_support/core_ext/string/filters.rb23
-rw-r--r--activesupport/lib/active_support/core_ext/string/indent.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/string/inflections.rb12
-rw-r--r--activesupport/lib/active_support/core_ext/string/multibyte.rb11
-rw-r--r--activesupport/lib/active_support/core_ext/string/output_safety.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/string/xchar.rb18
-rw-r--r--activesupport/lib/active_support/core_ext/string/zones.rb14
-rw-r--r--activesupport/lib/active_support/core_ext/struct.rb6
-rw-r--r--activesupport/lib/active_support/core_ext/thread.rb79
-rw-r--r--activesupport/lib/active_support/core_ext/time/calculations.rb79
-rw-r--r--activesupport/lib/active_support/core_ext/time/conversions.rb4
-rw-r--r--activesupport/lib/active_support/core_ext/time/marshal.rb2
-rw-r--r--activesupport/lib/active_support/core_ext/time/zones.rb22
-rw-r--r--activesupport/lib/active_support/dependencies.rb92
-rw-r--r--activesupport/lib/active_support/dependencies/autoload.rb2
-rw-r--r--activesupport/lib/active_support/deprecation.rb8
-rw-r--r--activesupport/lib/active_support/deprecation/behaviors.rb23
-rw-r--r--activesupport/lib/active_support/deprecation/instance_delegator.rb6
-rw-r--r--activesupport/lib/active_support/deprecation/method_wrappers.rb2
-rw-r--r--activesupport/lib/active_support/deprecation/proxy_wrappers.rb8
-rw-r--r--activesupport/lib/active_support/deprecation/reporting.rb7
-rw-r--r--activesupport/lib/active_support/descendants_tracker.rb53
-rw-r--r--activesupport/lib/active_support/duration.rb16
-rw-r--r--activesupport/lib/active_support/file_update_checker.rb6
-rw-r--r--activesupport/lib/active_support/gzip.rb4
-rw-r--r--activesupport/lib/active_support/hash_with_indifferent_access.rb40
-rw-r--r--activesupport/lib/active_support/i18n.rb5
-rw-r--r--activesupport/lib/active_support/inflections.rb1
-rw-r--r--activesupport/lib/active_support/inflector/inflections.rb46
-rw-r--r--activesupport/lib/active_support/inflector/methods.rb46
-rw-r--r--activesupport/lib/active_support/json/decoding.rb32
-rw-r--r--activesupport/lib/active_support/json/encoding.rb222
-rw-r--r--activesupport/lib/active_support/json/variable.rb17
-rw-r--r--activesupport/lib/active_support/key_generator.rb54
-rw-r--r--activesupport/lib/active_support/lazy_load_hooks.rb2
-rw-r--r--activesupport/lib/active_support/locale/en.yml6
-rw-r--r--activesupport/lib/active_support/log_subscriber.rb55
-rw-r--r--activesupport/lib/active_support/log_subscriber/test_helper.rb4
-rw-r--r--activesupport/lib/active_support/logger.rb4
-rw-r--r--activesupport/lib/active_support/logger_silence.rb24
-rw-r--r--activesupport/lib/active_support/message_encryptor.rb30
-rw-r--r--activesupport/lib/active_support/message_verifier.rb8
-rw-r--r--activesupport/lib/active_support/multibyte.rb2
-rw-r--r--activesupport/lib/active_support/multibyte/chars.rb3
-rw-r--r--activesupport/lib/active_support/multibyte/unicode.rb73
-rw-r--r--activesupport/lib/active_support/notifications.rb45
-rw-r--r--activesupport/lib/active_support/notifications/fanout.rb25
-rw-r--r--activesupport/lib/active_support/notifications/instrumenter.rb30
-rw-r--r--activesupport/lib/active_support/number_helper.rb31
-rw-r--r--activesupport/lib/active_support/ordered_options.rb8
-rw-r--r--activesupport/lib/active_support/per_thread_registry.rb52
-rw-r--r--activesupport/lib/active_support/proxy_object.rb13
-rw-r--r--activesupport/lib/active_support/queueing.rb103
-rw-r--r--activesupport/lib/active_support/rescuable.rb1
-rw-r--r--activesupport/lib/active_support/subscriber.rb116
-rw-r--r--activesupport/lib/active_support/tagged_logging.rb2
-rw-r--r--activesupport/lib/active_support/test_case.rb69
-rw-r--r--activesupport/lib/active_support/testing/assertions.rb44
-rw-r--r--activesupport/lib/active_support/testing/autorun.rb5
-rw-r--r--activesupport/lib/active_support/testing/constant_lookup.rb4
-rw-r--r--activesupport/lib/active_support/testing/declarative.rb43
-rw-r--r--activesupport/lib/active_support/testing/isolation.rb98
-rw-r--r--activesupport/lib/active_support/testing/mocha_module.rb22
-rw-r--r--activesupport/lib/active_support/testing/performance.rb271
-rw-r--r--activesupport/lib/active_support/testing/performance/jruby.rb115
-rw-r--r--activesupport/lib/active_support/testing/performance/rubinius.rb113
-rw-r--r--activesupport/lib/active_support/testing/performance/ruby.rb173
-rw-r--r--activesupport/lib/active_support/testing/setup_and_teardown.rb19
-rw-r--r--activesupport/lib/active_support/testing/tagged_logging.rb25
-rw-r--r--activesupport/lib/active_support/time.rb21
-rw-r--r--activesupport/lib/active_support/time_with_zone.rb55
-rw-r--r--activesupport/lib/active_support/values/time_zone.rb41
-rw-r--r--activesupport/lib/active_support/values/unicode_tables.datbin904408 -> 904483 bytes
-rw-r--r--activesupport/lib/active_support/version.rb13
-rw-r--r--activesupport/lib/active_support/xml_mini.rb38
-rw-r--r--activesupport/lib/active_support/xml_mini/jdom.rb6
-rw-r--r--activesupport/lib/active_support/xml_mini/libxmlsax.rb2
-rw-r--r--activesupport/lib/active_support/xml_mini/nokogirisax.rb2
-rw-r--r--activesupport/test/abstract_unit.rb15
-rw-r--r--activesupport/test/autoloading_fixtures/html/some_class.rb4
-rw-r--r--activesupport/test/autoloading_fixtures/should_not_be_required.rb1
-rw-r--r--activesupport/test/caching_test.rb178
-rw-r--r--activesupport/test/callback_inheritance_test.rb2
-rw-r--r--activesupport/test/callbacks_test.rb345
-rw-r--r--activesupport/test/clean_backtrace_test.rb21
-rw-r--r--activesupport/test/concern_test.rb24
-rw-r--r--activesupport/test/configurable_test.rb2
-rw-r--r--activesupport/test/constantize_test_cases.rb88
-rw-r--r--activesupport/test/core_ext/array_ext_test.rb43
-rw-r--r--activesupport/test/core_ext/bigdecimal_test.rb1
-rw-r--r--activesupport/test/core_ext/class/attribute_accessor_test.rb10
-rw-r--r--activesupport/test/core_ext/class/attribute_test.rb9
-rw-r--r--activesupport/test/core_ext/date_and_time_behavior.rb5
-rw-r--r--activesupport/test/core_ext/date_ext_test.rb18
-rw-r--r--activesupport/test/core_ext/date_time_ext_test.rb56
-rw-r--r--activesupport/test/core_ext/duration_test.rb21
-rw-r--r--activesupport/test/core_ext/enumerable_test.rb6
-rw-r--r--activesupport/test/core_ext/hash_ext_test.rb119
-rw-r--r--activesupport/test/core_ext/kernel_test.rb14
-rw-r--r--activesupport/test/core_ext/marshal_test.rb124
-rw-r--r--activesupport/test/core_ext/module/qualified_const_test.rb19
-rw-r--r--activesupport/test/core_ext/module_test.rb71
-rw-r--r--activesupport/test/core_ext/numeric_ext_test.rb48
-rw-r--r--activesupport/test/core_ext/object/inclusion_test.rb10
-rw-r--r--activesupport/test/core_ext/object/json_test.rb9
-rw-r--r--activesupport/test/core_ext/object/to_query_test.rb2
-rw-r--r--activesupport/test/core_ext/object_and_class_ext_test.rb4
-rw-r--r--activesupport/test/core_ext/proc_test.rb14
-rw-r--r--activesupport/test/core_ext/range_ext_test.rb29
-rw-r--r--activesupport/test/core_ext/string_ext_test.rb255
-rw-r--r--activesupport/test/core_ext/struct_test.rb10
-rw-r--r--activesupport/test/core_ext/thread_test.rb75
-rw-r--r--activesupport/test/core_ext/time_ext_test.rb196
-rw-r--r--activesupport/test/core_ext/time_with_zone_test.rb193
-rw-r--r--activesupport/test/dependencies/raises_exception_without_blame_file.rb5
-rw-r--r--activesupport/test/dependencies_test.rb86
-rw-r--r--activesupport/test/dependencies_test_helpers.rb27
-rw-r--r--activesupport/test/deprecation_test.rb31
-rw-r--r--activesupport/test/descendants_tracker_test_cases.rb20
-rw-r--r--activesupport/test/descendants_tracker_with_autoloading_test.rb16
-rw-r--r--activesupport/test/descendants_tracker_without_autoloading_test.rb10
-rw-r--r--activesupport/test/fixtures/xml/jdom_doctype.dtd1
-rw-r--r--activesupport/test/fixtures/xml/jdom_entities.txt1
-rw-r--r--activesupport/test/fixtures/xml/jdom_include.txt1
-rw-r--r--activesupport/test/gzip_test.rb18
-rw-r--r--activesupport/test/i18n_test.rb2
-rw-r--r--activesupport/test/inflector_test.rb64
-rw-r--r--activesupport/test/inflector_test_cases.rb2
-rw-r--r--activesupport/test/json/decoding_test.rb60
-rw-r--r--activesupport/test/json/encoding_test.rb70
-rw-r--r--activesupport/test/load_paths_test.rb4
-rw-r--r--activesupport/test/logger_test.rb10
-rw-r--r--activesupport/test/message_encryptor_test.rb13
-rw-r--r--activesupport/test/message_verifier_test.rb7
-rw-r--r--activesupport/test/multibyte_chars_test.rb8
-rw-r--r--activesupport/test/notifications/instrumenter_test.rb58
-rw-r--r--activesupport/test/notifications_test.rb26
-rw-r--r--activesupport/test/number_helper_test.rb7
-rw-r--r--activesupport/test/ordered_hash_test.rb9
-rw-r--r--activesupport/test/ordered_options_test.rb20
-rw-r--r--activesupport/test/queueing/synchronous_queue_test.rb27
-rw-r--r--activesupport/test/queueing/test_queue_test.rb146
-rw-r--r--activesupport/test/queueing/threaded_consumer_test.rb92
-rw-r--r--activesupport/test/rescuable_test.rb5
-rw-r--r--activesupport/test/spec_type_test.rb23
-rw-r--r--activesupport/test/string_inquirer_test.rb2
-rw-r--r--activesupport/test/subscriber_test.rb40
-rw-r--r--activesupport/test/test_case_test.rb109
-rw-r--r--activesupport/test/test_test.rb64
-rw-r--r--activesupport/test/testing/constant_lookup_test.rb12
-rw-r--r--activesupport/test/testing/performance_test.rb59
-rw-r--r--activesupport/test/time_zone_test.rb55
-rw-r--r--activesupport/test/transliterate_test.rb8
-rw-r--r--activesupport/test/ts_isolated.rb16
-rw-r--r--activesupport/test/xml_mini/jdom_engine_test.rb47
-rw-r--r--activesupport/test/xml_mini/libxml_engine_test.rb19
-rw-r--r--activesupport/test/xml_mini/libxmlsax_engine_test.rb19
-rw-r--r--activesupport/test/xml_mini/nokogiri_engine_test.rb19
-rw-r--r--activesupport/test/xml_mini/nokogirisax_engine_test.rb25
-rw-r--r--activesupport/test/xml_mini/rexml_engine_test.rb10
-rw-r--r--activesupport/test/xml_mini_test.rb70
-rwxr-xr-xci/travis.rb7
-rw-r--r--guides/.document (renamed from guides/code/getting_started/test/fixtures/.gitkeep)0
-rw-r--r--guides/CHANGELOG.md10
-rw-r--r--guides/Rakefile6
-rw-r--r--guides/assets/images/akshaysurve.jpgbin0 -> 3444 bytes
-rw-r--r--guides/assets/images/challenge.pngbin33373 -> 0 bytes
-rw-r--r--guides/assets/images/customized_error_messages.pngbin2561 -> 0 bytes
-rw-r--r--guides/assets/images/edge_badge.pngbin5964 -> 5695 bytes
-rw-r--r--guides/assets/images/error_messages.pngbin10964 -> 0 bytes
-rw-r--r--guides/assets/images/feature_tile.gifbin43 -> 35 bytes
-rw-r--r--guides/assets/images/footer_tile.gifbin44 -> 36 bytes
-rw-r--r--guides/assets/images/fxn.pngbin20664 -> 15436 bytes
-rw-r--r--guides/assets/images/getting_started/challenge.pngbin0 -> 31976 bytes
-rw-r--r--guides/assets/images/getting_started/forbidden_attributes_for_new_post.pngbin0 -> 19490 bytes
-rw-r--r--guides/assets/images/getting_started/new_post.pngbin5888 -> 5090 bytes
-rw-r--r--guides/assets/images/getting_started/rails_welcome.pngbin0 -> 50433 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_controller.pngbin6268 -> 5519 bytes
-rw-r--r--guides/assets/images/getting_started/routing_error_no_route_matches.pngbin6508 -> 6195 bytes
-rw-r--r--guides/assets/images/getting_started/template_is_missing_posts_new.pngbin5851 -> 11688 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_create_for_posts.pngbin4146 -> 6852 bytes
-rw-r--r--guides/assets/images/getting_started/unknown_action_new_for_posts.pngbin4208 -> 6998 bytes
-rw-r--r--guides/assets/images/header_tile.gifbin44 -> 36 bytes
-rw-r--r--guides/assets/images/icons/README2
-rw-r--r--guides/assets/images/icons/callouts/11.pngbin290 -> 176 bytes
-rw-r--r--guides/assets/images/icons/callouts/12.pngbin322 -> 186 bytes
-rw-r--r--guides/assets/images/icons/callouts/13.pngbin328 -> 188 bytes
-rw-r--r--guides/assets/images/icons/callouts/15.pngbin340 -> 191 bytes
-rw-r--r--guides/assets/images/icons/caution.pngbin2300 -> 2295 bytes
-rw-r--r--guides/assets/images/icons/example.pngbin2079 -> 2078 bytes
-rw-r--r--guides/assets/images/jaimeiniesta.jpgbin11913 -> 0 bytes
-rw-r--r--guides/assets/images/radar.pngbin19521 -> 17095 bytes
-rw-r--r--guides/assets/images/rails4_features.pngbin0 -> 67766 bytes
-rw-r--r--guides/assets/images/rails_guides_kindle_cover.jpgbin31785 -> 20955 bytes
-rw-r--r--guides/assets/images/rails_welcome.pngbin71979 -> 0 bytes
-rw-r--r--guides/assets/images/validation_error_messages.pngbin583 -> 0 bytes
-rw-r--r--guides/assets/images/vijaydev.jpgbin4610 -> 2897 bytes
-rw-r--r--guides/assets/javascripts/guides.js64
-rw-r--r--guides/assets/stylesheets/main.css113
-rw-r--r--guides/assets/stylesheets/print.css2
-rw-r--r--guides/bug_report_templates/action_controller_gem.rb42
-rw-r--r--guides/bug_report_templates/action_controller_master.rb51
-rw-r--r--guides/bug_report_templates/active_record_gem.rb37
-rw-r--r--guides/bug_report_templates/active_record_master.rb48
-rw-r--r--guides/code/getting_started/.gitignore16
-rw-r--r--guides/code/getting_started/Gemfile41
-rw-r--r--guides/code/getting_started/Gemfile.lock120
-rw-r--r--guides/code/getting_started/README.rdoc261
-rw-r--r--guides/code/getting_started/Rakefile1
-rw-r--r--guides/code/getting_started/app/assets/images/rails.pngbin6646 -> 0 bytes
-rw-r--r--guides/code/getting_started/app/assets/javascripts/application.js4
-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.css2
-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.rb4
-rw-r--r--guides/code/getting_started/app/controllers/comments_controller.rb10
-rw-r--r--guides/code/getting_started/app/controllers/concerns/.keep (renamed from guides/code/getting_started/test/functional/.gitkeep)0
-rw-r--r--guides/code/getting_started/app/controllers/posts_controller.rb38
-rw-r--r--guides/code/getting_started/app/mailers/.keep (renamed from guides/code/getting_started/test/integration/.gitkeep)0
-rw-r--r--guides/code/getting_started/app/models/.keep (renamed from guides/code/getting_started/test/unit/.gitkeep)0
-rw-r--r--guides/code/getting_started/app/models/concerns/.keep (renamed from guides/code/getting_started/vendor/plugins/.gitkeep)0
-rw-r--r--guides/code/getting_started/app/models/post.rb9
-rw-r--r--guides/code/getting_started/app/views/comments/_comment.html.erb6
-rw-r--r--guides/code/getting_started/app/views/layouts/application.html.erb4
-rw-r--r--guides/code/getting_started/app/views/posts/_form.html.erb22
-rw-r--r--guides/code/getting_started/app/views/posts/edit.html.erb8
-rw-r--r--guides/code/getting_started/app/views/posts/index.html.erb14
-rw-r--r--guides/code/getting_started/app/views/posts/new.html.erb6
-rw-r--r--guides/code/getting_started/app/views/posts/show.html.erb6
-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/application.rb43
-rw-r--r--guides/code/getting_started/config/boot.rb2
-rw-r--r--guides/code/getting_started/config/environment.rb4
-rw-r--r--guides/code/getting_started/config/environments/development.rb18
-rw-r--r--guides/code/getting_started/config/environments/production.rb41
-rw-r--r--guides/code/getting_started/config/environments/test.rb12
-rw-r--r--guides/code/getting_started/config/initializers/filter_parameter_logging.rb4
-rw-r--r--guides/code/getting_started/config/initializers/inflections.rb11
-rw-r--r--guides/code/getting_started/config/initializers/locale.rb9
-rw-r--r--guides/code/getting_started/config/initializers/secret_token.rb7
-rw-r--r--guides/code/getting_started/config/initializers/wrap_parameters.rb12
-rw-r--r--guides/code/getting_started/config/locales/en.yml22
-rw-r--r--guides/code/getting_started/config/routes.rb58
-rw-r--r--guides/code/getting_started/db/migrate/20110901012815_create_comments.rb12
-rw-r--r--guides/code/getting_started/db/migrate/20130122042648_create_posts.rb (renamed from guides/code/getting_started/db/migrate/20120420083127_create_posts.rb)0
-rw-r--r--guides/code/getting_started/db/migrate/20130122045842_create_comments.rb11
-rw-r--r--guides/code/getting_started/db/schema.rb27
-rw-r--r--guides/code/getting_started/doc/README_FOR_APP2
-rw-r--r--guides/code/getting_started/lib/assets/.keep (renamed from railties/lib/rails/generators/rails/app/templates/app/mailers/.empty_directory)0
-rw-r--r--guides/code/getting_started/lib/tasks/.keep (renamed from railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory)0
-rw-r--r--guides/code/getting_started/log/.keep (renamed from railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory)0
-rw-r--r--guides/code/getting_started/public/404.html54
-rw-r--r--guides/code/getting_started/public/422.html54
-rw-r--r--guides/code/getting_started/public/500.html54
-rwxr-xr-xguides/code/getting_started/script/rails6
-rw-r--r--guides/code/getting_started/test/controllers/.keep (renamed from railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/controllers/comments_controller_test.rb (renamed from guides/code/getting_started/test/functional/comments_controller_test.rb)0
-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/.keep (renamed from railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/fixtures/comments.yml6
-rw-r--r--guides/code/getting_started/test/fixtures/posts.yml2
-rw-r--r--guides/code/getting_started/test/functional/posts_controller_test.rb49
-rw-r--r--guides/code/getting_started/test/functional/welcome_controller_test.rb8
-rw-r--r--guides/code/getting_started/test/helpers/.keep (renamed from railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/helpers/comments_helper_test.rb (renamed from guides/code/getting_started/test/unit/helpers/comments_helper_test.rb)0
-rw-r--r--guides/code/getting_started/test/helpers/posts_helper_test.rb (renamed from guides/code/getting_started/test/unit/helpers/posts_helper_test.rb)0
-rw-r--r--guides/code/getting_started/test/helpers/welcome_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/integration/.keep (renamed from railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/mailers/.keep (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/models/.keep (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory)0
-rw-r--r--guides/code/getting_started/test/models/comment_test.rb (renamed from guides/code/getting_started/test/unit/comment_test.rb)0
-rw-r--r--guides/code/getting_started/test/models/post_test.rb (renamed from guides/code/getting_started/test/unit/post_test.rb)0
-rw-r--r--guides/code/getting_started/test/performance/browsing_test.rb12
-rw-r--r--guides/code/getting_started/test/test_helper.rb2
-rw-r--r--guides/code/getting_started/test/unit/helpers/home_helper_test.rb4
-rw-r--r--guides/code/getting_started/test/unit/tag_test.rb7
-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.rb23
-rw-r--r--guides/rails_guides/generator.rb10
-rw-r--r--guides/rails_guides/helpers.rb2
-rw-r--r--guides/rails_guides/kindle.rb119
-rw-r--r--guides/rails_guides/markdown.rb4
-rw-r--r--guides/rails_guides/markdown/renderer.rb4
-rw-r--r--guides/source/2_2_release_notes.md32
-rw-r--r--guides/source/2_3_release_notes.md18
-rw-r--r--guides/source/3_0_release_notes.md11
-rw-r--r--guides/source/3_1_release_notes.md6
-rw-r--r--guides/source/3_2_release_notes.md26
-rw-r--r--guides/source/4_0_release_notes.md906
-rw-r--r--guides/source/_welcome.html.erb7
-rw-r--r--guides/source/action_controller_overview.md453
-rw-r--r--guides/source/action_mailer_basics.md475
-rw-r--r--guides/source/action_view_overview.md424
-rw-r--r--guides/source/active_model_basics.md112
-rw-r--r--guides/source/active_record_basics.md295
-rw-r--r--guides/source/active_record_callbacks.md361
-rw-r--r--guides/source/active_record_querying.md373
-rw-r--r--guides/source/active_record_validations.md1167
-rw-r--r--guides/source/active_record_validations_callbacks.md1365
-rw-r--r--guides/source/active_support_core_extensions.md501
-rw-r--r--guides/source/active_support_instrumentation.md150
-rw-r--r--guides/source/ajax_on_rails.md316
-rw-r--r--guides/source/api_documentation_guidelines.md36
-rw-r--r--guides/source/asset_pipeline.md894
-rw-r--r--guides/source/association_basics.md459
-rw-r--r--guides/source/caching_with_rails.md259
-rw-r--r--guides/source/command_line.md131
-rw-r--r--guides/source/configuring.md227
-rw-r--r--guides/source/contributing_to_ruby_on_rails.md207
-rw-r--r--guides/source/credits.html.erb10
-rw-r--r--guides/source/debugging_rails_applications.md108
-rw-r--r--guides/source/development_dependencies_install.md124
-rw-r--r--guides/source/documents.yaml91
-rw-r--r--guides/source/engines.md190
-rw-r--r--guides/source/form_helpers.md185
-rw-r--r--guides/source/generators.md147
-rw-r--r--guides/source/getting_started.md971
-rw-r--r--guides/source/i18n.md203
-rw-r--r--guides/source/index.html.erb7
-rw-r--r--guides/source/initialization.md279
-rw-r--r--guides/source/kindle/KINDLE.md2
-rw-r--r--guides/source/kindle/rails_guides.opf.erb2
-rw-r--r--guides/source/kindle/toc.html.erb2
-rw-r--r--guides/source/layout.html.erb18
-rw-r--r--guides/source/layouts_and_rendering.md381
-rw-r--r--guides/source/maintenance_policy.md56
-rw-r--r--guides/source/migrations.md934
-rw-r--r--guides/source/nested_model_forms.md12
-rw-r--r--guides/source/performance_testing.md684
-rw-r--r--guides/source/plugins.md53
-rw-r--r--guides/source/rails_application_templates.md88
-rw-r--r--guides/source/rails_on_rack.md62
-rw-r--r--guides/source/routing.md526
-rw-r--r--guides/source/ruby_on_rails_guides_guidelines.md13
-rw-r--r--guides/source/security.md239
-rw-r--r--guides/source/testing.md419
-rw-r--r--guides/source/upgrading_ruby_on_rails.md321
-rw-r--r--guides/source/working_with_javascript_in_rails.md395
-rw-r--r--install.rb9
-rw-r--r--rails.gemspec28
-rw-r--r--railties/CHANGELOG.md166
-rw-r--r--railties/MIT-LICENSE2
-rw-r--r--railties/RDOC_MAIN.rdoc73
-rw-r--r--railties/Rakefile24
-rwxr-xr-xrailties/bin/rails4
-rw-r--r--railties/lib/rails.rb63
-rw-r--r--railties/lib/rails/all.rb3
-rw-r--r--railties/lib/rails/api/task.rb163
-rw-r--r--railties/lib/rails/app_rails_loader.rb61
-rw-r--r--railties/lib/rails/application.rb254
-rw-r--r--railties/lib/rails/application/bootstrap.rb3
-rw-r--r--railties/lib/rails/application/configuration.rb56
-rw-r--r--railties/lib/rails/application/default_middleware_stack.rb99
-rw-r--r--railties/lib/rails/application/finisher.rb51
-rw-r--r--railties/lib/rails/cli.rb7
-rw-r--r--railties/lib/rails/code_statistics.rb76
-rw-r--r--railties/lib/rails/code_statistics_calculator.rb79
-rw-r--r--railties/lib/rails/commands.rb105
-rw-r--r--railties/lib/rails/commands/application.rb24
-rw-r--r--railties/lib/rails/commands/benchmarker.rb34
-rw-r--r--railties/lib/rails/commands/commands_tasks.rb174
-rw-r--r--railties/lib/rails/commands/console.rb30
-rw-r--r--railties/lib/rails/commands/dbconsole.rb34
-rw-r--r--railties/lib/rails/commands/plugin.rb9
-rw-r--r--railties/lib/rails/commands/plugin_new.rb9
-rw-r--r--railties/lib/rails/commands/profiler.rb32
-rw-r--r--railties/lib/rails/commands/runner.rb8
-rw-r--r--railties/lib/rails/commands/server.rb20
-rw-r--r--railties/lib/rails/configuration.rb9
-rw-r--r--railties/lib/rails/console/helpers.rb6
-rw-r--r--railties/lib/rails/engine.rb42
-rw-r--r--railties/lib/rails/engine/commands.rb2
-rw-r--r--railties/lib/rails/engine/configuration.rb9
-rw-r--r--railties/lib/rails/engine/railties.rb21
-rw-r--r--railties/lib/rails/generators.rb10
-rw-r--r--railties/lib/rails/generators/actions.rb12
-rw-r--r--railties/lib/rails/generators/active_model.rb4
-rw-r--r--railties/lib/rails/generators/app_base.rb217
-rw-r--r--railties/lib/rails/generators/base.rb39
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb16
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/index.html.erb26
-rw-r--r--railties/lib/rails/generators/erb/scaffold/templates/show.html.erb3
-rw-r--r--railties/lib/rails/generators/generated_attribute.rb14
-rw-r--r--railties/lib/rails/generators/migration.rb9
-rw-r--r--railties/lib/rails/generators/named_base.rb16
-rw-r--r--railties/lib/rails/generators/rails/app/app_generator.rb135
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Gemfile41
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README259
-rw-r--r--railties/lib/rails/generators/rails/app/templates/README.rdoc28
-rw-r--r--railties/lib/rails/generators/rails/app/templates/Rakefile2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.pngbin6646 -> 0 bytes
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css8
-rw-r--r--railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/bundle2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rails3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/bin/rake3
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config.ru2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/application.rb33
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/boot.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml57
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environment.rb6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/development.rb.tt16
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt28
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/locale.rb7
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt4
-rw-r--r--railties/lib/rails/generators/rails/app/templates/config/routes.rb13
-rw-r--r--railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP2
-rw-r--r--railties/lib/rails/generators/rails/app/templates/gitignore6
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/404.html51
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/422.html52
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/500.html51
-rw-r--r--railties/lib/rails/generators/rails/app/templates/public/index.html241
-rw-r--r--railties/lib/rails/generators/rails/app/templates/script/rails5
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb12
-rw-r--r--railties/lib/rails/generators/rails/app/templates/test/test_helper.rb2
-rw-r--r--railties/lib/rails/generators/rails/controller/USAGE3
-rw-r--r--railties/lib/rails/generators/rails/controller/controller_generator.rb36
-rw-r--r--railties/lib/rails/generators/rails/generator/USAGE1
-rw-r--r--railties/lib/rails/generators/rails/generator/generator_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/migration/USAGE18
-rw-r--r--railties/lib/rails/generators/rails/model/USAGE42
-rw-r--r--railties/lib/rails/generators/rails/observer/USAGE12
-rw-r--r--railties/lib/rails/generators/rails/observer/observer_generator.rb7
-rw-r--r--railties/lib/rails/generators/rails/performance_test/USAGE10
-rw-r--r--railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb7
-rw-r--r--railties/lib/rails/generators/rails/plugin/USAGE (renamed from railties/lib/rails/generators/rails/plugin_new/USAGE)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/plugin_generator.rb371
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec27
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Gemfile43
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE (renamed from railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/README.rdoc (renamed from railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/Rakefile25
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt (renamed from railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt7
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/config/routes.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/gitignore (renamed from railties/lib/rails/generators/rails/plugin_new/templates/gitignore)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb6
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake (renamed from railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/application.rb18
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb5
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js13
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css15
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb (renamed from railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb)0
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb358
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec29
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/Gemfile38
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/Rakefile25
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb6
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb17
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb9
-rw-r--r--railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt7
-rw-r--r--railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb13
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb2
-rw-r--r--railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb71
-rw-r--r--railties/lib/rails/generators/resource_helpers.rb25
-rw-r--r--railties/lib/rails/generators/test_case.rb217
-rw-r--r--railties/lib/rails/generators/test_unit/generator/generator_generator.rb26
-rw-r--r--railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb16
-rw-r--r--railties/lib/rails/generators/test_unit/model/model_generator.rb12
-rw-r--r--railties/lib/rails/generators/test_unit/model/templates/fixtures.yml20
-rw-r--r--railties/lib/rails/generators/test_unit/observer/observer_generator.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb9
-rw-r--r--railties/lib/rails/generators/test_unit/performance/performance_generator.rb13
-rw-r--r--railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb12
-rw-r--r--railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb2
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb17
-rw-r--r--railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb2
-rw-r--r--railties/lib/rails/generators/testing/assertions.rb121
-rw-r--r--railties/lib/rails/generators/testing/behaviour.rb106
-rw-r--r--railties/lib/rails/generators/testing/setup_and_teardown.rb18
-rw-r--r--railties/lib/rails/info.rb14
-rw-r--r--railties/lib/rails/info_controller.rb12
-rw-r--r--railties/lib/rails/paths.rb45
-rw-r--r--railties/lib/rails/performance_test_help.rb3
-rw-r--r--railties/lib/rails/rack/debugger.rb4
-rw-r--r--railties/lib/rails/rack/log_tailer.rb2
-rw-r--r--railties/lib/rails/rack/logger.rb42
-rw-r--r--railties/lib/rails/railtie.rb42
-rw-r--r--railties/lib/rails/railtie/configuration.rb2
-rw-r--r--railties/lib/rails/ruby_version_check.rb4
-rw-r--r--railties/lib/rails/script_rails_loader.rb29
-rw-r--r--railties/lib/rails/source_annotation_extractor.rb51
-rw-r--r--railties/lib/rails/tasks.rb4
-rw-r--r--railties/lib/rails/tasks/documentation.rake153
-rw-r--r--railties/lib/rails/tasks/engine.rake2
-rw-r--r--railties/lib/rails/tasks/framework.rake24
-rw-r--r--railties/lib/rails/tasks/log.rake22
-rw-r--r--railties/lib/rails/tasks/routes.rake4
-rw-r--r--railties/lib/rails/templates/layouts/application.html.erb4
-rw-r--r--railties/lib/rails/templates/rails/info/routes.html.erb2
-rw-r--r--railties/lib/rails/templates/rails/welcome/index.html.erb245
-rw-r--r--railties/lib/rails/test_help.rb13
-rw-r--r--railties/lib/rails/test_unit/railtie.rb5
-rw-r--r--railties/lib/rails/test_unit/sub_test_task.rb120
-rw-r--r--railties/lib/rails/test_unit/testing.rake147
-rw-r--r--railties/lib/rails/version.rb6
-rw-r--r--railties/lib/rails/welcome_controller.rb7
-rw-r--r--railties/railties.gemspec32
-rw-r--r--railties/test/abstract_unit.rb5
-rw-r--r--railties/test/app_rails_loader_test.rb68
-rw-r--r--railties/test/application/asset_debugging_test.rb4
-rw-r--r--railties/test/application/assets_test.rb260
-rw-r--r--railties/test/application/configuration_test.rb143
-rw-r--r--railties/test/application/console_test.rb78
-rw-r--r--railties/test/application/generators_test.rb2
-rw-r--r--railties/test/application/initializers/boot_test.rb20
-rw-r--r--railties/test/application/initializers/frameworks_test.rb30
-rw-r--r--railties/test/application/initializers/i18n_test.rb6
-rw-r--r--railties/test/application/initializers/load_path_test.rb9
-rw-r--r--railties/test/application/loading_test.rb59
-rw-r--r--railties/test/application/middleware/best_practices_test.rb30
-rw-r--r--railties/test/application/middleware/cache_test.rb28
-rw-r--r--railties/test/application/middleware/cookies_test.rb2
-rw-r--r--railties/test/application/middleware/remote_ip_test.rb15
-rw-r--r--railties/test/application/middleware/session_test.rb215
-rw-r--r--railties/test/application/middleware/static_test.rb31
-rw-r--r--railties/test/application/middleware_test.rb48
-rw-r--r--railties/test/application/multiple_applications_test.rb148
-rw-r--r--railties/test/application/queue_test.rb153
-rw-r--r--railties/test/application/rake/dbs_test.rb42
-rw-r--r--railties/test/application/rake/migrations_test.rb4
-rw-r--r--railties/test/application/rake/notes_test.rb53
-rw-r--r--railties/test/application/rake_test.rb137
-rw-r--r--railties/test/application/rendering_test.rb45
-rw-r--r--railties/test/application/routing_test.rb196
-rw-r--r--railties/test/application/runner_test.rb30
-rw-r--r--railties/test/application/test_runner_test.rb320
-rw-r--r--railties/test/application/test_test.rb25
-rw-r--r--railties/test/application/url_generation_test.rb6
-rw-r--r--railties/test/code_statistics_calculator_test.rb288
-rw-r--r--railties/test/commands/console_test.rb55
-rw-r--r--railties/test/commands/dbconsole_test.rb24
-rw-r--r--railties/test/commands/server_test.rb62
-rw-r--r--railties/test/configuration/middleware_stack_proxy_test.rb3
-rw-r--r--railties/test/engine_test.rb2
-rw-r--r--railties/test/env_helpers.rb30
-rw-r--r--railties/test/fixtures/lib/app_builders/empty_builder.rb2
-rw-r--r--railties/test/fixtures/lib/app_builders/simple_builder.rb7
-rw-r--r--railties/test/fixtures/lib/app_builders/tweak_builder.rb7
-rw-r--r--railties/test/fixtures/lib/plugin_builders/empty_builder.rb2
-rw-r--r--railties/test/fixtures/lib/plugin_builders/simple_builder.rb7
-rw-r--r--railties/test/fixtures/lib/plugin_builders/spec_builder.rb19
-rw-r--r--railties/test/fixtures/lib/plugin_builders/tweak_builder.rb7
-rw-r--r--railties/test/generators/actions_test.rb39
-rw-r--r--railties/test/generators/app_generator_test.rb139
-rw-r--r--railties/test/generators/argv_scrubber_test.rb136
-rw-r--r--railties/test/generators/controller_generator_test.rb5
-rw-r--r--railties/test/generators/generated_attribute_test.rb11
-rw-r--r--railties/test/generators/generator_generator_test.rb12
-rw-r--r--railties/test/generators/generator_test.rb85
-rw-r--r--railties/test/generators/generators_test_helper.rb1
-rw-r--r--railties/test/generators/migration_generator_test.rb124
-rw-r--r--railties/test/generators/model_generator_test.rb35
-rw-r--r--railties/test/generators/named_base_test.rb19
-rw-r--r--railties/test/generators/namespaced_generators_test.rb60
-rw-r--r--railties/test/generators/observer_generator_test.rb27
-rw-r--r--railties/test/generators/performance_test_generator_test.rb12
-rw-r--r--railties/test/generators/plugin_generator_test.rb350
-rw-r--r--railties/test/generators/plugin_new_generator_test.rb402
-rw-r--r--railties/test/generators/scaffold_controller_generator_test.rb74
-rw-r--r--railties/test/generators/scaffold_generator_test.rb147
-rw-r--r--railties/test/generators/shared_generator_tests.rb75
-rw-r--r--railties/test/generators/task_generator_test.rb2
-rw-r--r--railties/test/generators_test.rb8
-rw-r--r--railties/test/initializable_test.rb10
-rw-r--r--railties/test/isolation/abstract_unit.rb19
-rw-r--r--railties/test/paths_test.rb4
-rw-r--r--railties/test/rack_logger_test.rb75
-rw-r--r--railties/test/rails_info_controller_test.rb4
-rw-r--r--railties/test/rails_info_test.rb7
-rw-r--r--railties/test/railties/engine_test.rb33
-rw-r--r--railties/test/railties/generators_test.rb2
-rw-r--r--railties/test/railties/mounted_engine_test.rb56
-rw-r--r--railties/test/railties/railtie_test.rb27
-rw-r--r--railties/test/script_rails_loader_test.rb22
-rw-r--r--railties/test/test_info_test.rb60
-rw-r--r--tasks/release.rb27
-rwxr-xr-xtools/profile23
-rw-r--r--version.rb6
2055 files changed, 86876 insertions, 66440 deletions
diff --git a/.gitignore b/.gitignore
index 854fdbf450..bc96284375 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,13 +1,12 @@
# Don't put *.swp, *.bak, etc here; those belong in a global ~/.gitignore.
-# Check out http://help.github.com/ignore-files/ for how to set that up.
+# Check out https://help.github.com/articles/ignoring-files for how to set that up.
debug.log
.Gemfile
/.bundle
-/.rbenv-version
-/.rvmrc
+/.ruby-version
/Gemfile.lock
-/pkg
+pkg
/dist
/doc/rdoc
/*/doc
@@ -21,4 +20,3 @@ debug.log
/railties/doc
/railties/tmp
/guides/output
-/RDOC_MAIN.rdoc
diff --git a/.travis.yml b/.travis.yml
index c6f868498d..a2f47e495c 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,15 +1,23 @@
script: 'ci/travis.rb'
before_install:
- - gem install bundler
+ - travis_retry gem install bundler
+ - "rvm current | grep 'jruby' && export AR_JDBC=true || echo"
rvm:
- 1.9.3
+ - 2.0.0
+ - jruby-head
+ - rbx-2.1.1
env:
- "GEM=railties"
- - "GEM=ap,am,amo,as"
+ - "GEM=ap,am,amo,as,av"
- "GEM=ar:mysql"
- "GEM=ar:mysql2"
- "GEM=ar:sqlite3"
- "GEM=ar:postgresql"
+matrix:
+ allow_failures:
+ - rvm: jruby-head
+ - rvm: rbx-2.1.1
notifications:
email: false
irc:
@@ -21,5 +29,5 @@ notifications:
on_success: change
on_failure: always
rooms:
- - secure: "CGWvthGkBKNnTnk9YSmf9AXKoiRI33fCl5D3jU4nx3cOPu6kv2R9nMjt9EAo\nOuS4Q85qNSf4VNQ2cUPNiNYSWQ+XiTfivKvDUw/QW9r1FejYyeWarMsSBWA+\n0fADjF1M2dkDIVLgYPfwoXEv7l+j654F1KLKB69F0F/netwP9CQ="
-bundler_args: --path vendor/bundle
+ - secure: "YA1alef1ESHWGFNVwvmVGCkMe4cUy4j+UcNvMUESraceiAfVyRMAovlQBGs6\n9kBRm7DHYBUXYC2ABQoJbQRLDr/1B5JPf/M8+Qd7BKu8tcDC03U01SMHFLpO\naOs/HLXcDxtnnpL07tGVsm0zhMc5N8tq4/L3SHxK7Vi+TacwQzI="
+bundler_args: --path vendor/bundle --without test
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index adc5eb6914..19b7b638b6 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -1,12 +1,16 @@
Ruby on Rails is a volunteer effort. We encourage you to pitch in. [Join the team](http://contributors.rubyonrails.org)!
-Please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide before submitting any code.
+* If you want to submit a bug report please make sure to follow our [reporting guidelines](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#reporting-an-issue).
+
+* If you want to submit a patch, please read the [Contributing to Ruby on Rails](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) guide.
+
+* If you want to contribute to Rails documentation, please read the [Contributing to the Rails Documentation](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation) section of the aforementioned guide.
*We only accept bug reports and pull requests in GitHub*.
-If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk).
+* If you have a question about how to use Ruby on Rails, please [ask the rubyonrails-talk mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-talk).
-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.
+* 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 />
Rails Team
diff --git a/Gemfile b/Gemfile
index 9afbac6e02..e82b4c2b27 100644
--- a/Gemfile
+++ b/Gemfile
@@ -2,34 +2,27 @@ source 'https://rubygems.org'
gemspec
-gem 'arel', github: 'rails/arel', branch: 'master'
+# This needs to be with require false as it is
+# loaded after loading the test library to
+# ensure correct loading order
+gem 'mocha', '~> 0.14', require: false
-gem 'mocha', '>= 0.11.2', :require => false
-gem 'rack-test', github: 'brynary/rack-test'
gem 'rack-cache', '~> 1.2'
-gem 'bcrypt-ruby', '~> 3.0.0'
-gem 'jquery-rails', '~> 2.1.4', github: 'rails/jquery-rails'
+gem 'bcrypt-ruby', '~> 3.1.2'
+gem 'jquery-rails', '~> 2.2.0'
gem 'turbolinks'
-gem 'coffee-rails', github: 'rails/coffee-rails'
-
-gem 'journey', github: 'rails/journey', branch: 'master'
-
-gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders', branch: 'master'
+gem 'coffee-rails', '~> 4.0.0'
+gem 'arel', github: 'rails/arel'
# This needs to be with require false to avoid
# it being automatically loaded by sprockets
-gem 'uglifier', require: false
-
-gem 'sprockets-rails', github: 'rails/sprockets-rails', branch: 'master'
+gem 'uglifier', '>= 1.3.0', require: false
group :doc do
- # The current sdoc cannot generate GitHub links due
- # to a bug, but the PR that fixes it has been there
- # for some weeks unapplied. As a temporary solution
- # this is our own fork with the fix.
- gem 'sdoc', github: 'fxn/sdoc'
- gem 'redcarpet', '~> 2.1.1'
+ gem 'sdoc'
+ gem 'redcarpet', '~> 2.2.2', platforms: :ruby
gem 'w3c_validators'
+ gem 'kindlerb'
end
# AS
@@ -37,45 +30,61 @@ gem 'dalli', '>= 2.2.1'
# Add your own local bundler stuff
local_gemfile = File.dirname(__FILE__) + "/.Gemfile"
-instance_eval File.read local_gemfile if File.exists? local_gemfile
+instance_eval File.read local_gemfile if File.exist? local_gemfile
-platforms :mri do
- group :test do
+group :test do
+ platforms :mri_19 do
gem 'ruby-prof', '~> 0.11.2'
- gem 'debugger' if !ENV['TRAVIS'] && RUBY_VERSION < "2.0" && RUBY_PATCHLEVEL < 286
end
+
+ platforms :mri_19, :mri_20 do
+ gem 'debugger'
+ end
+
+ gem 'benchmark-ips'
end
platforms :ruby do
- gem 'json'
gem 'yajl-ruby'
gem 'nokogiri', '>= 1.4.5'
+ # Needed for compiling the ActionDispatch::Journey parser
+ gem 'racc', '>=1.4.6', require: false
+
# AR
gem 'sqlite3', '~> 1.3.6'
group :db do
gem 'pg', '>= 0.11.0'
- gem 'mysql', '>= 2.8.1' if RUBY_VERSION < '2.0.0'
- gem 'mysql2', '>= 0.3.10'
+ gem 'mysql', '>= 2.9.0'
+ gem 'mysql2', '>= 0.3.13'
end
end
platforms :jruby do
gem 'json'
- gem 'activerecord-jdbcsqlite3-adapter', '>= 1.2.0'
-
- # This is needed by now to let tests work on JRuby
- # TODO: When the JRuby guys merge jruby-openssl in
- # jruby this will be removed
- gem 'jruby-openssl'
-
- group :db do
- gem 'activerecord-jdbcmysql-adapter', '>= 1.2.0'
- gem 'activerecord-jdbcpostgresql-adapter', '>= 1.2.0'
+ if ENV['AR_JDBC']
+ gem 'activerecord-jdbcsqlite3-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ group :db do
+ gem 'activerecord-jdbcmysql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ gem 'activerecord-jdbcpostgresql-adapter', github: 'jruby/activerecord-jdbc-adapter', branch: 'master'
+ end
+ else
+ gem 'activerecord-jdbcsqlite3-adapter', '>= 1.3.0'
+ group :db do
+ gem 'activerecord-jdbcmysql-adapter', '>= 1.3.0'
+ gem 'activerecord-jdbcpostgresql-adapter', '>= 1.3.0'
+ end
end
end
+platforms :rbx do
+ gem 'psych'
+ gem 'rubysl-mathn'
+ gem 'rubysl-matrix'
+ gem 'rubysl-rexml'
+end
+
# gems that are necessary for ActiveRecord tests with Oracle database
if ENV['ORACLE_ENHANCED']
platforms :ruby do
@@ -86,5 +95,3 @@ end
# A gem necessary for ActiveRecord tests with IBM DB
gem 'ibm_db' if ENV['IBM_DB']
-
-gem 'benchmark-ips'
diff --git a/RAILS_VERSION b/RAILS_VERSION
index e1e048d8f0..14b6456d31 100644
--- a/RAILS_VERSION
+++ b/RAILS_VERSION
@@ -1 +1 @@
-4.0.0.beta
+4.1.0.beta
diff --git a/README.md b/README.md
new file mode 100644
index 0000000000..5a68a166be
--- /dev/null
+++ b/README.md
@@ -0,0 +1,83 @@
+## 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.
+
+Understanding the MVC pattern is key to understanding Rails. MVC divides your
+application into three layers, each with a specific responsibility.
+
+The _Model layer_ represents your domain model (such as Account, Product,
+Person, Post, etc.) and encapsulates the business logic that is specific to
+your application. In Rails, database-backed model classes are derived from
+`ActiveRecord::Base`. Active Record allows you to present the data from
+database rows as objects and embellish these data objects with business logic
+methods. Although most Rails models are backed by a database, models can also
+be ordinary Ruby classes, or Ruby classes that implement a set of interfaces
+as provided by the Active Model module. You can read more about Active Record
+in its [README](activerecord/README.rdoc).
+
+The _Controller layer_ is responsible for handling incoming HTTP requests and
+providing a suitable response. Usually this means returning HTML, but Rails controllers
+can also generate XML, JSON, PDFs, mobile-specific views, and more. Controllers load and
+manipulate models, and render view templates in order to generate the appropriate HTTP response.
+In Rails, incoming requests are routed by Action Dispatch to an appropriate controller, and
+controller classes are derived from `ActionController::Base`. Action Dispatch and Action Controller
+are bundled together in Action Pack. You can read more about Action Pack in its
+[README](actionpack/README.rdoc).
+
+The _View layer_ is composed of "templates" that are responsible for providing
+appropriate representations of your application's resources. Templates can
+come in a variety of formats, but most view templates are HTML with embedded
+Ruby code (ERB files). Views are typically rendered to generate a controller response,
+or to generate the body of an email. In Rails, View generation is handled by Action View.
+You can read more about Action View in its [README](actionview/README.rdoc).
+
+Active Record, Action Pack, and Action View can each be used independently outside Rails.
+In addition to them, Rails also comes with Action Mailer ([README](actionmailer/README.rdoc)), a library
+to generate and send emails; and Active Support ([README](activesupport/README.rdoc)), a collection of
+utility classes and standard library extensions that are useful for Rails, and may also be used
+independently outside Rails.
+
+## Getting Started
+
+1. Install Rails at the command prompt if you haven't yet:
+
+ gem install rails
+
+2. At the command prompt, create a new Rails application:
+
+ rails new myapp
+
+ where "myapp" is the application name.
+
+3. Change directory to `myapp` and start the web server:
+
+ cd myapp
+ rails server
+
+ Run with `--help` or `-h` for options.
+
+4. Using a browser, go to http://localhost:3000 and you'll see: "Welcome aboard: You're riding Ruby on Rails!"
+
+5. Follow the guidelines to start developing your application. You may find
+ the following resources handy:
+ * [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)
+
+## Contributing
+
+We encourage you to contribute to Ruby on Rails! Please check out the
+[Contributing to Ruby on Rails guide](http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html) for guidelines about how to proceed. [Join us!](http://contributors.rubyonrails.org)
+
+## Code Status
+
+* [![Build Status](https://api.travis-ci.org/rails/rails.png)](https://travis-ci.org/rails/rails)
+* [![Dependencies](https://gemnasium.com/rails/rails.png?travis)](https://gemnasium.com/rails/rails)
+
+## License
+
+Ruby on Rails is released under the [MIT License](http://www.opensource.org/licenses/MIT).
diff --git a/README.rdoc b/README.rdoc
deleted file mode 100644
index c9fb595efb..0000000000
--- a/README.rdoc
+++ /dev/null
@@ -1,78 +0,0 @@
-== 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.
-
-Understanding the MVC pattern is key to understanding Rails. MVC divides your application
-into three layers, each with a specific responsibility.
-
-The View layer is composed of "templates" that are responsible for providing
-appropriate representations of your application's resources. Templates
-can come in a variety of formats, but most view templates are \HTML with embedded Ruby
-code (.erb files).
-
-The Model layer represents your domain model (such as Account, Product, Person, Post)
-and encapsulates the business logic that is specific to your application. In Rails,
-database-backed model classes are derived from ActiveRecord::Base. Active Record allows
-you to present the data from database rows as objects and embellish these data objects
-with business logic methods. Although most Rails models are backed by a database, models
-can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as
-provided by the ActiveModel module. You can read more about Active Record in its
-{README}[link:/rails/rails/blob/master/activerecord/README.rdoc].
-
-The Controller layer is responsible for handling incoming HTTP requests and providing a
-suitable response. Usually this means returning \HTML, but Rails controllers can also
-generate XML, JSON, PDFs, mobile-specific views, and more. Controllers manipulate models
-and render view templates in order to generate the appropriate HTTP response.
-
-In Rails, the Controller and View layers are handled together by Action Pack.
-These two layers are bundled in a single package due to their heavy interdependence.
-This is unlike the relationship between Active Record and Action Pack which are
-independent. Each of these packages can be used independently outside of Rails. You
-can read more about Action Pack in its {README}[link:/rails/rails/blob/master/actionpack/README.rdoc].
-
-== Getting Started
-
-1. Install Rails at the command prompt if you haven't yet:
-
- gem install rails
-
-2. At the command prompt, create a new Rails application:
-
- rails new myapp
-
- where "myapp" is the application name.
-
-3. Change directory to +myapp+ and start the web server:
-
- cd myapp; rails server
-
- Run with <tt>--help</tt> or <tt>-h</tt> for options.
-
-4. Go to http://localhost:3000 and you'll see:
-
- "Welcome aboard: You're riding Ruby on Rails!"
-
-5. Follow the guidelines to start developing your application. You may find the following resources handy:
-
-* The README file created within your application.
-* The {Getting Started with Rails}[http://guides.rubyonrails.org/getting_started.html].
-* The {Ruby on Rails Tutorial}[http://railstutorial.org/book].
-* The {Ruby on Rails Guides}[http://guides.rubyonrails.org].
-* The {API Documentation}[http://api.rubyonrails.org].
-
-== Contributing
-
-We encourage you to contribute to Ruby on Rails! Please check out the {Contributing to Rails
-guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how
-to proceed. {Join us}[http://contributors.rubyonrails.org]!
-
-== Build Status {<img src="https://secure.travis-ci.org/rails/rails.png"/>}[http://travis-ci.org/rails/rails]
-
-== Dependency Status {<img src="https://gemnasium.com/rails/rails.png?travis"/>}[https://gemnasium.com/rails/rails]
-
-== License
-
-Ruby on Rails is released under the MIT license:
-
-* http://www.opensource.org/licenses/MIT
diff --git a/RELEASING_RAILS.rdoc b/RELEASING_RAILS.rdoc
index af1def223a..aafbd25486 100644
--- a/RELEASING_RAILS.rdoc
+++ b/RELEASING_RAILS.rdoc
@@ -13,7 +13,7 @@ Today is mostly coordination tasks. Here are the things you must do today:
Do not release with a Red CI. You can find the CI status here:
- http://travis-ci.org/#!/rails/rails
+ http://travis-ci.org/rails/rails
=== Is Sam Ruby happy? If not, make him happy.
@@ -42,6 +42,9 @@ addressed, and that can impact your release date.
Ruby implementors have high stakes in making sure Rails works. Be kind and
give them a heads up that Rails will be released soonish.
+This only needs done for major and minor releases, bugfix releases aren't a
+big enough deal, and are supposed to be backwards compatible.
+
Send an email just giving a heads up about the upcoming release to these
lists:
@@ -110,7 +113,7 @@ what to do in case anything goes wrong:
$ git tag -m'tagging rc release' v3.0.10.rc1
$ git push
$ git push --tags
- $ for i in $(ls dist); do gem push $i; done
+ $ for i in $(ls pkg); do gem push $i; done
=== Send Rails release announcements
@@ -164,6 +167,9 @@ Today, do this stuff in this order:
* Update RAILS_VERSION to remove the rc
* Build and test the gem
* Release the gems
+* If releasing a new stable version:
+ - Trigger stable docs generation (see below)
+ - Update the version in the home page
* Email security lists
* Email general announcement lists
diff --git a/Rakefile b/Rakefile
index 99a27595e9..07d44fc94b 100644
--- a/Rakefile
+++ b/Rakefile
@@ -1,17 +1,17 @@
-require 'rdoc/task'
require 'sdoc'
require 'net/http'
$:.unshift File.expand_path('..', __FILE__)
require "tasks/release"
+require 'railties/lib/rails/api/task'
desc "Build gem files for all projects"
task :build => "all:build"
-desc "Release all gems to gemcutter and create a tag"
+desc "Release all gems to rubygems and create a tag"
task :release => "all:release"
-PROJECTS = %w(activesupport activemodel actionpack actionmailer activerecord railties)
+PROJECTS = %w(activesupport activemodel actionpack actionview actionmailer activerecord railties)
desc 'Run all tests by default'
task :default => %w(test test:isolated)
@@ -36,104 +36,13 @@ task :smoke do
end
desc "Install gems for all projects."
-task :install => :gem do
- version = File.read("RAILS_VERSION").strip
- (PROJECTS - ["railties"]).each do |project|
- puts "INSTALLING #{project}"
- system("gem install #{project}/pkg/#{project}-#{version}.gem --no-ri --no-rdoc")
- end
- system("gem install railties/pkg/railties-#{version}.gem --no-ri --no-rdoc")
- system("gem install pkg/rails-#{version}.gem --no-ri --no-rdoc")
-end
+task :install => "all:install"
desc "Generate documentation for the Rails framework"
-RDoc::Task.new do |rdoc|
- RDOC_MAIN = 'RDOC_MAIN.rdoc'
-
- # This is a hack.
- #
- # Backslashes are needed to prevent RDoc from autolinking "Rails" to the
- # documentation of the Rails module. On the other hand, as of this
- # writing README.rdoc is displayed in the front page of the project in
- # GitHub, where backslashes are shown and look weird.
- #
- # The temporary solution is to have a README.rdoc without backslashes for
- # GitHub, and gsub it to generate the main page of the API.
- #
- # Also, relative links in GitHub have to point to blobs, whereas in the API
- # they need to point to files.
- #
- # The idea for the future is to have totally different files, since the
- # API is no longer a generic entry point to Rails and deserves a
- # dedicated main page specifically thought as an API entry point.
- rdoc.before_running_rdoc do
- rdoc_main = File.read('README.rdoc')
-
- # The ^(?=\S) assertion prevents code blocks from being processed,
- # since no autolinking happens there and RDoc displays the backslash
- # otherwise.
- rdoc_main.gsub!(/^(?=\S).*?\b(?=Rails)\b/) { "#$&\\" }
- rdoc_main.gsub!(%r{link:/rails/rails/blob/master/(\w+)/README\.rdoc}, "link:files/\\1/README_rdoc.html")
-
- # Remove Travis and Gemnasium status images from API pages. Only GitHub
- # README page gets these images. Travis' https build image is used to avoid
- # GitHub caching: http://about.travis-ci.org/docs/user/status-images
- rdoc_main.gsub!(%r{^== (Build|Dependency) Status.*}, '')
-
- File.open(RDOC_MAIN, 'w') do |f|
- f.write(rdoc_main)
- end
-
- rdoc.rdoc_files.include(RDOC_MAIN)
- end
-
- rdoc.rdoc_dir = 'doc/rdoc'
- rdoc.title = "Ruby on Rails Documentation"
-
- rdoc.options << '-f' << 'sdoc'
- rdoc.options << '-T' << 'rails'
- rdoc.options << '-e' << 'UTF-8'
- rdoc.options << '-g' # SDoc flag, link methods to GitHub
- rdoc.options << '-m' << RDOC_MAIN
-
- rdoc.rdoc_files.include('railties/CHANGELOG.md')
- rdoc.rdoc_files.include('railties/MIT-LICENSE')
- rdoc.rdoc_files.include('railties/README.rdoc')
- rdoc.rdoc_files.include('railties/lib/**/*.rb')
- rdoc.rdoc_files.exclude('railties/lib/rails/generators/**/templates/**/*.rb')
-
- rdoc.rdoc_files.include('activerecord/README.rdoc')
- rdoc.rdoc_files.include('activerecord/CHANGELOG.md')
- rdoc.rdoc_files.include('activerecord/lib/active_record/**/*.rb')
- rdoc.rdoc_files.exclude('activerecord/lib/active_record/vendor/*')
-
- rdoc.rdoc_files.include('actionpack/README.rdoc')
- rdoc.rdoc_files.include('actionpack/CHANGELOG.md')
- rdoc.rdoc_files.include('actionpack/lib/abstract_controller/**/*.rb')
- rdoc.rdoc_files.include('actionpack/lib/action_controller/**/*.rb')
- rdoc.rdoc_files.include('actionpack/lib/action_dispatch/**/*.rb')
- rdoc.rdoc_files.include('actionpack/lib/action_view/**/*.rb')
- rdoc.rdoc_files.exclude('actionpack/lib/action_controller/vendor/*')
-
- rdoc.rdoc_files.include('actionmailer/README.rdoc')
- rdoc.rdoc_files.include('actionmailer/CHANGELOG.md')
- rdoc.rdoc_files.include('actionmailer/lib/action_mailer/**/*.rb')
- rdoc.rdoc_files.exclude('actionmailer/lib/action_mailer/vendor/*')
-
- rdoc.rdoc_files.include('activesupport/README.rdoc')
- rdoc.rdoc_files.include('activesupport/CHANGELOG.md')
- rdoc.rdoc_files.include('activesupport/lib/active_support/**/*.rb')
- rdoc.rdoc_files.exclude('activesupport/lib/active_support/vendor/*')
-
- rdoc.rdoc_files.include('activemodel/README.rdoc')
- rdoc.rdoc_files.include('activemodel/CHANGELOG.md')
- rdoc.rdoc_files.include('activemodel/lib/active_model/**/*.rb')
-end
-
-# Enhance rdoc task to copy referenced images also
-task :rdoc do
- FileUtils.mkdir_p "doc/rdoc/files/examples/"
- FileUtils.copy "activerecord/examples/associations.png", "doc/rdoc/files/examples/associations.png"
+if ENV['EDGE']
+ Rails::API::EdgeTask.new('rdoc')
+else
+ Rails::API::StableTask.new('rdoc')
end
desc 'Bump all versions to match version.rb'
@@ -141,13 +50,14 @@ task :update_versions do
require File.dirname(__FILE__) + "/version"
File.open("RAILS_VERSION", "w") do |f|
- f.write Rails::VERSION::STRING + "\n"
+ f.puts Rails::VERSION::STRING
end
constants = {
"activesupport" => "ActiveSupport",
"activemodel" => "ActiveModel",
"actionpack" => "ActionPack",
+ "actionview" => "ActionView",
"actionmailer" => "ActionMailer",
"activerecord" => "ActiveRecord",
"railties" => "Rails"
@@ -165,7 +75,7 @@ task :update_versions do
end
#
-# We have a webhook configured in Github that gets invoked after pushes.
+# We have a webhook configured in GitHub that gets invoked after pushes.
# This hook triggers the following tasks:
#
# * updates the local checkout
diff --git a/actionmailer/CHANGELOG.md b/actionmailer/CHANGELOG.md
index 68b34805ff..d84b95e6e9 100644
--- a/actionmailer/CHANGELOG.md
+++ b/actionmailer/CHANGELOG.md
@@ -1,34 +1,12 @@
-## Rails 4.0.0 (unreleased) ##
+* Instrument the generation of Action Mailer messages. The time it takes to
+ generate a message is written to the log.
-* Support `Mailer.deliver_foo(*args)` as a synonym for
- `Mailer.foo(*args).deliver`. This makes it easy to write e.g.
- `Mailer.expects(:deliver_foo)` when testing code that calls
- the mailer. *Jon Leighton*
+ *Daniel Schierbeck*
-* Allow delivery method options to be set per mail instance *Aditya Sanghi*
+* 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
- If your smtp delivery settings are dynamic,
- you can now override settings per mail instance for e.g.
+ *Alex Tsukernik*
- def my_mailer(user,company)
- mail to: user.email, subject: "Welcome!",
- delivery_method_options: {user_name: company.smtp_user,
- password: company.smtp_password}
- end
-
- This will ensure that your default SMTP settings will be overridden
- by the company specific ones. You only have to override the settings
- that are dynamic and leave the static setting in your environment
- configuration file (e.g. config/environments/production.rb)
-
-* Allow to set default Action Mailer options via `config.action_mailer.default_options=` *Robert Pankowecki*
-
-* Raise an `ActionView::MissingTemplate` exception when no implicit template could be found. *Damien Mathieu*
-
-* Asynchronously send messages via the Rails Queue *Brian Cardarella*
-
-* Allow callbacks to be defined in mailers similar to `ActionController::Base`. You can configure default
- settings, headers, attachments, delivery settings or change delivery using
- `before_filter`, `after_filter` etc. *Justin S. Leitgeb*
-
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/actionmailer/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for previous changes.
diff --git a/actionmailer/MIT-LICENSE b/actionmailer/MIT-LICENSE
index 810daf856c..5c668d9624 100644
--- a/actionmailer/MIT-LICENSE
+++ b/actionmailer/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionmailer/README.rdoc b/actionmailer/README.rdoc
index 59c33b7940..96dd0b1a2e 100644
--- a/actionmailer/README.rdoc
+++ b/actionmailer/README.rdoc
@@ -1,6 +1,6 @@
= Action Mailer -- Easy email delivery and testing
-Action Mailer is a framework for designing email-service layers. These layers
+Action Mailer is a framework for designing email service layers. These layers
are used to consolidate code for sending out forgotten passwords, welcome
wishes on signup, invoices for billing, and any other use case that requires
a written notification to either a person or another system.
@@ -61,24 +61,22 @@ generated would look like this:
Thank you for signing up!
-In previous version of Rails you would call <tt>create_method_name</tt> and
-<tt>deliver_method_name</tt>. Rails 3.0 has a much simpler interface - you
-simply call the method and optionally call +deliver+ on the return value.
+In order to send mails, you simply call the method and then call +deliver+ on the return value.
Calling the method returns a Mail Message object:
- message = Notifier.welcome # => Returns a Mail::Message object
- message.deliver # => delivers the email
+ message = Notifier.welcome("david@loudthinking.com") # => Returns a Mail::Message object
+ message.deliver # => delivers the email
Or you can just chain the methods together like:
- Notifier.welcome.deliver # Creates the email and sends it immediately
+ Notifier.welcome("david@loudthinking.com").deliver # Creates the email and sends it immediately
== Setting defaults
It is possible to set default values that will be used in every method in your Action Mailer class. To implement this functionality, you just call the public class method <tt>default</tt> which you get for free from ActionMailer::Base. This method accepts a Hash as the parameter. You can use any of the headers e-mail messages has, like <tt>:from</tt> as the key. You can also pass in a string as the key, like "Content-Type", but Action Mailer does this out of the box for you, so you won't need to worry about that. Finally, it is also possible to pass in a Proc that will get evaluated when it is needed.
-Note that every value you set with this method will get over written if you use the same key in your mailer method.
+Note that every value you set with this method will get overwritten if you use the same key in your mailer method.
Example:
@@ -98,7 +96,7 @@ Example:
class Mailman < ActionMailer::Base
def receive(email)
- page = Page.find_by_address(email.to.first)
+ page = Page.find_by(address: email.to.first)
page.emails.create(
subject: email.subject, body: email.body
)
@@ -119,8 +117,7 @@ trivial case like this:
rails runner 'Mailman.receive(STDIN.read)'
However, invoking Rails in the runner for each mail to be received is very resource intensive. A single
-instance of Rails should be run within a daemon, if it is going to be utilized to process more than just
-a limited number of email.
+instance of Rails should be run within a daemon, if it is going to process more than just a limited amount of email.
== Configuration
diff --git a/actionmailer/Rakefile b/actionmailer/Rakefile
index 45c238d302..5ddd90020b 100644
--- a/actionmailer/Rakefile
+++ b/actionmailer/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
desc "Default Task"
@@ -15,9 +14,8 @@ Rake::TestTask.new { |t|
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("test/**/*_test.rb").all? do |file|
- sh(ruby, '-Ilib:test', file)
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
end or raise "Failures"
end
end
@@ -28,7 +26,7 @@ Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
-desc "Release to gemcutter"
+desc "Release to rubygems"
task release: :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/actionmailer/actionmailer.gemspec b/actionmailer/actionmailer.gemspec
index 0c669e2e91..9b25feaf75 100644
--- a/actionmailer/actionmailer.gemspec
+++ b/actionmailer/actionmailer.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -6,17 +6,21 @@ Gem::Specification.new do |s|
s.version = version
s.summary = 'Email composition, delivery, and receiving framework (part of Rails).'
s.description = 'Email on Rails. Compose, deliver, receive, and test emails using the familiar controller/view pattern. First-class support for multipart email and attachments.'
+
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
s.require_path = 'lib'
s.requirements << 'none'
- s.add_dependency('actionpack', version)
- s.add_dependency('mail', '~> 2.4.4')
+ s.add_dependency 'actionpack', version
+ s.add_dependency 'actionview', version
+
+ s.add_dependency 'mail', '~> 2.5.4'
end
diff --git a/actionmailer/lib/action_mailer.rb b/actionmailer/lib/action_mailer.rb
index cfbe2f1cbd..5b6960c8fc 100644
--- a/actionmailer/lib/action_mailer.rb
+++ b/actionmailer/lib/action_mailer.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -22,7 +22,6 @@
#++
require 'abstract_controller'
-require 'action_view'
require 'action_mailer/version'
# Common Active Support usage in Action Mailer
diff --git a/actionmailer/lib/action_mailer/base.rb b/actionmailer/lib/action_mailer/base.rb
index fee4a64248..becd4126f4 100644
--- a/actionmailer/lib/action_mailer/base.rb
+++ b/actionmailer/lib/action_mailer/base.rb
@@ -1,10 +1,9 @@
require 'mail'
-require 'action_mailer/queued_message'
require 'action_mailer/collector'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/anonymous'
-require 'active_support/queueing'
+
require 'action_mailer/log_subscriber'
module ActionMailer
@@ -20,8 +19,6 @@ module ActionMailer
# used to generate an email message. In these methods, you can setup variables to be used in
# the mailer views, options on the mail itself such as the <tt>:from</tt> address, and attachments.
#
- # Examples:
- #
# class Notifier < ActionMailer::Base
# default from: 'no-reply@example.com',
# return_path: 'system@example.com'
@@ -70,7 +67,7 @@ module ActionMailer
# The block syntax is also useful in providing information specific to a part:
#
# mail(to: user.email) do |format|
- # format.text(:content_transfer_encoding => "base64")
+ # format.text(content_transfer_encoding: "base64")
# format.html
# end
#
@@ -129,12 +126,12 @@ module ActionMailer
# It is also possible to set a default host that will be used in all mailers by setting the <tt>:host</tt>
# option as a configuration option in <tt>config/application.rb</tt>:
#
- # config.action_mailer.default_url_options = { :host => "example.com" }
+ # config.action_mailer.default_url_options = { host: "example.com" }
#
# When you decide to set a default <tt>:host</tt> for your mailers, then you need to make sure to use the
- # <tt>:only_path => false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper
+ # <tt>only_path: false</tt> option when using <tt>url_for</tt>. Since the <tt>url_for</tt> view helper
# will generate relative URLs by default when a <tt>:host</tt> option isn't explicitly provided, passing
- # <tt>:only_path => false</tt> will ensure that absolute URLs are generated.
+ # <tt>only_path: false</tt> will ensure that absolute URLs are generated.
#
# = Sending mail
#
@@ -142,7 +139,6 @@ module ActionMailer
# for delivery later:
#
# Notifier.welcome(david).deliver # sends the email
- # Notifier.deliver_welcome(david) # synonym for the former
# mail = Notifier.welcome(david) # => a Mail::Message object
# mail.deliver # sends the email
#
@@ -156,9 +152,9 @@ module ActionMailer
#
# For example, if the following templates exist:
# * signup_notification.text.erb
- # * signup_notification.text.html.erb
- # * signup_notification.text.xml.builder
- # * signup_notification.text.yaml.erb
+ # * signup_notification.html.erb
+ # * signup_notification.xml.builder
+ # * signup_notification.yaml.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>,
@@ -180,7 +176,7 @@ module ActionMailer
# end
# end
#
- # Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.text.html.erb</tt>
+ # Which will (if it had both a <tt>welcome.text.erb</tt> and <tt>welcome.html.erb</tt>
# template in the view directory), send a complete <tt>multipart/mixed</tt> email with two parts,
# the first part being a <tt>multipart/alternative</tt> with the text and HTML email parts inside,
# and the second being a <tt>application/pdf</tt> with a Base64 encoded copy of the file.pdf book
@@ -247,10 +243,10 @@ module ActionMailer
# You can pass in any header value that a <tt>Mail::Message</tt> accepts. Out of the box,
# <tt>ActionMailer::Base</tt> sets the following:
#
- # * <tt>:mime_version => "1.0"</tt>
- # * <tt>:charset => "UTF-8",</tt>
- # * <tt>:content_type => "text/plain",</tt>
- # * <tt>:parts_order => [ "text/plain", "text/enriched", "text/html" ]</tt>
+ # * <tt>mime_version: "1.0"</tt>
+ # * <tt>charset: "UTF-8",</tt>
+ # * <tt>content_type: "text/plain",</tt>
+ # * <tt>parts_order: [ "text/plain", "text/enriched", "text/html" ]</tt>
#
# <tt>parts_order</tt> and <tt>charset</tt> are not actually valid <tt>Mail::Message</tt> header fields,
# but Action Mailer translates them appropriately and sets the correct values.
@@ -287,12 +283,12 @@ module ActionMailer
#
# = Callbacks
#
- # You can specify callbacks using before_filter and after_filter for configuring your messages.
+ # You can specify callbacks using before_action and after_action for configuring your messages.
# This may be useful, for example, when you want to add default inline attachments for all
# messages sent out by a certain mailer class:
#
# class Notifier < ActionMailer::Base
- # before_filter :add_inline_attachment!
+ # before_action :add_inline_attachment!
#
# def welcome
# mail
@@ -309,15 +305,15 @@ module ActionMailer
# can define and configure callbacks in the same manner that you would use callbacks in
# classes that inherit from ActionController::Base.
#
- # Note that unless you have a specific reason to do so, you should prefer using before_filter
- # rather than after_filter in your ActionMailer classes so that headers are parsed properly.
+ # 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.
#
# = Configuration options
#
# These options are specified on the class level, like
# <tt>ActionMailer::Base.raise_delivery_errors = true</tt>
#
- # * <tt>default</tt> - You can pass this in at a class level as well as within the class itself as
+ # * <tt>default_options</tt> - You can pass this in at a class level as well as within the class itself as
# per the above section.
#
# * <tt>logger</tt> - the logger is used for generating information on the mailing run if available.
@@ -339,8 +335,8 @@ module ActionMailer
# and starts to use it.
# * <tt>:openssl_verify_mode</tt> - When using TLS, you can set how OpenSSL checks the certificate. This is
# really useful if you need to validate a self-signed and/or a wildcard certificate. You can use the name
- # of an OpenSSL verify constant ('none', 'peer', 'client_once','fail_if_no_peer_cert') or directly the
- # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER,...).
+ # of an OpenSSL verify constant ('none', 'peer', 'client_once', 'fail_if_no_peer_cert') or directly the
+ # constant (OpenSSL::SSL::VERIFY_NONE, OpenSSL::SSL::VERIFY_PEER, ...).
#
# * <tt>sendmail_settings</tt> - Allows you to override options for the <tt>:sendmail</tt> delivery method.
# * <tt>:location</tt> - The location of the sendmail executable. Defaults to <tt>/usr/sbin/sendmail</tt>.
@@ -364,15 +360,14 @@ module ActionMailer
#
# * <tt>deliveries</tt> - Keeps an array of all the emails sent out through the Action Mailer with
# <tt>delivery_method :test</tt>. Most useful for unit and functional testing.
- #
- # * <tt>queue</> - The queue that will be used to deliver the mail. The queue should expect a job that responds to <tt>run</tt>.
class Base < AbstractController::Base
include DeliveryMethods
+
abstract!
- include AbstractController::Logger
include AbstractController::Rendering
- include AbstractController::Layouts
+
+ include AbstractController::Logger
include AbstractController::Helpers
include AbstractController::Translation
include AbstractController::AssetPaths
@@ -392,9 +387,6 @@ module ActionMailer
parts_order: [ "text/plain", "text/enriched", "text/html" ]
}.freeze
- class_attribute :queue
- self.queue = ActiveSupport::SynchronousQueue.new
-
class << self
# Register one or more Observers which will be notified when mail is delivered.
def register_observers(*observers)
@@ -408,7 +400,7 @@ module ActionMailer
# Register an Observer which will be notified when mail is delivered.
# Either a class or a string can be passed in as the Observer. If a string is passed in
- # it will be <tt>constantize</tt>d.
+ # it will be +constantize+d.
def register_observer(observer)
delivery_observer = (observer.is_a?(String) ? observer.constantize : observer)
Mail.register_observer(delivery_observer)
@@ -422,12 +414,20 @@ module ActionMailer
Mail.register_interceptor(delivery_interceptor)
end
+ # Returns the name of current mailer. This method is also being used as a path for a view lookup.
+ # If this is an anonymous mailer, this method will return +anonymous+ instead.
def mailer_name
@mailer_name ||= anonymous? ? "anonymous" : name.underscore
end
+ # Allows to set the name of current mailer.
attr_writer :mailer_name
alias :controller_path :mailer_name
+ # Sets the defaults through app configuration:
+ #
+ # config.action_mailer.default { from: "no-reply@example.org" }
+ #
+ # Aliased by ::default_options=
def default(value = nil)
self.default_params = default_params.merge(value).freeze if value
default_params
@@ -439,13 +439,15 @@ module ActionMailer
# Receives a raw email, parses it into an email object, decodes it,
# instantiates a new mailer, and passes the email object to the mailer
- # object's +receive+ method. If you want your mailer to be able to
- # process incoming messages, you'll need to implement a +receive+
- # method that accepts the raw email string as a parameter:
+ # object's +receive+ method.
+ #
+ # If you want your mailer to be able to process incoming messages, you'll
+ # need to implement a +receive+ method that accepts the raw email string
+ # as a parameter:
#
# class MyMailer < ActionMailer::Base
# def receive(mail)
- # ...
+ # # ...
# end
# end
def receive(raw_mail)
@@ -456,10 +458,12 @@ module ActionMailer
end
end
- # Wraps an email delivery inside of Active Support Notifications instrumentation. This
- # method is actually called by the <tt>Mail::Message</tt> object itself through a callback
- # when you call <tt>:deliver</tt> on the Mail::Message, calling +deliver_mail+ directly
- # and passing a Mail::Message will do nothing except tell the logger you sent the email.
+ # Wraps an email delivery inside of ActiveSupport::Notifications instrumentation.
+ #
+ # This method is actually called by the Mail::Message object itself
+ # through a callback when you call +:deliver+ on the Mail::Message,
+ # calling +deliver_mail+ directly and passing a Mail::Message will do
+ # nothing except tell the logger you sent the email.
def deliver_mail(mail) #:nodoc:
ActiveSupport::Notifications.instrument("deliver.action_mailer") do |payload|
set_payload_for_mail(payload, mail)
@@ -485,11 +489,9 @@ module ActionMailer
payload[:mail] = mail.encoded
end
- def method_missing(method_name, *args)
- if action_methods.include?(method_name.to_s)
- QueuedMessage.new(queue, self, method_name, *args)
- elsif method_name.to_s =~ /^deliver_(.+)$/ && action_methods.include?($1)
- public_send($1, *args).deliver
+ def method_missing(method_name, *args) # :nodoc:
+ if respond_to?(method_name)
+ new(method_name, *args).message
else
super
end
@@ -504,26 +506,45 @@ module ActionMailer
# method, for instance).
def initialize(method_name=nil, *args)
super()
+ @_mail_was_called = false
@_message = Mail.new
process(method_name, *args) if method_name
end
- def process(*args) #:nodoc:
- lookup_context.skip_default_locale!
- super
+ def process(method_name, *args) #:nodoc:
+ payload = {
+ :mailer => self.class.name,
+ :action => method_name
+ }
+
+ ActiveSupport::Notifications.instrument("process.action_mailer", payload) do
+ lookup_context.skip_default_locale!
+
+ super
+ @_message = NullMail.new unless @_mail_was_called
+ end
end
+ class NullMail #:nodoc:
+ def body; '' end
+
+ def method_missing(*args)
+ nil
+ end
+ end
+
+ # Returns the name of the mailer object.
def mailer_name
self.class.mailer_name
end
- # Allows you to pass random and unusual headers to the new <tt>Mail::Message</tt> object
- # which will add them to itself.
+ # Allows you to pass random and unusual headers to the new Mail::Message
+ # object which will add them to itself.
#
# headers['X-Special-Domain-Specific-Header'] = "SecretValue"
#
- # You can also pass a hash into headers of header field names and values, which
- # will then be set on the Mail::Message object:
+ # You can also pass a hash into headers of header field names and values,
+ # which will then be set on the Mail::Message object:
#
# headers 'X-Special-Domain-Specific-Header' => "SecretValue",
# 'In-Reply-To' => incoming.message_id
@@ -531,7 +552,7 @@ module ActionMailer
# The resulting Mail::Message will have the following in its header:
#
# X-Special-Domain-Specific-Header: SecretValue
- def headers(args=nil)
+ def headers(args = nil)
if args
@_message.headers(args)
else
@@ -579,44 +600,46 @@ module ActionMailer
# Both methods accept a headers hash. This hash allows you to specify the most used headers
# in an email message, these are:
#
- # * <tt>:subject</tt> - The subject of the message, if this is omitted, Action Mailer will
- # ask the Rails I18n class for a translated <tt>:subject</tt> in the scope of
+ # * +:subject+ - The subject of the message, if this is omitted, Action Mailer will
+ # ask the Rails I18n class for a translated +:subject+ in the scope of
# <tt>[mailer_scope, action_name]</tt> or if this is missing, will translate the
- # humanized version of the <tt>action_name</tt>
- # * <tt>:to</tt> - Who the message is destined for, can be a string of addresses, or an array
+ # humanized version of the +action_name+
+ # * +:to+ - Who the message is destined for, can be a string of addresses, or an array
# of addresses.
- # * <tt>:from</tt> - Who the message is from
- # * <tt>:cc</tt> - Who you would like to Carbon-Copy on this email, can be a string of addresses,
+ # * +:from+ - Who the message is from
+ # * +:cc+ - Who you would like to Carbon-Copy on this email, can be a string of addresses,
# or an array of addresses.
- # * <tt>:bcc</tt> - Who you would like to Blind-Carbon-Copy on this email, can be a string of
+ # * +:bcc+ - Who you would like to Blind-Carbon-Copy on this email, can be a string of
# addresses, or an array of addresses.
- # * <tt>:reply_to</tt> - Who to set the Reply-To header of the email to.
- # * <tt>:date</tt> - The date to say the email was sent on.
+ # * +:reply_to+ - Who to set the Reply-To header of the email to.
+ # * +:date+ - The date to say the email was sent on.
#
- # You can set default values for any of the above headers (except :date) by using the <tt>default</tt>
- # class method:
+ # You can set default values for any of the above headers (except +:date+)
+ # by using the ::default class method:
#
# class Notifier < ActionMailer::Base
- # self.default from: 'no-reply@test.lindsaar.net',
- # bcc: 'email_logger@test.lindsaar.net',
- # reply_to: 'bounces@test.lindsaar.net'
+ # default from: 'no-reply@test.lindsaar.net',
+ # bcc: 'email_logger@test.lindsaar.net',
+ # reply_to: 'bounces@test.lindsaar.net'
# end
#
# If you need other headers not listed above, you can either pass them in
# as part of the headers hash or use the <tt>headers['name'] = value</tt>
# method.
#
- # When a <tt>:return_path</tt> is specified as header, that value will be used as the 'envelope from'
- # address for the Mail message. Setting this is useful when you want delivery notifications
- # sent to a different address than the one in <tt>:from</tt>. Mail will actually use the
- # <tt>:return_path</tt> in preference to the <tt>:sender</tt> in preference to the <tt>:from</tt>
- # field for the 'envelope from' value.
+ # When a +:return_path+ is specified as header, that value will be used as
+ # the 'envelope from' address for the Mail message. Setting this is useful
+ # when you want delivery notifications sent to a different address than the
+ # one in +:from+. Mail will actually use the +:return_path+ in preference
+ # to the +:sender+ in preference to the +:from+ field for the 'envelope
+ # from' value.
#
- # If you do not pass a block to the +mail+ method, it will find all templates in the
- # view paths using by default the mailer name and the method name that it is being
- # called from, it will then create parts for each of these templates intelligently,
- # making educated guesses on correct content type and sequence, and return a fully
- # prepared Mail::Message ready to call <tt>:deliver</tt> on to send.
+ # If you do not pass a block to the +mail+ method, it will find all
+ # templates in the view paths using by default the mailer name and the
+ # method name that it is being called from, it will then create parts for
+ # each of these templates intelligently, making educated guesses on correct
+ # content type and sequence, and return a fully prepared Mail::Message
+ # ready to call +:deliver+ on to send.
#
# For example:
#
@@ -651,27 +674,27 @@ module ActionMailer
# format.html { render text: "<h1>Hello Mikel!</h1>" }
# end
#
- # Which will render a <tt>multipart/alternative</tt> email with <tt>text/plain</tt> and
- # <tt>text/html</tt> parts.
+ # Which will render a +multipart/alternative+ email with +text/plain+ and
+ # +text/html+ parts.
#
# The block syntax also allows you to customize the part headers if desired:
#
- # mail(:to => 'mikel@test.lindsaar.net') do |format|
+ # mail(to: 'mikel@test.lindsaar.net') do |format|
# format.text(content_transfer_encoding: "base64")
# format.html
# end
#
- def mail(headers={}, &block)
+ def mail(headers = {}, &block)
+ @_mail_was_called = true
m = @_message
- # At the beginning, do not consider class default for parts order neither content_type
+ # At the beginning, do not consider class default for content_type
content_type = headers[:content_type]
- parts_order = headers[:parts_order]
# Call all the procs (if any)
- class_default = self.class.default
- default_values = class_default.merge(class_default) do |k,v|
- v.respond_to?(:to_proc) ? instance_eval(&v) : v
+ default_values = {}
+ self.class.default.each do |k,v|
+ default_values[k] = v.is_a?(Proc) ? instance_eval(&v) : v
end
# Handle defaults
@@ -689,7 +712,7 @@ module ActionMailer
assignable.each { |k, v| m[k] = v }
# Render the templates and blocks
- responses, explicit_order = collect_responses_and_parts_order(headers, &block)
+ responses = collect_responses(headers, &block)
create_parts_from_responses(m, responses)
# Setup content type, reapply charset and handle parts order
@@ -697,8 +720,7 @@ module ActionMailer
m.charset = charset
if m.multipart?
- parts_order ||= explicit_order || headers[:parts_order]
- m.body.set_sort_order(parts_order)
+ m.body.set_sort_order(headers[:parts_order])
m.body.sort_parts!
end
@@ -707,6 +729,15 @@ module ActionMailer
protected
+ # Used by #mail to set the content type of the message.
+ #
+ # It will use the given +user_content_type+, or multipart if the mail
+ # message has any attachments. If the attachments are inline, the content
+ # type will be "multipart/related", otherwise "multipart/mixed".
+ #
+ # If there is no content type passed in via headers, and there are no
+ # attachments, or the message is multipart, then the default content type is
+ # used.
def set_content_type(m, user_content_type, class_default)
params = m.content_type_parameters || {}
case
@@ -728,19 +759,19 @@ module ActionMailer
# Translates the +subject+ using Rails I18n class under <tt>[mailer_scope, action_name]</tt> scope.
# If it does not find a translation for the +subject+ under the specified scope it will default to a
# humanized version of the <tt>action_name</tt>.
- def default_i18n_subject #:nodoc:
+ # If the subject has interpolations, you can pass them through the +interpolations+ parameter.
+ def default_i18n_subject(interpolations = {})
mailer_scope = self.class.mailer_name.tr('/', '.')
- I18n.t(:subject, scope: [mailer_scope, action_name], default: action_name.humanize)
+ I18n.t(:subject, interpolations.merge(scope: [mailer_scope, action_name], default: action_name.humanize))
end
- def collect_responses_and_parts_order(headers) #:nodoc:
- responses, parts_order = [], nil
+ def collect_responses(headers) #:nodoc:
+ responses = []
if block_given?
collector = ActionMailer::Collector.new(lookup_context) { render(action_name) }
yield(collector)
- parts_order = collector.responses.map { |r| r[:content_type] }
- responses = collector.responses
+ responses = collector.responses
elsif headers[:body]
responses << {
body: headers.delete(:body),
@@ -750,7 +781,7 @@ module ActionMailer
templates_path = headers.delete(:template_path) || self.class.mailer_name
templates_name = headers.delete(:template_name) || action_name
- each_template(templates_path, templates_name) do |template|
+ each_template(Array(templates_path), templates_name) do |template|
self.formats = template.formats
responses << {
@@ -760,13 +791,13 @@ module ActionMailer
end
end
- [responses, parts_order]
+ responses
end
def each_template(paths, name, &block) #:nodoc:
- templates = lookup_context.find_all(name, Array(paths))
+ templates = lookup_context.find_all(name, paths)
if templates.empty?
- raise ActionView::MissingTemplate.new([paths], name, [paths], false, 'mailer')
+ raise ActionView::MissingTemplate.new(paths, name, paths, false, 'mailer')
else
templates.uniq { |t| t.formats }.each(&block)
end
diff --git a/actionmailer/lib/action_mailer/collector.rb b/actionmailer/lib/action_mailer/collector.rb
index cbb2778fae..e8883a8235 100644
--- a/actionmailer/lib/action_mailer/collector.rb
+++ b/actionmailer/lib/action_mailer/collector.rb
@@ -20,7 +20,7 @@ module ActionMailer
end
alias :all :any
- def custom(mime, options={})
+ def custom(mime, options = {})
options.reverse_merge!(content_type: mime.to_s)
@context.formats = [mime.to_sym]
options[:body] = block_given? ? yield : @default_render.call
diff --git a/actionmailer/lib/action_mailer/delivery_methods.rb b/actionmailer/lib/action_mailer/delivery_methods.rb
index caea3d7535..aedcd81e52 100644
--- a/actionmailer/lib/action_mailer/delivery_methods.rb
+++ b/actionmailer/lib/action_mailer/delivery_methods.rb
@@ -38,6 +38,7 @@ module ActionMailer
add_delivery_method :test, Mail::TestMailer
end
+ # Helpers for creating and wrapping delivery behavior, used by DeliveryMethods.
module ClassMethods
# Provides a list of emails that have been delivered by Mail::TestMailer
delegate :deliveries, :deliveries=, to: Mail::TestMailer
@@ -63,7 +64,7 @@ module ActionMailer
raise "Delivery method cannot be nil"
when Symbol
if klass = delivery_methods[method]
- mail.delivery_method(klass,(send(:"#{method}_settings") || {}).merge!(options || {}))
+ mail.delivery_method(klass, (send(:"#{method}_settings") || {}).merge(options || {}))
else
raise "Invalid delivery method #{method.inspect}"
end
diff --git a/actionmailer/lib/action_mailer/log_subscriber.rb b/actionmailer/lib/action_mailer/log_subscriber.rb
index 3fe64759ac..eb6fb11d81 100644
--- a/actionmailer/lib/action_mailer/log_subscriber.rb
+++ b/actionmailer/lib/action_mailer/log_subscriber.rb
@@ -1,5 +1,10 @@
+require 'active_support/log_subscriber'
+
module ActionMailer
+ # Implements the ActiveSupport::LogSubscriber for logging notifications when
+ # email is delivered and received.
class LogSubscriber < ActiveSupport::LogSubscriber
+ # An email was delivered.
def deliver(event)
return unless logger.info?
recipients = Array(event.payload[:to]).join(', ')
@@ -7,12 +12,21 @@ module ActionMailer
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])
end
+ # An email was generated.
+ def process(event)
+ mailer = event.payload[:mailer]
+ action = event.payload[:action]
+ debug("\n#{mailer}##{action}: processed outbound mail in #{event.duration.round(1)}ms")
+ end
+
+ # Use the logger configured for ActionMailer::Base
def logger
ActionMailer::Base.logger
end
diff --git a/actionmailer/lib/action_mailer/mail_helper.rb b/actionmailer/lib/action_mailer/mail_helper.rb
index ec84256491..54ad9f066f 100644
--- a/actionmailer/lib/action_mailer/mail_helper.rb
+++ b/actionmailer/lib/action_mailer/mail_helper.rb
@@ -1,4 +1,7 @@
module ActionMailer
+ # Provides helper methods for ActionMailer::Base that can be used for easily
+ # formatting messages, accessing mailer or message instances, and the
+ # attachments list.
module MailHelper
# Take the text and format it, indented two spaces for each line, and
# wrapped at 72 columns.
@@ -46,8 +49,9 @@ module ActionMailer
end
end
- sentences.map { |sentence|
- "#{" " * indent}#{sentence.join(' ')}"
+ indentation = " " * indent
+ sentences.map! { |sentence|
+ "#{indentation}#{sentence.join(' ')}"
}.join "\n"
end
end
diff --git a/actionmailer/lib/action_mailer/queued_message.rb b/actionmailer/lib/action_mailer/queued_message.rb
deleted file mode 100644
index 8d200617c4..0000000000
--- a/actionmailer/lib/action_mailer/queued_message.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'delegate'
-
-module ActionMailer
- class QueuedMessage < ::Delegator
- attr_reader :queue
-
- def initialize(queue, mailer_class, method_name, *args)
- @queue = queue
- @job = DeliveryJob.new(mailer_class, method_name, args)
- end
-
- def __getobj__
- @job.message
- end
-
- # Queues the message for delivery.
- def deliver
- tap { @queue.push @job }
- end
-
- class DeliveryJob
- def initialize(mailer_class, method_name, args)
- @mailer_class = mailer_class
- @method_name = method_name
- @args = args
- end
-
- def message
- @message ||= @mailer_class.send(:new, @method_name, *@args).message
- end
-
- def run
- message.deliver
- end
- end
- end
-end
diff --git a/actionmailer/lib/action_mailer/railtie.rb b/actionmailer/lib/action_mailer/railtie.rb
index abf6ad80cf..7677ff3a7c 100644
--- a/actionmailer/lib/action_mailer/railtie.rb
+++ b/actionmailer/lib/action_mailer/railtie.rb
@@ -19,10 +19,7 @@ module ActionMailer
options.javascripts_dir ||= paths["public/javascripts"].first
options.stylesheets_dir ||= paths["public/stylesheets"].first
- options.queue ||= app.queue
-
# make sure readers methods get compiled
- options.asset_path ||= app.config.asset_path
options.asset_host ||= app.config.asset_host
options.relative_url_root ||= app.config.relative_url_root
diff --git a/actionmailer/lib/action_mailer/test_case.rb b/actionmailer/lib/action_mailer/test_case.rb
index 80f323873d..207f949fe2 100644
--- a/actionmailer/lib/action_mailer/test_case.rb
+++ b/actionmailer/lib/action_mailer/test_case.rb
@@ -10,13 +10,6 @@ module ActionMailer
end
class TestCase < ActiveSupport::TestCase
-
- # Use AM::TestCase for the base class when describing a mailer
- register_spec_type(self) do |desc|
- Class === desc && desc < ActionMailer::Base
- end
- register_spec_type(/Mailer( ?Test)?\z/i, self)
-
module Behavior
extend ActiveSupport::Concern
diff --git a/actionmailer/lib/action_mailer/test_helper.rb b/actionmailer/lib/action_mailer/test_helper.rb
index 3ab37d8142..6452bf616c 100644
--- a/actionmailer/lib/action_mailer/test_helper.rb
+++ b/actionmailer/lib/action_mailer/test_helper.rb
@@ -4,9 +4,9 @@ module ActionMailer
#
# def test_emails
# assert_emails 0
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver
# assert_emails 1
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver
# assert_emails 2
# end
#
@@ -15,12 +15,12 @@ module ActionMailer
#
# def test_emails_again
# assert_emails 1 do
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver
# end
#
# assert_emails 2 do
- # ContactMailer.deliver_contact
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver
+ # ContactMailer.welcome.deliver
# end
# end
def assert_emails(number)
@@ -38,7 +38,7 @@ module ActionMailer
#
# def test_emails
# assert_no_emails
- # ContactMailer.deliver_contact
+ # ContactMailer.welcome.deliver
# assert_emails 1
# end
#
diff --git a/actionmailer/lib/action_mailer/version.rb b/actionmailer/lib/action_mailer/version.rb
index 0cb5dc6d64..9d00091972 100644
--- a/actionmailer/lib/action_mailer/version.rb
+++ b/actionmailer/lib/action_mailer/version.rb
@@ -1,10 +1,11 @@
module ActionMailer
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActionMailer as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActionMailer.version.segments
+ STRING = ActionMailer.version.to_s
end
end
diff --git a/actionmailer/lib/rails/generators/mailer/USAGE b/actionmailer/lib/rails/generators/mailer/USAGE
index 7470289fd3..323bb8a87f 100644
--- a/actionmailer/lib/rails/generators/mailer/USAGE
+++ b/actionmailer/lib/rails/generators/mailer/USAGE
@@ -10,9 +10,8 @@ Example:
========
rails generate mailer Notifications signup forgot_password invoice
- creates a Notifications mailer class, views, test, and fixtures:
+ creates a Notifications mailer class, views, and test:
Mailer: app/mailers/notifications.rb
- Views: app/views/notifications/signup.erb [...]
+ Views: app/views/notifications/signup.text.erb [...]
Test: test/mailers/notifications_test.rb
- Fixtures: test/fixtures/notifications/signup [...]
diff --git a/actionmailer/test/abstract_unit.rb b/actionmailer/test/abstract_unit.rb
index 4b38d4bd31..aa18c512c7 100644
--- a/actionmailer/test/abstract_unit.rb
+++ b/actionmailer/test/abstract_unit.rb
@@ -8,15 +8,14 @@ silence_warnings do
Encoding.default_external = "UTF-8"
end
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'action_mailer'
require 'action_mailer/test_case'
-require 'active_support/queueing'
+require 'mail'
-silence_warnings do
- # These external dependencies have warnings :/
- require 'mail'
-end
+# Emulate AV railtie
+require 'action_view'
+ActionMailer::Base.send(:include, ActionView::Layouts)
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
@@ -60,5 +59,3 @@ end
def restore_delivery_method
ActionMailer::Base.delivery_method = @old_delivery_method
end
-
-ActiveSupport::Deprecation.silenced = true
diff --git a/actionmailer/test/asset_host_test.rb b/actionmailer/test/asset_host_test.rb
index 32ae76a7c5..00f1348a53 100644
--- a/actionmailer/test/asset_host_test.rb
+++ b/actionmailer/test/asset_host_test.rb
@@ -16,7 +16,6 @@ class AssetHostTest < ActiveSupport::TestCase
ActionMailer::Base.deliveries.clear
AssetHostMailer.configure do |c|
c.asset_host = "http://www.example.com"
- c.assets_dir = ''
end
end
diff --git a/actionmailer/test/base_test.rb b/actionmailer/test/base_test.rb
index 1cb3ce63fe..b74728ae34 100644
--- a/actionmailer/test/base_test.rb
+++ b/actionmailer/test/base_test.rb
@@ -3,13 +3,11 @@ require 'abstract_unit'
require 'set'
require 'action_dispatch'
-require 'active_support/queueing'
require 'active_support/time'
require 'mailers/base_mailer'
require 'mailers/proc_mailer'
require 'mailers/asset_mailer'
-require 'mailers/async_mailer'
class BaseTest < ActiveSupport::TestCase
def teardown
@@ -211,6 +209,12 @@ class BaseTest < ActiveSupport::TestCase
assert_equal "New Subject!", email.subject
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
+ 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
@@ -322,19 +326,6 @@ class BaseTest < ActiveSupport::TestCase
assert_not_nil(mail.content_type_parameters[:boundary])
end
- test "explicit multipart does not sort order" do
- order = ["text/html", "text/plain"]
- with_default BaseMailer, parts_order: order do
- email = BaseMailer.explicit_multipart
- assert_equal("text/plain", email.parts[0].mime_type)
- assert_equal("text/html", email.parts[1].mime_type)
-
- email = BaseMailer.explicit_multipart(parts_order: order.reverse)
- assert_equal("text/plain", email.parts[0].mime_type)
- assert_equal("text/html", email.parts[1].mime_type)
- end
- end
-
test "explicit multipart with attachments creates nested parts" do
email = BaseMailer.explicit_multipart(attachments: true)
assert_equal("application/pdf", email.parts[0].mime_type)
@@ -349,10 +340,10 @@ class BaseTest < ActiveSupport::TestCase
email = BaseMailer.explicit_multipart_templates
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
- assert_equal("text/html", email.parts[0].mime_type)
- assert_equal("HTML Explicit Multipart Templates", email.parts[0].body.encoded)
- assert_equal("text/plain", email.parts[1].mime_type)
- assert_equal("TEXT Explicit Multipart Templates", email.parts[1].body.encoded)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("TEXT Explicit Multipart Templates", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("HTML Explicit Multipart Templates", email.parts[1].body.encoded)
end
test "explicit multipart with format.any" do
@@ -387,10 +378,23 @@ class BaseTest < ActiveSupport::TestCase
email = BaseMailer.explicit_multipart_with_one_template
assert_equal(2, email.parts.size)
assert_equal("multipart/alternative", email.mime_type)
- assert_equal("text/html", email.parts[0].mime_type)
- assert_equal("[:html]", email.parts[0].body.encoded)
- assert_equal("text/plain", email.parts[1].mime_type)
- assert_equal("[:text]", email.parts[1].body.encoded)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("[:text]", email.parts[0].body.encoded)
+ assert_equal("text/html", email.parts[1].mime_type)
+ assert_equal("[:html]", email.parts[1].body.encoded)
+ end
+
+ test "explicit multipart with sort order" do
+ order = ["text/html", "text/plain"]
+ with_default BaseMailer, parts_order: order do
+ email = BaseMailer.explicit_multipart
+ assert_equal("text/html", email.parts[0].mime_type)
+ assert_equal("text/plain", email.parts[1].mime_type)
+
+ email = BaseMailer.explicit_multipart(parts_order: order.reverse)
+ assert_equal("text/plain", email.parts[0].mime_type)
+ assert_equal("text/html", email.parts[1].mime_type)
+ end
end
# Class level API with method missing
@@ -422,17 +426,6 @@ class BaseTest < ActiveSupport::TestCase
assert_equal(1, BaseMailer.deliveries.length)
end
- test "delivering message asynchronously" do
- AsyncMailer.delivery_method = :test
- AsyncMailer.deliveries.clear
-
- AsyncMailer.welcome.deliver
- assert_equal 0, AsyncMailer.deliveries.length
-
- AsyncMailer.queue.drain
- assert_equal 1, AsyncMailer.deliveries.length
- end
-
test "calling deliver, ActionMailer should yield back to mail to let it call :do_delivery on itself" do
mail = Mail::Message.new
mail.expects(:do_delivery).once
@@ -499,6 +492,18 @@ class BaseTest < ActiveSupport::TestCase
end
end
+ test 'the view is not rendered when mail was never called' do
+ mail = BaseMailer.without_mail_call
+ assert_equal('', mail.body.to_s.strip)
+ mail.deliver
+ end
+
+ test 'the return value of mailer methods is not relevant' do
+ mail = BaseMailer.with_nil_as_return_value
+ assert_equal('Welcome', mail.body.to_s.strip)
+ mail.deliver
+ end
+
# Before and After hooks
class MyObserver
@@ -573,14 +578,18 @@ class BaseTest < ActiveSupport::TestCase
assert(mail1.to_s.to_i > mail2.to_s.to_i)
end
+ test 'default values which have to_proc (e.g. symbols) should not be considered procs' do
+ assert(ProcMailer.welcome['x-has-to-proc'].to_s == 'symbol')
+ end
+
test "we can call other defined methods on the class as needed" do
mail = ProcMailer.welcome
assert_equal("Thanks for signing up this afternoon", mail.subject)
end
- test "modifying the mail message with a before_filter" do
- class BeforeFilterMailer < ActionMailer::Base
- before_filter :add_special_header!
+ test "modifying the mail message with a before_action" do
+ class BeforeActionMailer < ActionMailer::Base
+ before_action :add_special_header!
def welcome ; mail ; end
@@ -590,12 +599,12 @@ class BaseTest < ActiveSupport::TestCase
end
end
- assert_equal('Wow, so special', BeforeFilterMailer.welcome['X-Special-Header'].to_s)
+ assert_equal('Wow, so special', BeforeActionMailer.welcome['X-Special-Header'].to_s)
end
- test "modifying the mail message with an after_filter" do
- class AfterFilterMailer < ActionMailer::Base
- after_filter :add_special_header!
+ test "modifying the mail message with an after_action" do
+ class AfterActionMailer < ActionMailer::Base
+ after_action :add_special_header!
def welcome ; mail ; end
@@ -605,12 +614,12 @@ class BaseTest < ActiveSupport::TestCase
end
end
- assert_equal('Testing', AfterFilterMailer.welcome['X-Special-Header'].to_s)
+ assert_equal('Testing', AfterActionMailer.welcome['X-Special-Header'].to_s)
end
- test "adding an inline attachment using a before_filter" do
+ test "adding an inline attachment using a before_action" do
class DefaultInlineAttachmentMailer < ActionMailer::Base
- before_filter :add_inline_attachment!
+ before_action :add_inline_attachment!
def welcome ; mail ; end
@@ -662,13 +671,6 @@ class BaseTest < ActiveSupport::TestCase
assert_equal ["robert.pankowecki@gmail.com"], DefaultFromMailer.welcome.from
end
- test "Mailer.deliver_welcome calls Mailer.welcome.deliver" do
- BaseMailer.deliveries.clear
- BaseMailer.deliver_welcome(subject: 'omg')
- assert_equal 1, BaseMailer.deliveries.length
- assert_equal 'omg', BaseMailer.deliveries.first.subject
- end
-
protected
# Execute the block setting the given values and restoring old values after
diff --git a/actionmailer/test/delivery_methods_test.rb b/actionmailer/test/delivery_methods_test.rb
index 61a037ea18..20412c7bb2 100644
--- a/actionmailer/test/delivery_methods_test.rb
+++ b/actionmailer/test/delivery_methods_test.rb
@@ -152,6 +152,9 @@ class MailDeliveryTest < ActiveSupport::TestCase
assert_equal "overridden", delivery_method_instance.settings[:user_name]
assert_equal "somethingobtuse", delivery_method_instance.settings[:password]
assert_equal delivery_method_instance.settings.merge(overridden_options), delivery_method_instance.settings
+
+ # make sure that overriding delivery method options per mail instance doesn't affect the Base setting
+ assert_equal settings, ActionMailer::Base.smtp_settings
end
test "non registered delivery methods raises errors" do
diff --git a/actionmailer/test/fixtures/base_mailer/without_mail_call.erb b/actionmailer/test/fixtures/base_mailer/without_mail_call.erb
new file mode 100644
index 0000000000..290379d5fb
--- /dev/null
+++ b/actionmailer/test/fixtures/base_mailer/without_mail_call.erb
@@ -0,0 +1 @@
+<% raise 'the template should not be rendered' %> \ No newline at end of file
diff --git a/actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb b/actionmailer/test/fixtures/base_test/after_action_mailer/welcome.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionmailer/test/fixtures/base_test/after_filter_mailer/welcome.html.erb
+++ b/actionmailer/test/fixtures/base_test/after_action_mailer/welcome.html.erb
diff --git a/actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb b/actionmailer/test/fixtures/base_test/before_action_mailer/welcome.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionmailer/test/fixtures/base_test/before_filter_mailer/welcome.html.erb
+++ b/actionmailer/test/fixtures/base_test/before_action_mailer/welcome.html.erb
diff --git a/actionmailer/test/fixtures/raw_email10 b/actionmailer/test/fixtures/raw_email10
deleted file mode 100644
index edad5ccff1..0000000000
--- a/actionmailer/test/fixtures/raw_email10
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=X-UNKNOWN
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email12 b/actionmailer/test/fixtures/raw_email12
deleted file mode 100644
index 2cd31720d3..0000000000
--- a/actionmailer/test/fixtures/raw_email12
+++ /dev/null
@@ -1,32 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: image/jpeg
-Content-Transfer-Encoding: base64
-Content-Location: Photo25.jpg
-Content-ID: <qbFGyPQAS8>
-Content-Disposition: inline
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email13 b/actionmailer/test/fixtures/raw_email13
deleted file mode 100644
index 7d9314e36a..0000000000
--- a/actionmailer/test/fixtures/raw_email13
+++ /dev/null
@@ -1,29 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-13-196941151
-Content-Type: text/x-ruby-script; name="hello.rb"
-Content-Transfer-Encoding: 7bit
-Content-Disposition: attachment;
- filename="api.rb"
-
-puts "Hello, world!"
-gets
-
---Apple-Mail-13-196941151--
-
diff --git a/actionmailer/test/fixtures/raw_email2 b/actionmailer/test/fixtures/raw_email2
deleted file mode 100644
index 9f87bb2a98..0000000000
--- a/actionmailer/test/fixtures/raw_email2
+++ /dev/null
@@ -1,114 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-X-Original-To: xxxxx@xxxxx.xxxxxxxxx.com
-Delivered-To: xxxxx@xxxxx.xxxxxxxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 06C9DA98D
- for <xxxxx@xxxxx.xxxxxxxxx.com>; Sun, 8 May 2005 19:09:13 +0000 (GMT)
-Received: from xxxxx.xxxxxxxxx.com ([127.0.0.1])
- by localhost (xxxxx.xxxxxxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 88783-08 for <xxxxx@xxxxx.xxxxxxxxx.com>;
- Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from xxxxxxx.xxxxxxxxx.com (xxxxxxx.xxxxxxxxx.com [69.36.39.150])
- by xxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 10D8BA960
- for <xxxxx@xxxxxxxxx.org>; Sun, 8 May 2005 19:09:12 +0000 (GMT)
-Received: from zproxy.gmail.com (zproxy.gmail.com [64.233.162.199])
- by xxxxxxx.xxxxxxxxx.com (Postfix) with ESMTP id 9EBC4148EAB
- for <xxxxx@xxxxxxxxx.com>; Sun, 8 May 2005 14:09:11 -0500 (CDT)
-Received: by zproxy.gmail.com with SMTP id 13so1233405nzp
- for <xxxxx@xxxxxxxxx.com>; Sun, 08 May 2005 12:09:11 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=gmail.com;
- h=received:message-id:date:from:reply-to:to:subject:in-reply-to:mime-version:content-type:references;
- b=cid1mzGEFa3gtRa06oSrrEYfKca2CTKu9sLMkWxjbvCsWMtp9RGEILjUz0L5RySdH5iO661LyNUoHRFQIa57bylAbXM3g2DTEIIKmuASDG3x3rIQ4sHAKpNxP7Pul+mgTaOKBv+spcH7af++QEJ36gHFXD2O/kx9RePs3JNf/K8=
-Received: by 10.36.10.16 with SMTP id 16mr1012493nzj;
- Sun, 08 May 2005 12:09:11 -0700 (PDT)
-Received: by 10.36.5.10 with HTTP; Sun, 8 May 2005 12:09:11 -0700 (PDT)
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Just started to use my xxxxxxxx account (to set-up a GTD system,
-natch) and noticed that when I send content via email the signature/
-certificate from my email account gets added as a file (e.g.
-"smime.p7s").
-
-Obviously I can uncheck the signature option in the Mail compose
-window but how often will I remember to do that?
-
-Is there any way these kind of files could be ignored, e.g. via some
-sort of exclusions list?
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/pkcs7-signature; name=smime.p7s
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="smime.p7s"
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-o8xS3A0a1LXealcmlEbJibmKkEaoXci3MhryLgpaa+Kk/sH02SNatDO1vS28bPsibZpcc6deFrla
-hSYnL+PW54mDTGHIcCN2fbx/Y6qspzqmtKaXrv75NBtuy9cB6KzU4j2xXbTkAwz3pRSghJJaAwdp
-+yIivAD3vr0kJE3p+Ez34HMh33EXEpFoWcN+MCEQZD9WnmFViMrvfvMXLGVFQfAAcC060eGFSRJ1
-ZQ9UVQIDAQABoy0wKzAbBgNVHREEFDASgRBzbWhhdW5jaEBtYWMuY29tMAwGA1UdEwEB/wQCMAAw
-DQYJKoZIhvcNAQEEBQADgYEAQMrg1n2pXVWteP7BBj+Pk3UfYtbuHb42uHcLJjfjnRlH7AxnSwrd
-L3HED205w3Cq8T7tzVxIjRRLO/ljq0GedSCFBky7eYo1PrXhztGHCTSBhsiWdiyLWxKlOxGAwJc/
-lMMnwqLOdrQcoF/YgbjeaUFOQbUh94w9VDNpWZYCZwcwggM/MIICqKADAgECAgENMA0GCSqGSIb3
-DQEBBQUAMIHRMQswCQYDVQQGEwJaQTEVMBMGA1UECBMMV2VzdGVybiBDYXBlMRIwEAYDVQQHEwlD
-YXBlIFRvd24xGjAYBgNVBAoTEVRoYXd0ZSBDb25zdWx0aW5nMSgwJgYDVQQLEx9DZXJ0aWZpY2F0
-aW9uIFNlcnZpY2VzIERpdmlzaW9uMSQwIgYDVQQDExtUaGF3dGUgUGVyc29uYWwgRnJlZW1haWwg
-Q0ExKzApBgkqhkiG9w0BCQEWHHBlcnNvbmFsLWZyZWVtYWlsQHRoYXd0ZS5jb20wHhcNMDMwNzE3
-MDAwMDAwWhcNMTMwNzE2MjM1OTU5WjBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3RlIENv
-bnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWlsIElz
-c3VpbmcgQ0EwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGBAMSmPFVzVftOucqZWh5owHUEcJ3f
-6f+jHuy9zfVb8hp2vX8MOmHyv1HOAdTlUAow1wJjWiyJFXCO3cnwK4Vaqj9xVsuvPAsH5/EfkTYk
-KhPPK9Xzgnc9A74r/rsYPge/QIACZNenprufZdHFKlSFD0gEf6e20TxhBEAeZBlyYLf7AgMBAAGj
-gZQwgZEwEgYDVR0TAQH/BAgwBgEB/wIBADBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsLnRo
-YXd0ZS5jb20vVGhhd3RlUGVyc29uYWxGcmVlbWFpbENBLmNybDALBgNVHQ8EBAMCAQYwKQYDVR0R
-BCIwIKQeMBwxGjAYBgNVBAMTEVByaXZhdGVMYWJlbDItMTM4MA0GCSqGSIb3DQEBBQUAA4GBAEiM
-0VCD6gsuzA2jZqxnD3+vrL7CF6FDlpSdf0whuPg2H6otnzYvwPQcUCCTcDz9reFhYsPZOhl+hLGZ
-GwDFGguCdJ4lUJRix9sncVcljd2pnDmOjCBPZV+V2vf3h9bGCE6u9uo05RAaWzVNd+NWIXiC3CEZ
-Nd4ksdMdRv9dX2VPMYIC5zCCAuMCAQEwaTBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhhd3Rl
-IENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVtYWls
-IElzc3VpbmcgQ0ECAw5c+TAJBgUrDgMCGgUAoIIBUzAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcB
-MBwGCSqGSIb3DQEJBTEPFw0wNTA1MDgxODE3NDZaMCMGCSqGSIb3DQEJBDEWBBQSkG9j6+hB0pKp
-fV9tCi/iP59sNTB4BgkrBgEEAYI3EAQxazBpMGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIDDlz5MHoGCyqGSIb3DQEJEAILMWugaTBiMQswCQYDVQQGEwJaQTElMCMG
-A1UEChMcVGhhd3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNv
-bmFsIEZyZWVtYWlsIElzc3VpbmcgQ0ECAw5c+TANBgkqhkiG9w0BAQEFAASCAQAm1GeF7dWfMvrW
-8yMPjkhE+R8D1DsiCoWSCp+5gAQm7lcK7V3KrZh5howfpI3TmCZUbbaMxOH+7aKRKpFemxoBY5Q8
-rnCkbpg/++/+MI01T69hF/rgMmrGcrv2fIYy8EaARLG0xUVFSZHSP+NQSYz0TTmh4cAESHMzY3JA
-nHOoUkuPyl8RXrimY1zn0lceMXlweZRouiPGuPNl1hQKw8P+GhOC5oLlM71UtStnrlk3P9gqX5v7
-Tj7Hx057oVfY8FMevjxGwU3EK5TczHezHbWWgTyum9l2ZQbUQsDJxSniD3BM46C1VcbDLPaotAZ0
-fTYLZizQfm5hcWEbfYVzkSzLAAAAAAAA
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email3 b/actionmailer/test/fixtures/raw_email3
deleted file mode 100644
index 3a0927490a..0000000000
--- a/actionmailer/test/fixtures/raw_email3
+++ /dev/null
@@ -1,70 +0,0 @@
-From xxxx@xxxx.com Tue May 10 11:28:07 2005
-Return-Path: <xxxx@xxxx.com>
-X-Original-To: xxxx@xxxx.com
-Delivered-To: xxxx@xxxx.com
-Received: from localhost (localhost [127.0.0.1])
- by xxx.xxxxx.com (Postfix) with ESMTP id 50FD3A96F
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:50 +0000 (GMT)
-Received: from xxx.xxxxx.com ([127.0.0.1])
- by localhost (xxx.xxxxx.com [127.0.0.1]) (amavisd-new, port 10024)
- with LMTP id 70060-03 for <xxxx@xxxx.com>;
- Tue, 10 May 2005 17:26:49 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [69.36.39.150])
- by xxx.xxxxx.com (Postfix) with ESMTP id 8B957A94B
- for <xxxx@xxxx.com>; Tue, 10 May 2005 17:26:48 +0000 (GMT)
-Received: from xxx.xxxxx.com (xxx.xxxxx.com [64.233.184.203])
- by xxx.xxxxx.com (Postfix) with ESMTP id 9972514824C
- for <xxxx@xxxx.com>; Tue, 10 May 2005 12:26:40 -0500 (CDT)
-Received: by xxx.xxxxx.com with SMTP id 68so1694448wri
- for <xxxx@xxxx.com>; Tue, 10 May 2005 10:26:40 -0700 (PDT)
-DomainKey-Signature: a=rsa-sha1; q=dns; c=nofws;
- s=beta; d=xxxxx.com;
- h=received:message-id:date:from:reply-to:to:subject:mime-version:content-type;
- b=g8ZO5ttS6GPEMAz9WxrRk9+9IXBUfQIYsZLL6T88+ECbsXqGIgfGtzJJFn6o9CE3/HMrrIGkN5AisxVFTGXWxWci5YA/7PTVWwPOhJff5BRYQDVNgRKqMl/SMttNrrRElsGJjnD1UyQ/5kQmcBxq2PuZI5Zc47u6CILcuoBcM+A=
-Received: by 10.54.96.19 with SMTP id t19mr621017wrb;
- Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Received: by 10.54.110.5 with HTTP; Tue, 10 May 2005 10:26:39 -0700 (PDT)
-Message-ID: <xxxx@xxxx.com>
-Date: Tue, 10 May 2005 11:26:39 -0600
-From: Test Tester <xxxx@xxxx.com>
-Reply-To: Test Tester <xxxx@xxxx.com>
-To: xxxx@xxxx.com, xxxx@xxxx.com
-Subject: Another PDF
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_2192_32400445.1115745999735"
-X-Virus-Scanned: amavisd-new at textdrive.com
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-Just attaching another PDF, here, to see what the message looks like,
-and to see if I can figure out what is going wrong here.
-
-------=_Part_2192_32400445.1115745999735
-Content-Type: application/pdf; name="broken.pdf"
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment; filename="broken.pdf"
-
-JVBERi0xLjQNCiXk9tzfDQoxIDAgb2JqDQo8PCAvTGVuZ3RoIDIgMCBSDQogICAvRmlsdGVyIC9G
-bGF0ZURlY29kZQ0KPj4NCnN0cmVhbQ0KeJy9Wt2KJbkNvm/od6jrhZxYln9hWEh2p+8HBvICySaE
-ycLuTV4/1ifJ9qnq09NpSBimu76yLUuy/qzqcPz7+em3Ixx/CDc6CsXxs3b5+fvfjr/8cPz6/BRu
-rbfAx/n3739/fuJylJ5u5fjX81OuDr4deK4Bz3z/aDP+8fz0yw8g0Ofq7ktr1Mn+u28rvhy/jVeD
-QSa+9YNKHP/pxjvDNfVAx/m3MFz54FhvTbaseaxiDoN2LeMVMw+yA7RbHSCDzxZuaYB2E1Yay7QU
-x89vz0+tyFDKMlAHK5yqLmnjF+c4RjEiQIUeKwblXMe+AsZjN1J5yGQL5DHpDHksurM81rF6PKab
-gK6zAarIDzIiUY23rJsN9iorAE816aIu6lsgAdQFsuhhkHOUFgVjp2GjMqSewITXNQ27jrMeamkg
-1rPI3iLWG2CIaSBB+V1245YVRICGbbpYKHc2USFDl6M09acQVQYhlwIrkBNLISvXhGlF1wi5FHCw
-wxZkoGNJlVeJCEsqKA+3YAV5AMb6KkeaqEJQmFKKQU8T1pRi2ihE1Y4CDrqoYFFXYjJJOatsyzuI
-8SIlykuxKTMibWK8H1PgEvqYgs4GmQSrEjJAalgGirIhik+p4ZQN9E3ETFPAHE1b8pp1l/0Rc1gl
-fQs0ABWvyoZZzU8VnPXwVVcO9BEsyjEJaO6eBoZRyKGlrKoYoOygA8BGIzgwN3RQ15ouigG5idZQ
-fx2U4Db2CqiLO0WHAZoylGiCAqhniNQjFjQPSkmjwfNTgQ6M1Ih+eWo36wFmjIxDJZiGUBiWsAyR
-xX3EekGOizkGI96Ol9zVZTAivikURhRsHh2E3JhWMpSTZCnnonrLhMCodgrNcgo4uyJUJc6qnVss
-nrGd1Ptr0YwisCOYyIbUwVjV4xBUNLbguSO2YHujonAMJkMdSI7bIw91Akq2AUlMUWGFTMAOamjU
-OvZQCxIkY2pCpMFo/IwLdVLHs6nddwTRrgoVbvLU9eB0G4EMndV0TNoxHbt3JBWwK6hhv3iHfDtF
-yokB302IpEBTnWICde4uYc/1khDbSIkQopO6lcqamGBu1OSE3N5IPSsZX00CkSHRiiyx6HQIShsS
-HSVNswdVsaOUSAWq9aYhDtGDaoG5a3lBGkYt/lFlBFt1UqrYnzVtUpUQnLiZeouKgf1KhRBViRRk
-ExepJCzTwEmFDalIRbLEGtw0gfpESOpIAF/NnpPzcVCG86s0g2DuSyd41uhNGbEgaSrWEXORErbw
-------=_Part_2192_32400445.1115745999735--
-
diff --git a/actionmailer/test/fixtures/raw_email4 b/actionmailer/test/fixtures/raw_email4
deleted file mode 100644
index 639ad40e49..0000000000
--- a/actionmailer/test/fixtures/raw_email4
+++ /dev/null
@@ -1,59 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id 6AAEE3B4D23 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:23 -0500
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id j48HUC213279 for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:13 -0500
-Received: from conversion-xxx.xxxx.xxx.net by xxx.xxxx.xxx id <0IG600901LQ64I@xxx.xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Received: from agw1 by xxx.xxxx.xxx with ESMTP id <0IG600JFYLYCAxxx@xxxx.xxx> for <xxx@xxxx.xxx>; Sun, 8 May 2005 12:30:12 -0500
-Date: Sun, 8 May 2005 12:30:08 -0500
-From: xxx@xxxx.xxx
-To: xxx@xxxx.xxx
-Message-Id: <7864245.1115573412626.JavaMxxx@xxxx.xxx>
-Subject: Filth
-Mime-Version: 1.0
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4ca329_133ae40413c81ef
-X-Mms-Priority: 1
-X-Mms-Transaction-Id: 3198421808-0
-X-Mms-Message-Type: 0
-X-Mms-Sender-Visibility: 1
-X-Mms-Read-Reply: 1
-X-Original-To: xxx@xxxx.xxx
-X-Mms-Message-Class: 0
-X-Mms-Delivery-Report: 0
-X-Mms-Mms-Version: 16
-Delivered-To: xxx@xxxx.xxx
-X-Nokia-Ag-Version: 2.0
-
-This is a multi-part message in MIME format.
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: multipart/mixed; boundary=mimepart_427e4cb4cbd97_133ae40413c8217
-
-
-
---mimepart_427e4cb4cbd97_133ae40413c8217
-Content-Type: text/plain; charset=utf-8
-Content-Transfer-Encoding: 7bit
-Content-Disposition: inline
-Content-Location: text.txt
-
-Some text
-
---mimepart_427e4cb4cbd97_133ae40413c8217--
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-Content-Type: text/plain; charset=us-ascii
-Content-Transfer-Encoding: 7bit
-
-
---
-This Orange Multi Media Message was sent wirefree from an Orange
-MMS phone. If you would like to reply, please text or phone the
-sender directly by using the phone number listed in the sender's
-address. To learn more about Orange's Multi Media Messaging
-Service, find us on the Web at xxx.xxxx.xxx.uk/mms
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef
-
-
---mimepart_427e4cb4ca329_133ae40413c81ef-
-
diff --git a/actionmailer/test/fixtures/raw_email5 b/actionmailer/test/fixtures/raw_email5
deleted file mode 100644
index bbe31bcdc5..0000000000
--- a/actionmailer/test/fixtures/raw_email5
+++ /dev/null
@@ -1,19 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email6 b/actionmailer/test/fixtures/raw_email6
deleted file mode 100644
index 8e37bd7392..0000000000
--- a/actionmailer/test/fixtures/raw_email6
+++ /dev/null
@@ -1,20 +0,0 @@
-Return-Path: <xxx@xxxx.xxx>
-Received: from xxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id C1B953B4CB6 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:05 -0500
-Received: from SMS-GTYxxx.xxxx.xxx by xxx.xxxx.xxx with ESMTP id ca for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:04 -0500
-Received: from xxx.xxxx.xxx by SMS-GTYxxx.xxxx.xxx with ESMTP id j4AKR3r23323 for <xxxxx@Exxx.xxxx.xxx>; Tue, 10 May 2005 15:27:03 -0500
-Date: Tue, 10 May 2005 15:27:03 -0500
-From: xxx@xxxx.xxx
-Sender: xxx@xxxx.xxx
-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Message-Id: <xxx@xxxx.xxx>
-X-Original-To: xxxxxxxxxxx@xxxx.xxxx.xxx
-Delivered-To: xxx@xxxx.xxx
-Importance: normal
-Content-Type: text/plain; charset=us-ascii
-
-Test test. Hi. Waving. m
-
-----------------------------------------------------------------
-Sent via Bell Mobility's Text Messaging service.
-Envoyé par le service de messagerie texte de Bell Mobilité.
-----------------------------------------------------------------
diff --git a/actionmailer/test/fixtures/raw_email7 b/actionmailer/test/fixtures/raw_email7
deleted file mode 100644
index da64ada8a5..0000000000
--- a/actionmailer/test/fixtures/raw_email7
+++ /dev/null
@@ -1,66 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Content-Type: multipart/mixed; boundary=Apple-Mail-13-196941151
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-
-
---Apple-Mail-13-196941151
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-12-196940926
-
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain;
- charset=ISO-8859-1;
- delsp=yes;
- format=flowed
-
-This is the first part.
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/x-ruby-script;
- x-unix-mode=0666;
- name="test.rb"
-Content-Disposition: attachment;
- filename=test.rb
-
-puts "testing, testing"
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: base64
-Content-Type: application/pdf;
- x-unix-mode=0666;
- name="test.pdf"
-Content-Disposition: inline;
- filename=test.pdf
-
-YmxhaCBibGFoIGJsYWg=
-
---Apple-Mail-12-196940926
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-
-
---Apple-Mail-12-196940926--
-
---Apple-Mail-13-196941151
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-jamisSqGSIb3DQEHAqCAMIjamisxCzAJBgUrDgMCGgUAMIAGCSqGSjamisEHAQAAoIIFSjCCBUYw
-ggQujamisQICBD++ukQwDQYJKojamisNAQEFBQAwMTELMAkGA1UEBhMCRjamisAKBgNVBAoTA1RE
-QzEUMBIGjamisxMLVERDIE9DRVMgQ0jamisNMDQwMjI5MTE1OTAxWhcNMDYwMjamisIyOTAxWjCB
-gDELMAkGA1UEjamisEsxKTAnBgNVBAoTIEjamisuIG9yZ2FuaXNhdG9yaXNrIHRpbjamisRuaW5=
-
---Apple-Mail-13-196941151--
diff --git a/actionmailer/test/fixtures/raw_email8 b/actionmailer/test/fixtures/raw_email8
deleted file mode 100644
index 79996365b3..0000000000
--- a/actionmailer/test/fixtures/raw_email8
+++ /dev/null
@@ -1,47 +0,0 @@
-From xxxxxxxxx.xxxxxxx@gmail.com Sun May 8 19:07:09 2005
-Return-Path: <xxxxxxxxx.xxxxxxx@gmail.com>
-Message-ID: <e85734b90505081209eaaa17b@mail.gmail.com>
-Date: Sun, 8 May 2005 14:09:11 -0500
-From: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-Reply-To: xxxxxxxxx xxxxxxx <xxxxxxxxx.xxxxxxx@gmail.com>
-To: xxxxx xxxx <xxxxx@xxxxxxxxx.com>
-Subject: Fwd: Signed email causes file attachments
-In-Reply-To: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-Mime-Version: 1.0
-Content-Type: multipart/mixed;
- boundary="----=_Part_5028_7368284.1115579351471"
-References: <F6E2D0B4-CC35-4A91-BA4C-C7C712B10C13@mac.com>
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: text/plain; charset=ISO-8859-1
-Content-Transfer-Encoding: quoted-printable
-Content-Disposition: inline
-
-We should not include these files or vcards as attachments.
-
----------- Forwarded message ----------
-From: xxxxx xxxxxx <xxxxxxxx@xxx.com>
-Date: May 8, 2005 1:17 PM
-Subject: Signed email causes file attachments
-To: xxxxxxx@xxxxxxxxxx.com
-
-
-Hi,
-
-Test attachments oddly encoded with japanese charset.
-
-
-------=_Part_5028_7368284.1115579351471
-Content-Type: application/octet-stream; name*=iso-2022-jp'ja'01%20Quien%20Te%20Dij%8aat.%20Pitbull.mp3
-Content-Transfer-Encoding: base64
-Content-Disposition: attachment
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGFDCCAs0w
-ggI2oAMCAQICAw5c+TANBgkqhkiG9w0BAQQFADBiMQswCQYDVQQGEwJaQTElMCMGA1UEChMcVGhh
-d3RlIENvbnN1bHRpbmcgKFB0eSkgTHRkLjEsMCoGA1UEAxMjVGhhd3RlIFBlcnNvbmFsIEZyZWVt
-YWlsIElzc3VpbmcgQ0EwHhcNMDUwMzI5MDkzOTEwWhcNMDYwMzI5MDkzOTEwWjBCMR8wHQYDVQQD
-ExZUaGF3dGUgRnJlZW1haWwgTWVtYmVyMR8wHQYJKoZIhvcNAQkBFhBzbWhhdW5jaEBtYWMuY29t
-MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAn90dPsYS3LjfMY211OSYrDQLzwNYPlAL
-7+/0XA+kdy8/rRnyEHFGwhNCDmg0B6pxC7z3xxJD/8GfCd+IYUUNUQV5m9MkxfP9pTVXZVIYLaBw
-------=_Part_5028_7368284.1115579351471--
-
diff --git a/actionmailer/test/fixtures/raw_email9 b/actionmailer/test/fixtures/raw_email9
deleted file mode 100644
index 02ea0b05c5..0000000000
--- a/actionmailer/test/fixtures/raw_email9
+++ /dev/null
@@ -1,28 +0,0 @@
-Received: from xxx.xxx.xxx ([xxx.xxx.xxx.xxx] verified)
- by xxx.com (CommuniGate Pro SMTP 4.2.8)
- with SMTP id 2532598 for xxx@xxx.com; Wed, 23 Feb 2005 17:51:49 -0500
-Received-SPF: softfail
- receiver=xxx.com; client-ip=xxx.xxx.xxx.xxx; envelope-from=xxx@xxx.xxx
-quite Delivered-To: xxx@xxx.xxx
-Received: by xxx.xxx.xxx (Wostfix, from userid xxx)
- id 0F87F333; Wed, 23 Feb 2005 16:16:17 -0600
-Date: Wed, 23 Feb 2005 18:20:17 -0400
-From: "xxx xxx" <xxx@xxx.xxx>
-Message-ID: <4D6AA7EB.6490534@xxx.xxx>
-To: xxx@xxx.com
-Subject: Stop adware/spyware once and for all.
-X-Scanned-By: MIMEDefang 2.11 (www dot roaringpenguin dot com slash mimedefang)
-
-You are infected with:
-Ad Ware and Spy Ware
-
-Get your free scan and removal download now,
-before it gets any worse.
-
-http://xxx.xxx.info?aid=3D13&?stat=3D4327kdzt
-
-
-
-
-no more? (you will still be infected)
-http://xxx.xxx.info/discon/?xxx@xxx.com
diff --git a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a b/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
deleted file mode 100644
index 8a2c25a5dd..0000000000
--- a/actionmailer/test/fixtures/raw_email_quoted_with_0d0a
+++ /dev/null
@@ -1,14 +0,0 @@
-Mime-Version: 1.0 (Apple Message framework v730)
-Message-Id: <9169D984-4E0B-45EF-82D4-8F5E53AD7012@example.com>
-From: foo@example.com
-Subject: testing
-Date: Mon, 6 Jun 2005 22:21:22 +0200
-To: blah@example.com
-Content-Transfer-Encoding: quoted-printable
-Content-Type: text/plain
-
-A fax has arrived from remote ID ''.=0D=0A-----------------------=
--------------------------------------=0D=0ATime: 3/9/2006 3:50:52=
- PM=0D=0AReceived from remote ID: =0D=0AInbound user ID XXXXXXXXXX, r=
-outing code XXXXXXXXX=0D=0AResult: (0/352;0/0) Successful Send=0D=0AP=
-age record: 1 - 1=0D=0AElapsed time: 00:58 on channel 11=0D=0A
diff --git a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type b/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
deleted file mode 100644
index a8ff7ed4cb..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_invalid_characters_in_content_type
+++ /dev/null
@@ -1,104 +0,0 @@
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
-Return-Path: <mikel.other@baci>
-Received: from some.isp.com by baci with ESMTP id 632BD5758 for <mikel.lindsaar@baci>; Sun, 21 Oct 2007 19:38:21 +1000
-Date: Sun, 21 Oct 2007 19:38:13 +1000
-From: Mikel Lindsaar <mikel.other@baci>
-Reply-To: Mikel Lindsaar <mikel.other@baci>
-To: mikel.lindsaar@baci
-Message-Id: <009601c813c6$19df3510$0437d30a@mikel091a>
-Subject: Testing outlook
-Mime-Version: 1.0
-Content-Type: multipart/alternative; boundary=----=_NextPart_000_0093_01C81419.EB75E850
-Delivered-To: mikel.lindsaar@baci
-X-Mimeole: Produced By Microsoft MimeOLE V6.00.2900.3138
-X-Msmail-Priority: Normal
-
-This is a multi-part message in MIME format.
-
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/plain; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-Hello
-This is an outlook test
-
-So there.
-
-Me.
-
-------=_NextPart_000_0093_01C81419.EB75E850
-Content-Type: text/html; charset=iso-8859-1
-Content-Transfer-Encoding: Quoted-printable
-
-<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
-<HTML><HEAD>
-<META http-equiv=3DContent-Type content=3D"text/html; =
-charset=3Diso-8859-1">
-<META content=3D"MSHTML 6.00.6000.16525" name=3DGENERATOR>
-<STYLE></STYLE>
-</HEAD>
-<BODY bgColor=3D#ffffff>
-<DIV><FONT face=3DArial size=3D2>Hello</FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>This is an outlook=20
-test</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG></STRONG></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2><STRONG>So there.</STRONG></FONT></DIV>
-<DIV><FONT face=3DArial size=3D2></FONT>&nbsp;</DIV>
-<DIV><FONT face=3DArial size=3D2>Me.</FONT></DIV></BODY></HTML>
-
-
-------=_NextPart_000_0093_01C81419.EB75E850--
-
-
diff --git a/actionmailer/test/fixtures/raw_email_with_nested_attachment b/actionmailer/test/fixtures/raw_email_with_nested_attachment
deleted file mode 100644
index 429c408c5d..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_nested_attachment
+++ /dev/null
@@ -1,100 +0,0 @@
-From jamis@37signals.com Thu Feb 22 11:20:31 2007
-Mime-Version: 1.0 (Apple Message framework v752.3)
-Message-Id: <2CCE0408-10C7-4045-9B16-A1C11C31469B@37signals.com>
-Content-Type: multipart/signed;
- micalg=sha1;
- boundary=Apple-Mail-42-587703407;
- protocol="application/pkcs7-signature"
-To: Jamis Buck <jamis@jamisbuck.org>
-Subject: Testing attachments
-From: Jamis Buck <jamis@37signals.com>
-Date: Thu, 22 Feb 2007 11:20:31 -0700
-
-
---Apple-Mail-42-587703407
-Content-Type: multipart/mixed;
- boundary=Apple-Mail-41-587703287
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: 7bit
-Content-Type: text/plain;
- charset=US-ASCII;
- format=flowed
-
-Here is a test of an attachment via email.
-
-- Jamis
-
-
---Apple-Mail-41-587703287
-Content-Transfer-Encoding: base64
-Content-Type: image/png;
- x-unix-mode=0644;
- name=byo-ror-cover.png
-Content-Disposition: inline;
- filename=truncated.png
-
-iVBORw0KGgoAAAANSUhEUgAAAKUAAADXCAYAAAB7wZEQAAAABHNCSVQICAgIfAhkiAAAAAlwSFlz
-AAALEgAACxIB0t1+/AAAABd0RVh0Q3JlYXRpb24gVGltZQAxLzI1LzIwMDeD9CJVAAAAGHRFWHRT
-b2Z0d2FyZQBBZG9iZSBGaXJld29ya3NPsx9OAAAyBWlUWHRYTUw6Y29tLmFkb2JlLnhtcDw/eHBh
-Y2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+Cjx4OnhtcG1l
-dGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDQuMS1j
-MDIwIDEuMjU1NzE2LCBUdWUgT2N0IDEwIDIwMDYgMjM6MTY6MzQiPgogICA8cmRmOlJERiB4bWxu
-czpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgogICAg
-ICA8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIgogICAgICAgICAgICB4bWxuczp4YXA9Imh0
-dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgogICAgICAgICA8eGFwOkNyZWF0b3JUb29sPkFk
-b2JlIEZpcmV3b3JrcyBDUzM8L3hhcDpDcmVhdG9yVG9vbD4KICAgICAgICAgPHhhcDpDcmVhdGVE
-YXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6Q3JlYXRlRGF0ZT4KICAgICAgICAgPHhhcDpN
-b2RpZnlEYXRlPjIwMDctMDEtMjVUMDU6Mjg6MjFaPC94YXA6TW9kaWZ5RGF0ZT4KICAgICAgPC9y
-ZGY6RGVzY3JpcHRpb24+CiAgICAgIDxyZGY6RGVzY3JpcHRpb24gcmRmOmFib3V0PSIiCiAgICAg
-ICAgICAgIHhtbG5zOmRjPSJodHRwOi8vcHVybC5vcmcvZGMvZWxlbWVudHMvMS4xLyI+CiAgICAg
-ICAgIDxkYzpmb3JtYXQ+aW1hZ2UvcG5nPC9kYzpmb3JtYXQ+CiAgICAgIDwvcmRmOkRlc2NyaXB0
-hhojpmnJMfaYFmSkXWg5PGCmHXVj/c9At0hSK2xGdd8F3muk0VFjb4f5Ue0ksQ8qAcq0delaXhdb
-DjKNnF+3B3t9kObZYmk7AZgWYqO9anpR3wpM9sQ5XslB9a+kWyTtNb0fOmudzGHfPFBQDKesyycm
-DBL7Cw5bXjIEuci+SSOm/LYnXDZu6iuPEj8lYBb+OU8xx1f9m+e5rhJiYKqjo5vHfiZp+VUkW9xc
-Ufd6JHNWc47PkQqb9ie3SLEZB/ZqyAssiqURY+G35iOMZUrHbasHnb80QAPv9FHtAbJIyro7bi5b
-ai2TEAKen5+LJNWrglZjm3UbZvt7KryA2J5b5J1jZF8kL6GzvG1Zqx54Y1y7J7n20wMOt9frG2sW
-uwGP07kNz3732vf6bfvAvLldfS+9fts2euXY37D+R29FGZdlnhzV4TTFmPJduBP2RbNNua4rTqcT
-Qt7Xy1KUB0AHSdP5AZQYvHZg7WD1XvYeMO1A9HhZPqMX5KXbMBrn2efxns/ee21674efxz4Tp/fq
-2HZ648dgYaC1i3Vq1IbNPq3PvDTPezY9FaRISjvnzWqdgcWN8EJgjnNq+Z7ktOm9l2Nfth28EZi4
-bG/we5JwxM+Tql47/D/X6b38I8/RyxvxPJrX6zvQbo3h9jyJx+C0ALX327QETHl5eYlaYCT5rPTb
-+5/rAq26t3lKIxV/p88hq6ptngdgCzoPjJqndiLfc/6y5A14WeDFGNPct4iUsJBV2bYzLEV7m83s
-6Rp63VPhHKC/g/LzaU9qexJRr56043JWinqAtfZqsSm1sjoznthl54dtCqv+uL4nIY+oYWuc3+nH
-kGfn8b0HQpvOYLQAZUDanbJs3jQhITZEgdarZK+cO6ySlL13rut5nFaN23s7u3Snz6eRPTkCoc2/
-Vp1zHfZVFpZ87FiMVLV1iqyK5rlzfji2GzjfDsodlD+Weo5UD4h6PwKqzQMqID0tq2VjjFVSMpis
-ZLRAs7sePZBZAHI+gIanB8I7MD+femAceeUe2Kxa5jS950kZ1p5eNEdeX1+jFmSpZ+1EdWCsDcne
-NPNgUHNw3aYpnzv9PGTX0uo94EtN9qq1rOdxe3kc79T8ukeHJJ8Fnxej6qlylbLLsjQLOy6Xy2a1
-kefs/N+nM7+S7IG5/E5Yc7F003pWErLjbH0O5cGadiMptSB/DZ5U5DI9yeg5MFYyMj8lC/Y7/Xjq
-OZlWcnpg9aQfXz2HRq+Wn5xOp6gN8tWq8R44e2pfyzLYemEgprst+XXk2Zj2nXlbsG05BprndTMv
-C3QRaXczshhVsHnMgfYn80Y2g5JureA6wBasPeP7LkE/jvZMJAaf/g/U2RelHsisvan5FqweIAHg
-Pwc7L68GxvVDAAAAAElFTkSuQmCC
-
---Apple-Mail-41-587703287--
-
---Apple-Mail-42-587703407
-Content-Transfer-Encoding: base64
-Content-Type: application/pkcs7-signature;
- name=smime.p7s
-Content-Disposition: attachment;
- filename=smime.p7s
-
-MIAGCSqGSIb3DQEHAqCAMIACAQExCzAJBgUrDgMCGgUAMIAGCSqGSIb3DQEHAQAAoIIGJzCCAuAw
-ggJJoAMCAQICEFjnFNYXwDEZRWY5EkfzopUwDQYJKoZIhvcNAQEFBQAwYjELMAkGA1UEBhMCWkEx
-JTAjBgNVBAoTHFRoYXd0ZSBDb25zdWx0aW5nIChQdHkpIEx0ZC4xLDAqBgNVBAMTI1RoYXd0ZSBQ
-ZXJzb25hbCBGcmVlbWFpbCBJc3N1aW5nIENBMB4XDTA2MDkxMjE3MDExMloXDTA3MDkxMjE3MDEx
-MlowRTEfMB0GA1UEAxMWVGhhd3RlIEZyZWVtYWlsIE1lbWJlcjEiMCAGCSqGSIb3DQEJARYTamFt
-aXNAMzdzaWduYWxzLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAO2A9JeOFIFJ
-G6z8pTcAldrZ2nMe+Xb1tNrbHgoVzN/QhHXM4qst2Ml93cmFLjMmwG7P9RJeU4oNx+jTqVoBB7NV
-Ne1/o56Do0KhfMZ9iUDQdPLbkZMq4EEpFMdm6PyM3muRKwPhj66iAWe/osCb8DowUK2f66vaRx0Z
-Y0MQHIIrXE02Ta4IfAhIfPqBLkZ4WgTYBHN9vMdYea1jF0GO4gqGk1wqwb3yxv2QMYMbwJ6SI+k/
-ZjkSR/OilTCBhwYLKoZIhvcNAQkQAgsxeKB2MGIxCzAJBgNVBAYTAlpBMSUwIwYDVQQKExxUaGF3
-dGUgQ29uc3VsdGluZyAoUHR5KSBMdGQuMSwwKgYDVQQDEyNUaGF3dGUgUGVyc29uYWwgRnJlZW1h
-aWwgSXNzdWluZyBDQQIQWOcU1hfAMRlFZjkSR/OilTANBgkqhkiG9w0BAQEFAASCAQCfwQiC3v6/
-yleRDGv3bJ4nQYQ+c3mz3+mn3Xi6uU35n3piwxZZaWRdmLyiXPvU+QReHpSf3l2qsEZM3sdE0XF9
-eRul/+QTFJcDNXOEAxG1zC2Gpz+6c6RrX4Ou12Pwkp+pNrZWTSY/mZgdqcArupOBcZi7qBjoWcy5
-wb54dfvSSjrjmqLbkH/E8ww/6gGQuU/xXpAUZgUrTmQHrNKeIdSh5oDkOxFaFWvnmb8Z/2ixKqW/
-Ux6WqamyvBtTs/5YBEtnpZOk+uVoscYEUBhU+DVJ2OSvTdXSivMtBdXmGTsG22k+P1NGUHi/A7ev
-xPaO0uk4V8xyjNlN4HPuGpkrlXwPAAAAAAAA
-
---Apple-Mail-42-587703407--
diff --git a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject b/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
deleted file mode 100644
index e86108da1e..0000000000
--- a/actionmailer/test/fixtures/raw_email_with_partially_quoted_subject
+++ /dev/null
@@ -1,14 +0,0 @@
-From jamis@37signals.com Mon May 2 16:07:05 2005
-Mime-Version: 1.0 (Apple Message framework v622)
-Content-Transfer-Encoding: base64
-Message-Id: <d3b8cf8e49f04480850c28713a1f473e@37signals.com>
-Content-Type: text/plain;
- charset=EUC-KR;
- format=flowed
-To: jamis@37signals.com
-From: Jamis Buck <jamis@37signals.com>
-Subject: Re: Test: =?UTF-8?B?Iua8ouWtlyI=?= mid =?UTF-8?B?Iua8ouWtlyI=?= tail
-Date: Mon, 2 May 2005 16:07:05 -0600
-
-tOu6zrrQwMcguLbC+bChwfa3ziwgv+y4rrTCIMfPs6q01MC7ILnPvcC0z7TZLg0KDQrBpiDAzLin
-wLogSmFtaXPA1LTPtNku
diff --git a/actionmailer/test/i18n_with_controller_test.rb b/actionmailer/test/i18n_with_controller_test.rb
index a3e93c9c31..ab5eaaa9d5 100644
--- a/actionmailer/test/i18n_with_controller_test.rb
+++ b/actionmailer/test/i18n_with_controller_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'action_view'
require 'action_controller'
class I18nTestMailer < ActionMailer::Base
@@ -14,6 +15,9 @@ class I18nTestMailer < ActionMailer::Base
end
end
+# Emulate AV railtie
+ActionController::Base.superclass.send(:include, ActionView::Layouts)
+
class TestController < ActionController::Base
def send_mail
I18nTestMailer.mail_with_i18n_subject("test@localhost").deliver
diff --git a/actionmailer/test/log_subscriber_test.rb b/actionmailer/test/log_subscriber_test.rb
index 5f52a1bd69..5f0bee88fd 100644
--- a/actionmailer/test/log_subscriber_test.rb
+++ b/actionmailer/test/log_subscriber_test.rb
@@ -24,10 +24,13 @@ class AMLogSubscriberTest < ActionMailer::TestCase
def test_deliver_is_notified
BaseMailer.welcome.deliver
wait
+
assert_equal(1, @logger.logged(:info).size)
assert_match(/Sent mail to system@test.lindsaar.net/, @logger.logged(:info).first)
- assert_equal(1, @logger.logged(:debug).size)
- assert_match(/Welcome/, @logger.logged(:debug).first)
+
+ assert_equal(2, @logger.logged(:debug).size)
+ assert_match(/BaseMailer#welcome: processed outbound mail in [\d.]+ms/, @logger.logged(:debug).first)
+ assert_match(/Welcome/, @logger.logged(:debug).second)
end
def test_receive_is_notified
@@ -39,4 +42,4 @@ class AMLogSubscriberTest < ActionMailer::TestCase
assert_equal(1, @logger.logged(:debug).size)
assert_match(/Jamis/, @logger.logged(:debug).first)
end
-end \ No newline at end of file
+end
diff --git a/actionmailer/test/mailers/async_mailer.rb b/actionmailer/test/mailers/async_mailer.rb
deleted file mode 100644
index c21a464f38..0000000000
--- a/actionmailer/test/mailers/async_mailer.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-class AsyncMailer < BaseMailer
- self.queue = ActiveSupport::TestQueue.new
-end
diff --git a/actionmailer/test/mailers/base_mailer.rb b/actionmailer/test/mailers/base_mailer.rb
index f25d9b9aff..6584bf5195 100644
--- a/actionmailer/test/mailers/base_mailer.rb
+++ b/actionmailer/test/mailers/base_mailer.rb
@@ -38,7 +38,7 @@ class BaseMailer < ActionMailer::Base
end
def attachment_with_hash
- attachments['invoice.jpg'] = { data: "\312\213\254\232)b",
+ attachments['invoice.jpg'] = { data: ::Base64.encode64("\312\213\254\232)b"),
mime_type: "image/x-jpg",
transfer_encoding: "base64" }
mail
@@ -115,4 +115,16 @@ class BaseMailer < ActionMailer::Base
def email_with_translations
mail body: render("email_with_translations", formats: [:html])
end
+
+ def without_mail_call
+ end
+
+ def with_nil_as_return_value
+ mail(:template_name => "welcome")
+ nil
+ end
+
+ def with_subject_interpolations
+ mail(subject: default_i18n_subject(rapper_or_impersonator: 'Slim Shady'), body: '')
+ end
end
diff --git a/actionmailer/test/mailers/proc_mailer.rb b/actionmailer/test/mailers/proc_mailer.rb
index 733633b575..7e189d861f 100644
--- a/actionmailer/test/mailers/proc_mailer.rb
+++ b/actionmailer/test/mailers/proc_mailer.rb
@@ -1,7 +1,8 @@
class ProcMailer < ActionMailer::Base
default to: 'system@test.lindsaar.net',
'X-Proc-Method' => Proc.new { Time.now.to_i.to_s },
- subject: Proc.new { give_a_greeting }
+ subject: Proc.new { give_a_greeting },
+ 'x-has-to-proc' => :symbol
def welcome
mail
diff --git a/actionmailer/test/spec_type_test.rb b/actionmailer/test/spec_type_test.rb
deleted file mode 100644
index 90db59c2d2..0000000000
--- a/actionmailer/test/spec_type_test.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'abstract_unit'
-
-class NotificationMailer < ActionMailer::Base; end
-class Notifications < ActionMailer::Base; end
-
-class SpecTypeTest < ActiveSupport::TestCase
- def assert_mailer actual
- assert_equal ActionMailer::TestCase, actual
- end
-
- def refute_mailer actual
- refute_equal ActionMailer::TestCase, actual
- end
-
- def test_spec_type_resolves_for_class_constants
- assert_mailer MiniTest::Spec.spec_type(NotificationMailer)
- assert_mailer MiniTest::Spec.spec_type(Notifications)
- end
-
- def test_spec_type_resolves_for_matching_strings
- assert_mailer MiniTest::Spec.spec_type("WidgetMailer")
- assert_mailer MiniTest::Spec.spec_type("WidgetMailerTest")
- assert_mailer MiniTest::Spec.spec_type("Widget Mailer Test")
- # And is not case sensitive
- assert_mailer MiniTest::Spec.spec_type("widgetmailer")
- assert_mailer MiniTest::Spec.spec_type("widgetmailertest")
- assert_mailer MiniTest::Spec.spec_type("widget mailer test")
- end
-
- def test_spec_type_wont_match_non_space_characters
- refute_mailer MiniTest::Spec.spec_type("Widget Mailer\tTest")
- refute_mailer MiniTest::Spec.spec_type("Widget Mailer\rTest")
- refute_mailer MiniTest::Spec.spec_type("Widget Mailer\nTest")
- refute_mailer MiniTest::Spec.spec_type("Widget Mailer\fTest")
- refute_mailer MiniTest::Spec.spec_type("Widget MailerXTest")
- end
-end
diff --git a/actionmailer/test/test_test.rb b/actionmailer/test/test_test.rb
index 139eb53359..86fd37bea6 100644
--- a/actionmailer/test/test_test.rb
+++ b/actionmailer/test/test_test.rb
@@ -26,147 +26,3 @@ class CrazyStringNameMailerTest < ActionMailer::TestCase
assert_equal TestTestMailer, self.class.mailer_class
end
end
-
-describe TestTestMailer do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe TestTestMailer, :action do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe TestTestMailer do
- describe "nested" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe TestTestMailer, :action do
- describe "nested" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "TestTestMailer" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "TestTestMailerTest" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "TestTestMailer" do
- describe "nested" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "TestTestMailerTest" do
- describe "nested" do
- it "gets the mailer from the test name" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "AnotherCrazySymbolNameMailerTest" do
- tests :test_test_mailer
-
- it "gets the mailer after setting it with a symbol" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "AnotherCrazyStringNameMailerTest" do
- tests 'test_test_mailer'
-
- it "gets the mailer after setting it with a string" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "Another Crazy Name Mailer Test" do
- tests TestTestMailer
-
- it "gets the mailer after setting it manually" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "Another Crazy Symbol Name Mailer Test" do
- tests :test_test_mailer
-
- it "gets the mailer after setting it with a symbol" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "Another Crazy String Name Mailer Test" do
- tests 'test_test_mailer'
-
- it "gets the mailer after setting it with a string" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
-
-describe "AnotherCrazySymbolNameMailerTest" do
- tests :test_test_mailer
-
- describe "nested" do
- it "gets the mailer after setting it with a symbol" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "AnotherCrazyStringNameMailerTest" do
- tests 'test_test_mailer'
-
- describe "nested" do
- it "gets the mailer after setting it with a string" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "Another Crazy Name Mailer Test" do
- tests TestTestMailer
-
- describe "nested" do
- it "gets the mailer after setting it manually" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "Another Crazy Symbol Name Mailer Test" do
- tests :test_test_mailer
-
- describe "nested" do
- it "gets the mailer after setting it with a symbol" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
- end
-end
-
-describe "Another Crazy String Name Mailer Test" do
- tests 'test_test_mailer'
-
- it "gets the mailer after setting it with a string" do
- assert_equal TestTestMailer, self.class.mailer_class
- end
-end
diff --git a/actionmailer/test/url_test.rb b/actionmailer/test/url_test.rb
index dcd80f46b5..589944fa69 100644
--- a/actionmailer/test/url_test.rb
+++ b/actionmailer/test/url_test.rb
@@ -41,18 +41,9 @@ class ActionMailerUrlTest < ActionMailer::TestCase
end
def setup
- set_delivery_method :test
- ActionMailer::Base.perform_deliveries = true
- ActionMailer::Base.deliveries.clear
- ActiveSupport::Deprecation.silenced = false
-
@recipient = 'test@localhost'
end
- def teardown
- restore_delivery_method
- end
-
def test_signed_up_with_url
UrlTestMailer.delivery_method = :test
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md
index c8773c9179..dea80abfcd 100644
--- a/actionpack/CHANGELOG.md
+++ b/actionpack/CHANGELOG.md
@@ -1,593 +1,224 @@
-## Rails 4.0.0 (unreleased) ##
+* Add `session#fetch` method
-* `assert_template` can be used to assert on the same template with different locals
- Fix #3675
+ fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch),
+ with the exception that the returned value is always saved into the session.
- *Yves Senn*
-
-* Remove old asset tag concatenation (no longer needed now that we have the asset pipeline). *Josh Peek*
-
-* Accept :remote as symbolic option for `link_to` helper. *Riley Lynch*
-
-* Warn when the `:locals` option is passed to `assert_template` outside of a view test case
- Fix #3415
-
- *Yves Senn*
-
-* The `Rack::Cache` middleware is now disabled by default. To enable it,
- set `config.action_dispatch.rack_cache = true` and add `gem rack-cache` to your Gemfile.
+ It returns a value from the hash for the given key.
+ If the key can’t be found, there are several options:
- *Guillermo Iguaran*
+ * 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.
-* `ActionController::Base.page_cache_extension` option is deprecated
- in favour of `ActionController::Base.default_static_extension`.
+ *Damien Mathieu*
- *Francesco Rodriguez*
+* Don't let strong parameters mutate the given hash via `fetch`
-* Action and Page caching has been extracted from Action Dispatch
- as `actionpack-action_caching` and `actionpack-page_caching` gems.
- Please read the `README.md` file on both gems for the usage.
-
- *Francesco Rodriguez*
-
-* Failsafe exception returns text/plain. *Steve Klabnik*
-
-* Remove `rack-cache` dependency from Action Pack and declare it on Gemfile
-
- *Guillermo Iguaran*
-
-* Rename internal variables on ActionController::TemplateAssertions to prevent
- naming collisions. @partials, @templates and @layouts are now prefixed with an underscore.
- Fix #7459
-
- *Yves Senn*
-
-* `resource` and `resources` don't modify the passed options hash
- Fix #7777
-
- *Yves Senn*
+ 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.
-* Precompiled assets include aliases from foo.js to foo/index.js and vice versa.
+ *Brendon Murphy*, *Doug Cole*
- # Precompiles phone-<digest>.css and aliases phone/index.css to phone.css.
- config.assets.precompile = [ 'phone.css' ]
+* Add `params` option to `button_to` form helper, which renders the given hash
+ as hidden form fields.
- # Precompiles phone/index-<digest>.css and aliases phone.css to phone/index.css.
- config.assets.precompile = [ 'phone/index.css' ]
+ *Andy Waite*
- # Both of these work with either precompile thanks to their aliases.
- <%= stylesheet_link_tag 'phone', media: 'all' %>
- <%= stylesheet_link_tag 'phone/index', media: 'all' %>
+* Make assets helpers work in the controllers like it works in the views.
- *Jeremy Kemper*
-
-* `assert_template` is no more passing with what ever string that matches
- with the template name.
-
- Before when we have a template `/layout/hello.html.erb`, `assert_template`
- was passing with any string that matches. This behavior allowed false
- positive like:
-
- assert_template "layout"
- assert_template "out/hello"
-
- Now it only passes with:
-
- assert_template "layout/hello"
- assert_template "hello"
-
- Fixes #3849.
-
- *Hugolnx*
-
-* `image_tag` will set the same width and height for image if numerical value
- passed to `size` option.
-
- *Nihad Abbasov*
-
-* Deprecate Mime::Type#verify_request? and Mime::Type.browser_generated_types,
- since they are no longer used inside of Rails, they will be removed in Rails 4.1
-
- *Michael Grosser*
-
-* `ActionDispatch::Http::UploadedFile` now delegates `close` to its tempfile. *Sergio Gil*
-
-* Add `ActionController::StrongParameters`, this module converts `params` hash into
- an instance of ActionController::Parameters that allows whitelisting of permitted
- parameters. Non-permitted parameters are forbidden to be used in Active Model by default
- For more details check the documentation of the module or the
- [strong_parameters gem](https://github.com/rails/strong_parameters)
-
- *DHH + Guillermo Iguaran*
-
-* Remove Integration between `attr_accessible`/`attr_protected` and
- `ActionController::ParamsWrapper`. ParamWrapper now wraps all the parameters returned
- by the class method attribute_names
-
- *Guillermo Iguaran*
+ Example:
-* Fix #7646, the log now displays the correct status code when an exception is raised.
+ # config/application.rb
+ config.asset_host = 'http://mycdn.com'
- *Yves Senn*
+ ActionController::Base.helpers.asset_path('fallback.png')
+ # => http://mycdn.com/assets/fallback.png
-* Allow pass couple extensions to `ActionView::Template.register_template_handler` call.
+ Fixes #10051.
*Tima Maslyuchenko*
-* Fixed a bug with shorthand routes scoped with the `:module` option not
- adding the module to the controller as described in issue #6497.
- This should now work properly:
-
- scope :module => "engine" do
- get "api/version" # routes to engine/api#version
- end
-
- *Luiz Felipe Garcia Pereira*
-
-* Sprockets integration has been extracted from Action Pack to the `sprockets-rails`
- gem. `rails` gem is depending on `sprockets-rails` by default.
-
- *Guillermo Iguaran*
-
-* `ActionDispatch::Session::MemCacheStore` now uses `dalli` instead of the deprecated
- `memcache-client` gem. As side effect the autoloading of unloaded classes objects
- saved as values in session isn't supported anymore when mem_cache session store is
- used, this can have an impact in apps only when config.cache_classes is false.
-
- *Arun Agrawal + Guillermo Iguaran*
-
-* Support multiple etags in If-None-Match header. *Travis Warlick*
-
-* Allow to configure how unverified request will be handled using `:with`
- option in `protect_from_forgery` method.
-
- Valid unverified request handling methods are:
-
- - `:exception` - Raises ActionController::InvalidAuthenticityToken exception.
- - `:reset_session` - Resets the session.
- - `:null_session` - Provides an empty session during request but doesn't
- reset it completely. Used as default if `:with` option is not specified.
-
- New applications are generated with:
-
- protect_from_forgery :with => :exception
-
- *Sergey Nartimov*
-
-* Add .ruby template handler, this handler simply allows arbitrary Ruby code as a template. *Guillermo Iguaran*
-
-* Add `separator` option for `ActionView::Helpers::TextHelper#excerpt`:
-
- excerpt('This is a very beautiful morning', 'very', :separator => ' ', :radius => 1)
- # => ...a very beautiful...
-
- *Guirec Corbel*
-
-* Added controller-level etag additions that will be part of the action etag computation *Jeremy Kemper/DHH*
-
- class InvoicesController < ApplicationController
- etag { current_user.try :id }
-
- def show
- # Etag will differ even for the same invoice when it's viewed by a different current_user
- @invoice = Invoice.find(params[:id])
- fresh_when(@invoice)
- end
- end
-
-* Add automatic template digests to all `CacheHelper#cache` calls (originally spiked in the cache_digests plugin) *DHH*
-
-* When building a URL fails, add missing keys provided by Journey. Failed URL
- generation now returns a 500 status instead of a 404.
-
- *Richard Schneeman*
-
-* Deprecate availbility of `ActionView::RecordIdentifier` in controllers by default.
- It's view specific and can be easily included in controller manually if someone
- really needs it. RecordIdentifier will be removed from `ActionController::Base`
- in Rails 4.1. *Piotr Sarnacki*
-
-* Fix `ActionView::RecordIdentifier` to work as a singleton. *Piotr Sarnacki*
-
-* Deprecate `Template#mime_type`, it will be removed in Rails 4.1 in favor of `#type`.
- *Piotr Sarnacki*
-
-* Move vendored html-scanner from `action_controller` to `action_view` directory. If you
- require it directly, please use 'action_view/vendor/html-scanner', reference to
- 'action_controller/vendor/html-scanner' will be removed in Rails 4.1. *Piot Sarnacki*
-
-* Fix handling of date selects when using both disabled and discard options.
- Fixes #7431.
-
- *Vasiliy Ermolovich*
-
-* `ActiveRecord::SessionStore` is extracted out of Rails into a gem `activerecord-session_store`.
- Setting `config.session_store` to `:active_record_store` will no longer work and will break
- if the `activerecord-session_store` gem isn't available. *Prem Sichanugrist*
-
-* Fix `select_tag` when `option_tags` is nil.
- Fixes #7404.
-
- *Sandeep Ravichandran*
-
-* Add `Request#formats=(extensions)` that lets you set multiple formats directly in a prioritized order.
-
- Example of using this for custom iphone views with an HTML fallback:
-
- class ApplicationController < ActionController::Base
- before_filter :adjust_format_for_iphone_with_html_fallback
+* Respect `SCRIPT_NAME` when using `redirect` with a relative path
- private
- def adjust_format_for_iphone_with_html_fallback
- request.formats = [ :iphone, :html ] if request.env["HTTP_USER_AGENT"][/iPhone/]
- end
- end
-
- *DHH*
-
-* Add Routing Concerns to declare common routes that can be reused inside
- others resources and routes.
-
- Code before:
-
- resources :messages do
- resources :comments
- end
-
- resources :posts do
- resources :comments
- resources :images, only: :index
- end
-
- Code after:
-
- concern :commentable do
- resources :comments
- end
-
- concern :image_attachable do
- resources :images, only: :index
- end
-
- resources :messages, concerns: :commentable
-
- resources :posts, concerns: [:commentable, :image_attachable]
-
- *DHH + Rafael Mendonça França*
-
-* Add `start_hour` and `end_hour` options to the `select_hour` helper. *Evan Tann*
-
-* Raises an `ArgumentError` when the first argument in `form_for` contain `nil`
- or is empty.
-
- *Richard Schneeman*
-
-* Add 'X-Frame-Options' => 'SAMEORIGIN'
- 'X-XSS-Protection' => '1; mode=block' and
- 'X-Content-Type-Options' => 'nosniff'
- as default headers.
-
- *Egor Homakov*
-
-* Allow data attributes to be set as a first-level option for form_for, so you can write `form_for @record, data: { behavior: 'autosave' }` instead of `form_for @record, html: { data: { behavior: 'autosave' } }` *DHH*
-
-* Deprecate `button_to_function` and `link_to_function` helpers.
-
- We recommend the use of Unobtrusive JavaScript instead. For example:
-
- link_to "Greeting", "#", :class => "nav_link"
-
- $(function() {
- $('.nav_link').click(function() {
- // Some complex code
-
- return false;
- });
- });
-
- or
-
- link_to "Greeting", '#', onclick: "alert('Hello world!'); return false", class: "nav_link"
-
- for simple cases.
-
- *Rafael Mendonça França*
-
-* `javascript_include_tag :all` will now not include `application.js` if the file does not exists. *Prem Sichanugrist*
-
-* Send an empty response body when call `head` with status between 100 and 199, 204, 205 or 304.
-
- *Armand du Plessis*
-
-* Fixed issue with where digest authentication would not work behind a proxy. *Arthur Smith*
-
-* Added `ActionController::Live`. Mix it in to your controller and you can
- stream data to the client live. For example:
-
- class FooController < ActionController::Base
- include ActionController::Live
-
- def index
- 100.times {
- # Client will see this as it's written
- response.stream.write "hello world\n"
- sleep 1
- }
- response.stream.close
- end
- end
-
- *Aaron Patterson*
+ Example:
-* Remove `ActionDispatch::Head` middleware in favor of `Rack::Head`. *Santiago Pastorino*
+ # application routes.rb
+ mount BlogEngine => '/blog'
-* Deprecate `:confirm` in favor of `:data => { :confirm => "Text" }` option for `button_to`, `button_tag`, `image_submit_tag`, `link_to` and `submit_tag` helpers.
+ # engine routes.rb
+ get '/admin' => redirect('admin/dashboard')
- *Carlos Galdino + Rafael Mendonça França*
+ 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.
-* Show routes in exception page while debugging a `RoutingError` in development. *Richard Schneeman and Mattt Thompson*
+ Fixes #7977.
-* Add `ActionController::Flash.add_flash_types` method to allow people to register their own flash types. e.g.:
+ *Andrew White*
- class ApplicationController
- add_flash_types :error, :warning
- end
+* 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.
- If you add the above code, you can use `<%= error %>` in an erb, and `redirect_to /foo, :error => 'message'` in a controller.
+ Fixes #12029.
- *kennyj*
+ *bluehotdog*
-* Remove Active Model dependency from Action Pack. *Guillermo Iguaran*
+* Fix `ActionDispatch::RemoteIp::GetIp#calculate_ip` to only check for spoofing
+ attacks if both `HTTP_CLIENT_IP` and `HTTP_X_FORWARDED_FOR` are set.
-* Support unicode characters in routes. Route will be automatically escaped, so instead of manually escaping:
+ Fixes #10844.
- get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯') => 'home#index'
+ *Tamir Duberstein*
- You just have to write the unicode route:
+* Strong parameters should permit nested number as key.
- get 'ã“ã‚“ã«ã¡ã¯' => 'home#index'
+ Fixes #12293.
*kennyj*
-* Return proper format on exceptions. *Santiago Pastorino*
+* Fix regex used to detect URI schemes in `redirect_to` to be consistent with
+ RFC 3986.
-* Allow to use `mounted_helpers` (helpers for accessing mounted engines) in `ActionView::TestCase`. *Piotr Sarnacki*
+ *Derek Prior*
-* Include `mounted_helpers` (helpers for accessing mounted engines) in `ActionDispatch::IntegrationTest` by default. *Piotr Sarnacki*
+* Fix incorrect `assert_redirected_to` failure message for protocol-relative
+ URLs.
-* Extracted redirect logic from `ActionController::ForceSSL::ClassMethods.force_ssl` into `ActionController::ForceSSL#force_ssl_redirect`
+ *Derek Prior*
- *Jeremy Friesen*
+* Fix an issue where router can't recognize downcased url encoding path.
-* Make possible to use a block in `button_to` if the button text is hard
- to fit into the name parameter, e.g.:
+ Fixes #12269.
- <%= button_to [:make_happy, @user] do %>
- Make happy <strong><%= @user.name %></strong>
- <% end %>
- # => "<form method="post" action="/users/1/make_happy" class="button_to">
- # <div>
- # <button type="submit">
- # Make happy <strong>Name</strong>
- # </button>
- # </div>
- # </form>"
-
- *Sergey Nartimov*
+ *kennyj*
-* change a way of ordering helpers from several directories. Previously,
- when loading helpers from multiple paths, all of the helpers files were
- gathered into one array an then they were sorted. Helpers from different
- directories should not be mixed before loading them to make loading more
- predictable. The most common use case for such behavior is loading helpers
- from engines. When you load helpers from application and engine Foo, in
- that order, first rails will load all of the helpers from application,
- sorted alphabetically and then it will do the same for Foo engine.
+* Fix custom flash type definition. Misusage of the `_flash_types` class variable
+ caused an error when reloading controllers with custom flash types.
- *Piotr Sarnacki*
+ Fixes #12057.
-* `truncate` now always returns an escaped HTML-safe string. The option `:escape` can be used as
- false to not escape the result.
+ *Ricardo de Cillo*
- *Li Ellis Gallardo + Rafael Mendonça França*
+* Do not break params filtering on `nil` values.
-* `truncate` now accepts a block to show extra content when the text is truncated. *Li Ellis Gallardo*
+ Fixes #12149.
-* Add `week_field`, `week_field_tag`, `month_field`, `month_field_tag`, `datetime_local_field`,
- `datetime_local_field_tag`, `datetime_field` and `datetime_field_tag` helpers. *Carlos Galdino*
-
-* Add `color_field` and `color_field_tag` helpers. *Carlos Galdino*
+ *Vasiliy Ermolovich*
-* `assert_generates`, `assert_recognizes`, and `assert_routing` all raise
- `Assertion` instead of `RoutingError` *David Chelimsky*
+* Separate Action View completely from Action Pack.
-* URL path parameters with invalid encoding now raise ActionController::BadRequest. *Andrew White*
+ *Åukasz StrzaÅ‚kowski*
-* Malformed query and request parameter hashes now raise ActionController::BadRequest. *Andrew White*
+* Development mode exceptions are rendered in text format in case of XHR request.
-* Add `divider` option to `grouped_options_for_select` to generate a separator
- `optgroup` automatically, and deprecate `prompt` as third argument, in favor
- of using an options hash. *Nicholas Greenfield*
+ *Kir Shatrov*
-* Add `time_field` and `time_field_tag` helpers which render an `input[type="time"]` tag. *Alex Soulim*
+* 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.
-* Removed old text_helper apis for highlight, excerpt and word_wrap *Jeremy Walker*
+ Fixes #11799.
-* Templates without a handler extension now raises a deprecation warning but still
- defaults to ERb. In future releases, it will simply return the template contents. *Steve Klabnik*
+ *Nicholas Jakobsen*
-* Deprecate `:disable_with` in favor of `:data => { :disable_with => "Text" }` option from `submit_tag`, `button_tag` and `button_to` helpers.
+* Fix an issue where `assert_dom_equal` and `assert_dom_not_equal` were
+ ignoring the passed failure message argument.
- *Carlos Galdino + Rafael Mendonça França*
+ Fixes #11751.
-* Remove `:mouseover` option from `image_tag` helper. *Rafael Mendonça França*
+ *Ryan McGeary*
-* The `select` method (select tag) forces :include_blank if `required` is true and
- `display size` is one and `multiple` is not true. *Angelo Capilleri*
+* Allow REMOTE_ADDR, HTTP_HOST and HTTP_USER_AGENT to be overridden from
+ the environment passed into `ActionDispatch::TestRequest.new`.
-* Copy literal route constraints to defaults so that url generation know about them.
- The copied constraints are `:protocol`, `:subdomain`, `:domain`, `:host` and `:port`.
+ Fixes #11590.
*Andrew White*
-* `respond_to` and `respond_with` now raise ActionController::UnknownFormat instead
- of directly returning head 406. The exception is rescued and converted to 406
- in the exception handling middleware. *Steven Soroka*
-
-* Allows `assert_redirected_to` to match against a regular expression. *Andy Lindeman*
-
-* Add backtrace to development routing error page. *Richard Schneeman*
-
-* Replace `include_seconds` boolean argument with `:include_seconds => true` option
- in `distance_of_time_in_words` and `time_ago_in_words` signature. *Dmitriy Kiriyenko*
-
-* Make current object and counter (when it applies) variables accessible when
- rendering templates with :object / :collection. *Carlos Antonio da Silva*
-
-* JSONP now uses mimetype text/javascript instead of application/json. *omjokine*
-
-* Allow to lazy load `default_form_builder` by passing a `String` instead of a constant. *Piotr Sarnacki*
-
-* Session arguments passed to `process` calls in functional tests are now merged into
- the existing session, whereas previously they would replace the existing session.
- This change may break some existing tests if they are asserting the exact contents of
- the session but should not break existing tests that only assert individual keys.
+* 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*
-* Add `index` method to FormBuilder class. *Jorge Bejar*
-
-* Remove the leading \n added by textarea on assert_select. *Santiago Pastorino*
-
-* Changed default value for `config.action_view.embed_authenticity_token_in_remote_forms`
- to `false`. This change breaks remote forms that need to work also without javascript,
- so if you need such behavior, you can either set it to `true` or explicitly pass
- `:authenticity_token => true` in form options
-
-* Added ActionDispatch::SSL middleware that when included force all the requests to be under HTTPS protocol. *Rafael Mendonça França*
-
-* Add `include_hidden` option to select tag. With `:include_hidden => false` select with `multiple` attribute doesn't generate hidden input with blank value. *Vasiliy Ermolovich*
-
-* Removed default `size` option from the `text_field`, `search_field`, `telephone_field`, `url_field`, `email_field` helpers. *Philip Arndt*
-
-* Removed default `cols` and `rows` options from the `text_area` helper. *Philip Arndt*
-
-* Adds support for layouts when rendering a partial with a given collection. *serabe*
+* 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.
-* Allows the route helper `root` to take a string argument. For example, `root 'pages#main'`. *bcardarella*
+ Fixes #8018.
-* Forms of persisted records use always PATCH (via the `_method` hack). *fxn*
-
-* For resources, both PATCH and PUT are routed to the `update` action. *fxn*
-
-* Don't ignore `force_ssl` in development. This is a change of behavior - use a `:if` condition to recreate the old behavior.
-
- class AccountsController < ApplicationController
- force_ssl :if => :ssl_configured?
-
- def ssl_configured?
- !Rails.env.development?
- end
- end
-
- *Pat Allan*
-
-* Adds support for the PATCH verb:
- * Request objects respond to `patch?`.
- * Routes have a new `patch` method, and understand `:patch` in the
- existing places where a verb is configured, like `:via`.
- * New method `patch` available in functional tests.
- * If `:patch` is the default verb for updates, edits are
- tunneled as PATCH rather than as PUT, and routing acts accordingly.
- * New method `patch_via_redirect` available in integration tests.
-
- *dlee*
-
-* Integration tests support the `OPTIONS` method. *Jeremy Kemper*
-
-* `expires_in` accepts a `must_revalidate` flag. If true, "must-revalidate"
- is added to the Cache-Control header. *fxn*
+ *Andrew White*
-* Add `date_field` and `date_field_tag` helpers which render an `input[type="date"]` tag *Olek Janiszewski*
+* Move `MissingHelperError` out of the `ClassMethods` module.
-* Adds `image_url`, `javascript_url`, `stylesheet_url`, `audio_url`, `video_url`, and `font_url`
- to assets tag helper. These URL helpers will return the full path to your assets. This is useful
- when you are going to reference this asset from external host. *Prem Sichanugrist*
+ *Yves Senn*
-* Default responder will now always use your overridden block in `respond_with` to render your response. *Prem Sichanugrist*
+* 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`.
-* Allow `value_method` and `text_method` arguments from `collection_select` and
- `options_from_collection_for_select` to receive an object that responds to `:call`,
- such as a `proc`, to evaluate the option in the current element context. This works
- the same way with `collection_radio_buttons` and `collection_check_boxes`.
+ *Piotr Niełacny*
- *Carlos Antonio da Silva + Rafael Mendonça França*
+* Fix `ActionDispatch::ParamsParser#parse_formatted_parameters` to rewind body input stream on
+ parsing json params.
-* Add `collection_check_boxes` form helper, similar to `collection_select`:
- Example:
+ Fixes #11345.
- collection_check_boxes :post, :author_ids, Author.all, :id, :name
- # Outputs something like:
- <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" />
- <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 name="post[author_ids][]" type="hidden" value="" />
+ *Yuri Bol*, *Paul Nikitochkin*
- The label/check_box pairs can be customized with a block.
+* Ignore spaces around delimiter in Set-Cookie header.
- *Carlos Antonio da Silva + Rafael Mendonça França*
-
-* Add `collection_radio_buttons` form helper, similar to `collection_select`:
- Example:
+ *Yamagishi Kazutoshi*
- collection_radio_buttons :post, :author_id, Author.all, :id, :name
- # Outputs something like:
- <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" />
- <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>
+* Remove deprecated Rails application fallback for integration testing, set
+ `ActionDispatch.test_app` instead.
- The label/radio_button pairs can be customized with a block.
+ *Carlos Antonio da Silva*
- *Carlos Antonio da Silva + Rafael Mendonça França*
+* Remove deprecated `page_cache_extension` config.
-* check_box with `:form` html5 attribute will now replicate the `:form`
- attribute to the hidden field as well. *Carlos Antonio da Silva*
-
-* Turn off verbose mode of rack-cache, we still have X-Rack-Cache to
- check that info. Closes #5245. *Santiago Pastorino*
-
-* `label` form helper accepts :for => nil to not generate the attribute. *Carlos Antonio da Silva*
+ *Francesco Rodriguez*
-* Add `:format` option to number_to_percentage *Rodrigo Flores*
+* Remove deprecated constants from Action Controller:
-* Add `config.action_view.logger` to configure logger for Action View. *Rafael Mendonça França*
+ 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
-* Deprecated `ActionController::Integration` in favour of `ActionDispatch::Integration`.
+ *Carlos Antonio da Silva*
-* Deprecated `ActionController::IntegrationTest` in favour of `ActionDispatch::IntegrationTest`.
+* 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.
-* Deprecated `ActionController::PerformanceTest` in favour of `ActionDispatch::PerformanceTest`.
+ Fixes #10965.
-* Deprecated `ActionController::AbstractRequest` in favour of `ActionDispatch::Request`.
+ *Becker*
-* Deprecated `ActionController::Request` in favour of `ActionDispatch::Request`.
+* 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).
-* Deprecated `ActionController::AbstractResponse` in favour of `ActionDispatch::Response`.
+ Fixes #9856.
-* Deprecated `ActionController::Response` in favour of `ActionDispatch::Response`.
+ *Yves Senn*, *Andrew White*
-* Deprecated `ActionController::Routing` in favour of `ActionDispatch::Routing`.
+* ActionView extracted from ActionPack
-* `check_box helper` with `:disabled => true` will generate a disabled
- hidden field to conform with the HTML convention where disabled fields are
- not submitted with the form. This is a behavior change, previously the hidden
- tag had a value of the disabled checkbox. *Tadas Tamosauskas*
+ *Piotr Sarnacki*, *Åukasz StrzaÅ‚kowski*
-* `favicon_link_tag` helper will now use the favicon in app/assets by default. *Lucas Caton*
+* Fix removing trailing slash for mounted apps #3215
-* `ActionView::Helpers::TextHelper#highlight` now defaults to the
- HTML5 `mark` element. *Brian Cardarella*
+ *Piotr Sarnacki*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/actionpack/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionpack/MIT-LICENSE b/actionpack/MIT-LICENSE
index 810daf856c..5c668d9624 100644
--- a/actionpack/MIT-LICENSE
+++ b/actionpack/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/actionpack/README.rdoc b/actionpack/README.rdoc
index ccd0193515..2f6575c3b5 100644
--- a/actionpack/README.rdoc
+++ b/actionpack/README.rdoc
@@ -10,18 +10,13 @@ It consists of several modules:
* Action Dispatch, which parses information about the web request, handles
routing as defined by the user, and does advanced processing related to HTTP
- such as MIME-type negotiation, decoding parameters in POST/PUT bodies,
+ such as MIME-type negotiation, decoding parameters in POST, PATCH, or PUT bodies,
handling HTTP caching logic, cookies and sessions.
* Action Controller, which provides a base controller class that can be
subclassed to implement filters and actions to handle requests. The result
of an action is typically content generated from views.
-* Action View, which handles view template lookup and rendering, and provides
- view helpers that assist when building HTML forms, Atom feeds and more.
- Template formats that Action View handles are ERB (embedded Ruby, typically
- used to inline short Ruby snippets inside HTML), and XML Builder.
-
With the Ruby on Rails framework, users only directly interface with the
Action Controller module. Necessary Action Dispatch functionality is activated
by default and Action View rendering is implicitly triggered by Action
diff --git a/actionpack/RUNNING_UNIT_TESTS b/actionpack/RUNNING_UNIT_TESTS
deleted file mode 100644
index 1b29abd2d1..0000000000
--- a/actionpack/RUNNING_UNIT_TESTS
+++ /dev/null
@@ -1,27 +0,0 @@
-== Running with Rake
-
-The easiest way to run the unit tests is through Rake. The default task runs
-the entire test suite for all classes. For more information, checkout the
-full array of rake tasks with "rake -T"
-
-Rake can be found at http://rake.rubyforge.org
-
-== Running by hand
-
-To run a single test suite
-
- rake test TEST=path/to/test.rb
-
-which can be further narrowed down to one test:
-
- rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"
-
-== Dependency on Active Record and database setup
-
-Test cases in the test/active_record/ directory depend on having
-activerecord and sqlite installed. If Active Record is not in
-actionpack/../activerecord directory, or the sqlite rubygem is not installed,
-these tests are skipped.
-
-Other tests are runnable from a fresh copy of actionpack without any configuration.
-
diff --git a/actionpack/RUNNING_UNIT_TESTS.rdoc b/actionpack/RUNNING_UNIT_TESTS.rdoc
new file mode 100644
index 0000000000..08767ae133
--- /dev/null
+++ b/actionpack/RUNNING_UNIT_TESTS.rdoc
@@ -0,0 +1,17 @@
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all classes. For more information, checkout the
+full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+To run a single test suite
+
+ rake test TEST=path/to/test.rb
+
+which can be further narrowed down to one test:
+
+ rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"
diff --git a/actionpack/Rakefile b/actionpack/Rakefile
index 50e3bb0d48..7eab972595 100644
--- a/actionpack/Rakefile
+++ b/actionpack/Rakefile
@@ -1,51 +1,38 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
+test_files = Dir.glob('test/{abstract,controller,dispatch,assertions,journey,routing}/**/*_test.rb')
+
desc "Default Task"
task :default => :test
# Run the unit tests
-
-desc "Run all unit tests"
-task :test => [:test_action_pack, :test_active_record_integration]
-
-Rake::TestTask.new(:test_action_pack) do |t|
+Rake::TestTask.new do |t|
t.libs << 'test'
# make sure we include the tests in alphabetical order as on some systems
# this will not happen automatically and the tests (as a whole) will error
- t.test_files = Dir.glob('test/{abstract,controller,dispatch,template,assertions}/**/*_test.rb').sort
+ t.test_files = test_files.sort
t.warning = true
t.verbose = true
end
namespace :test do
- Rake::TestTask.new(:isolated) do |t|
- t.libs << 'test'
- t.pattern = 'test/ts_isolated.rb'
- end
-
- Rake::TestTask.new(:template) do |t|
- t.libs << 'test'
- t.pattern = 'test/template/**/*.rb'
+ task :isolated do
+ test_files.all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
+ end or raise "Failures"
end
end
-desc 'ActiveRecord Integration Tests'
-Rake::TestTask.new(:test_active_record_integration) do |t|
- t.libs << 'test'
- t.test_files = Dir.glob("test/activerecord/*_test.rb")
-end
-
spec = eval(File.read('actionpack.gemspec'))
Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
@@ -75,3 +62,9 @@ task :lines do
puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
end
+
+rule '.rb' => '.y' do |t|
+ sh "racc -l -o #{t.name} #{t.source}"
+end
+
+task compile: 'lib/action_dispatch/journey/parser.rb'
diff --git a/actionpack/actionpack.gemspec b/actionpack/actionpack.gemspec
index 7d292ac17c..8a85bf346a 100644
--- a/actionpack/actionpack.gemspec
+++ b/actionpack/actionpack.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -6,24 +6,24 @@ Gem::Specification.new do |s|
s.version = version
s.summary = 'Web-flow and rendering framework putting the VC in MVC (part of Rails).'
s.description = 'Web apps on Rails. Simple, battle-tested conventions for building and testing MVC web applications. Works with any Rack-compatible server.'
+
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
s.require_path = 'lib'
s.requirements << 'none'
- s.add_dependency('activesupport', version)
- s.add_dependency('builder', '~> 3.1.0')
- s.add_dependency('rack', '~> 1.4.1')
- s.add_dependency('rack-test', '~> 0.6.1')
- s.add_dependency('journey', '~> 2.0.0')
- s.add_dependency('erubis', '~> 2.7.0')
+ s.add_dependency 'activesupport', version
+
+ s.add_dependency 'rack', '~> 1.5.2'
+ s.add_dependency 'rack-test', '~> 0.6.2'
- s.add_development_dependency('activemodel', version)
- s.add_development_dependency('tzinfo', '~> 0.3.33')
+ s.add_development_dependency 'actionview', version
+ s.add_development_dependency 'activemodel', version
end
diff --git a/actionpack/lib/abstract_controller.rb b/actionpack/lib/abstract_controller.rb
index 867a7954e0..fe9802e395 100644
--- a/actionpack/lib/abstract_controller.rb
+++ b/actionpack/lib/abstract_controller.rb
@@ -10,12 +10,11 @@ module AbstractController
autoload :Base
autoload :Callbacks
autoload :Collector
+ autoload :DoubleRenderError, "abstract_controller/rendering"
autoload :Helpers
- autoload :Layouts
autoload :Logger
autoload :Rendering
autoload :Translation
autoload :AssetPaths
- autoload :ViewPaths
autoload :UrlFor
end
diff --git a/actionpack/lib/abstract_controller/asset_paths.rb b/actionpack/lib/abstract_controller/asset_paths.rb
index 822254b1a4..e6170228d9 100644
--- a/actionpack/lib/abstract_controller/asset_paths.rb
+++ b/actionpack/lib/abstract_controller/asset_paths.rb
@@ -3,7 +3,7 @@ module AbstractController
extend ActiveSupport::Concern
included do
- config_accessor :asset_host, :asset_path, :assets_dir, :javascripts_dir,
+ config_accessor :asset_host, :assets_dir, :javascripts_dir,
:stylesheets_dir, :default_asset_host_protocol, :relative_url_root
end
end
diff --git a/actionpack/lib/abstract_controller/base.rb b/actionpack/lib/abstract_controller/base.rb
index 388e043f0b..af5de815bb 100644
--- a/actionpack/lib/abstract_controller/base.rb
+++ b/actionpack/lib/abstract_controller/base.rb
@@ -34,6 +34,15 @@ module AbstractController
@abstract = true
end
+ def inherited(klass) # :nodoc:
+ # Define the abstract ivar on subclasses so that we don't get
+ # uninitialized ivar warnings
+ unless klass.instance_variable_defined?(:@abstract)
+ klass.instance_variable_set(:@abstract, false)
+ end
+ super
+ end
+
# A list of all internal methods for a controller. This finds the first
# abstract superclass of a controller, and gets a list of all public
# instance methods on that abstract class. Public instance methods of
@@ -42,6 +51,7 @@ module AbstractController
# (ActionController::Metal and ActionController::Base are defined as abstract)
def internal_methods
controller = self
+
controller = controller.superclass until controller.abstract?
controller.public_instance_methods(true)
end
diff --git a/actionpack/lib/abstract_controller/callbacks.rb b/actionpack/lib/abstract_controller/callbacks.rb
index 5705ab590c..d6c941832f 100644
--- a/actionpack/lib/abstract_controller/callbacks.rb
+++ b/actionpack/lib/abstract_controller/callbacks.rb
@@ -8,7 +8,9 @@ module AbstractController
include ActiveSupport::Callbacks
included do
- define_callbacks :process_action, :terminator => "response_body", :skip_after_callbacks_if_terminated => true
+ define_callbacks :process_action,
+ terminator: ->(controller,_) { controller.response_body },
+ skip_after_callbacks_if_terminated: true
end
# Override AbstractController::Base's process_action to run the
@@ -29,29 +31,33 @@ module AbstractController
# * <tt>only</tt> - The callback should be run only for this action
# * <tt>except</tt> - The callback should be run for all actions except this action
def _normalize_callback_options(options)
- if only = options[:only]
- only = Array(only).map {|o| "action_name == '#{o}'"}.join(" || ")
- options[:if] = Array(options[:if]) << only
- end
- if except = options[:except]
- except = Array(except).map {|e| "action_name == '#{e}'"}.join(" || ")
- options[:unless] = Array(options[:unless]) << except
+ _normalize_callback_option(options, :only, :if)
+ _normalize_callback_option(options, :except, :unless)
+ end
+
+ def _normalize_callback_option(options, from, to) # :nodoc:
+ if from = options[from]
+ from = Array(from).map {|o| "action_name == '#{o}'"}.join(" || ")
+ options[to] = Array(options[to]).unshift(from)
end
end
- # Skip before, after, and around filters matching any of the names
+ # Skip before, after, and around action callbacks matching any of the names
+ # Aliased as skip_filter.
#
# ==== 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
- def skip_filter(*names)
- skip_before_filter(*names)
- skip_after_filter(*names)
- skip_around_filter(*names)
+ 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,
# then call the block with each callback. This allows us to abstract
# the normalization across several methods that use it.
@@ -65,7 +71,7 @@ module AbstractController
# * <tt>name</tt> - The callback to be added
# * <tt>options</tt> - A hash of options to be used when adding the callback
def _insert_callbacks(callbacks, block = nil)
- options = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
+ options = callbacks.extract_options!
_normalize_callback_options(options)
callbacks.push(block) if block
callbacks.each do |callback|
@@ -74,119 +80,138 @@ module AbstractController
end
##
- # :method: before_filter
+ # :method: before_action
#
- # :call-seq: before_filter(names, block)
+ # :call-seq: before_action(names, block)
#
- # Append a before filter. See _insert_callbacks for parameter details.
+ # Append a callback before actions. See _insert_callbacks for parameter details.
+ # Aliased as before_filter.
##
- # :method: prepend_before_filter
+ # :method: prepend_before_action
#
- # :call-seq: prepend_before_filter(names, block)
+ # :call-seq: prepend_before_action(names, block)
#
- # Prepend a before filter. See _insert_callbacks for parameter details.
+ # Prepend a callback before actions. See _insert_callbacks for parameter details.
+ # Aliased as prepend_before_filter.
##
- # :method: skip_before_filter
+ # :method: skip_before_action
#
- # :call-seq: skip_before_filter(names)
+ # :call-seq: skip_before_action(names)
#
- # Skip a before filter. See _insert_callbacks for parameter details.
+ # Skip a callback before actions. See _insert_callbacks for parameter details.
+ # Aliased as skip_before_filter.
##
- # :method: append_before_filter
+ # :method: append_before_action
#
- # :call-seq: append_before_filter(names, block)
+ # :call-seq: append_before_action(names, block)
#
- # Append a before filter. See _insert_callbacks for parameter details.
+ # Append a callback before actions. See _insert_callbacks for parameter details.
+ # Aliased as append_before_filter.
##
- # :method: after_filter
+ # :method: after_action
#
- # :call-seq: after_filter(names, block)
+ # :call-seq: after_action(names, block)
#
- # Append an after filter. See _insert_callbacks for parameter details.
+ # Append a callback after actions. See _insert_callbacks for parameter details.
+ # Aliased as after_filter.
##
- # :method: prepend_after_filter
+ # :method: prepend_after_action
#
- # :call-seq: prepend_after_filter(names, block)
+ # :call-seq: prepend_after_action(names, block)
#
- # Prepend an after filter. See _insert_callbacks for parameter details.
+ # Prepend a callback after actions. See _insert_callbacks for parameter details.
+ # Aliased as prepend_after_filter.
##
- # :method: skip_after_filter
+ # :method: skip_after_action
#
- # :call-seq: skip_after_filter(names)
+ # :call-seq: skip_after_action(names)
#
- # Skip an after filter. See _insert_callbacks for parameter details.
+ # Skip a callback after actions. See _insert_callbacks for parameter details.
+ # Aliased as skip_after_filter.
##
- # :method: append_after_filter
+ # :method: append_after_action
#
- # :call-seq: append_after_filter(names, block)
+ # :call-seq: append_after_action(names, block)
#
- # Append an after filter. See _insert_callbacks for parameter details.
+ # Append a callback after actions. See _insert_callbacks for parameter details.
+ # Aliased as append_after_filter.
##
- # :method: around_filter
+ # :method: around_action
#
- # :call-seq: around_filter(names, block)
+ # :call-seq: around_action(names, block)
#
- # Append an around filter. See _insert_callbacks for parameter details.
+ # Append a callback around actions. See _insert_callbacks for parameter details.
+ # Aliased as around_filter.
##
- # :method: prepend_around_filter
+ # :method: prepend_around_action
#
- # :call-seq: prepend_around_filter(names, block)
+ # :call-seq: prepend_around_action(names, block)
#
- # Prepend an around filter. See _insert_callbacks for parameter details.
+ # Prepend a callback around actions. See _insert_callbacks for parameter details.
+ # Aliased as prepend_around_filter.
##
- # :method: skip_around_filter
+ # :method: skip_around_action
#
- # :call-seq: skip_around_filter(names)
+ # :call-seq: skip_around_action(names)
#
- # Skip an around filter. See _insert_callbacks for parameter details.
+ # Skip a callback around actions. See _insert_callbacks for parameter details.
+ # Aliased as skip_around_filter.
##
- # :method: append_around_filter
+ # :method: append_around_action
#
- # :call-seq: append_around_filter(names, block)
+ # :call-seq: append_around_action(names, block)
#
- # Append an around filter. See _insert_callbacks for parameter details.
+ # Append a callback around actions. See _insert_callbacks for parameter details.
+ # Aliased as append_around_filter.
- # set up before_filter, prepend_before_filter, skip_before_filter, etc.
+ # set up before_action, prepend_before_action, skip_before_action, etc.
# for each of before, after, and around.
- [:before, :after, :around].each do |filter|
+ [:before, :after, :around].each do |callback|
class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- # Append a before, after or around filter. See _insert_callbacks
+ # Append a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
- def #{filter}_filter(*names, &blk) # def before_filter(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{filter}, name, options) # set_callback(:process_action, :before, name, options)
- end # end
- end # end
+ 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
- # Prepend a before, after or around filter. See _insert_callbacks
+ alias_method :#{callback}_filter, :#{callback}_action
+
+ # Prepend a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
- def prepend_#{filter}_filter(*names, &blk) # def prepend_before_filter(*names, &blk)
- _insert_callbacks(names, blk) do |name, options| # _insert_callbacks(names, blk) do |name, options|
- set_callback(:process_action, :#{filter}, name, options.merge(:prepend => true)) # set_callback(:process_action, :before, name, options.merge(:prepend => true))
- end # end
- end # end
+ 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 filter. See _insert_callbacks
+ # Skip a before, after or around callback. See _insert_callbacks
# for details on the allowed parameters.
- def skip_#{filter}_filter(*names) # def skip_before_filter(*names)
- _insert_callbacks(names) do |name, options| # _insert_callbacks(names) do |name, options|
- skip_callback(:process_action, :#{filter}, name, options) # skip_callback(:process_action, :before, name, options)
- end # end
- end # end
-
- # *_filter is the same as append_*_filter
- alias_method :append_#{filter}_filter, :#{filter}_filter # alias_method :append_before_filter, :before_filter
+ 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
end
end
diff --git a/actionpack/lib/abstract_controller/helpers.rb b/actionpack/lib/abstract_controller/helpers.rb
index d3929b685c..e77e4e01e9 100644
--- a/actionpack/lib/abstract_controller/helpers.rb
+++ b/actionpack/lib/abstract_controller/helpers.rb
@@ -12,14 +12,31 @@ module AbstractController
self._helper_methods = Array.new
end
+ class MissingHelperError < LoadError
+ def initialize(error, path)
+ @error = error
+ @path = "helpers/#{path}.rb"
+ set_backtrace error.backtrace
+
+ if error.path =~ /^#{path}(\.rb)?$/
+ super("Missing helper file helpers/%s.rb" % path)
+ else
+ raise error
+ end
+ end
+ end
+
module ClassMethods
+ MissingHelperError = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('AbstractController::Helpers::ClassMethods::MissingHelperError',
+ 'AbstractController::Helpers::MissingHelperError')
+
# When a class is inherited, wrap its helper module in a new module.
# This ensures that the parent class's module can be changed
# independently of the child class's.
def inherited(klass)
helpers = _helpers
klass._helpers = Module.new { include helpers }
- klass.class_eval { default_helper_module! unless anonymous? }
+ klass.class_eval { default_helper_module! } unless klass.anonymous?
super
end
@@ -29,7 +46,7 @@ module AbstractController
# helper_method :current_user, :logged_in?
#
# def current_user
- # @current_user ||= User.find_by_id(session[:user])
+ # @current_user ||= User.find_by(id: session[:user])
# end
#
# def logged_in?
@@ -58,11 +75,10 @@ module AbstractController
# The +helper+ class method can take a series of helper module names, a block, or both.
#
- # ==== Parameters
- # * <tt>*args</tt> - Module, Symbol, String, :all
+ # ==== Options
+ # * <tt>*args</tt> - Module, Symbol, String
# * <tt>block</tt> - A block defining helper methods
#
- # ==== Examples
# When the argument is a module it will be included directly in the template class.
# helper FooHelper # => includes FooHelper
#
@@ -114,7 +130,7 @@ module AbstractController
# helpers with the following behavior:
#
# String or Symbol:: :FooBar or "FooBar" becomes "foo_bar_helper",
- # and "foo_bar_helper.rb" is loaded using require_dependency.
+ # and "foo_bar_helper.rb" is loaded using require_dependency.
#
# Module:: No further processing
#
@@ -135,7 +151,7 @@ module AbstractController
begin
require_dependency(file_name)
rescue LoadError => e
- raise MissingHelperError.new(e, file_name)
+ raise AbstractController::Helpers::MissingHelperError.new(e, file_name)
end
file_name.camelize.constantize
when Module
@@ -146,15 +162,6 @@ module AbstractController
end
end
- class MissingHelperError < LoadError
- def initialize(error, path)
- @error = error
- @path = "helpers/#{path}.rb"
- set_backtrace error.backtrace
- super("Missing helper file helpers/%s.rb" % path)
- end
- end
-
private
# Makes all the (instance) methods in the helper module available to templates
# rendered through this controller.
diff --git a/actionpack/lib/abstract_controller/layouts.rb b/actionpack/lib/abstract_controller/layouts.rb
deleted file mode 100644
index c1b3994035..0000000000
--- a/actionpack/lib/abstract_controller/layouts.rb
+++ /dev/null
@@ -1,410 +0,0 @@
-require "active_support/core_ext/module/remove_method"
-
-module AbstractController
- # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
- # repeated setups. The inclusion pattern has pages that look like this:
- #
- # <%= render "shared/header" %>
- # Hello World
- # <%= render "shared/footer" %>
- #
- # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
- # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
- #
- # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
- # that the header and footer are only mentioned in one place, like this:
- #
- # // The header part of this layout
- # <%= yield %>
- # // The footer part of this layout
- #
- # And then you have content pages that look like this:
- #
- # hello world
- #
- # At rendering time, the content page is computed and then inserted in the layout, like this:
- #
- # // The header part of this layout
- # hello world
- # // The footer part of this layout
- #
- # == Accessing shared variables
- #
- # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
- # references that won't materialize before rendering time:
- #
- # <h1><%= @page_title %></h1>
- # <%= yield %>
- #
- # ...and content pages that fulfill these references _at_ rendering time:
- #
- # <% @page_title = "Welcome" %>
- # Off-world colonies offers you a chance to start a new life
- #
- # The result after rendering is:
- #
- # <h1>Welcome</h1>
- # Off-world colonies offers you a chance to start a new life
- #
- # == Layout assignment
- #
- # You can either specify a layout declaratively (using the #layout class method) or give
- # it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
- # If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
- #
- # For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
- # that template will be used for all actions in PostsController and controllers inheriting
- # from PostsController.
- #
- # If you use a module, for instance Weblog::PostsController, you will need a template named
- # <tt>app/views/layouts/weblog/posts.html.erb</tt>.
- #
- # Since all your controllers inherit from ApplicationController, they will use
- # <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
- # or provided.
- #
- # == Inheritance Examples
- #
- # class BankController < ActionController::Base
- # # bank.html.erb exists
- #
- # class ExchangeController < BankController
- # # exchange.html.erb exists
- #
- # class CurrencyController < BankController
- #
- # class InformationController < BankController
- # layout "information"
- #
- # class TellerController < InformationController
- # # teller.html.erb exists
- #
- # class EmployeeController < InformationController
- # # employee.html.erb exists
- # layout nil
- #
- # class VaultController < BankController
- # layout :access_level_layout
- #
- # class TillController < BankController
- # layout false
- #
- # In these examples, we have three implicit lookup scenarios:
- # * The BankController uses the "bank" layout.
- # * The ExchangeController uses the "exchange" layout.
- # * The CurrencyController inherits the layout from BankController.
- #
- # However, when a layout is explicitly set, the explicitly set layout wins:
- # * The InformationController uses the "information" layout, explicitly set.
- # * The TellerController also uses the "information" layout, because the parent explicitly set it.
- # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration.
- # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
- # * The TillController does not use a layout at all.
- #
- # == Types of layouts
- #
- # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
- # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
- # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
- #
- # The method reference is the preferred approach to variable layouts and is used like this:
- #
- # class WeblogController < ActionController::Base
- # layout :writers_and_readers
- #
- # def index
- # # fetching posts
- # end
- #
- # private
- # def writers_and_readers
- # logged_in? ? "writer_layout" : "reader_layout"
- # end
- # end
- #
- # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
- # is logged in or not.
- #
- # If you want to use an inline method, such as a proc, do something like this:
- #
- # class WeblogController < ActionController::Base
- # layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
- # end
- #
- # If an argument isn't given to the proc, it's evaluated in the context of
- # the current controller anyway.
- #
- # class WeblogController < ActionController::Base
- # layout proc { logged_in? ? "writer_layout" : "reader_layout" }
- # end
- #
- # Of course, the most common way of specifying a layout is still just as a plain template name:
- #
- # class WeblogController < ActionController::Base
- # layout "weblog_standard"
- # end
- #
- # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
- # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
- #
- # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
- # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent:
- #
- # class ApplicationController < ActionController::Base
- # layout "application"
- # end
- #
- # class PostsController < ApplicationController
- # # Will use "application" layout
- # end
- #
- # class CommentsController < ApplicationController
- # # Will search for "comments" layout and fallback "application" layout
- # layout nil
- # end
- #
- # == Conditional layouts
- #
- # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
- # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
- # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
- #
- # class WeblogController < ActionController::Base
- # layout "weblog_standard", :except => :rss
- #
- # # ...
- #
- # end
- #
- # This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
- # be rendered directly, without wrapping a layout around the rendered view.
- #
- # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
- # #<tt>:except => [ :rss, :text_only ]</tt> is valid, as is <tt>:except => :rss</tt>.
- #
- # == Using a different layout in the action render call
- #
- # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
- # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
- # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
- #
- # class WeblogController < ActionController::Base
- # layout "weblog_standard"
- #
- # def help
- # render :action => "help", :layout => "help"
- # end
- # end
- #
- # This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
- module Layouts
- extend ActiveSupport::Concern
-
- include Rendering
-
- included do
- class_attribute :_layout, :_layout_conditions, :instance_accessor => false
- self._layout = nil
- self._layout_conditions = {}
- _write_layout_method
- end
-
- delegate :_layout_conditions, :to => "self.class"
-
- module ClassMethods
- def inherited(klass)
- super
- klass._write_layout_method
- end
-
- # This module is mixed in if layout conditions are provided. This means
- # that if no layout conditions are used, this method is not used
- module LayoutConditions
- # Determines whether the current action has a layout by checking the
- # action name against the :only and :except conditions set on the
- # layout.
- #
- # ==== Returns
- # * <tt> Boolean</tt> - True if the action has a layout, false otherwise.
- def conditional_layout?
- return unless super
-
- conditions = _layout_conditions
-
- if only = conditions[:only]
- only.include?(action_name)
- elsif except = conditions[:except]
- !except.include?(action_name)
- else
- true
- end
- end
- end
-
- # Specify the layout to use for this class.
- #
- # If the specified layout is a:
- # String:: the String is the template name
- # Symbol:: call the method specified by the symbol, which will return the template name
- # false:: There is no layout
- # true:: raise an ArgumentError
- # nil:: Force default layout behavior with inheritance
- #
- # ==== Parameters
- # * <tt>layout</tt> - The layout to use.
- #
- # ==== Options (conditions)
- # * :only - A list of actions to apply this layout to.
- # * :except - Apply this layout to all actions but this one.
- def layout(layout, conditions = {})
- include LayoutConditions unless conditions.empty?
-
- conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
- self._layout_conditions = conditions
-
- self._layout = layout
- _write_layout_method
- end
-
- # If no layout is supplied, look for a template named the return
- # value of this method.
- #
- # ==== Returns
- # * <tt>String</tt> - A template name
- def _implied_layout_name
- controller_path
- end
-
- # Creates a _layout method to be called by _default_layout .
- #
- # If a layout is not explicitly mentioned then look for a layout with the controller's name.
- # if nothing is found then try same procedure to find super class's layout.
- def _write_layout_method
- remove_possible_method(:_layout)
-
- prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
- name_clause = if name
- <<-RUBY
- lookup_context.find_all("#{_implied_layout_name}", #{prefixes.inspect}).first || super
- RUBY
- else
- <<-RUBY
- super
- RUBY
- end
-
- layout_definition = case _layout
- when String
- _layout.inspect
- when Symbol
- <<-RUBY
- #{_layout}.tap do |layout|
- unless layout.is_a?(String) || !layout
- raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
- "should have returned a String, false, or nil"
- end
- end
- RUBY
- when Proc
- define_method :_layout_from_proc, &_layout
- _layout.arity == 0 ? "_layout_from_proc" : "_layout_from_proc(self)"
- when false
- nil
- when true
- raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
- when nil
- name_clause
- end
-
- self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def _layout
- if conditional_layout?
- #{layout_definition}
- else
- #{name_clause}
- end
- end
- private :_layout
- RUBY
- end
- end
-
- def _normalize_options(options)
- super
-
- if _include_layout?(options)
- layout = options.delete(:layout) { :default }
- options[:layout] = _layout_for_option(layout)
- end
- end
-
- attr_internal_writer :action_has_layout
-
- def initialize(*)
- @_action_has_layout = true
- super
- end
-
- def action_has_layout?
- @_action_has_layout
- end
-
- def conditional_layout?
- true
- end
-
- private
-
- # This will be overwritten by _write_layout_method
- def _layout; end
-
- # Determine the layout for a given name, taking into account the name type.
- #
- # ==== Parameters
- # * <tt>name</tt> - The name of the template
- def _layout_for_option(name)
- case name
- when String then _normalize_layout(name)
- when Proc then name
- when true then Proc.new { _default_layout(true) }
- when :default then Proc.new { _default_layout(false) }
- when false, nil then nil
- else
- raise ArgumentError,
- "String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
- end
- end
-
- def _normalize_layout(value)
- value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
- end
-
- # Returns the default layout for this controller.
- # Optionally raises an exception if the layout could not be found.
- #
- # ==== Parameters
- # * <tt>require_layout</tt> - If set to true and layout is not found,
- # an ArgumentError exception is raised (defaults to false)
- #
- # ==== Returns
- # * <tt>template</tt> - The template object for the default layout (or nil)
- def _default_layout(require_layout = false)
- begin
- value = _layout if action_has_layout?
- rescue NameError => e
- raise e, "Could not render layout: #{e.message}"
- end
-
- if require_layout && action_has_layout? && !value
- raise ArgumentError,
- "There was no default layout for #{self.class} in #{view_paths.inspect}"
- end
-
- _normalize_layout(value)
- end
-
- def _include_layout?(options)
- (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
- end
- end
-end
diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb
index 3da2834af0..6f6079d3c5 100644
--- a/actionpack/lib/abstract_controller/rendering.rb
+++ b/actionpack/lib/abstract_controller/rendering.rb
@@ -1,5 +1,5 @@
-require "abstract_controller/base"
-require "action_view"
+require 'active_support/concern'
+require 'active_support/core_ext/class/attribute'
module AbstractController
class DoubleRenderError < Error
@@ -10,119 +10,55 @@ module AbstractController
end
end
- # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
- # it will trigger the lookup_context and consequently expire the cache.
- class I18nProxy < ::I18n::Config #:nodoc:
- attr_reader :original_config, :lookup_context
-
- def initialize(original_config, lookup_context)
- original_config = original_config.original_config if original_config.respond_to?(:original_config)
- @original_config, @lookup_context = original_config, lookup_context
- end
-
- def locale
- @original_config.locale
- end
-
- def locale=(value)
- @lookup_context.locale = value
- end
- end
-
module Rendering
extend ActiveSupport::Concern
- include AbstractController::ViewPaths
included do
class_attribute :protected_instance_variables
self.protected_instance_variables = []
end
- # Overwrite process to setup I18n proxy.
- def process(*) #:nodoc:
- old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
- super
- ensure
- I18n.config = old_config
- end
-
- module ClassMethods
- def view_context_class
- @view_context_class ||= begin
- routes = respond_to?(:_routes) && _routes
- helpers = respond_to?(:_helpers) && _helpers
-
- Class.new(ActionView::Base) do
- if routes
- include routes.url_helpers
- include routes.mounted_helpers
- end
-
- if helpers
- include helpers
- end
- end
- end
- end
- end
-
- attr_internal_writer :view_context_class
-
- def view_context_class
- @_view_context_class ||= self.class.view_context_class
- end
-
- # An instance of a view class. The default view class is ActionView::Base
- #
- # The view class must have the following methods:
- # View.new[lookup_context, assigns, controller]
- # Create a new ActionView instance for a controller
- # View#render[options]
- # Returns String with the rendered template
- #
- # Override this method in a module to change the default behavior.
- def view_context
- view_context_class.new(view_renderer, view_assigns, self)
- end
-
- # Returns an object that is able to render templates.
- def view_renderer
- @_view_renderer ||= ActionView::Renderer.new(lookup_context)
- end
-
# Normalize arguments, options and then delegates render_to_body and
# sticks the result in self.response_body.
+ # :api: public
def render(*args, &block)
options = _normalize_render(*args, &block)
self.response_body = render_to_body(options)
+ _process_format(rendered_format)
+ self.response_body
end
- # Raw rendering of a template to a string. Just convert the results of
- # render_response into a String.
+ # Raw rendering of a template to a string.
+ #
+ # It is similar to render, except that it does not
+ # set the response_body and it should be guaranteed
+ # to always return a string.
+ #
+ # If a component extends the semantics of response_body
+ # (as Action Controller extends it to be anything that
+ # responds to the method each), this method needs to be
+ # overridden in order to still return a string.
# :api: plugin
def render_to_string(*args, &block)
options = _normalize_render(*args, &block)
render_to_body(options)
end
- # Raw rendering of a template to a Rack-compatible body.
- # :api: plugin
+ # Performs the actual template rendering.
+ # :api: public
def render_to_body(options = {})
- _process_options(options)
- _render_template(options)
end
- # Find and renders a template based on the options given.
- # :api: private
- def _render_template(options) #:nodoc:
- lookup_context.rendered_format = nil if options[:formats]
- view_renderer.render(view_context, options)
+ # Return Content-Type of rendered content
+ # :api: public
+ def rendered_format
+ Mime::TEXT
end
- DEFAULT_PROTECTED_INSTANCE_VARIABLES = [
- :@_action_name, :@_response_body, :@_formats, :@_prefixes, :@_config,
- :@_view_context_class, :@_view_renderer, :@_lookup_context
- ]
+ DEFAULT_PROTECTED_INSTANCE_VARIABLES = %w(
+ @_action_name @_response_body @_formats @_prefixes @_config
+ @_view_context_class @_view_renderer @_lookup_context
+ )
# This method should return a hash with assigns.
# You can overwrite this configuration per controller.
@@ -136,53 +72,40 @@ module AbstractController
hash
end
- private
-
- # Normalize args and options.
- # :api: private
- def _normalize_render(*args, &block)
- options = _normalize_args(*args, &block)
- _normalize_options(options)
- options
- end
-
# Normalize args by converting render "foo" to render :action => "foo" and
# render "foo/bar" to render :file => "foo/bar".
# :api: plugin
def _normalize_args(action=nil, options={})
- case action
- when NilClass
- when Hash
- options = action
- when String, Symbol
- action = action.to_s
- key = action.include?(?/) ? :file : :action
- options[key] = action
+ if action.is_a? Hash
+ action
else
- options[:partial] = action
+ options
end
-
- options
end
# Normalize options.
# :api: plugin
def _normalize_options(options)
- if options[:partial] == true
- options[:partial] = action_name
- end
-
- if (options.keys & [:partial, :file, :template]).empty?
- options[:prefixes] ||= _prefixes
- end
-
- options[:template] ||= (options[:action] || action_name).to_s
options
end
# Process extra options.
# :api: plugin
def _process_options(options)
+ options
+ end
+
+ # Process the rendered format.
+ # :api: private
+ def _process_format(format)
+ end
+
+ # Normalize args and options.
+ # :api: private
+ def _normalize_render(*args, &block)
+ options = _normalize_args(*args, &block)
+ _normalize_options(options)
+ options
end
end
end
diff --git a/actionpack/lib/abstract_controller/translation.rb b/actionpack/lib/abstract_controller/translation.rb
index b6c484d188..02028d8e05 100644
--- a/actionpack/lib/abstract_controller/translation.rb
+++ b/actionpack/lib/abstract_controller/translation.rb
@@ -1,9 +1,17 @@
module AbstractController
module Translation
+ # Delegates to <tt>I18n.translate</tt>. Also aliased as <tt>t</tt>.
+ #
+ # When the given key starts with a period, it will be scoped by the current
+ # controller and action. So if you call <tt>translate(".foo")</tt> from
+ # <tt>PeopleController#index</tt>, it will convert the call to
+ # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
+ # to translate many keys within the same controller / action and gives you a
+ # simple framework for scoping them consistently.
def translate(*args)
key = args.first
if key.is_a?(String) && (key[0] == '.')
- key = "#{ controller_path.gsub('/', '.') }.#{ action_name }#{ key }"
+ key = "#{ controller_path.tr('/', '.') }.#{ action_name }#{ key }"
args[0] = key
end
@@ -11,6 +19,7 @@ module AbstractController
end
alias :t :translate
+ # Delegates to <tt>I18n.localize</tt>. Also aliased as <tt>l</tt>.
def localize(*args)
I18n.localize(*args)
end
diff --git a/actionpack/lib/abstract_controller/view_paths.rb b/actionpack/lib/abstract_controller/view_paths.rb
deleted file mode 100644
index c08b3a0e2a..0000000000
--- a/actionpack/lib/abstract_controller/view_paths.rb
+++ /dev/null
@@ -1,96 +0,0 @@
-require 'action_view/base'
-
-module AbstractController
- module ViewPaths
- extend ActiveSupport::Concern
-
- included do
- class_attribute :_view_paths
- self._view_paths = ActionView::PathSet.new
- self._view_paths.freeze
- end
-
- delegate :template_exists?, :view_paths, :formats, :formats=,
- :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
-
- prefixes
- end
- 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
- end
-
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. Check ActionView::LookupContext for more
- # information.
- def lookup_context
- @_lookup_context ||=
- ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
- end
-
- def details_for_lookup
- { }
- end
-
- def append_view_path(path)
- lookup_context.view_paths.push(*path)
- end
-
- def prepend_view_path(path)
- lookup_context.view_paths.unshift(*path)
- end
-
- module ClassMethods
- # Append a path to the list of view paths for this controller.
- #
- # ==== Parameters
- # * <tt>path</tt> - If a String is provided, it gets converted into
- # the default view path. You may also provide a custom view path
- # (see ActionView::PathSet for more information)
- def append_view_path(path)
- self._view_paths = view_paths + Array(path)
- end
-
- # Prepend a path to the list of view paths for this controller.
- #
- # ==== Parameters
- # * <tt>path</tt> - If a String is provided, it gets converted into
- # the default view path. You may also provide a custom view path
- # (see ActionView::PathSet for more information)
- def prepend_view_path(path)
- self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
- end
-
- # A list of all of the default view paths for this controller.
- def view_paths
- _view_paths
- end
-
- # Set the view paths.
- #
- # ==== Parameters
- # * <tt>paths</tt> - If a PathSet is provided, use that;
- # otherwise, process the parameter into a PathSet.
- def view_paths=(paths)
- self._view_paths = ActionView::PathSet.new(Array(paths))
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller.rb b/actionpack/lib/action_controller.rb
index 1a13d7af29..417d2efec2 100644
--- a/actionpack/lib/action_controller.rb
+++ b/actionpack/lib/action_controller.rb
@@ -40,32 +40,15 @@ module ActionController
autoload :UrlFor
end
- autoload :Integration, 'action_controller/deprecated/integration_test'
- autoload :IntegrationTest, 'action_controller/deprecated/integration_test'
- autoload :PerformanceTest, 'action_controller/deprecated/performance_test'
- autoload :Routing, 'action_controller/deprecated'
autoload :TestCase, 'action_controller/test_case'
autoload :TemplateAssertions, 'action_controller/test_case'
- eager_autoload do
- autoload :RecordIdentifier
- end
-
def self.eager_load!
super
ActionController::Caching.eager_load!
- HTML.eager_load!
end
end
-# All of these simply register additional autoloads
-require 'action_view'
-require 'action_view/vendor/html-scanner'
-
-ActiveSupport.on_load(:action_view) do
- ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
-end
-
# Common Active Support usage in Action Controller
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/load_error'
diff --git a/actionpack/lib/action_controller/base.rb b/actionpack/lib/action_controller/base.rb
index 9b3bf99fc3..3b0d094f4f 100644
--- a/actionpack/lib/action_controller/base.rb
+++ b/actionpack/lib/action_controller/base.rb
@@ -1,8 +1,23 @@
require "action_controller/log_subscriber"
+require "action_controller/metal/params_wrapper"
module ActionController
+ # The <tt>metal</tt> anonymous class was introduced to solve issue with including modules in <tt>ActionController::Base</tt>.
+ # Modules needs to be included in particluar order. First we need to have <tt>AbstractController::Rendering</tt> included,
+ # next we should include actuall implementation which would be for example <tt>ActionView::Rendering</tt> and after that
+ # <tt>ActionController::Rendering</tt>. This order must be preserved and as we want to have middle module included dynamicaly
+ # <tt>metal</tt> class was introduced. It has <tt>AbstractController::Rendering</tt> included and is parent class of
+ # <tt>ActionController::Base</tt> which includes <tt>ActionController::Rendering</tt>. If we include <tt>ActionView::Rendering</tt>
+ # beetween them to perserve the required order, we can simply do this by:
+ #
+ # ActionController::Base.superclass.send(:include, ActionView::Rendering)
+ #
+ metal = Class.new(Metal) do
+ include AbstractController::Rendering
+ end
+
# Action Controllers are the core of a web request in \Rails. They are made up of one or more actions that are executed
- # on request and then either render a template or redirect to another action. An action is defined as a public method
+ # on request and then either it renders a template or redirects to another action. An action is defined as a public method
# on the controller, which will automatically be made accessible to the web-server through \Rails Routes.
#
# By default, only the ApplicationController in a \Rails application inherits from <tt>ActionController::Base</tt>. All other
@@ -58,7 +73,7 @@ module ActionController
# <input type="text" name="post[address]" value="hyacintvej">
#
# A request stemming from a form holding these inputs will include <tt>{ "post" => { "name" => "david", "address" => "hyacintvej" } }</tt>.
- # If the address input had been named "post[address][street]", the params would have included
+ # If the address input had been named <tt>post[address][street]</tt>, the params would have included
# <tt>{ "post" => { "address" => { "street" => "hyacintvej" } } }</tt>. There's no limit to the depth of the nesting.
#
# == Sessions
@@ -159,7 +174,7 @@ module ActionController
# render action: "overthere" # won't be called if monkeys is nil
# end
#
- class Base < Metal
+ class Base < metal
abstract!
# We document the request and response methods here because albeit they are
@@ -199,7 +214,6 @@ module ActionController
end
MODULES = [
- AbstractController::Layouts,
AbstractController::Translation,
AbstractController::AssetPaths,
@@ -222,7 +236,6 @@ module ActionController
ForceSSL,
Streaming,
DataStreaming,
- RecordIdentifier,
HttpAuthentication::Basic::ControllerMethods,
HttpAuthentication::Digest::ControllerMethods,
HttpAuthentication::Token::ControllerMethods,
diff --git a/actionpack/lib/action_controller/caching.rb b/actionpack/lib/action_controller/caching.rb
index 462f147371..12d798d0c1 100644
--- a/actionpack/lib/action_controller/caching.rb
+++ b/actionpack/lib/action_controller/caching.rb
@@ -6,11 +6,10 @@ module ActionController
# \Caching is a cheap way of speeding up slow applications by keeping the result of
# calculations, renderings, and database calls around for subsequent requests.
#
- # You can read more about each approach and the sweeping assistance by clicking the
- # modules below.
+ # You can read more about each approach by clicking the modules below.
#
- # Note: To turn off all caching and sweeping, set
- # config.action_controller.perform_caching = false.
+ # Note: To turn off all caching, set
+ # config.action_controller.perform_caching = false
#
# == \Caching stores
#
@@ -30,8 +29,6 @@ module ActionController
eager_autoload do
autoload :Fragments
- autoload :Sweeper, 'action_controller/caching/sweeping'
- autoload :Sweeping, 'action_controller/caching/sweeping'
end
module ConfigMethods
@@ -54,7 +51,6 @@ module ActionController
include ConfigMethods
include Fragments
- include Sweeping if defined?(ActiveRecord)
included do
extend ConfigMethods
@@ -62,22 +58,22 @@ module ActionController
config_accessor :default_static_extension
self.default_static_extension ||= '.html'
- def self.page_cache_extension=(extension)
- ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension)
- self.default_static_extension = extension
- end
-
- def self.page_cache_extension
- ActiveSupport::Deprecation.deprecation_warning(:page_cache_extension, :default_static_extension)
- default_static_extension
- end
-
config_accessor :perform_caching
self.perform_caching = true if perform_caching.nil?
+
+ class_attribute :_view_cache_dependencies
+ self._view_cache_dependencies = []
+ helper_method :view_cache_dependencies if respond_to?(:helper_method)
+ end
+
+ module ClassMethods
+ def view_cache_dependency(&dependency)
+ self._view_cache_dependencies += [dependency]
+ end
end
- def caching_allowed?
- request.get? && response.status == 200
+ def view_cache_dependencies
+ self.class._view_cache_dependencies.map { |dep| instance_exec(&dep) }.compact
end
protected
diff --git a/actionpack/lib/action_controller/caching/sweeping.rb b/actionpack/lib/action_controller/caching/sweeping.rb
deleted file mode 100644
index 317ac74b40..0000000000
--- a/actionpack/lib/action_controller/caching/sweeping.rb
+++ /dev/null
@@ -1,116 +0,0 @@
-module ActionController
- module Caching
- # Sweepers are the terminators of the caching world and responsible for expiring
- # caches when Active Record objects change. They do this by being half-observers,
- # half-filters and implementing callbacks for both roles.
- #
- # class ListSweeper < ActionController::Caching::Sweeper
- # observe List, Item
- #
- # def after_save(record)
- # list = record.is_a?(List) ? record : record.list
- # expire_page(controller: 'lists', action: %w( show public feed ), id: list.id)
- # expire_action(controller: 'lists', action: 'all')
- # list.shares.each { |share| expire_page(controller: 'lists', action: 'show', id: share.url_key) }
- # end
- # end
- #
- # The sweeper is assigned in the controllers that wish to have its job performed using
- # the +cache_sweeper+ class method:
- #
- # class ListsController < ApplicationController
- # caches_action :index, :show, :public, :feed
- # cache_sweeper :list_sweeper, only: [ :edit, :destroy, :share ]
- # end
- #
- # In the example above, four actions are cached and three actions are responsible for expiring those caches.
- #
- # You can also name an explicit class in the declaration of a sweeper, which is needed
- # if the sweeper is in a module:
- #
- # class ListsController < ApplicationController
- # caches_action :index, :show, :public, :feed
- # cache_sweeper OpenBar::Sweeper, only: [ :edit, :destroy, :share ]
- # end
- module Sweeping
- extend ActiveSupport::Concern
-
- module ClassMethods # :nodoc:
- def cache_sweeper(*sweepers)
- configuration = sweepers.extract_options!
-
- sweepers.each do |sweeper|
- ActiveRecord::Base.observers << sweeper if defined?(ActiveRecord) and defined?(ActiveRecord::Base)
- sweeper_instance = (sweeper.is_a?(Symbol) ? Object.const_get(sweeper.to_s.classify) : sweeper).instance
-
- if sweeper_instance.is_a?(Sweeper)
- around_filter(sweeper_instance, :only => configuration[:only])
- else
- after_filter(sweeper_instance, :only => configuration[:only])
- end
- end
- end
- end
- end
-
- if defined?(ActiveRecord) and defined?(ActiveRecord::Observer)
- class Sweeper < ActiveRecord::Observer # :nodoc:
- attr_accessor :controller
-
- def initialize(*args)
- super
- @controller = nil
- end
-
- def before(controller)
- self.controller = controller
- callback(:before) if controller.perform_caching
- true # before method from sweeper should always return true
- end
-
- def after(controller)
- self.controller = controller
- callback(:after) if controller.perform_caching
- end
-
- def around(controller)
- before(controller)
- yield
- after(controller)
- ensure
- clean_up
- end
-
- protected
- # gets the action cache path for the given options.
- def action_path_for(options)
- Actions::ActionCachePath.new(controller, options).path
- end
-
- # Retrieve instance variables set in the controller.
- def assigns(key)
- controller.instance_variable_get("@#{key}")
- end
-
- private
- def clean_up
- # Clean up, so that the controller can be collected after this request
- self.controller = nil
- end
-
- def callback(timing)
- controller_callback_method_name = "#{timing}_#{controller.controller_name.underscore}"
- action_callback_method_name = "#{controller_callback_method_name}_#{controller.action_name}"
-
- __send__(controller_callback_method_name) if respond_to?(controller_callback_method_name, true)
- __send__(action_callback_method_name) if respond_to?(action_callback_method_name, true)
- end
-
- def method_missing(method, *arguments, &block)
- return super unless @controller
- @controller.__send__(method, *arguments, &block)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_controller/deprecated.rb b/actionpack/lib/action_controller/deprecated.rb
deleted file mode 100644
index 2405bebb97..0000000000
--- a/actionpack/lib/action_controller/deprecated.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-ActionController::AbstractRequest = ActionController::Request = ActionDispatch::Request
-ActionController::AbstractResponse = ActionController::Response = ActionDispatch::Response
-ActionController::Routing = ActionDispatch::Routing
-
-ActiveSupport::Deprecation.warn 'ActionController::AbstractRequest and ActionController::Request are deprecated and will be removed, use ActionDispatch::Request instead.'
-ActiveSupport::Deprecation.warn 'ActionController::AbstractResponse and ActionController::Response are deprecated and will be removed, use ActionDispatch::Response instead.'
-ActiveSupport::Deprecation.warn 'ActionController::Routing is deprecated and will be removed, use ActionDispatch::Routing instead.' \ No newline at end of file
diff --git a/actionpack/lib/action_controller/deprecated/integration_test.rb b/actionpack/lib/action_controller/deprecated/integration_test.rb
deleted file mode 100644
index 54eae48f47..0000000000
--- a/actionpack/lib/action_controller/deprecated/integration_test.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-ActionController::Integration = ActionDispatch::Integration
-ActionController::IntegrationTest = ActionDispatch::IntegrationTest
-
-ActiveSupport::Deprecation.warn 'ActionController::Integration is deprecated and will be removed, use ActionDispatch::Integration instead.'
-ActiveSupport::Deprecation.warn 'ActionController::IntegrationTest is deprecated and will be removed, use ActionDispatch::IntegrationTest instead.'
diff --git a/actionpack/lib/action_controller/deprecated/performance_test.rb b/actionpack/lib/action_controller/deprecated/performance_test.rb
deleted file mode 100644
index c7ba5a2fe7..0000000000
--- a/actionpack/lib/action_controller/deprecated/performance_test.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-ActionController::PerformanceTest = ActionDispatch::PerformanceTest
-
-ActiveSupport::Deprecation.warn 'ActionController::PerformanceTest is deprecated and will be removed, use ActionDispatch::PerformanceTest instead.'
diff --git a/actionpack/lib/action_controller/log_subscriber.rb b/actionpack/lib/action_controller/log_subscriber.rb
index 3d274e7dd7..9279d8bcea 100644
--- a/actionpack/lib/action_controller/log_subscriber.rb
+++ b/actionpack/lib/action_controller/log_subscriber.rb
@@ -33,7 +33,7 @@ module ActionController
end
def halted_callback(event)
- info("Filter chain halted as #{event.payload[:filter]} rendered or redirected")
+ info("Filter chain halted as #{event.payload[:filter].inspect} rendered or redirected")
end
def send_file(event)
@@ -48,6 +48,11 @@ module ActionController
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(", ")}")
+ end
+
%w(write_fragment read_fragment exist_fragment?
expire_fragment expire_page write_page).each do |method|
class_eval <<-METHOD, __FILE__, __LINE__ + 1
diff --git a/actionpack/lib/action_controller/metal.rb b/actionpack/lib/action_controller/metal.rb
index f5ab1e2350..b84c9e78c3 100644
--- a/actionpack/lib/action_controller/metal.rb
+++ b/actionpack/lib/action_controller/metal.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/array/extract_options'
require 'action_dispatch/middleware/stack'
module ActionController
@@ -5,7 +6,7 @@ module ActionController
# allowing the following syntax in controllers:
#
# class PostsController < ApplicationController
- # use AuthenticationMiddleware, :except => [:index, :show]
+ # use AuthenticationMiddleware, except: [:index, :show]
# end
#
class MiddlewareStack < ActionDispatch::MiddlewareStack #:nodoc:
@@ -35,8 +36,7 @@ module ActionController
raise "MiddlewareStack#build requires an app" unless app
middlewares.reverse.inject(app) do |a, middleware|
- middleware.valid?(action) ?
- middleware.build(a) : a
+ middleware.valid?(action) ? middleware.build(a) : a
end
end
end
@@ -56,7 +56,7 @@ module ActionController
# And then to route requests to your metal controller, you would add
# something like this to <tt>config/routes.rb</tt>:
#
- # match 'hello', :to => HelloController.action(:index)
+ # get 'hello', to: HelloController.action(:index)
#
# The +action+ method returns a valid Rack application for the \Rails
# router to dispatch to.
diff --git a/actionpack/lib/action_controller/metal/conditional_get.rb b/actionpack/lib/action_controller/metal/conditional_get.rb
index 3f37a6a618..6e0cd51d8b 100644
--- a/actionpack/lib/action_controller/metal/conditional_get.rb
+++ b/actionpack/lib/action_controller/metal/conditional_get.rb
@@ -1,4 +1,4 @@
-require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/hash/keys'
module ActionController
module ConditionalGet
@@ -42,7 +42,7 @@ module ActionController
# * <tt>:public</tt> By default the Cache-Control header is private, set this to
# +true+ if you want your application to be cachable by other devices (proxy caches).
#
- # === Example:
+ # === Example:
#
# def show
# @article = Article.find(params[:id])
@@ -64,7 +64,7 @@ module ActionController
#
# def show
# @article = Article.find(params[:id])
- # fresh_when(@article, :public => true)
+ # fresh_when(@article, public: true)
# end
def fresh_when(record_or_options, additional_options = {})
if record_or_options.is_a? Hash
diff --git a/actionpack/lib/action_controller/metal/data_streaming.rb b/actionpack/lib/action_controller/metal/data_streaming.rb
index 5422cb93c4..75c4d3ef99 100644
--- a/actionpack/lib/action_controller/metal/data_streaming.rb
+++ b/actionpack/lib/action_controller/metal/data_streaming.rb
@@ -47,11 +47,11 @@ module ActionController #:nodoc:
#
# Show a JPEG in the browser:
#
- # send_file '/path/to.jpeg', :type => 'image/jpeg', :disposition => 'inline'
+ # send_file '/path/to.jpeg', type: 'image/jpeg', disposition: 'inline'
#
# Show a 404 page in the browser:
#
- # send_file '/path/to/404.html', :type => 'text/html; charset=utf-8', :status => 404
+ # send_file '/path/to/404.html', type: 'text/html; charset=utf-8', status: 404
#
# Read about the other Content-* HTTP headers if you'd like to
# provide the user with more information (such as Content-Description) in
@@ -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 text: 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.
@@ -117,11 +117,11 @@ module ActionController #:nodoc:
#
# Download a dynamically-generated tarball:
#
- # send_data generate_tgz('dir'), :filename => 'dir.tgz'
+ # send_data generate_tgz('dir'), filename: 'dir.tgz'
#
# Display an image Active Record in the browser:
#
- # send_data image.data, :type => image.content_type, :disposition => 'inline'
+ # send_data image.data, type: image.content_type, disposition: 'inline'
#
# See +send_file+ for more information on HTTP Content-* headers and caching.
def send_data(data, options = {}) #:doc:
@@ -150,6 +150,7 @@ module ActionController #:nodoc:
disposition = options.fetch(:disposition, DEFAULT_SEND_FILE_DISPOSITION)
unless disposition.nil?
+ disposition = disposition.to_s
disposition += %(; filename="#{options[:filename]}") if options[:filename]
headers['Content-Disposition'] = disposition
end
diff --git a/actionpack/lib/action_controller/metal/exceptions.rb b/actionpack/lib/action_controller/metal/exceptions.rb
index 3c9d0c86a7..3844dbf2a6 100644
--- a/actionpack/lib/action_controller/metal/exceptions.rb
+++ b/actionpack/lib/action_controller/metal/exceptions.rb
@@ -3,6 +3,15 @@ module ActionController
end
class BadRequest < ActionControllerError #:nodoc:
+ attr_reader :original_exception
+
+ def initialize(type = nil, e = nil)
+ return super() unless type && e
+
+ super("Invalid #{type} parameters: #{e.message}")
+ @original_exception = e
+ set_backtrace e.backtrace
+ end
end
class RenderError < ActionControllerError #:nodoc:
diff --git a/actionpack/lib/action_controller/metal/flash.rb b/actionpack/lib/action_controller/metal/flash.rb
index b078beb675..65351284b9 100644
--- a/actionpack/lib/action_controller/metal/flash.rb
+++ b/actionpack/lib/action_controller/metal/flash.rb
@@ -11,6 +11,23 @@ module ActionController #:nodoc:
end
module ClassMethods
+ # Creates new flash types. You can pass as many types as you want to create
+ # flash types other than the default <tt>alert</tt> and <tt>notice</tt> in
+ # your controllers and views. For instance:
+ #
+ # # in application_controller.rb
+ # class ApplicationController < ActionController::Base
+ # add_flash_types :warning
+ # end
+ #
+ # # in your controller
+ # redirect_to user_path(@user), warning: "Incomplete profile"
+ #
+ # # in your view
+ # <%= warning %>
+ #
+ # This method will automatically define a new method for each of the given
+ # names, and it will be available in your views.
def add_flash_types(*types)
types.each do |type|
next if _flash_types.include?(type)
@@ -20,7 +37,7 @@ module ActionController #:nodoc:
end
helper_method type
- _flash_types << type
+ self._flash_types += [type]
end
end
end
diff --git a/actionpack/lib/action_controller/metal/force_ssl.rb b/actionpack/lib/action_controller/metal/force_ssl.rb
index e905a3cf1d..a2cb6d1e66 100644
--- a/actionpack/lib/action_controller/metal/force_ssl.rb
+++ b/actionpack/lib/action_controller/metal/force_ssl.rb
@@ -1,3 +1,6 @@
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+
module ActionController
# This module provides a method which will redirect browser to use HTTPS
# protocol. This will ensure that user's sensitive information will be
@@ -14,6 +17,10 @@ module ActionController
extend ActiveSupport::Concern
include AbstractController::Callbacks
+ ACTION_OPTIONS = [:only, :except, :if, :unless]
+ URL_OPTIONS = [:protocol, :host, :domain, :subdomain, :port, :path]
+ REDIRECT_OPTIONS = [:status, :flash, :alert, :notice]
+
module ClassMethods
# Force the request to this particular controller or specified actions to be
# under HTTPS protocol.
@@ -22,25 +29,41 @@ module ActionController
# an +:if+ or +:unless+ condition.
#
# class AccountsController < ApplicationController
- # force_ssl :if => :ssl_configured?
+ # force_ssl if: :ssl_configured?
#
# def ssl_configured?
# !Rails.env.development?
# end
# end
#
- # ==== Options
- # * <tt>host</tt> - Redirect to a different host name
- # * <tt>only</tt> - The callback should be run only for this action
- # * <tt>except</tt> - The callback should be run for all actions except this action
- # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback
- # will be called only when it returns a true value.
- # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback
- # will be called only when it returns a false value.
+ # ==== URL Options
+ # You can pass any of the following options to affect the redirect url
+ # * <tt>host</tt> - Redirect to a different host name
+ # * <tt>subdomain</tt> - Redirect to a different subdomain
+ # * <tt>domain</tt> - Redirect to a different domain
+ # * <tt>port</tt> - Redirect to a non-standard port
+ # * <tt>path</tt> - Redirect to a different path
+ #
+ # ==== Redirect Options
+ # You can pass any of the following options to affect the redirect status and response
+ # * <tt>status</tt> - Redirect with a custom status (default is 301 Moved Permanently)
+ # * <tt>flash</tt> - Set a flash message when redirecting
+ # * <tt>alert</tt> - Set an alert message when redirecting
+ # * <tt>notice</tt> - Set a notice message when redirecting
+ #
+ # ==== Action Options
+ # You can pass any of the following options to affect the before_action callback
+ # * <tt>only</tt> - The callback should be run only for this action
+ # * <tt>except</tt> - The callback should be run for all actions except this action
+ # * <tt>if</tt> - A symbol naming an instance method or a proc; the callback
+ # will be called only when it returns a true value.
+ # * <tt>unless</tt> - A symbol naming an instance method or a proc; the callback
+ # will be called only when it returns a false value.
def force_ssl(options = {})
- host = options.delete(:host)
- before_filter(options) do
- force_ssl_redirect(host)
+ action_options = options.slice(*ACTION_OPTIONS)
+ redirect_options = options.except(*ACTION_OPTIONS)
+ before_action(action_options) do
+ force_ssl_redirect(redirect_options)
end
end
end
@@ -48,14 +71,26 @@ module ActionController
# Redirect the existing request to use the HTTPS protocol.
#
# ==== Parameters
- # * <tt>host</tt> - Redirect to a different host name
- def force_ssl_redirect(host = nil)
+ # * <tt>host_or_options</tt> - Either a host name or any of the url & redirect options
+ # available to the <tt>force_ssl</tt> method.
+ def force_ssl_redirect(host_or_options = nil)
unless request.ssl?
- redirect_options = {:protocol => 'https://', :status => :moved_permanently}
- redirect_options.merge!(:host => host) if host
- redirect_options.merge!(:params => request.query_parameters)
+ options = {
+ :protocol => 'https://',
+ :host => request.host,
+ :path => request.fullpath,
+ :status => :moved_permanently
+ }
+
+ if host_or_options.is_a?(Hash)
+ options.merge!(host_or_options)
+ elsif host_or_options
+ options.merge!(:host => host_or_options)
+ end
+
+ secure_url = ActionDispatch::Http::URL.url_for(options.slice(*URL_OPTIONS))
flash.keep if respond_to?(:flash)
- redirect_to redirect_options
+ redirect_to secure_url, options.slice(*REDIRECT_OPTIONS)
end
end
end
diff --git a/actionpack/lib/action_controller/metal/head.rb b/actionpack/lib/action_controller/metal/head.rb
index 747e1273be..424473801d 100644
--- a/actionpack/lib/action_controller/metal/head.rb
+++ b/actionpack/lib/action_controller/metal/head.rb
@@ -1,15 +1,13 @@
module ActionController
module Head
- extend ActiveSupport::Concern
-
# Return a response that has no content (merely headers). The options
# argument is interpreted to be a hash of header names and values.
# This allows you to easily return a response that consists only of
# significant headers:
#
- # head :created, :location => person_path(@person)
+ # head :created, location: person_path(@person)
#
- # head :created, :location => @person
+ # head :created, location: @person
#
# It can also be used to return exceptional conditions:
#
@@ -31,6 +29,7 @@ module ActionController
if include_content?(self.status)
self.content_type = content_type || (Mime[formats.first] if formats)
+ self.response.charset = false if self.response
self.response_body = " "
else
headers.delete('Content-Type')
diff --git a/actionpack/lib/action_controller/metal/helpers.rb b/actionpack/lib/action_controller/metal/helpers.rb
index d2cbbd3330..a9c3e438fb 100644
--- a/actionpack/lib/action_controller/metal/helpers.rb
+++ b/actionpack/lib/action_controller/metal/helpers.rb
@@ -1,4 +1,3 @@
-
module ActionController
# The \Rails framework provides a large number of helpers for working with assets, dates, forms,
# numbers and model objects, to name a few. These helpers are available to all templates
@@ -6,7 +5,7 @@ module ActionController
#
# In addition to using the standard template helpers provided, creating custom helpers to
# extract complicated logic or reusable functionality is strongly encouraged. By default, each controller
- # will include all helpers.
+ # will include all helpers. These helpers are only accessible on the controller through <tt>.helpers</tt>
#
# In previous versions of \Rails the controller will include a helper whose
# name matches that of the controller, e.g., <tt>MyController</tt> will automatically
@@ -74,7 +73,11 @@ module ActionController
# Provides a proxy to access helpers methods from outside the view.
def helpers
- @helper_proxy ||= ActionView::Base.new.extend(_helpers)
+ @helper_proxy ||= begin
+ proxy = ActionView::Base.new
+ proxy.config = config.inheritable_copy
+ proxy.extend(_helpers)
+ end
end
# Overwrite modules_for_helpers to accept :all as argument, which loads
@@ -91,11 +94,10 @@ module ActionController
end
def all_helpers_from_path(path)
- helpers = []
- Array(path).each do |_path|
- extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
+ helpers = Array(path).flat_map do |_path|
+ extract = /^#{Regexp.quote(_path.to_s)}\/?(.*)_helper.rb$/
names = Dir["#{_path}/**/*_helper.rb"].map { |file| file.sub(extract, '\1') }
- helpers += names.sort
+ names.sort!
end
helpers.uniq!
helpers
diff --git a/actionpack/lib/action_controller/metal/hide_actions.rb b/actionpack/lib/action_controller/metal/hide_actions.rb
index 420b22cf56..af36ffa240 100644
--- a/actionpack/lib/action_controller/metal/hide_actions.rb
+++ b/actionpack/lib/action_controller/metal/hide_actions.rb
@@ -26,20 +26,14 @@ module ActionController
self.hidden_actions = hidden_actions.dup.merge(args.map(&:to_s)).freeze
end
- def inherited(klass)
- klass.class_eval { @visible_actions = {} }
- super
- end
-
def visible_action?(action_name)
- return @visible_actions[action_name] if @visible_actions.key?(action_name)
- @visible_actions[action_name] = !hidden_actions.include?(action_name)
+ not hidden_actions.include?(action_name)
end
# Overrides AbstractController::Base#action_methods to remove any methods
# that are listed as hidden methods.
def action_methods
- @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) })
+ @action_methods ||= Set.new(super.reject { |name| hidden_actions.include?(name) }).freeze
end
end
end
diff --git a/actionpack/lib/action_controller/metal/http_authentication.rb b/actionpack/lib/action_controller/metal/http_authentication.rb
index 03b8d8db1a..158d552ec7 100644
--- a/actionpack/lib/action_controller/metal/http_authentication.rb
+++ b/actionpack/lib/action_controller/metal/http_authentication.rb
@@ -8,14 +8,14 @@ module ActionController
# === Simple \Basic example
#
# class PostsController < ApplicationController
- # http_basic_authenticate_with :name => "dhh", :password => "secret", :except => :index
+ # http_basic_authenticate_with name: "dhh", password: "secret", except: :index
#
# def index
- # render :text => "Everyone can see me!"
+ # render text: "Everyone can see me!"
# end
#
# def edit
- # render :text => "I'm only accessible if you know the password"
+ # render text: "I'm only accessible if you know the password"
# end
# end
#
@@ -25,11 +25,11 @@ module ActionController
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
- # before_filter :set_account, :authenticate
+ # before_action :set_account, :authenticate
#
# protected
# def set_account
- # @account = Account.find_by_url_name(request.subdomains.first)
+ # @account = Account.find_by(url_name: request.subdomains.first)
# end
#
# def authenticate
@@ -68,7 +68,7 @@ module ActionController
module ClassMethods
def http_basic_authenticate_with(options = {})
- before_filter(options.except(:name, :password, :realm)) do
+ before_action(options.except(:name, :password, :realm)) do
authenticate_or_request_with_http_basic(options[:realm] || "Application") do |name, password|
name == options[:name] && password == options[:password]
end
@@ -124,14 +124,14 @@ module ActionController
# USERS = {"dhh" => "secret", #plain text password
# "dap" => Digest::MD5.hexdigest(["dap",REALM,"secret"].join(":"))} #ha1 digest password
#
- # before_filter :authenticate, :except => [:index]
+ # before_action :authenticate, except: [:index]
#
# def index
- # render :text => "Everyone can see me!"
+ # render text: "Everyone can see me!"
# end
#
# def edit
- # render :text => "I'm only accessible if you know the password"
+ # render text: "I'm only accessible if you know the password"
# end
#
# private
@@ -228,7 +228,7 @@ module ActionController
end
def decode_credentials(header)
- HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/,'').split(',').map do |pair|
+ ActiveSupport::HashWithIndifferentAccess[header.to_s.gsub(/^Digest\s+/, '').split(',').map do |pair|
key, value = pair.split('=', 2)
[key.strip, value.to_s.gsub(/^"|"$/,'').delete('\'')]
end]
@@ -249,9 +249,9 @@ module ActionController
end
def secret_token(request)
- secret = request.env["action_dispatch.secret_token"]
- raise "You must set config.secret_token in your app's config" if secret.blank?
- secret
+ key_generator = request.env["action_dispatch.key_generator"]
+ http_auth_salt = request.env["action_dispatch.http_auth_salt"]
+ key_generator.generate_key(http_auth_salt)
end
# Uses an MD5 digest based on time to generate a value to be used only once.
@@ -299,6 +299,7 @@ module ActionController
# allow a user to use new nonce without prompting user again for their
# username and password.
def validate_nonce(secret_key, request, value, seconds_to_timeout=5*60)
+ return false if value.nil?
t = ::Base64.decode64(value).split(":").first.to_i
nonce(secret_key, t) == value && (t - Time.now.to_i).abs <= seconds_to_timeout
end
@@ -317,14 +318,14 @@ module ActionController
# class PostsController < ApplicationController
# TOKEN = "secret"
#
- # before_filter :authenticate, :except => [ :index ]
+ # before_action :authenticate, except: [ :index ]
#
# def index
- # render :text => "Everyone can see me!"
+ # render text: "Everyone can see me!"
# end
#
# def edit
- # render :text => "I'm only accessible if you know the password"
+ # render text: "I'm only accessible if you know the password"
# end
#
# private
@@ -340,11 +341,11 @@ module ActionController
# the regular HTML interface is protected by a session approach:
#
# class ApplicationController < ActionController::Base
- # before_filter :set_account, :authenticate
+ # before_action :set_account, :authenticate
#
# protected
# def set_account
- # @account = Account.find_by_url_name(request.subdomains.first)
+ # @account = Account.find_by(url_name: request.subdomains.first)
# end
#
# def authenticate
@@ -384,6 +385,8 @@ module ActionController
#
# RewriteRule ^(.*)$ dispatch.fcgi [E=X-HTTP_AUTHORIZATION:%{HTTP:Authorization},QSA,L]
module Token
+ TOKEN_REGEX = /^Token /
+ AUTHN_PAIR_DELIMITERS = /(?:,|;|\t+)/
extend self
module ControllerMethods
@@ -424,27 +427,41 @@ module ActionController
# Parses the token and options out of the token authorization header. If
# the header looks like this:
# Authorization: Token token="abc", nonce="def"
- # Then the returned token is "abc", and the options is {:nonce => "def"}
+ # Then the returned token is "abc", and the options is {nonce: "def"}
#
# request - ActionDispatch::Request instance with the current headers.
#
# Returns an Array of [String, Hash] if a token is present.
# Returns nil if no token is found.
def token_and_options(request)
- if request.authorization.to_s[/^Token (.*)/]
- values = Hash[$1.split(',').map do |value|
- value.strip! # remove any spaces between commas and values
- key, value = value.split(/\=\"?/) # split key=value pairs
- if value
- value.chomp!('"') # chomp trailing " in value
- value.gsub!(/\\\"/, '"') # unescape remaining quotes
- [key, value]
- end
- end.compact]
- [values.delete("token"), values.with_indifferent_access]
+ 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]
end
end
+ def token_params_from(auth)
+ rewrite_param_values params_array_from raw_params auth
+ end
+
+ # Takes raw_params and turns it into an array of parameters
+ def params_array_from(raw_params)
+ raw_params.map { |param| param.split %r/=(.+)?/ }
+ end
+
+ # This removes the `"` characters wrapping the value.
+ def rewrite_param_values(array_params)
+ array_params.each { |param| param.last.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*/)
+ end
+
# Encodes the given token and options into an Authorization header value.
#
# token - String token.
diff --git a/actionpack/lib/action_controller/metal/instrumentation.rb b/actionpack/lib/action_controller/metal/instrumentation.rb
index ca4ae532ca..d3aa8f90c5 100644
--- a/actionpack/lib/action_controller/metal/instrumentation.rb
+++ b/actionpack/lib/action_controller/metal/instrumentation.rb
@@ -60,7 +60,7 @@ module ActionController
ActiveSupport::Notifications.instrument("redirect_to.action_controller") do |payload|
result = super
payload[:status] = response.status
- payload[:location] = response.location
+ payload[:location] = response.filtered_location
result
end
end
diff --git a/actionpack/lib/action_controller/metal/live.rb b/actionpack/lib/action_controller/metal/live.rb
index 32e5afa335..0dd788645b 100644
--- a/actionpack/lib/action_controller/metal/live.rb
+++ b/actionpack/lib/action_controller/metal/live.rb
@@ -1,5 +1,6 @@
require 'action_dispatch/http/response'
require 'delegate'
+require 'active_support/json'
module ActionController
# Mix this module in to your controller, and all actions in that controller
@@ -14,6 +15,7 @@ module ActionController
# response.stream.write "hello world\n"
# sleep 1
# }
+ # ensure
# response.stream.close
# end
# end
@@ -31,8 +33,82 @@ module ActionController
# the main thread. Make sure your actions are thread safe, and this shouldn't
# be a problem (don't share state across threads, etc).
module Live
+ # This class provides the ability to write an SSE (Server Sent Event)
+ # to an IO stream. The class is initialized with a stream and can be used
+ # to either write a JSON string or an object which can be converted to JSON.
+ #
+ # Writing an object will convert it into standard SSE format with whatever
+ # options you have configured. You may choose to set the following options:
+ #
+ # 1) Event. If specified, an event with this name will be dispatched on
+ # the browser.
+ # 2) Retry. The reconnection time in milliseconds used when attempting
+ # to send the event.
+ # 3) Id. If the connection dies while sending an SSE to the browser, then
+ # the server will receive a +Last-Event-ID+ header with value equal to +id+.
+ #
+ # After setting an option in the constructor of the SSE object, all future
+ # SSEs sent accross the stream will use those options unless overridden.
+ #
+ # Example Usage:
+ #
+ # class MyController < ActionController::Base
+ # include ActionController::Live
+ #
+ # def index
+ # response.headers['Content-Type'] = 'text/event-stream'
+ # sse = SSE.new(response.stream, retry: 300, event: "event-name")
+ # sse.write({ name: 'John'})
+ # sse.write({ name: 'John'}, id: 10)
+ # sse.write({ name: 'John'}, id: 10, event: "other-event")
+ # sse.write({ name: 'John'}, id: 10, event: "other-event", retry: 500)
+ # ensure
+ # sse.close
+ # end
+ # end
+ #
+ # Note: SSEs are not currently supported by IE. However, they are supported
+ # by Chrome, Firefox, Opera, and Safari.
+ class SSE
+
+ WHITELISTED_OPTIONS = %w( retry event id )
+
+ def initialize(stream, options = {})
+ @stream = stream
+ @options = options
+ end
+
+ def close
+ @stream.close
+ end
+
+ def write(object, options = {})
+ case object
+ when String
+ perform_write(object, options)
+ else
+ perform_write(ActiveSupport::JSON.encode(object), options)
+ end
+ end
+
+ private
+
+ def perform_write(json, options)
+ current_options = @options.merge(options).stringify_keys
+
+ WHITELISTED_OPTIONS.each do |option_name|
+ if (option_value = current_options[option_name])
+ @stream.write "#{option_name}: #{option_value}\n"
+ end
+ end
+
+ @stream.write "data: #{json}\n\n"
+ end
+ end
+
class Buffer < ActionDispatch::Response::Buffer #:nodoc:
def initialize(response)
+ @error_callback = nil
super(response, SizedQueue.new(10))
end
@@ -55,6 +131,14 @@ module ActionController
super
@buf.push nil
end
+
+ def on_error(&block)
+ @error_callback = block
+ end
+
+ def call_on_error
+ @error_callback.call
+ end
end
class Response < ActionDispatch::Response #:nodoc: all
@@ -97,6 +181,10 @@ module ActionController
def merge_default_headers(original, default)
Header.new self, super
end
+
+ def handle_conditional_get!
+ super unless committed?
+ end
end
def process(name)
@@ -116,6 +204,16 @@ 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
+ end
ensure
@_response.commit!
end
@@ -124,6 +222,16 @@ module ActionController
@_response.await_commit
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")
+ end
+
def response_body=(body)
super
response.stream.close if response
diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb
index 18ae2c3bfc..a072fce1a1 100644
--- a/actionpack/lib/action_controller/metal/mime_responds.rb
+++ b/actionpack/lib/action_controller/metal/mime_responds.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/array/extract_options'
require 'abstract_controller/collector'
module ActionController #:nodoc:
@@ -23,13 +24,13 @@ module ActionController #:nodoc:
# <tt>:except</tt> with an array of actions or a single action:
#
# respond_to :html
- # respond_to :xml, :json, :except => [ :edit ]
+ # respond_to :xml, :json, except: [ :edit ]
#
# This specifies that all actions respond to <tt>:html</tt>
# and all actions except <tt>:edit</tt> respond to <tt>:xml</tt> and
# <tt>:json</tt>.
#
- # respond_to :json, :only => :create
+ # respond_to :json, only: :create
#
# This specifies that the <tt>:create</tt> action and no other responds
# to <tt>:json</tt>.
@@ -70,7 +71,7 @@ module ActionController #:nodoc:
#
# respond_to do |format|
# format.html
- # format.xml { render :xml => @people }
+ # format.xml { render xml: @people }
# end
# end
#
@@ -82,7 +83,7 @@ module ActionController #:nodoc:
# (by name) if it does not already exist, without web-services, it might look like this:
#
# def create
- # @company = Company.find_or_create_by_name(params[:company][:name])
+ # @company = Company.find_or_create_by(name: params[:company][:name])
# @person = @company.people.create(params[:person])
#
# redirect_to(person_list_url)
@@ -92,13 +93,13 @@ module ActionController #:nodoc:
#
# def create
# company = params[:person].delete(:company)
- # @company = Company.find_or_create_by_name(company[:name])
+ # @company = Company.find_or_create_by(name: company[:name])
# @person = @company.people.create(params[:person])
#
# respond_to do |format|
# format.html { redirect_to(person_list_url) }
# format.js
- # format.xml { render :xml => @person.to_xml(:include => @company) }
+ # format.xml { render xml: @person.to_xml(include: @company) }
# end
# end
#
@@ -120,7 +121,7 @@ module ActionController #:nodoc:
# Note, however, the extra bit at the top of that action:
#
# company = params[:person].delete(:company)
- # @company = Company.find_or_create_by_name(company[:name])
+ # @company = Company.find_or_create_by(name: company[:name])
#
# This is because the incoming XML document (if a web-service request is in process) can only contain a
# single root-node. So, we have to rearrange things so that the request looks like this (url-encoded):
@@ -162,11 +163,11 @@ module ActionController #:nodoc:
#
# In the example above, if the format is xml, it will render:
#
- # render :xml => @people
+ # render xml: @people
#
# Or if the format is json:
#
- # render :json => @people
+ # render json: @people
#
# Since this is a common pattern, you can use the class method respond_to
# with the respond_with method to have the same results:
@@ -227,7 +228,7 @@ module ActionController #:nodoc:
# i.e. its +show+ action.
# 2. If there are validation errors, the response
# renders a default action, which is <tt>:new</tt> for a
- # +post+ request or <tt>:edit</tt> for +put+.
+ # +post+ request or <tt>:edit</tt> for +patch+ or +put+.
# Thus an example like this -
#
# respond_to :html, :xml
@@ -246,10 +247,10 @@ module ActionController #:nodoc:
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
- # format.xml { render :xml => @user }
+ # format.xml { render xml: @user }
# else
- # format.html { render :action => "new" }
- # format.xml { render :xml => @user }
+ # format.html { render action: "new" }
+ # format.xml { render xml: @user }
# end
# end
# end
@@ -260,7 +261,7 @@ module ActionController #:nodoc:
# the resource passed to +respond_with+ responds to <code>to_<format></code>,
# the method attempts to render the resource in the requested format
# directly, e.g. for an xml request, the response is equivalent to calling
- # <code>render :xml => resource</code>.
+ # <code>render xml: resource</code>.
#
# === Nested resources
#
@@ -309,7 +310,7 @@ module ActionController #:nodoc:
# Also, a hash passed to +respond_with+ immediately after the specified
# resource(s) is interpreted as a set of options relevant to all
# formats. Any option accepted by +render+ can be used, e.g.
- # respond_with @people, :status => 200
+ # respond_with @people, status: 200
# However, note that these options are ignored after an unsuccessful attempt
# to save a resource, e.g. when automatically rendering <tt>:new</tt>
# after a post request.
@@ -320,11 +321,12 @@ module ActionController #:nodoc:
# 2. <tt>:action</tt> - overwrites the default render action used after an
# unsuccessful html +post+ request.
def respond_with(*resources, &block)
- raise "In order to use respond_with, first you need to declare the formats your " <<
+ raise "In order to use respond_with, first you need to declare the formats your " \
"controller responds to in the class level" if self.class.mimes_for_respond_to.empty?
if collector = retrieve_collector_from_mimes(&block)
options = resources.size == 1 ? {} : resources.extract_options!
+ options = options.clone
options[:default_response] = collector.response
(options.delete(:responder) || self.class.responder).call(self, resources, options)
end
@@ -363,9 +365,7 @@ module ActionController #:nodoc:
format = collector.negotiate_format(request)
if format
- self.content_type ||= format.to_s
- lookup_context.formats = [format.to_sym]
- lookup_context.rendered_format = lookup_context.formats.first
+ _process_format(format)
collector
else
raise ActionController::UnknownFormat
@@ -381,7 +381,7 @@ module ActionController #:nodoc:
#
# respond_to do |format|
# format.html
- # format.xml { render :xml => @people }
+ # format.xml { render xml: @people }
# end
#
# In this usage, the argument passed to the block (+format+ above) is an
@@ -419,7 +419,7 @@ module ActionController #:nodoc:
end
def response
- @responses[format] || @responses[Mime::ALL]
+ @responses.fetch(format, @responses[Mime::ALL])
end
def negotiate_format(request)
diff --git a/actionpack/lib/action_controller/metal/params_wrapper.rb b/actionpack/lib/action_controller/metal/params_wrapper.rb
index 88b9e78da7..c9f1d8dcb4 100644
--- a/actionpack/lib/action_controller/metal/params_wrapper.rb
+++ b/actionpack/lib/action_controller/metal/params_wrapper.rb
@@ -1,7 +1,8 @@
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/anonymous'
-require 'action_dispatch/http/mime_types'
+require 'active_support/core_ext/struct'
+require 'action_dispatch/http/mime_type'
module ActionController
# Wraps the parameters hash into a nested hash. This will allow clients to submit
@@ -72,17 +73,104 @@ module ActionController
EXCLUDE_PARAMETERS = %w(authenticity_token _method utf8)
+ require 'mutex_m'
+
+ class Options < Struct.new(:name, :format, :include, :exclude, :klass, :model) # :nodoc:
+ include Mutex_m
+
+ def self.from_hash(hash)
+ name = hash[:name]
+ format = Array(hash[:format])
+ include = hash[:include] && Array(hash[:include]).collect(&:to_s)
+ exclude = hash[:exclude] && Array(hash[:exclude]).collect(&:to_s)
+ new name, format, include, exclude, nil, nil
+ end
+
+ def initialize(name, format, include, exclude, klass, model) # nodoc
+ super
+ @include_set = include
+ @name_set = name
+ end
+
+ def model
+ super || synchronize { super || self.model = _default_wrap_model }
+ end
+
+ def include
+ return super if @include_set
+
+ m = model
+ synchronize do
+ return super if @include_set
+
+ @include_set = true
+
+ unless super || exclude
+ if m.respond_to?(:attribute_names) && m.attribute_names.any?
+ self.include = m.attribute_names
+ end
+ end
+ end
+ end
+
+ def name
+ return super if @name_set
+
+ m = model
+ synchronize do
+ return super if @name_set
+
+ @name_set = true
+
+ unless super || klass.anonymous?
+ self.name = m ? m.to_s.demodulize.underscore :
+ klass.controller_name.singularize
+ end
+ end
+ end
+
+ private
+ # Determine the wrapper model from the controller's name. By convention,
+ # this could be done by trying to find the defined model that has the
+ # same singularize name as the controller. For example, +UsersController+
+ # will try to find if the +User+ model exists.
+ #
+ # This method also does namespace lookup. Foo::Bar::UsersController will
+ # try to find Foo::Bar::User, Foo::User and finally User.
+ def _default_wrap_model #:nodoc:
+ return nil if klass.anonymous?
+ model_name = klass.name.sub(/Controller$/, '').classify
+
+ begin
+ if model_klass = model_name.safe_constantize
+ model_klass
+ else
+ namespaces = model_name.split("::")
+ namespaces.delete_at(-2)
+ break if namespaces.last == model_name
+ model_name = namespaces.join("::")
+ end
+ end until model_klass
+
+ model_klass
+ end
+ end
+
included do
class_attribute :_wrapper_options
- self._wrapper_options = { :format => [] }
+ self._wrapper_options = Options.from_hash(format: [])
end
module ClassMethods
+ def _set_wrapper_options(options)
+ self._wrapper_options = Options.from_hash(options)
+ end
+
# Sets the name of the wrapper key, or the model which +ParamsWrapper+
# would use to determine the attribute names from.
#
# ==== Examples
- # wrap_parameters :format => :xml
+ # wrap_parameters format: :xml
# # enables the parameter wrapper for XML format
#
# wrap_parameters :person
@@ -92,7 +180,7 @@ module ActionController
# # wraps parameters by determining the wrapper key from Person class
# (+person+, in this case) and the list of attribute names
#
- # wrap_parameters :include => [:username, :title]
+ # wrap_parameters include: [:username, :title]
# # wraps only +:username+ and +:title+ attributes from parameters.
#
# wrap_parameters false
@@ -119,68 +207,24 @@ module ActionController
model = name_or_model_or_options
end
- _set_wrapper_defaults(_wrapper_options.slice(:format).merge(options), model)
+ opts = Options.from_hash _wrapper_options.to_h.slice(:format).merge(options)
+ opts.model = model
+ opts.klass = self
+
+ self._wrapper_options = opts
end
# Sets the default wrapper key or model which will be used to determine
# wrapper key and attribute names. Will be called automatically when the
# module is inherited.
def inherited(klass)
- if klass._wrapper_options[:format].present?
- klass._set_wrapper_defaults(klass._wrapper_options.slice(:format))
+ if klass._wrapper_options.format.any?
+ params = klass._wrapper_options.dup
+ params.klass = klass
+ klass._wrapper_options = params
end
super
end
-
- protected
-
- # Determine the wrapper model from the controller's name. By convention,
- # this could be done by trying to find the defined model that has the
- # same singularize name as the controller. For example, +UsersController+
- # will try to find if the +User+ model exists.
- #
- # This method also does namespace lookup. Foo::Bar::UsersController will
- # try to find Foo::Bar::User, Foo::User and finally User.
- def _default_wrap_model #:nodoc:
- return nil if self.anonymous?
- model_name = self.name.sub(/Controller$/, '').classify
-
- begin
- if model_klass = model_name.safe_constantize
- model_klass
- else
- namespaces = model_name.split("::")
- namespaces.delete_at(-2)
- break if namespaces.last == model_name
- model_name = namespaces.join("::")
- end
- end until model_klass
-
- model_klass
- end
-
- def _set_wrapper_defaults(options, model=nil)
- options = options.dup
-
- unless options[:include] || options[:exclude]
- model ||= _default_wrap_model
- if model.respond_to?(:attribute_names) && model.attribute_names.present?
- options[:include] = model.attribute_names
- end
- end
-
- unless options[:name] || self.anonymous?
- model ||= _default_wrap_model
- options[:name] = model ? model.to_s.demodulize.underscore :
- controller_name.singularize
- end
-
- options[:include] = Array(options[:include]).collect(&:to_s) if options[:include]
- options[:exclude] = Array(options[:exclude]).collect(&:to_s) if options[:exclude]
- options[:format] = Array(options[:format])
-
- self._wrapper_options = options
- end
end
# Performs parameters wrapping upon the request. Will be called automatically
@@ -205,20 +249,20 @@ module ActionController
# Returns the wrapper key which will use to stored wrapped parameters.
def _wrapper_key
- _wrapper_options[:name]
+ _wrapper_options.name
end
# Returns the list of enabled formats.
def _wrapper_formats
- _wrapper_options[:format]
+ _wrapper_options.format
end
# Returns the list of parameters which will be selected for wrapped.
def _wrap_parameters(parameters)
- value = if include_only = _wrapper_options[:include]
+ value = if include_only = _wrapper_options.include
parameters.slice(*include_only)
else
- exclude = _wrapper_options[:exclude] || []
+ exclude = _wrapper_options.exclude || []
parameters.except(*(exclude + EXCLUDE_PARAMETERS))
end
diff --git a/actionpack/lib/action_controller/metal/redirecting.rb b/actionpack/lib/action_controller/metal/redirecting.rb
index ee0e69d87c..ab14a61b97 100644
--- a/actionpack/lib/action_controller/metal/redirecting.rb
+++ b/actionpack/lib/action_controller/metal/redirecting.rb
@@ -24,7 +24,7 @@ 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>
#
- # redirect_to :action => "show", :id => 5
+ # redirect_to action: "show", id: 5
# redirect_to post
# redirect_to "http://www.rubyonrails.org"
# redirect_to "/images/screenshot.jpg"
@@ -32,12 +32,12 @@ module ActionController
# redirect_to :back
# redirect_to proc { edit_post_url(@post) }
#
- # The redirection happens as a "302 Moved" header unless otherwise specified.
+ # The redirection happens as a "302 Found" header unless otherwise specified.
#
- # redirect_to post_url(@post), :status => :found
- # redirect_to :action=>'atom', :status => :moved_permanently
- # redirect_to post_url(@post), :status => 301
- # redirect_to :action=>'atom', :status => 302
+ # redirect_to post_url(@post), status: :found
+ # redirect_to action: 'atom', status: :moved_permanently
+ # redirect_to post_url(@post), status: 301
+ # redirect_to action: 'atom', status: 302
#
# The status code can either be a standard {HTTP Status code}[http://www.iana.org/assignments/http-status-codes] as an
# integer, or a symbol representing the downcased, underscored and symbolized description.
@@ -49,32 +49,51 @@ module ActionController
# around this you can return a <tt>303 See Other</tt> status code which will be
# followed using a GET request.
#
- # redirect_to posts_url, :status => :see_other
- # redirect_to :action => 'index', :status => 303
+ # redirect_to posts_url, status: :see_other
+ # redirect_to action: 'index', status: 303
#
# It is also possible to assign a flash message as part of the redirection. There are two special accessors for the commonly used flash names
# +alert+ and +notice+ as well as a general purpose +flash+ bucket.
#
- # redirect_to post_url(@post), :alert => "Watch it, mister!"
- # redirect_to post_url(@post), :status=> :found, :notice => "Pay attention to the road"
- # redirect_to post_url(@post), :status => 301, :flash => { :updated_post_id => @post.id }
- # redirect_to { :action=>'atom' }, :alert => "Something serious happened"
+ # redirect_to post_url(@post), alert: "Watch it, mister!"
+ # redirect_to post_url(@post), status: :found, notice: "Pay attention to the road"
+ # redirect_to post_url(@post), status: 301, flash: { updated_post_id: @post.id }
+ # redirect_to { action: 'atom' }, alert: "Something serious happened"
#
# 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.
def redirect_to(options = {}, response_status = {}) #:doc:
raise ActionControllerError.new("Cannot redirect to nil!") unless options
raise AbstractController::DoubleRenderError if response_body
- logger.debug { "Redirected by #{caller(1).first rescue "unknown"}" } if logger
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>"
end
+ def _compute_redirect_to_location(options) #:nodoc:
+ case options
+ # The scheme name consist of a letter followed by any combination of
+ # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
+ # characters; and is terminated by a colon (":").
+ # See http://tools.ietf.org/html/rfc3986#section-3.1
+ # The protocol relative scheme starts with a double slash "//".
+ when /\A([a-z][a-z\d\-+\.]*:|\/\/).*/i
+ options
+ when String
+ request.protocol + request.host_with_port + options
+ when :back
+ request.headers["Referer"] or raise RedirectBackError
+ when Proc
+ _compute_redirect_to_location options.call
+ else
+ url_for(options)
+ end.delete("\0\r\n")
+ end
+
private
def _extract_redirect_to_status(options, response_status)
- status = if options.is_a?(Hash) && options.key?(:status)
+ if options.is_a?(Hash) && options.key?(:status)
Rack::Utils.status_code(options.delete(:status))
elsif response_status.key?(:status)
Rack::Utils.status_code(response_status[:status])
@@ -82,25 +101,5 @@ module ActionController
302
end
end
-
- def _compute_redirect_to_location(options)
- case options
- # The scheme name consist of a letter followed by any combination of
- # letters, digits, and the plus ("+"), period ("."), or hyphen ("-")
- # characters; and is terminated by a colon (":").
- # The protocol relative scheme starts with a double slash "//"
- when %r{^(\w[\w+.-]*:|//).*}
- options
- when String
- request.protocol + request.host_with_port + options
- when :back
- raise RedirectBackError unless refer = request.headers["Referer"]
- refer
- when Proc
- _compute_redirect_to_location options.call
- else
- url_for(options)
- end.delete("\0\r\n")
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/renderers.rb b/actionpack/lib/action_controller/metal/renderers.rb
index 78aeeef2bf..62a3844b04 100644
--- a/actionpack/lib/action_controller/metal/renderers.rb
+++ b/actionpack/lib/action_controller/metal/renderers.rb
@@ -6,6 +6,12 @@ module ActionController
Renderers.add(key, &block)
end
+ class MissingRenderer < LoadError
+ def initialize(format)
+ super "No renderer defined for format: #{format}"
+ end
+ end
+
module Renderers
extend ActiveSupport::Concern
@@ -52,8 +58,8 @@ module ActionController
# ActionController::Renderers.add :csv do |obj, options|
# filename = options[:filename] || 'data'
# str = obj.respond_to?(:to_csv) ? obj.to_csv : obj.to_s
- # send_data str, :type => Mime::CSV,
- # :disposition => "attachment; filename=#{filename}.csv"
+ # send_data str, type: Mime::CSV,
+ # disposition: "attachment; filename=#{filename}.csv"
# end
#
# Note that we used Mime::CSV for the csv mime type as it comes with Rails.
@@ -66,7 +72,7 @@ module ActionController
# @csvable = Csvable.find(params[:id])
# respond_to do |format|
# format.html
- # format.csv { render :csv => @csvable, :filename => @csvable.name }
+ # format.csv { render csv: @csvable, filename: @csvable.name }
# }
# end
# To use renderers and their mime types in more concise ways, see
diff --git a/actionpack/lib/action_controller/metal/rendering.rb b/actionpack/lib/action_controller/metal/rendering.rb
index c5e7d4e357..90f0ef0b1c 100644
--- a/actionpack/lib/action_controller/metal/rendering.rb
+++ b/actionpack/lib/action_controller/metal/rendering.rb
@@ -2,39 +2,45 @@ module ActionController
module Rendering
extend ActiveSupport::Concern
- include AbstractController::Rendering
-
# Before processing, set the request formats in current controller formats.
def process_action(*) #:nodoc:
- self.formats = request.formats.map { |x| x.ref }
+ self.formats = request.formats.map(&:ref).compact
super
end
# Check for double render errors and set the content_type after rendering.
def render(*args) #:nodoc:
- raise ::AbstractController::DoubleRenderError if response_body
+ raise ::AbstractController::DoubleRenderError if self.response_body
super
- self.content_type ||= Mime[lookup_context.rendered_format].to_s
- response_body
end
# Overwrite render_to_string because body can now be set to a rack body.
def render_to_string(*)
- if self.response_body = super
+ result = super
+ if result.respond_to?(:each)
string = ""
- response_body.each { |r| string << r }
+ result.each { |r| string << r }
string
+ else
+ result
end
- ensure
- self.response_body = nil
end
- def render_to_body(*)
- super || " "
+ def render_to_body(options = {})
+ super || if options[:text].present?
+ options[:text]
+ else
+ " "
+ end
end
private
+ def _process_format(format)
+ super
+ self.content_type ||= format.to_s
+ end
+
# Normalize arguments by catching blocks and setting them on :update.
def _normalize_args(action=nil, options={}, &blk) #:nodoc:
options = super
diff --git a/actionpack/lib/action_controller/metal/request_forgery_protection.rb b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
index 17d4a793ac..bd64b1f812 100644
--- a/actionpack/lib/action_controller/metal/request_forgery_protection.rb
+++ b/actionpack/lib/action_controller/metal/request_forgery_protection.rb
@@ -19,7 +19,7 @@ module ActionController #:nodoc:
#
# class ApplicationController < ActionController::Base
# protect_from_forgery
- # skip_before_filter :verify_authenticity_token, :if => :json_request?
+ # skip_before_action :verify_authenticity_token, if: :json_request?
#
# protected
#
@@ -50,6 +50,10 @@ module ActionController #:nodoc:
config_accessor :request_forgery_protection_token
self.request_forgery_protection_token ||= :authenticity_token
+ # Holds the class which implements the request forgery protection.
+ config_accessor :forgery_protection_strategy
+ self.forgery_protection_strategy = nil
+
# Controls whether request forgery protection is turned on or not. Turned off by default only in test mode.
config_accessor :allow_forgery_protection
self.allow_forgery_protection = true if allow_forgery_protection.nil?
@@ -62,19 +66,19 @@ module ActionController #:nodoc:
# Turn on request forgery protection. Bear in mind that only non-GET, HTML/JavaScript requests are checked.
#
# class FooController < ApplicationController
- # protect_from_forgery :except => :index
+ # protect_from_forgery except: :index
#
# You can disable csrf protection on controller-by-controller basis:
#
- # skip_before_filter :verify_authenticity_token
+ # skip_before_action :verify_authenticity_token
#
# It can also be disabled for specific controller actions:
#
- # skip_before_filter :verify_authenticity_token, :except => [:create]
+ # skip_before_action :verify_authenticity_token, except: [:create]
#
# Valid Options:
#
- # * <tt>:only/:except</tt> - Passed to the <tt>before_filter</tt> call. Set which actions are verified.
+ # * <tt>:only/:except</tt> - Passed to the <tt>before_action</tt> call. Set which actions are verified.
# * <tt>:with</tt> - Set the method to handle unverified request.
#
# Valid unverified request handling methods are:
@@ -82,14 +86,14 @@ module ActionController #:nodoc:
# * <tt>:reset_session</tt> - Resets the session.
# * <tt>:null_session</tt> - Provides an empty session during request but doesn't reset it completely. Used as default if <tt>:with</tt> option is not specified.
def protect_from_forgery(options = {})
- include protection_method_module(options[:with] || :null_session)
+ self.forgery_protection_strategy = protection_method_class(options[:with] || :null_session)
self.request_forgery_protection_token ||= :authenticity_token
- prepend_before_filter :verify_authenticity_token, options
+ prepend_before_action :verify_authenticity_token, options
end
private
- def protection_method_module(name)
+ def protection_method_class(name)
ActionController::RequestForgeryProtection::ProtectionMethods.const_get(name.to_s.classify)
rescue NameError
raise ArgumentError, 'Invalid request forgery protection method, use :null_session, :exception, or :reset_session'
@@ -97,23 +101,32 @@ module ActionController #:nodoc:
end
module ProtectionMethods
- module NullSession
- protected
+ class NullSession
+ def initialize(controller)
+ @controller = controller
+ end
# This is the method that defines the application behavior when a request is found to be unverified.
def handle_unverified_request
- request.session = NullSessionHash.new
+ request = @controller.request
+ request.session = NullSessionHash.new(request.env)
request.env['action_dispatch.request.flash_hash'] = nil
request.env['rack.session.options'] = { skip: true }
request.env['action_dispatch.cookies'] = NullCookieJar.build(request)
end
+ protected
+
class NullSessionHash < Rack::Session::Abstract::SessionHash #:nodoc:
- def initialize
- super(nil, nil)
+ def initialize(env)
+ super(nil, env)
+ @data = {}
@loaded = true
end
+ # no-op
+ def destroy; end
+
def exists?
true
end
@@ -121,11 +134,11 @@ module ActionController #:nodoc:
class NullCookieJar < ActionDispatch::Cookies::CookieJar #:nodoc:
def self.build(request)
- secret = request.env[ActionDispatch::Cookies::TOKEN_KEY]
- host = request.host
- secure = request.ssl?
+ key_generator = request.env[ActionDispatch::Cookies::GENERATOR_KEY]
+ host = request.host
+ secure = request.ssl?
- new(secret, host, secure)
+ new(key_generator, host, secure, options_for_env({}))
end
def write(*)
@@ -134,16 +147,20 @@ module ActionController #:nodoc:
end
end
- module ResetSession
- protected
+ class ResetSession
+ def initialize(controller)
+ @controller = controller
+ end
def handle_unverified_request
- reset_session
+ @controller.reset_session
end
end
- module Exception
- protected
+ class Exception
+ def initialize(controller)
+ @controller = controller
+ end
def handle_unverified_request
raise ActionController::InvalidAuthenticityToken
@@ -152,7 +169,11 @@ module ActionController #:nodoc:
end
protected
- # The actual before_filter that is used. Modify this to change how you handle unverified requests.
+ def handle_unverified_request
+ forgery_protection_strategy.new(self).handle_unverified_request
+ end
+
+ # The actual before_action that is used. Modify this to change how you handle unverified requests.
def verify_authenticity_token
unless verified_request?
logger.warn "Can't verify CSRF token authenticity" if logger
@@ -162,11 +183,11 @@ module ActionController #:nodoc:
# Returns true or false if a request is verified. Checks:
#
- # * is it a GET request? Gets should be safe and idempotent
+ # * is it a GET or HEAD request? Gets should be safe and idempotent
# * Does the form_authenticity_token match the given token value from the params?
# * Does the X-CSRF-Token header match the form_authenticity_token
def verified_request?
- !protect_against_forgery? || request.get? ||
+ !protect_against_forgery? || request.get? || request.head? ||
form_authenticity_token == params[request_forgery_protection_token] ||
form_authenticity_token == request.headers['X-CSRF-Token']
end
@@ -181,6 +202,7 @@ module ActionController #:nodoc:
params[request_forgery_protection_token]
end
+ # Checks if the controller allows forgery protection.
def protect_against_forgery?
allow_forgery_protection
end
diff --git a/actionpack/lib/action_controller/metal/responder.rb b/actionpack/lib/action_controller/metal/responder.rb
index 42a0959a58..b4ba169e8f 100644
--- a/actionpack/lib/action_controller/metal/responder.rb
+++ b/actionpack/lib/action_controller/metal/responder.rb
@@ -45,10 +45,10 @@ module ActionController #:nodoc:
# if @user.save
# flash[:notice] = 'User was successfully created.'
# format.html { redirect_to(@user) }
- # format.xml { render :xml => @user, :status => :created, :location => @user }
+ # format.xml { render xml: @user, status: :created, location: @user }
# else
- # format.html { render :action => "new" }
- # format.xml { render :xml => @user.errors, :status => :unprocessable_entity }
+ # format.html { render action: "new" }
+ # format.xml { render xml: @user.errors, status: :unprocessable_entity }
# end
# end
# end
@@ -92,18 +92,22 @@ module ActionController #:nodoc:
# @project = Project.find(params[:project_id])
# @task = @project.tasks.build(params[:task])
# flash[:notice] = 'Task was successfully created.' if @task.save
- # respond_with(@project, @task, :status => 201)
+ # respond_with(@project, @task, status: 201)
# end
#
# This will return status 201 if the task was saved successfully. If not,
# it will simply ignore the given options and return status 422 and the
- # resource errors. To customize the failure scenario, you can pass a
- # a block to <code>respond_with</code>:
+ # resource errors. You can also override the location to redirect to:
+ #
+ # respond_with(@project, location: root_path)
+ #
+ # To customize the failure scenario, you can pass a block to
+ # <code>respond_with</code>:
#
# def create
# @project = Project.find(params[:project_id])
# @task = @project.tasks.build(params[:task])
- # respond_with(@project, @task, :status => 201) do |format|
+ # respond_with(@project, @task, status: 201) do |format|
# if @task.save
# flash[:notice] = 'Task was successfully created.'
# else
@@ -140,7 +144,7 @@ module ActionController #:nodoc:
undef_method(:to_json) if method_defined?(:to_json)
undef_method(:to_yaml) if method_defined?(:to_yaml)
- # Initializes a new responder an invoke the proper format. If the format is
+ # Initializes a new responder and invokes the proper format. If the format is
# not defined, call to_format.
#
def self.call(*args)
@@ -198,6 +202,7 @@ module ActionController #:nodoc:
# This is the common behavior for formats associated with APIs, such as :xml and :json.
def api_behavior(error)
raise error unless resourceful?
+ raise MissingRenderer.new(format) unless has_renderer?
if get?
display resource
@@ -236,20 +241,20 @@ module ActionController #:nodoc:
# Display is just a shortcut to render a resource with the current format.
#
- # display @user, :status => :ok
+ # display @user, status: :ok
#
# For XML requests it's equivalent to:
#
- # render :xml => @user, :status => :ok
+ # render xml: @user, status: :ok
#
# Options sent by the user are also used:
#
- # respond_with(@user, :status => :created)
- # display(@user, :status => :ok)
+ # respond_with(@user, status: :created)
+ # display(@user, status: :ok)
#
# Results in:
#
- # render :xml => @user, :status => :created
+ # render xml: @user, status: :created
#
def display(resource, given_options={})
controller.render given_options.merge!(options).merge!(format => resource)
@@ -265,6 +270,11 @@ module ActionController #:nodoc:
resource.respond_to?(:errors) && !resource.errors.empty?
end
+ # Check whether the neceessary Renderer is available
+ def has_renderer?
+ Renderers::RENDERERS.include?(format)
+ end
+
# By default, render the <code>:edit</code> action for HTML requests with errors, unless
# the verb was POST.
#
diff --git a/actionpack/lib/action_controller/metal/streaming.rb b/actionpack/lib/action_controller/metal/streaming.rb
index 9f3c997024..62d5931b45 100644
--- a/actionpack/lib/action_controller/metal/streaming.rb
+++ b/actionpack/lib/action_controller/metal/streaming.rb
@@ -21,15 +21,13 @@ module ActionController #:nodoc:
# supports fibers (fibers are supported since version 1.9.2 of the main
# Ruby implementation).
#
- # == Examples
- #
# Streaming can be added to a given template easily, all you need to do is
# to pass the :stream option.
#
# class PostsController
# def index
- # @posts = Post.scoped
- # render :stream => true
+ # @posts = Post.all
+ # render stream: true
# end
# end
#
@@ -53,10 +51,10 @@ module ActionController #:nodoc:
#
# def dashboard
# # Allow lazy execution of the queries
- # @posts = Post.scoped
- # @pages = Page.scoped
- # @articles = Article.scoped
- # render :stream => true
+ # @posts = Post.all
+ # @pages = Page.all
+ # @articles = Article.all
+ # render stream: true
# end
#
# Notice that :stream only works with templates. Rendering :json
@@ -176,7 +174,7 @@ module ActionController #:nodoc:
# need to create a config file as follow:
#
# # unicorn.config.rb
- # listen 3000, :tcp_nopush => false
+ # listen 3000, tcp_nopush: false
#
# And use it on initialization:
#
@@ -195,31 +193,29 @@ module ActionController #:nodoc:
module Streaming
extend ActiveSupport::Concern
- include AbstractController::Rendering
-
protected
- # Set proper cache control and transfer encoding when streaming
- def _process_options(options) #:nodoc:
- super
- if options[:stream]
- if env["HTTP_VERSION"] == "HTTP/1.0"
- options.delete(:stream)
- else
- headers["Cache-Control"] ||= "no-cache"
- headers["Transfer-Encoding"] = "chunked"
- headers.delete("Content-Length")
+ # Set proper cache control and transfer encoding when streaming
+ def _process_options(options) #:nodoc:
+ super
+ if options[:stream]
+ if env["HTTP_VERSION"] == "HTTP/1.0"
+ options.delete(:stream)
+ else
+ headers["Cache-Control"] ||= "no-cache"
+ headers["Transfer-Encoding"] = "chunked"
+ headers.delete("Content-Length")
+ end
end
end
- end
- # Call render_body if we are streaming instead of usual +render+.
- def _render_template(options) #:nodoc:
- if options.delete(:stream)
- Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
- else
- super
+ # Call render_body if we are streaming instead of usual +render+.
+ def _render_template(options) #:nodoc:
+ if options.delete(:stream)
+ Rack::Chunked::Body.new view_renderer.render_body(view_context, options)
+ else
+ super
+ end
end
- end
end
end
diff --git a/actionpack/lib/action_controller/metal/strong_parameters.rb b/actionpack/lib/action_controller/metal/strong_parameters.rb
index 6f46954266..8ae7e474a3 100644
--- a/actionpack/lib/action_controller/metal/strong_parameters.rb
+++ b/actionpack/lib/action_controller/metal/strong_parameters.rb
@@ -1,6 +1,8 @@
-require 'active_support/concern'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/array/wrap'
require 'active_support/rescuable'
+require 'action_dispatch/http/upload'
+require 'stringio'
module ActionController
# Raised when a required parameter is missing.
@@ -8,8 +10,6 @@ module ActionController
# params = ActionController::Parameters.new(a: {})
# params.fetch(:b)
# # => ActionController::ParameterMissing: param not found: b
- # params.require(:a)
- # # => ActionController::ParameterMissing: param not found: a
class ParameterMissing < KeyError
attr_reader :param # :nodoc:
@@ -19,13 +19,41 @@ module ActionController
end
end
- # == Action Controller Parameters
+ # Raised when a required parameter value is empty.
+ #
+ # params = ActionController::Parameters.new(a: {})
+ # params.require(:a)
+ # # => ActionController::EmptyParameter: value is empty for required key: a
+ class EmptyParameter < IndexError
+ attr_reader :param
+
+ def initialize(param)
+ @param = param
+ super("value is empty for required key: #{param}")
+ end
+ end
+
+ # Raised when a supplied parameter is not expected.
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unexpected keys: a, b
+ class UnpermittedParameters < IndexError
+ attr_reader :params # :nodoc:
+
+ def initialize(params) # :nodoc:
+ @params = params
+ super("found unpermitted parameters: #{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.
# 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.
+ # as permitted and limit which attributes should be allowed for mass updating.
#
# params = ActionController::Parameters.new({
# person: {
@@ -40,13 +68,20 @@ module ActionController
# permitted.class # => ActionController::Parameters
# permitted.permitted? # => true
#
- # Person.first.update_attributes!(permitted)
+ # Person.first.update!(permitted)
# # => #<Person id: 1, name: "Francesco", age: 22, role: "user">
#
- # It provides a +permit_all_parameters+ option that controls the top-level
- # behaviour of new instances. If it's +true+, all the parameters will be
- # permitted by default. The default value for +permit_all_parameters+
- # option is +false+.
+ # It provides two options that controls the top-level behavior of new instances:
+ #
+ # * +permit_all_parameters+ - If it's +true+, all the parameters will be
+ # permitted by default. The default is +false+.
+ # * +action_on_unpermitted_parameters+ - Allow to control the behavior when parameters
+ # that are not explicitly permitted are found. The values can be <tt>:log</tt> to
+ # write a message on the logger or <tt>:raise</tt> to raise
+ # ActionController::UnpermittedParameters exception. The default value is <tt>:log</tt>
+ # in test and development environments, +false+ otherwise.
+ #
+ # Examples:
#
# params = ActionController::Parameters.new
# params.permitted? # => false
@@ -56,6 +91,16 @@ module ActionController
# params = ActionController::Parameters.new
# params.permitted? # => true
#
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => {}
+ #
+ # ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ #
+ # params = ActionController::Parameters.new(a: "123", b: "456")
+ # params.permit(:c)
+ # # => ActionController::UnpermittedParameters: found unpermitted keys: a, b
+ #
# <tt>ActionController::Parameters</tt> is inherited from
# <tt>ActiveSupport::HashWithIndifferentAccess</tt>, this means
# that you can fetch values using either <tt>:key</tt> or <tt>"key"</tt>.
@@ -65,24 +110,27 @@ module ActionController
# params["key"] # => "value"
class Parameters < ActiveSupport::HashWithIndifferentAccess
cattr_accessor :permit_all_parameters, instance_accessor: false
- attr_accessor :permitted # :nodoc:
+ 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 )
# Returns a new instance of <tt>ActionController::Parameters</tt>.
# Also, sets the +permitted+ attribute to the default value of
# <tt>ActionController::Parameters.permit_all_parameters</tt>.
#
- # class Person
- # include ActiveRecord::Base
+ # class Person < ActiveRecord::Base
# end
#
# params = ActionController::Parameters.new(name: 'Francesco')
# params.permitted? # => false
- # Person.new(params) # => ActiveModel::ForbiddenAttributesError
+ # Person.new(params) # => ActiveModel::ForbiddenAttributesError
#
# ActionController::Parameters.permit_all_parameters = true
#
# params = ActionController::Parameters.new(name: 'Francesco')
- # params.permitted? # => true
+ # params.permitted? # => true
# Person.new(params) # => #<Person id: nil, name: "Francesco">
def initialize(attributes = nil)
super(attributes)
@@ -106,7 +154,7 @@ module ActionController
# end
#
# params = ActionController::Parameters.new(name: 'Francesco')
- # params.permitted? # => false
+ # params.permitted? # => false
# Person.new(params) # => ActiveModel::ForbiddenAttributesError
# params.permit!
# params.permitted? # => true
@@ -121,41 +169,59 @@ module ActionController
self
end
- # Ensures that a parameter is present. If it's present, returns
- # the parameter at the given +key+, otherwise raises an
- # <tt>ActionController::ParameterMissing</tt> error.
+ # Ensures that a parameter is present. If it's present and not empty,
+ # returns the parameter at the given +key+, if it's empty raises
+ # an <tt>ActionController::EmptyParameter</tt> error, otherwise
+ # raises an <tt>ActionController::ParameterMissing</tt> error.
#
- # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)
- # # => {"name"=>"Francesco"}
- #
- # ActionController::Parameters.new(person: nil).require(:person)
- # # => ActionController::ParameterMissing: param not found: person
+ # ActionController::Parameters.new(person: { name: 'Francesco' }).require(:person)
+ # # => {"name"=>"Francesco"}
#
# ActionController::Parameters.new(person: {}).require(:person)
+ # # => ActionController::EmptyParameter: value is empty for required key: person
+ #
+ # ActionController::Parameters.new(name: {}).require(:person)
# # => ActionController::ParameterMissing: param not found: person
def require(key)
- self[key].presence || raise(ParameterMissing.new(key))
+ raise(ActionController::ParameterMissing.new(key)) unless self.key?(key)
+ self[key].presence || raise(ActionController::EmptyParameter.new(key))
end
# Alias of #require.
alias :required :require
# Returns a new <tt>ActionController::Parameters</tt> instance that
- # includes only the given +filters+ and sets the +permitted+ for the
- # object to +true+. This is useful for limiting which attributes
+ # includes only the given +filters+ and sets the +permitted+ attribute
+ # for the object to +true+. This is useful for limiting which attributes
# should be allowed for mass updating.
#
# params = ActionController::Parameters.new(user: { name: 'Francesco', age: 22, role: 'admin' })
# permitted = params.require(:user).permit(:name, :age)
- # permitted.permitted? # => true
+ # permitted.permitted? # => true
# permitted.has_key?(:name) # => true
# permitted.has_key?(:age) # => true
# permitted.has_key?(:role) # => false
#
+ # Only permitted scalars pass the filter. For example, given
+ #
+ # params.permit(:name)
+ #
+ # +:name+ passes it is a key of +params+ whose associated value is of type
+ # +String+, +Symbol+, +NilClass+, +Numeric+, +TrueClass+, +FalseClass+,
+ # +Date+, +Time+, +DateTime+, +StringIO+, +IO+,
+ # +ActionDispatch::Http::UploadedFile+ or +Rack::Test::UploadedFile+.
+ # Otherwise, the key +:name+ is filtered out.
+ #
+ # You may declare that the parameter should be an array of permitted scalars
+ # by mapping it to an empty array:
+ #
+ # params = ActionController::Parameters.new(tags: ['rails', 'parameters'])
+ # params.permit(tags: [])
+ #
# You can also use +permit+ on nested parameters, like:
#
# params = ActionController::Parameters.new({
- # person: {
+ # person: {
# name: 'Francesco',
# age: 22,
# pets: [{
@@ -165,10 +231,10 @@ module ActionController
# }
# })
#
- # permitted = params.permit(person: [ :name, { pets: :name } ])
+ # permitted = params.permit(person: [ :name, { pets: :name } ])
# permitted.permitted? # => true
# permitted[:person][:name] # => "Francesco"
- # permitted[:person][:age] # => nil
+ # permitted[:person][:age] # => nil
# permitted[:person][:pets][0][:name] # => "Purplish"
# permitted[:person][:pets][0][:category] # => nil
#
@@ -179,7 +245,7 @@ module ActionController
# params = ActionController::Parameters.new({
# person: {
# contact: {
- # email: 'none@test.com'
+ # email: 'none@test.com',
# phone: '555-1234'
# }
# }
@@ -189,47 +255,32 @@ module ActionController
# # => {}
#
# params.require(:person).permit(contact: :phone)
- # # => {"contact"=>{"phone"=>"555-1234"}}
+ # # => {"contact"=>{"phone"=>"555-1234"}}
#
# params.require(:person).permit(contact: [ :email, :phone ])
# # => {"contact"=>{"email"=>"none@test.com", "phone"=>"555-1234"}}
def permit(*filters)
params = self.class.new
- filters.each do |filter|
+ filters.flatten.each do |filter|
case filter
- when Symbol, String then
- if has_key?(filter)
- _value = self[filter]
- params[filter] = _value unless Hash === _value
- end
- keys.grep(/\A#{Regexp.escape(filter)}\(\di\)\z/) { |key| params[key] = self[key] }
+ when Symbol, String
+ permitted_scalar_filter(params, filter)
when Hash then
- self.slice(*filter.keys).each do |key, values|
- return unless values
-
- key = key.to_sym
-
- params[key] = each_element(values) do |value|
- # filters are a Hash, so we expect value to be a Hash too
- next if filter.is_a?(Hash) && !value.is_a?(Hash)
-
- value = self.class.new(value) if !value.respond_to?(:permit)
-
- value.permit(*Array.wrap(filter[key]))
- end
- end
+ hash_filter(params, filter)
end
end
+ unpermitted_parameters!(params) if self.class.action_on_unpermitted_parameters
+
params.permit!
end
# Returns a parameter for the given +key+. If not found,
# returns +nil+.
#
- # params = ActionController::Parameters.new(person: { name: 'Francesco' })
- # params[:person] # => {"name"=>"Francesco"}
+ # params = ActionController::Parameters.new(person: { name: 'Francesco' })
+ # params[:person] # => {"name"=>"Francesco"}
# params[:none] # => nil
def [](key)
convert_hashes_to_parameters(key, super)
@@ -242,12 +293,19 @@ module ActionController
# is given, then that will be run and its result returned.
#
# params = ActionController::Parameters.new(person: { name: 'Francesco' })
- # params.fetch(:person) # => {"name"=>"Francesco"}
- # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none
+ # params.fetch(:person) # => {"name"=>"Francesco"}
+ # params.fetch(:none) # => ActionController::ParameterMissing: param not found: none
# params.fetch(:none, 'Francesco') # => "Francesco"
- # params.fetch(:none) { 'Francesco' } # => "Francesco"
+ # params.fetch(:none) { 'Francesco' } # => "Francesco"
def fetch(key, *args)
- convert_hashes_to_parameters(key, super)
+ value = super
+ # Don't rely on +convert_hashes_to_parameters+
+ # so as to not mutate via a +fetch+
+ if value.is_a?(Hash)
+ value = self.class.new(value)
+ value.permit! if permitted?
+ end
+ value
rescue KeyError
raise ActionController::ParameterMissing.new(key)
end
@@ -260,7 +318,9 @@ module ActionController
# params.slice(:a, :b) # => {"a"=>1, "b"=>2}
# params.slice(:d) # => {}
def slice(*keys)
- self.class.new(super)
+ self.class.new(super).tap do |new_instance|
+ new_instance.permitted = @permitted
+ end
end
# Returns an exact copy of the <tt>ActionController::Parameters</tt>
@@ -273,10 +333,15 @@ module ActionController
# copy_params.permitted? # => true
def dup
super.tap do |duplicate|
- duplicate.instance_variable_set :@permitted, @permitted
+ duplicate.permitted = @permitted
end
end
+ protected
+ def permitted=(new_permitted)
+ @permitted = new_permitted
+ end
+
private
def convert_hashes_to_parameters(key, value)
if value.is_a?(Parameters) || !value.is_a?(Hash)
@@ -290,7 +355,7 @@ module ActionController
def each_element(object)
if object.is_a?(Array)
object.map { |el| yield el }.compact
- elsif object.is_a?(Hash) && object.keys.all? { |k| k =~ /\A-?\d+\z/ }
+ elsif fields_for_style?(object)
hash = object.class.new
object.each { |k,v| hash[k] = yield v }
hash
@@ -298,12 +363,111 @@ module ActionController
yield object
end
end
+
+ def fields_for_style?(object)
+ object.is_a?(Hash) && object.all? { |k, v| k =~ /\A-?\d+\z/ && v.is_a?(Hash) }
+ end
+
+ def unpermitted_parameters!(params)
+ unpermitted_keys = unpermitted_keys(params)
+ if unpermitted_keys.any?
+ case self.class.action_on_unpermitted_parameters
+ when :log
+ name = "unpermitted_parameters.action_controller"
+ ActiveSupport::Notifications.instrument(name, keys: unpermitted_keys)
+ when :raise
+ raise ActionController::UnpermittedParameters.new(unpermitted_keys)
+ end
+ end
+ end
+
+ def unpermitted_keys(params)
+ self.keys - params.keys - NEVER_UNPERMITTED_PARAMS
+ end
+
+ #
+ # --- Filtering ----------------------------------------------------------
+ #
+
+ # This is a white list of permitted scalar types that includes the ones
+ # supported in XML and JSON requests.
+ #
+ # This list is in particular used to filter ordinary requests, String goes
+ # as first element to quickly short-circuit the common case.
+ #
+ # If you modify this collection please update the API of +permit+ above.
+ PERMITTED_SCALAR_TYPES = [
+ String,
+ Symbol,
+ NilClass,
+ Numeric,
+ TrueClass,
+ FalseClass,
+ Date,
+ Time,
+ # DateTimes are Dates, we document the type but avoid the redundant check.
+ StringIO,
+ IO,
+ ActionDispatch::Http::UploadedFile,
+ Rack::Test::UploadedFile,
+ ]
+
+ def permitted_scalar?(value)
+ PERMITTED_SCALAR_TYPES.any? {|type| value.is_a?(type)}
+ end
+
+ def permitted_scalar_filter(params, key)
+ if has_key?(key) && permitted_scalar?(self[key])
+ params[key] = self[key]
+ end
+
+ keys.grep(/\A#{Regexp.escape(key)}\(\d+[if]?\)\z/) do |k|
+ if permitted_scalar?(self[k])
+ params[k] = self[k]
+ end
+ end
+ end
+
+ def array_of_permitted_scalars?(value)
+ if value.is_a?(Array)
+ value.all? {|element| permitted_scalar?(element)}
+ end
+ end
+
+ def array_of_permitted_scalars_filter(params, key)
+ if has_key?(key) && array_of_permitted_scalars?(self[key])
+ params[key] = self[key]
+ end
+ end
+
+ EMPTY_ARRAY = []
+ def hash_filter(params, filter)
+ filter = filter.with_indifferent_access
+
+ # Slicing filters out non-declared keys.
+ slice(*filter.keys).each do |key, value|
+ next unless value
+
+ if filter[key] == EMPTY_ARRAY
+ # Declaration { comment_ids: [] }.
+ array_of_permitted_scalars_filter(params, key)
+ else
+ # Declaration { user: :name } or { user: [:name, :age, { address: ... }] }.
+ params[key] = each_element(value) do |element|
+ if element.is_a?(Hash)
+ element = self.class.new(element) unless element.respond_to?(:permit)
+ element.permit(*Array.wrap(filter[key]))
+ end
+ end
+ end
+ end
+ end
end
# == Strong \Parameters
#
# It provides an interface for protecting attributes from end-user
- # assignment. This makes Action Controller parameters forbidden
+ # assignment. This makes Action Controller parameters forbidden
# to be used in Active Model mass assignment until they have been
# whitelisted.
#
@@ -326,13 +490,13 @@ module ActionController
# # into a 400 Bad Request reply.
# def update
# redirect_to current_account.people.find(params[:id]).tap { |person|
- # person.update_attributes!(person_params)
+ # person.update!(person_params)
# }
# end
#
# private
# # Using a private method to encapsulate the permissible parameters is
- # # just a good pattern since you'll be able to reuse the same permit
+ # # just a good pattern since you'll be able to reuse the same permit
# # list between create and update. Also, you can specialize this method
# # with per-user checking of permissible attributes.
# def person_params
@@ -340,18 +504,37 @@ module ActionController
# end
# end
#
+ # In order to use <tt>accepts_nested_attribute_for</tt> with Strong \Parameters, you
+ # will need to specify which nested attributes should be whitelisted.
+ #
+ # class Person
+ # has_many :pets
+ # accepts_nested_attributes_for :pets
+ # end
+ #
+ # class PeopleController < ActionController::Base
+ # def create
+ # Person.create(person_params)
+ # end
+ #
+ # ...
+ #
+ # private
+ #
+ # def person_params
+ # # It's mandatory to specify the nested attributes that should be whitelisted.
+ # # If you use `permit` with just the key that points to the nested attributes hash,
+ # # it will return an empty hash.
+ # params.require(:person).permit(:name, :age, pets_attributes: [ :name, :category ])
+ # end
+ # end
+ #
# See ActionController::Parameters.require and ActionController::Parameters.permit
# for more information.
module StrongParameters
extend ActiveSupport::Concern
include ActiveSupport::Rescuable
- included do
- rescue_from(ActionController::ParameterMissing) do |parameter_missing_exception|
- render text: "Required parameter missing: #{parameter_missing_exception.param}", status: :bad_request
- end
- end
-
# Returns a new ActionController::Parameters object that
# has been instantiated with the <tt>request.parameters</tt>.
def params
diff --git a/actionpack/lib/action_controller/metal/url_for.rb b/actionpack/lib/action_controller/metal/url_for.rb
index 0cdd17bc2e..754249cbc8 100644
--- a/actionpack/lib/action_controller/metal/url_for.rb
+++ b/actionpack/lib/action_controller/metal/url_for.rb
@@ -10,7 +10,7 @@ module ActionController
# include ActionController::UrlFor
# include Rails.application.routes.url_helpers
#
- # delegate :env, :request, :to => :controller
+ # delegate :env, :request, to: :controller
#
# def initialize(controller)
# @controller = controller
@@ -32,7 +32,8 @@ module ActionController
if (same_origin = _routes.equal?(env["action_dispatch.routes"])) ||
(script_name = env["ROUTES_#{_routes.object_id}_SCRIPT_NAME"]) ||
- (original_script_name = env['SCRIPT_NAME'])
+ (original_script_name = env['ORIGINAL_SCRIPT_NAME'])
+
@_url_options.dup.tap do |options|
if original_script_name
options[:original_script_name] = original_script_name
diff --git a/actionpack/lib/action_controller/railtie.rb b/actionpack/lib/action_controller/railtie.rb
index ee0f053bad..0833e65d23 100644
--- a/actionpack/lib/action_controller/railtie.rb
+++ b/actionpack/lib/action_controller/railtie.rb
@@ -1,7 +1,6 @@
require "rails"
require "action_controller"
require "action_dispatch/railtie"
-require "action_view/railtie"
require "abstract_controller/railties/routes_helpers"
require "action_controller/railties/helpers"
@@ -20,23 +19,27 @@ module ActionController
end
initializer "action_controller.parameters_config" do |app|
- ActionController::Parameters.permit_all_parameters = app.config.action_controller.delete(:permit_all_parameters) { false }
+ options = app.config.action_controller
+
+ ActionController::Parameters.permit_all_parameters = options.delete(:permit_all_parameters) { false }
+ ActionController::Parameters.action_on_unpermitted_parameters = options.delete(:action_on_unpermitted_parameters) do
+ (Rails.env.test? || Rails.env.development?) ? :log : false
+ end
end
initializer "action_controller.set_configs" do |app|
paths = app.config.paths
options = app.config.action_controller
- options.logger ||= Rails.logger
- options.cache_store ||= Rails.cache
+ options.logger ||= Rails.logger
+ options.cache_store ||= Rails.cache
- options.javascripts_dir ||= paths["public/javascripts"].first
- options.stylesheets_dir ||= paths["public/stylesheets"].first
+ options.javascripts_dir ||= paths["public/javascripts"].first
+ options.stylesheets_dir ||= paths["public/stylesheets"].first
# Ensure readers methods get compiled
- options.asset_path ||= app.config.asset_path
- options.asset_host ||= app.config.asset_host
- options.relative_url_root ||= app.config.relative_url_root
+ options.asset_host ||= app.config.asset_host
+ options.relative_url_root ||= app.config.relative_url_root
ActiveSupport.on_load(:action_controller) do
include app.routes.mounted_helpers
diff --git a/actionpack/lib/action_controller/record_identifier.rb b/actionpack/lib/action_controller/record_identifier.rb
deleted file mode 100644
index bffd2a02d0..0000000000
--- a/actionpack/lib/action_controller/record_identifier.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require 'active_support/deprecation'
-require 'action_view/record_identifier'
-
-module ActionController
- module RecordIdentifier
- MESSAGE = 'method will no longer be included by default in controllers since Rails 4.1. ' +
- 'If you would like to use it in controllers, please include ' +
- 'ActionView::RecodIdentifier module.'
-
- def dom_id(record, prefix = nil)
- ActiveSupport::Deprecation.warn 'dom_id ' + MESSAGE
- ActionView::RecordIdentifier.dom_id(record, prefix)
- end
-
- def dom_class(record, prefix = nil)
- ActiveSupport::Deprecation.warn 'dom_class ' + MESSAGE
- ActionView::RecordIdentifier.dom_class(record, prefix)
- end
- end
-end
diff --git a/actionpack/lib/action_controller/test_case.rb b/actionpack/lib/action_controller/test_case.rb
index d911d47a1d..5ed3d2ebc1 100644
--- a/actionpack/lib/action_controller/test_case.rb
+++ b/actionpack/lib/action_controller/test_case.rb
@@ -1,6 +1,7 @@
require 'rack/session/abstract/id'
require 'active_support/core_ext/object/to_query'
require 'active_support/core_ext/module/anonymous'
+require 'active_support/core_ext/hash/keys'
module ActionController
module TemplateAssertions
@@ -15,8 +16,9 @@ module ActionController
@_partials = Hash.new(0)
@_templates = Hash.new(0)
@_layouts = Hash.new(0)
+ @_files = Hash.new(0)
- ActiveSupport::Notifications.subscribe("render_template.action_view") do |name, start, finish, id, payload|
+ ActiveSupport::Notifications.subscribe("render_template.action_view") do |_name, _start, _finish, _id, payload|
path = payload[:layout]
if path
@_layouts[path] += 1
@@ -26,7 +28,7 @@ module ActionController
end
end
- ActiveSupport::Notifications.subscribe("!render_template.action_view") do |name, start, finish, id, payload|
+ ActiveSupport::Notifications.subscribe("!render_template.action_view") do |_name, _start, _finish, _id, payload|
path = payload[:virtual_path]
next unless path
partial = path =~ /^.*\/_[^\/]*$/
@@ -38,6 +40,16 @@ module ActionController
@_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
+
+ path = payload[:identifier]
+ if path
+ @_files[path] += 1
+ @_files[path.split("/").last] += 1
+ end
+ end
end
def teardown_subscriptions
@@ -61,28 +73,27 @@ module ActionController
# assert_template %r{\Aadmin/posts/new\Z}
#
# # assert that the layout 'admin' was rendered
- # assert_template :layout => 'admin'
- # assert_template :layout => 'layouts/admin'
- # assert_template :layout => :admin
+ # assert_template layout: 'admin'
+ # assert_template layout: 'layouts/admin'
+ # assert_template layout: :admin
#
# # assert that no layout was rendered
- # assert_template :layout => nil
- # assert_template :layout => false
+ # assert_template layout: nil
+ # assert_template layout: false
#
# # assert that the "_customer" partial was rendered twice
- # assert_template :partial => '_customer', :count => 2
+ # assert_template partial: '_customer', count: 2
#
# # assert that no partials were rendered
- # assert_template :partial => false
+ # assert_template partial: false
#
# In a view test case, you can also assert that specific locals are passed
# to partials:
#
# # assert that the "_customer" partial was rendered with a specific object
- # assert_template :partial => '_customer', :locals => { :customer => @customer }
+ # assert_template partial: '_customer', locals: { customer: @customer }
def assert_template(options = {}, message = nil)
- # Force body to be read in case the
- # template is being streamed
+ # Force body to be read in case the template is being streamed.
response.body
case options
@@ -94,7 +105,7 @@ module ActionController
matches_template =
case options
when String
- rendered.any? do |t, num|
+ !options.empty? && rendered.any? do |t, num|
options_splited = options.split(File::SEPARATOR)
t_splited = t.split(File::SEPARATOR)
t_splited.last(options_splited.size) == options_splited
@@ -106,6 +117,8 @@ module ActionController
end
assert matches_template, msg
when Hash
+ options.assert_valid_keys(:layout, :partial, :locals, :count, :file)
+
if options.key?(:layout)
expected_layout = options[:layout]
msg = message || sprintf("expecting layout <%s> but action rendered <%s>",
@@ -121,10 +134,18 @@ module ActionController
end
end
+ if options[:file]
+ assert_includes @_files.keys, options[:file]
+ end
+
if expected_partial = options[:partial]
if expected_locals = options[:locals]
if defined?(@_rendered_views)
- view = expected_partial.to_s.sub(/^_/,'')
+ view = expected_partial.to_s.sub(/^_/, '').sub(/\/_(?=[^\/]+\z)/, '/')
+
+ partial_was_not_rendered_msg = "expected %s to be rendered but it was not." % view
+ assert_includes @_rendered_views.rendered_views, view, partial_was_not_rendered_msg
+
msg = 'expecting %s to be rendered with %s but was with %s' % [expected_partial,
expected_locals,
@_rendered_views.locals_for(view)]
@@ -234,18 +255,39 @@ module ActionController
end
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:
DEFAULT_OPTIONS = Rack::Session::Abstract::ID::DEFAULT_OPTIONS
def initialize(session = {})
super(nil, nil)
- replace(session.stringify_keys)
+ @id = SecureRandom.hex(16)
+ @data = stringify_keys(session)
@loaded = true
end
def exists?
true
end
+
+ def keys
+ @data.keys
+ end
+
+ def values
+ @data.values
+ end
+
+ def destroy
+ clear
+ end
+
+ private
+
+ def load!
+ @id
+ end
end
# Superclass for ActionController functional tests. Functional tests allow you to
@@ -267,21 +309,21 @@ module ActionController
# class BooksControllerTest < ActionController::TestCase
# def test_create
# # Simulate a POST response with the given HTTP parameters.
- # post(:create, :book => { :title => "Love Hina" })
+ # post(:create, book: { title: "Love Hina" })
#
# # Assert that the controller tried to redirect us to
# # the created book's URI.
# assert_response :found
#
# # Assert that the controller really put the book in the database.
- # assert_not_nil Book.find_by_title("Love Hina")
+ # assert_not_nil Book.find_by(title: "Love Hina")
# end
# end
#
# You can also send a real document in the simulated HTTP request.
#
# def test_create
- # json = {:book => { :title => "Love Hina" }}.to_json
+ # json = {book: { title: "Love Hina" }}.to_json
# post :create, json
# end
#
@@ -356,15 +398,8 @@ module ActionController
#
# If you're using named routes, they can be easily tested using the original named routes' methods straight in the test case.
#
- # assert_redirected_to page_url(:title => 'foo')
+ # assert_redirected_to page_url(title: 'foo')
class TestCase < ActiveSupport::TestCase
-
- # Use AC::TestCase for the base class when describing a controller
- register_spec_type(self) do |desc|
- Class === desc && desc < ActionController::Metal
- end
- register_spec_type(/Controller( ?Test)?\z/i, self)
-
module Behavior
extend ActiveSupport::Concern
include ActionDispatch::TestProcess
@@ -416,41 +451,54 @@ module ActionController
end
- # Executes a request simulating GET HTTP method and set/volley the response
+ # Simulate a GET request with the given parameters.
+ #
+ # - +action+: The controller action to call.
+ # - +parameters+: The HTTP parameters that you want to pass. This may
+ # be +nil+, a hash, or a string that is appropriately encoded
+ # (<tt>application/x-www-form-urlencoded</tt> or <tt>multipart/form-data</tt>).
+ # - +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+.
+ #
+ # Note that the request method is not verified. The different methods are
+ # available to make the tests more expressive.
def get(action, *args)
process(action, "GET", *args)
end
- # Executes a request simulating POST HTTP method and set/volley the response
+ # Simulate a POST request with the given parameters and set/volley the response.
+ # See +get+ for more details.
def post(action, *args)
process(action, "POST", *args)
end
- # Executes a request simulating PATCH HTTP method and set/volley the response
+ # Simulate a PATCH request with the given parameters and set/volley the response.
+ # See +get+ for more details.
def patch(action, *args)
process(action, "PATCH", *args)
end
- # Executes a request simulating PUT HTTP method and set/volley the response
+ # Simulate a PUT request with the given parameters and set/volley the response.
+ # See +get+ for more details.
def put(action, *args)
process(action, "PUT", *args)
end
- # Executes a request simulating DELETE HTTP method and set/volley the response
+ # Simulate a DELETE request with the given parameters and set/volley the response.
+ # See +get+ for more details.
def delete(action, *args)
process(action, "DELETE", *args)
end
- # Executes a request simulating HEAD HTTP method and set/volley the response
+ # Simulate a HEAD request with the given parameters and set/volley the response.
+ # See +get+ for more details.
def head(action, *args)
process(action, "HEAD", *args)
end
- # Executes a request simulating OPTIONS HTTP method and set/volley the response
- def options(action, *args)
- process(action, "OPTIONS", *args)
- end
-
def xml_http_request(request_method, action, parameters = nil, session = nil, flash = nil)
@request.env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
@request.env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
@@ -476,7 +524,6 @@ module ActionController
def process(action, http_method = 'GET', *args)
check_required_ivars
- http_method, args = handle_old_process_api(http_method, args)
if args.first.is_a?(String) && http_method != 'HEAD'
@request.env['RAW_POST_DATA'] = args.shift
@@ -504,12 +551,12 @@ module ActionController
parameters ||= {}
controller_class_name = @controller.class.anonymous? ?
"anonymous" :
- @controller.class.name.underscore.sub(/_controller$/, '')
+ @controller.class.controller_path
@request.assign_parameters(@routes, controller_class_name, action.to_s, parameters)
@request.session.update(session) if session
- @request.session["flash"] = @request.flash.update(flash || {})
+ @request.flash.update(flash || {})
@controller.request = @request
@controller.response = @response
@@ -526,6 +573,7 @@ module ActionController
@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?
@response
end
@@ -579,17 +627,6 @@ module ActionController
end
end
- def handle_old_process_api(http_method, args)
- # 4.0: Remove this method.
- if http_method.is_a?(Hash)
- ActiveSupport::Deprecation.warn("TestCase#process now expects the HTTP method as second argument: process(action, http_method, params, session, flash)")
- args.unshift(http_method)
- http_method = args.last.is_a?(String) ? args.last : "GET"
- end
-
- [http_method, args]
- end
-
def build_request_uri(action, parameters)
unless @request.env["PATH_INFO"]
options = @controller.respond_to?(:url_options) ? @controller.__send__(:url_options).merge(parameters) : parameters
diff --git a/actionpack/lib/action_controller/vendor/html-scanner.rb b/actionpack/lib/action_controller/vendor/html-scanner.rb
deleted file mode 100644
index 896208bc05..0000000000
--- a/actionpack/lib/action_controller/vendor/html-scanner.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-require 'action_view/vendor/html-scanner'
-require 'active_support/deprecation'
-
-ActiveSupport::Deprecation.warn 'Vendored html-scanner was moved to action_view, please require "action_view/vendor/html-scanner" instead. ' +
- 'This file will be removed in Rails 4.1'
diff --git a/actionpack/lib/action_dispatch.rb b/actionpack/lib/action_dispatch.rb
index 0ec355246e..24a3d4741e 100644
--- a/actionpack/lib/action_dispatch.rb
+++ b/actionpack/lib/action_dispatch.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -47,7 +47,6 @@ module ActionDispatch
autoload_under 'middleware' do
autoload :RequestId
- autoload :BestStandardsSupport
autoload :Callbacks
autoload :Cookies
autoload :DebugExceptions
@@ -63,6 +62,7 @@ module ActionDispatch
autoload :Static
end
+ autoload :Journey
autoload :MiddlewareStack, 'action_dispatch/middleware/stack'
autoload :Routing
@@ -75,6 +75,7 @@ module ActionDispatch
autoload :Parameters
autoload :ParameterFilter
autoload :FilterParameters
+ autoload :FilterRedirect
autoload :Upload
autoload :UploadedFile, 'action_dispatch/http/upload'
autoload :URL
@@ -93,7 +94,6 @@ module ActionDispatch
autoload :Assertions
autoload :Integration
autoload :IntegrationTest, 'action_dispatch/testing/integration'
- autoload :PerformanceTest
autoload :TestProcess
autoload :TestRequest
autoload :TestResponse
diff --git a/actionpack/lib/action_dispatch/http/cache.rb b/actionpack/lib/action_dispatch/http/cache.rb
index 0d6015d993..f9b278349e 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
- SPESHUL_KEYS = %w[extras no-cache max-age public must-revalidate]
+ SPECIAL_KEYS = %w[extras no-cache max-age public must-revalidate]
def cache_control_segments
if cache_control = self[CACHE_CONTROL]
@@ -108,7 +108,7 @@ module ActionDispatch
cache_control_segments.each do |segment|
directive, argument = segment.split('=', 2)
- if SPESHUL_KEYS.include? directive
+ if SPECIAL_KEYS.include? directive
key = directive.tr('-', '_')
cache_control[key.to_sym] = argument || true
else
diff --git a/actionpack/lib/action_dispatch/http/filter_parameters.rb b/actionpack/lib/action_dispatch/http/filter_parameters.rb
index 47cf41cfa3..289e204ac8 100644
--- a/actionpack/lib/action_dispatch/http/filter_parameters.rb
+++ b/actionpack/lib/action_dispatch/http/filter_parameters.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/object/duplicable'
+require 'action_dispatch/http/parameter_filter'
module ActionDispatch
module Http
@@ -20,9 +21,16 @@ module ActionDispatch
# end
# => reverses the value to all keys matching /secret/i
module FilterParameters
- extend ActiveSupport::Concern
+ ENV_MATCH = [/RAW_POST_DATA/, "rack.request.form_vars"] # :nodoc:
+ NULL_PARAM_FILTER = ParameterFilter.new # :nodoc:
+ NULL_ENV_FILTER = ParameterFilter.new ENV_MATCH # :nodoc:
- @@parameter_filter_for = {}
+ def initialize(env)
+ super
+ @filtered_parameters = nil
+ @filtered_env = nil
+ @filtered_path = nil
+ end
# Return a hash of parameters with all sensitive data replaced.
def filtered_parameters
@@ -42,15 +50,20 @@ module ActionDispatch
protected
def parameter_filter
- parameter_filter_for(@env["action_dispatch.parameter_filter"])
+ parameter_filter_for @env.fetch("action_dispatch.parameter_filter") {
+ return NULL_PARAM_FILTER
+ }
end
def env_filter
- parameter_filter_for(Array(@env["action_dispatch.parameter_filter"]) + [/RAW_POST_DATA/, "rack.request.form_vars"])
+ user_key = @env.fetch("action_dispatch.parameter_filter") {
+ return NULL_ENV_FILTER
+ }
+ parameter_filter_for(Array(user_key) + ENV_MATCH)
end
def parameter_filter_for(filters)
- @@parameter_filter_for[filters] ||= ParameterFilter.new(filters)
+ ParameterFilter.new(filters)
end
KV_RE = '[^&;=]+'
@@ -60,7 +73,6 @@ module ActionDispatch
parameter_filter.filter([[$1, $2]]).first.join("=")
end
end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/filter_redirect.rb b/actionpack/lib/action_dispatch/http/filter_redirect.rb
new file mode 100644
index 0000000000..900ce1c646
--- /dev/null
+++ b/actionpack/lib/action_dispatch/http/filter_redirect.rb
@@ -0,0 +1,37 @@
+module ActionDispatch
+ module Http
+ module FilterRedirect
+
+ FILTERED = '[FILTERED]'.freeze # :nodoc:
+
+ def filtered_location
+ if !location_filter.empty? && location_filter_match?
+ FILTERED
+ else
+ location
+ end
+ end
+
+ private
+
+ def location_filter
+ if request.present?
+ request.env['action_dispatch.redirect_filter'] || []
+ else
+ []
+ end
+ end
+
+ def location_filter_match?
+ location_filter.any? do |filter|
+ if String === filter
+ location.include?(filter)
+ elsif Regexp === filter
+ location.match(filter)
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/http/headers.rb b/actionpack/lib/action_dispatch/http/headers.rb
index a3bb25f75a..2666cd4b0a 100644
--- a/actionpack/lib/action_dispatch/http/headers.rb
+++ b/actionpack/lib/action_dispatch/http/headers.rb
@@ -1,32 +1,63 @@
module ActionDispatch
module Http
- class Headers < ::Hash
- @@env_cache = Hash.new { |h,k| h[k] = "HTTP_#{k.upcase.gsub(/-/, '_')}" }
+ 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
+ )
+ HTTP_HEADER = /\A[A-Za-z0-9-]+\z/
- def initialize(*args)
+ include Enumerable
+ attr_reader :env
- if args.size == 1 && args[0].is_a?(Hash)
- super()
- update(args[0])
- else
- super
- end
+ def initialize(env = {})
+ @env = env
+ end
+
+ def [](key)
+ @env[env_name(key)]
+ end
+
+ def []=(key, value)
+ @env[env_name(key)] = value
+ end
+
+ def key?(key); @env.key? key; end
+ alias :include? :key?
+
+ def fetch(key, *args, &block)
+ @env.fetch env_name(key), *args, &block
+ end
+
+ def each(&block)
+ @env.each(&block)
end
- def [](header_name)
- super env_name(header_name)
+ def merge(headers_or_env)
+ headers = Http::Headers.new(env.dup)
+ headers.merge!(headers_or_env)
+ headers
end
- def fetch(header_name, default=nil, &block)
- super env_name(header_name), default, &block
+ def merge!(headers_or_env)
+ headers_or_env.each do |key, value|
+ self[env_name(key)] = value
+ end
end
private
- # Converts a HTTP header name to an environment variable name if it is
- # not contained within the headers hash.
- def env_name(header_name)
- include?(header_name) ? header_name : @@env_cache[header_name]
+ def env_name(key)
+ key = key.to_s
+ if key =~ HTTP_HEADER
+ key = key.upcase.tr('-', '_')
+ key = "HTTP_" + key unless CGI_VARIABLES.include?(key)
end
+ key
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
index 0f98e84788..40bb060d52 100644
--- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb
+++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb
@@ -68,7 +68,7 @@ module ActionDispatch
# that are not controlled by the extension.
#
# class ApplicationController < ActionController::Base
- # before_filter :adjust_format_for_iphone
+ # before_action :adjust_format_for_iphone
#
# private
# def adjust_format_for_iphone
@@ -87,7 +87,7 @@ module ActionDispatch
# to the :html format.
#
# class ApplicationController < ActionController::Base
- # before_filter :adjust_format_for_iphone_with_html_fallback
+ # before_action :adjust_format_for_iphone_with_html_fallback
#
# private
# def adjust_format_for_iphone_with_html_fallback
@@ -121,8 +121,8 @@ module ActionDispatch
BROWSER_LIKE_ACCEPTS = /,\s*\*\/\*|\*\/\*\s*,/
def valid_accept_header
- (xhr? && (accept || content_mime_type)) ||
- (accept && accept !~ BROWSER_LIKE_ACCEPTS)
+ (xhr? && (accept.present? || content_mime_type)) ||
+ (accept.present? && accept !~ BROWSER_LIKE_ACCEPTS)
end
def use_accept_header
diff --git a/actionpack/lib/action_dispatch/http/mime_type.rb b/actionpack/lib/action_dispatch/http/mime_type.rb
index 3d560518e1..ef144c3c76 100644
--- a/actionpack/lib/action_dispatch/http/mime_type.rb
+++ b/actionpack/lib/action_dispatch/http/mime_type.rb
@@ -27,7 +27,7 @@ module Mime
class << self
def [](type)
return type if type.is_a?(Type)
- Type.lookup_by_extension(type)
+ Type.lookup_by_extension(type) || NullType.new
end
def fetch(type)
@@ -44,8 +44,8 @@ module Mime
#
# respond_to do |format|
# format.html
- # format.ics { render :text => post.to_ics, :mime_type => Mime::Type["text/calendar"] }
- # format.xml { render :xml => @people }
+ # format.ics { render text: post.to_ics, mime_type: Mime::Type["text/calendar"] }
+ # format.xml { render xml: @people }
# end
# end
# end
@@ -53,10 +53,6 @@ module Mime
@@html_types = Set.new [:html, :all]
cattr_reader :html_types
- # These are the content types which browsers can generate without using ajax, flash, etc
- # i.e. following a link, getting an image or posting a form. CSRF protection
- # only needs to protect against these types.
- @@browser_generated_types = Set.new [:html, :url_encoded_form, :multipart_form, :text]
attr_reader :symbol
@register_callbacks = []
@@ -179,7 +175,7 @@ module Mime
def parse(accept_header)
if accept_header !~ /,/
accept_header = accept_header.split(PARAMETER_SEPARATOR_REGEXP).first
- parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)]
+ parse_trailing_star(accept_header) || [Mime::Type.lookup(accept_header)].compact
else
list, index = AcceptList.new, 0
accept_header.split(',').each do |header|
@@ -223,8 +219,8 @@ module Mime
Mime.instance_eval { remove_const(symbol) }
SET.delete_if { |v| v.eql?(mime) }
- LOOKUP.delete_if { |k,v| v.eql?(mime) }
- EXTENSION_LOOKUP.delete_if { |k,v| v.eql?(mime) }
+ LOOKUP.delete_if { |_,v| v.eql?(mime) }
+ EXTENSION_LOOKUP.delete_if { |_,v| v.eql?(mime) }
end
end
@@ -272,34 +268,46 @@ module Mime
end
end
- # Returns true if Action Pack should check requests using this Mime Type for possible request forgery. See
- # ActionController::RequestForgeryProtection.
- def verify_request?
- ActiveSupport::Deprecation.warn "Mime::Type#verify_request? is deprecated and will be removed in Rails 4.1"
- @@browser_generated_types.include?(to_sym)
- end
-
- def self.browser_generated_types
- ActiveSupport::Deprecation.warn "Mime::Type.browser_generated_types is deprecated and will be removed in Rails 4.1"
- @@browser_generated_types
- end
-
def html?
@@html_types.include?(to_sym) || @string =~ /html/
end
+
private
- def method_missing(method, *args)
- if method.to_s.ends_with? '?'
- method[0..-2].downcase.to_sym == to_sym
- else
- super
- end
- end
- def respond_to_missing?(method, include_private = false) #:nodoc:
- method.to_s.ends_with? '?'
+ def to_ary; end
+ def to_a; end
+
+ def method_missing(method, *args)
+ if method.to_s.ends_with? '?'
+ method[0..-2].downcase.to_sym == to_sym
+ else
+ super
end
+ end
+
+ def respond_to_missing?(method, include_private = false) #:nodoc:
+ method.to_s.ends_with? '?'
+ end
+ end
+
+ class NullType
+ def nil?
+ true
+ end
+
+ def ref
+ nil
+ end
+
+ def respond_to_missing?(method, include_private = false)
+ method.to_s.ends_with? '?'
+ end
+
+ private
+ def method_missing(method, *args)
+ false if method.to_s.ends_with? '?'
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameter_filter.rb b/actionpack/lib/action_dispatch/http/parameter_filter.rb
index 490b46c990..b655a54865 100644
--- a/actionpack/lib/action_dispatch/http/parameter_filter.rb
+++ b/actionpack/lib/action_dispatch/http/parameter_filter.rb
@@ -1,74 +1,72 @@
module ActionDispatch
module Http
class ParameterFilter
+ FILTERED = '[FILTERED]'.freeze # :nodoc:
- def initialize(filters)
+ def initialize(filters = [])
@filters = filters
end
def filter(params)
- if enabled?
- compiled_filter.call(params)
- else
- params.dup
- end
+ compiled_filter.call(params)
end
private
- def enabled?
- @filters.present?
+ def compiled_filter
+ @compiled_filter ||= CompiledFilter.compile(@filters)
end
- FILTERED = '[FILTERED]'.freeze
+ class CompiledFilter # :nodoc:
+ def self.compile(filters)
+ return lambda { |params| params.dup } if filters.empty?
- def compiled_filter
- @compiled_filter ||= begin
- regexps, blocks = compile_filter
+ strings, regexps, blocks = [], [], []
- lambda do |original_params|
- filtered_params = {}
+ filters.each do |item|
+ case item
+ when Proc
+ blocks << item
+ when Regexp
+ regexps << item
+ else
+ strings << item.to_s
+ end
+ end
- original_params.each do |key, value|
- if regexps.find { |r| key =~ r }
- value = FILTERED
- elsif value.is_a?(Hash)
- value = filter(value)
- elsif value.is_a?(Array)
- value = value.map { |v| v.is_a?(Hash) ? filter(v) : v }
- elsif blocks.present?
- key = key.dup
- value = value.dup if value.duplicable?
- blocks.each { |b| b.call(key, value) }
- end
+ regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
+ new regexps, blocks
+ end
- filtered_params[key] = value
- end
+ attr_reader :regexps, :blocks
- filtered_params
- end
+ def initialize(regexps, blocks)
+ @regexps = regexps
+ @blocks = blocks
end
- end
- def compile_filter
- strings, regexps, blocks = [], [], []
+ def call(original_params)
+ filtered_params = {}
+
+ original_params.each do |key, value|
+ if regexps.any? { |r| key =~ r }
+ value = FILTERED
+ elsif value.is_a?(Hash)
+ value = call(value)
+ elsif value.is_a?(Array)
+ value = value.map { |v| v.is_a?(Hash) ? call(v) : v }
+ elsif blocks.any?
+ key = key.dup
+ value = value.dup if value.duplicable?
+ blocks.each { |b| b.call(key, value) }
+ end
- @filters.each do |item|
- case item
- when NilClass
- when Proc
- blocks << item
- when Regexp
- regexps << item
- else
- strings << item.to_s
+ filtered_params[key] = value
end
- end
- regexps << Regexp.new(strings.join('|'), true) unless strings.empty?
- [regexps, blocks]
+ filtered_params
+ end
end
-
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/parameters.rb b/actionpack/lib/action_dispatch/http/parameters.rb
index 9a7b5bc8c7..dcb299ed03 100644
--- a/actionpack/lib/action_dispatch/http/parameters.rb
+++ b/actionpack/lib/action_dispatch/http/parameters.rb
@@ -12,9 +12,13 @@ module ActionDispatch
# Returns both GET and POST \parameters in a single hash.
def parameters
@env["action_dispatch.request.parameters"] ||= begin
- params = request_parameters.merge(query_parameters)
+ params = begin
+ request_parameters.merge(query_parameters)
+ rescue EOFError
+ query_parameters.dup
+ end
params.merge!(path_parameters)
- encode_params(params).with_indifferent_access
+ params.with_indifferent_access
end
end
alias :params :parameters
@@ -46,39 +50,31 @@ module ActionDispatch
private
+ # Convert nested Hash to HashWithIndifferentAccess
+ # and UTF-8 encode both keys and values in nested Hash.
+ #
# 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 encode_params(params)
- if params.is_a?(String)
- return params.force_encoding("UTF-8").encode!
- elsif !params.is_a?(Hash)
- return params
- end
-
- params.each do |k, v|
- case v
- when Hash
- encode_params(v)
- when Array
- v.map! {|el| encode_params(el) }
+ 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
- encode_params(v)
+ params.each_with_object({}) do |(key, val), new_hash|
+ new_key = key.is_a?(String) ? key.dup.force_encoding(Encoding::UTF_8).encode! : key
+ new_hash[new_key] = if val.is_a?(Array)
+ val.map! { |el| normalize_encode_params(el) }
+ else
+ normalize_encode_params(val)
+ end
+ end.with_indifferent_access
end
- end
- end
-
- # Convert nested Hash to HashWithIndifferentAccess
- def normalize_parameters(value)
- case value
- when Hash
- h = {}
- value.each { |k, v| h[k] = normalize_parameters(v) }
- h.with_indifferent_access
- when Array
- value.map { |e| normalize_parameters(e) }
else
- value
+ params
end
end
end
diff --git a/actionpack/lib/action_dispatch/http/request.rb b/actionpack/lib/action_dispatch/http/request.rb
index b8ebeb408f..aba8f66118 100644
--- a/actionpack/lib/action_dispatch/http/request.rb
+++ b/actionpack/lib/action_dispatch/http/request.rb
@@ -1,12 +1,16 @@
-require 'tempfile'
require 'stringio'
-require 'strscan'
-require 'active_support/core_ext/hash/indifferent_access'
-require 'active_support/core_ext/string/access'
require 'active_support/inflector'
require 'action_dispatch/http/headers'
require 'action_controller/metal/exceptions'
+require 'rack/request'
+require 'action_dispatch/http/cache'
+require 'action_dispatch/http/mime_negotiation'
+require 'action_dispatch/http/parameters'
+require 'action_dispatch/http/filter_parameters'
+require 'action_dispatch/http/upload'
+require 'action_dispatch/http/url'
+require 'active_support/core_ext/array/conversions'
module ActionDispatch
class Request < Rack::Request
@@ -14,10 +18,10 @@ module ActionDispatch
include ActionDispatch::Http::MimeNegotiation
include ActionDispatch::Http::Parameters
include ActionDispatch::Http::FilterParameters
- include ActionDispatch::Http::Upload
include ActionDispatch::Http::URL
autoload :Session, 'action_dispatch/request/session'
+ autoload :Utils, 'action_dispatch/request/utils'
LOCALHOST = Regexp.union [/^127\.0\.0\.\d{1,3}$/, /^::1$/, /^0:0:0:0:0:0:0:1(%.*)?$/]
@@ -70,7 +74,13 @@ module ActionDispatch
RFC5789 = %w(PATCH)
HTTP_METHODS = RFC2616 + RFC2518 + RFC3253 + RFC3648 + RFC3744 + RFC5323 + RFC5789
- HTTP_METHOD_LOOKUP = Hash.new { |h, m| h[m] = m.underscore.to_sym if HTTP_METHODS.include?(m) }
+
+ HTTP_METHOD_LOOKUP = {}
+
+ # Populate the HTTP method lookup cache
+ HTTP_METHODS.each { |method|
+ HTTP_METHOD_LOOKUP[method] = method.underscore.to_sym
+ }
# Returns the HTTP \method that the application should see.
# In the case where the \method was overridden by a middleware
@@ -146,14 +156,29 @@ module ActionDispatch
@original_fullpath ||= (env["ORIGINAL_FULLPATH"] || fullpath)
end
+ # Returns the +String+ full path including params of the last URL requested.
+ #
+ # # get "/articles"
+ # request.fullpath # => "/articles"
+ #
+ # # get "/articles?page=2"
+ # request.fullpath # => "/articles?page=2"
def fullpath
@fullpath ||= super
end
+ # Returns the original request URL as a +String+.
+ #
+ # # get "/articles?page=2"
+ # request.original_url # => "http://www.example.com/articles?page=2"
def original_url
base_url + original_fullpath
end
+ # The +String+ MIME type of the request.
+ #
+ # # get "/articles"
+ # request.media_type # => "application/x-www-form-urlencoded"
def media_type
content_mime_type.to_s
end
@@ -199,8 +224,9 @@ module ActionDispatch
# work with raw requests directly.
def raw_post
unless @env.include? 'RAW_POST_DATA'
- @env['RAW_POST_DATA'] = body.read(@env['CONTENT_LENGTH'].to_i)
- body.rewind if body.respond_to?(:rewind)
+ raw_post_body = body
+ @env['RAW_POST_DATA'] = raw_post_body.read(content_length)
+ raw_post_body.rewind if raw_post_body.respond_to?(:rewind)
end
@env['RAW_POST_DATA']
end
@@ -245,21 +271,17 @@ module ActionDispatch
# Override Rack's GET method to support indifferent access
def GET
- begin
- @env["action_dispatch.request.query_parameters"] ||= (normalize_parameters(super) || {})
- rescue TypeError => e
- raise ActionController::BadRequest, "Invalid query parameters: #{e.message}"
- end
+ @env["action_dispatch.request.query_parameters"] ||= (normalize_encode_params(super) || {})
+ rescue TypeError => e
+ raise ActionController::BadRequest.new(:query, e)
end
alias :query_parameters :GET
# Override Rack's POST method to support indifferent access
def POST
- begin
- @env["action_dispatch.request.request_parameters"] ||= (normalize_parameters(super) || {})
- rescue TypeError => e
- raise ActionController::BadRequest, "Invalid request parameters: #{e.message}"
- end
+ @env["action_dispatch.request.request_parameters"] ||= (normalize_encode_params(super) || {})
+ rescue TypeError => e
+ raise ActionController::BadRequest.new(:request, e)
end
alias :request_parameters :POST
@@ -279,23 +301,8 @@ module ActionDispatch
protected
- # Remove nils from the params hash
- def deep_munge(hash)
- hash.each_value do |v|
- case v
- when Array
- v.grep(Hash) { |x| deep_munge(x) }
- v.compact!
- when Hash
- deep_munge(v)
- end
- end
-
- hash
- end
-
def parse_query(qs)
- deep_munge(super)
+ Utils.deep_munge(super)
end
private
diff --git a/actionpack/lib/action_dispatch/http/response.rb b/actionpack/lib/action_dispatch/http/response.rb
index 11b7534ea4..5247e61a23 100644
--- a/actionpack/lib/action_dispatch/http/response.rb
+++ b/actionpack/lib/action_dispatch/http/response.rb
@@ -1,4 +1,3 @@
-require 'digest/md5'
require 'active_support/core_ext/class/attribute_accessors'
require 'monitor'
@@ -32,10 +31,17 @@ module ActionDispatch # :nodoc:
# end
# end
class Response
- attr_accessor :request, :header
+ # The request that the response is responding to.
+ attr_accessor :request
+
+ # The HTTP status code.
attr_reader :status
+
attr_writer :sending_file
+ # Get and set headers for this response.
+ attr_accessor :header
+
alias_method :headers=, :header=
alias_method :headers, :header
@@ -50,17 +56,22 @@ module ActionDispatch # :nodoc:
# If a character set has been defined for this response (see charset=) then
# the character set information will also be included in the content type
# information.
- attr_accessor :charset
attr_reader :content_type
+ # The charset of the response. HTML wants to know the encoding of the
+ # content you're giving them, so we need to send that along.
+ attr_accessor :charset
+
CONTENT_TYPE = "Content-Type".freeze
SET_COOKIE = "Set-Cookie".freeze
LOCATION = "Location".freeze
+ NO_CONTENT_CODES = [204, 304]
cattr_accessor(:default_charset) { "utf-8" }
cattr_accessor(:default_headers)
include Rack::Response::Helpers
+ include ActionDispatch::Http::FilterRedirect
include ActionDispatch::Http::Cache::Response
include MonitorMixin
@@ -92,6 +103,7 @@ module ActionDispatch # :nodoc:
end
end
+ # The underlying body, as a streamable object.
attr_reader :stream
def initialize(status = 200, header = {}, body = [])
@@ -136,32 +148,42 @@ module ActionDispatch # :nodoc:
@committed
end
+ # Sets the HTTP status code.
def status=(status)
@status = Rack::Utils.status_code(status)
end
+ # Sets the HTTP content type.
def content_type=(content_type)
@content_type = content_type.to_s
end
- # The response code of the request
+ # The response code of the request.
def response_code
@status
end
- # Returns a String to ensure compatibility with Net::HTTPResponse
+ # Returns a string to ensure compatibility with <tt>Net::HTTPResponse</tt>.
def code
@status.to_s
end
+ # Returns the corresponding message for the current HTTP status code:
+ #
+ # response.status = 200
+ # response.message # => "OK"
+ #
+ # response.status = 404
+ # response.message # => "Not Found"
+ #
def message
Rack::Utils::HTTP_STATUS_CODES[@status]
end
alias_method :status_message, :message
- def respond_to?(method)
- if method.to_sym == :to_path
- stream.respond_to?(:to_path)
+ def respond_to?(method, include_private = false)
+ if method.to_s == 'to_path'
+ stream.respond_to?(method)
else
super
end
@@ -171,6 +193,8 @@ module ActionDispatch # :nodoc:
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
strings = []
each { |part| strings << part.to_s }
@@ -179,13 +203,16 @@ module ActionDispatch # :nodoc:
EMPTY = " "
+ # Allows you to manually set or override the response body.
def body=(body)
@blank = true if body == EMPTY
if body.respond_to?(:to_path)
@stream = body
else
- @stream = build_buffer self, munge_body_object(body)
+ synchronize do
+ @stream = build_buffer self, munge_body_object(body)
+ end
end
end
@@ -203,11 +230,13 @@ module ActionDispatch # :nodoc:
::Rack::Utils.delete_cookie_header!(header, key, value)
end
+ # The location header we'll be responding with.
def location
headers[LOCATION]
end
alias_method :redirect_url, :location
+ # Sets the location header we'll be responding with.
def location=(url)
headers[LOCATION] = url
end
@@ -216,11 +245,13 @@ module ActionDispatch # :nodoc:
stream.close if stream.respond_to?(:close)
end
+ # Turns the Response into a Rack-compatible array of the status, headers,
+ # and body.
def to_a
rack_response @status, @header.to_hash
end
alias prepare! to_a
- alias to_ary to_a # For implicit splat on 1.9.2
+ alias to_ary to_a
# Returns the response cookies, converted to a Hash of (name => value) pairs
#
@@ -259,21 +290,25 @@ module ActionDispatch # :nodoc:
return if headers[CONTENT_TYPE].present?
@content_type ||= Mime::HTML
- @charset ||= self.class.default_charset
+ @charset ||= self.class.default_charset unless @charset == false
type = @content_type.to_s.dup
- type << "; charset=#{@charset}" unless @sending_file
+ type << "; charset=#{@charset}" if append_charset?
headers[CONTENT_TYPE] = type
end
+ def append_charset?
+ !@sending_file && @charset != false
+ end
+
def rack_response(status, header)
assign_default_content_type_and_charset!(header)
handle_conditional_get!
header[SET_COOKIE] = header[SET_COOKIE].join("\n") if header[SET_COOKIE].respond_to?(:join)
- if [204, 304].include?(@status)
+ if NO_CONTENT_CODES.include?(@status)
header.delete CONTENT_TYPE
[status, header, []]
else
diff --git a/actionpack/lib/action_dispatch/http/upload.rb b/actionpack/lib/action_dispatch/http/upload.rb
index 79437d6e85..a8d2dc3950 100644
--- a/actionpack/lib/action_dispatch/http/upload.rb
+++ b/actionpack/lib/action_dispatch/http/upload.rb
@@ -6,7 +6,7 @@ module ActionDispatch
# of its interface is available directly for convenience.
#
# Uploaded files are temporary files whose lifespan is one request. When
- # the object is finalized Ruby unlinks the file, so there is not need to
+ # the object is finalized Ruby unlinks the file, so there is no need to
# clean them with a separate maintenance task.
class UploadedFile
# The basename of the file in the client.
@@ -19,7 +19,7 @@ module ActionDispatch
# its interface is available directly.
attr_accessor :tempfile
- # TODO.
+ # A string with the headers of the multipart request.
attr_accessor :headers
def initialize(hash) # :nodoc:
@@ -70,21 +70,8 @@ module ActionDispatch
def encode_filename(filename)
# Encode the filename in the utf8 encoding, unless it is nil
- filename.force_encoding("UTF-8").encode! if filename
+ filename.force_encoding(Encoding::UTF_8).encode! if filename
end
end
-
- module Upload # :nodoc:
- # Convert nested Hash to HashWithIndifferentAccess and replace
- # file upload hash with UploadedFile objects
- def normalize_parameters(value)
- if Hash === value && value.has_key?(:tempfile)
- UploadedFile.new(value)
- else
- super
- end
- end
- private :normalize_parameters
- end
end
end
diff --git a/actionpack/lib/action_dispatch/http/url.rb b/actionpack/lib/action_dispatch/http/url.rb
index 8aa02ec482..6f5a52c568 100644
--- a/actionpack/lib/action_dispatch/http/url.rb
+++ b/actionpack/lib/action_dispatch/http/url.rb
@@ -1,21 +1,28 @@
+require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/hash/slice'
+
module ActionDispatch
module Http
module URL
- IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+ IP_HOST_REGEXP = /\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/
+ HOST_REGEXP = /(^.*:\/\/)?([^:]+)(?::(\d+$))?/
+ PROTOCOL_REGEXP = /^([^:]+)(:)?(\/\/)?$/
mattr_accessor :tld_length
self.tld_length = 1
class << self
def extract_domain(host, tld_length = @@tld_length)
- return nil unless named_host?(host)
- host.split('.').last(1 + tld_length).join('.')
+ host.split('.').last(1 + tld_length).join('.') if named_host?(host)
end
def extract_subdomains(host, tld_length = @@tld_length)
- return [] unless named_host?(host)
- parts = host.split('.')
- parts[0..-(tld_length+2)]
+ if named_host?(host)
+ parts = host.split('.')
+ parts[0..-(tld_length + 2)]
+ else
+ []
+ end
end
def extract_subdomain(host, tld_length = @@tld_length)
@@ -23,16 +30,23 @@ module ActionDispatch
end
def url_for(options = {})
- path = ""
- path << options.delete(:script_name).to_s.chomp("/")
+ options = options.dup
+ path = options.delete(:script_name).to_s.chomp("/")
path << options.delete(:path).to_s
- params = options[:params] || {}
- params.reject! {|k,v| v.to_param.nil? }
+ params = options[:params].is_a?(Hash) ? options[:params] : options.slice(:params)
+ params.reject! { |_,v| v.to_param.nil? }
result = build_host_url(options)
-
- result << (options[:trailing_slash] ? path.sub(/\?|\z/) { "/" + $& } : path)
+ if options[:trailing_slash]
+ if path.include?('?')
+ result << path.sub(/\?/, '/\&')
+ else
+ result << path.sub(/[^\/]\z|\A\z/, '\&/')
+ end
+ else
+ result << path
+ end
result << "?#{params.to_query}" unless params.empty?
result << "##{Journey::Router::Utils.escape_fragment(options[:anchor].to_param.to_s)}" if options[:anchor]
result
@@ -48,14 +62,20 @@ module ActionDispatch
result = ""
unless options[:only_path]
- unless options[:protocol] == false
- result << (options[:protocol] || "http")
- result << ":" unless result.match(%r{:|//})
+ 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
- result << "//" unless result.match("//")
+
+ options[:protocol] = normalize_protocol(options)
+ options[:host] = normalize_host(options)
+ options[:port] = normalize_port(options)
+
+ result << options[:protocol]
result << rewrite_authentication(options)
- result << host_or_subdomain_and_domain(options)
- result << ":#{options.delete(:port)}" if options[:port]
+ result << options[:host]
+ result << ":#{options[:port]}" if options[:port]
end
result
end
@@ -64,6 +84,10 @@ module ActionDispatch
host && IP_HOST_REGEXP !~ host
end
+ def same_host?(options)
+ (options[:subdomain] == true || !options.key?(:subdomain)) && options[:domain].nil?
+ end
+
def rewrite_authentication(options)
if options[:user] && options[:password]
"#{Rack::Utils.escape(options[:user])}:#{Rack::Utils.escape(options[:password])}@"
@@ -72,19 +96,47 @@ module ActionDispatch
end
end
- def host_or_subdomain_and_domain(options)
- return options[:host] if !named_host?(options[:host]) || (options[:subdomain].nil? && options[:domain].nil?)
+ def normalize_protocol(options)
+ case options[:protocol]
+ when nil
+ "http://"
+ when false, "//"
+ "//"
+ when PROTOCOL_REGEXP
+ "#{$1}://"
+ else
+ raise ArgumentError, "Invalid :protocol option: #{options[:protocol].inspect}"
+ end
+ end
+
+ def normalize_host(options)
+ return options[:host] if !named_host?(options[:host]) || same_host?(options)
tld_length = options[:tld_length] || @@tld_length
host = ""
- unless options[:subdomain] == false
- host << (options[:subdomain] || extract_subdomain(options[:host], tld_length)).to_param
- 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
end
+ host << "." unless host.empty?
host << (options[:domain] || extract_domain(options[:host], tld_length))
host
end
+
+ def normalize_port(options)
+ return nil if options[:port].nil? || options[:port] == false
+
+ case options[:protocol]
+ when "//"
+ nil
+ when "https://"
+ options[:port].to_i == 443 ? nil : options[:port]
+ else
+ options[:port].to_i == 80 ? nil : options[:port]
+ end
+ end
end
def initialize(env)
diff --git a/actionpack/lib/action_dispatch/journey.rb b/actionpack/lib/action_dispatch/journey.rb
new file mode 100644
index 0000000000..ad42713482
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey.rb
@@ -0,0 +1,5 @@
+require 'action_dispatch/journey/router'
+require 'action_dispatch/journey/gtg/builder'
+require 'action_dispatch/journey/gtg/simulator'
+require 'action_dispatch/journey/nfa/builder'
+require 'action_dispatch/journey/nfa/simulator'
diff --git a/actionpack/lib/action_dispatch/journey/backwards.rb b/actionpack/lib/action_dispatch/journey/backwards.rb
new file mode 100644
index 0000000000..3bd20fdf81
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/backwards.rb
@@ -0,0 +1,5 @@
+module Rack # :nodoc:
+ Mount = ActionDispatch::Journey::Router
+ Mount::RouteSet = ActionDispatch::Journey::Router
+ Mount::RegexpWithNamedGroups = ActionDispatch::Journey::Path::Pattern
+end
diff --git a/actionpack/lib/action_dispatch/journey/formatter.rb b/actionpack/lib/action_dispatch/journey/formatter.rb
new file mode 100644
index 0000000000..7764763791
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/formatter.rb
@@ -0,0 +1,150 @@
+require 'action_controller/metal/exceptions'
+
+module ActionDispatch
+ module Journey
+ # The Formatter class is used for formatting URLs. For example, parameters
+ # passed to +url_for+ in Rails will eventually call Formatter#generate.
+ class Formatter # :nodoc:
+ attr_reader :routes
+
+ def initialize(routes)
+ @routes = routes
+ @cache = nil
+ end
+
+ def generate(type, name, options, recall = {}, parameterize = nil)
+ constraints = recall.merge(options)
+ missing_keys = []
+
+ match_route(name, constraints) do |route|
+ parameterized_parts = extract_parameterized_parts(route, options, recall, 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
+ # hash passed to url_for matches a Rack application or a redirect.
+ next unless name || route.dispatcher?
+
+ missing_keys = missing_keys(route, parameterized_parts)
+ next unless missing_keys.empty?
+ params = options.dup.delete_if do |key, _|
+ parameterized_parts.key?(key) || route.defaults.key?(key)
+ end
+
+ return [route.format(parameterized_parts), params]
+ end
+
+ message = "No route matches #{constraints.inspect}"
+ message << " missing required keys: #{missing_keys.inspect}" if name
+
+ raise ActionController::UrlGenerationError, message
+ end
+
+ def clear
+ @cache = nil
+ end
+
+ private
+
+ def extract_parameterized_parts(route, options, recall, parameterize = nil)
+ parameterized_parts = recall.merge(options)
+
+ keys_to_keep = route.parts.reverse.drop_while { |part|
+ !options.key?(part) || (options[part] || recall[part]).nil?
+ } | route.required_parts
+
+ (parameterized_parts.keys - keys_to_keep).each do |bad_key|
+ parameterized_parts.delete(bad_key)
+ end
+
+ if parameterize
+ parameterized_parts.each do |k, v|
+ parameterized_parts[k] = parameterize.call(k, v)
+ end
+ end
+
+ parameterized_parts.keep_if { |_, v| v }
+ parameterized_parts
+ end
+
+ def named_routes
+ routes.named_routes
+ end
+
+ def match_route(name, options)
+ if named_routes.key?(name)
+ yield named_routes[name]
+ else
+ routes = non_recursive(cache, options.to_a)
+
+ hash = routes.group_by { |_, r| r.score(options) }
+
+ hash.keys.sort.reverse_each do |score|
+ next if score < 0
+
+ hash[score].sort_by { |i, _| i }.each do |_, route|
+ yield route
+ end
+ end
+ end
+ end
+
+ def non_recursive(cache, options)
+ routes = []
+ stack = [cache]
+
+ while stack.any?
+ c = stack.shift
+ routes.concat(c[:___routes]) if c.key?(:___routes)
+
+ options.each do |pair|
+ stack << c[pair] if c.key?(pair)
+ end
+ end
+
+ routes
+ end
+
+ # Returns an array populated with missing keys if any are present.
+ def missing_keys(route, parts)
+ missing_keys = []
+ tests = route.path.requirements
+ route.required_parts.each { |key|
+ if tests.key?(key)
+ missing_keys << key unless /\A#{tests[key]}\Z/ === parts[key]
+ else
+ missing_keys << key unless parts[key]
+ end
+ }
+ missing_keys
+ end
+
+ def possibles(cache, options, depth = 0)
+ cache.fetch(:___routes) { [] } + options.find_all { |pair|
+ cache.key?(pair)
+ }.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
+ root = { ___routes: [] }
+ routes.each_with_index do |route, i|
+ leaf = route.required_defaults.inject(root) do |h, tuple|
+ h[tuple] ||= {}
+ end
+ (leaf[:___routes] ||= []) << [i, route]
+ end
+ root
+ end
+
+ def cache
+ @cache ||= build_cache
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/builder.rb b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
new file mode 100644
index 0000000000..7d2791714b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/builder.rb
@@ -0,0 +1,162 @@
+require 'action_dispatch/journey/gtg/transition_table'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class Builder # :nodoc:
+ DUMMY = Nodes::Dummy.new
+
+ attr_reader :root, :ast, :endpoints
+
+ def initialize(root)
+ @root = root
+ @ast = Nodes::Cat.new root, DUMMY
+ @followpos = nil
+ end
+
+ def transition_table
+ dtrans = TransitionTable.new
+ marked = {}
+ state_id = Hash.new { |h,k| h[k] = h.length }
+
+ start = firstpos(root)
+ dstates = [start]
+ until dstates.empty?
+ s = dstates.shift
+ next if marked[s]
+ marked[s] = true # mark s
+
+ s.group_by { |state| symbol(state) }.each do |sym, ps|
+ u = ps.map { |l| followpos(l) }.flatten
+ next if u.empty?
+
+ if u.uniq == [DUMMY]
+ from = state_id[s]
+ to = state_id[Object.new]
+ dtrans[from, to] = sym
+
+ dtrans.add_accepting(to)
+ ps.each { |state| dtrans.add_memo(to, state.memo) }
+ else
+ dtrans[state_id[s], state_id[u]] = sym
+
+ if u.include?(DUMMY)
+ to = state_id[u]
+
+ accepting = ps.find_all { |l| followpos(l).include?(DUMMY) }
+
+ accepting.each { |accepting_state|
+ dtrans.add_memo(to, accepting_state.memo)
+ }
+
+ dtrans.add_accepting(state_id[u])
+ end
+ end
+
+ dstates << u
+ end
+ end
+
+ dtrans
+ end
+
+ def nullable?(node)
+ case node
+ when Nodes::Group
+ true
+ when Nodes::Star
+ true
+ when Nodes::Or
+ node.children.any? { |c| nullable?(c) }
+ when Nodes::Cat
+ nullable?(node.left) && nullable?(node.right)
+ when Nodes::Terminal
+ !node.left
+ when Nodes::Unary
+ nullable?(node.left)
+ else
+ raise ArgumentError, 'unknown nullable: %s' % node.class.name
+ end
+ end
+
+ def firstpos(node)
+ case node
+ when Nodes::Star
+ firstpos(node.left)
+ when Nodes::Cat
+ if nullable?(node.left)
+ firstpos(node.left) | firstpos(node.right)
+ else
+ firstpos(node.left)
+ end
+ when Nodes::Or
+ node.children.map { |c| firstpos(c) }.flatten.uniq
+ when Nodes::Unary
+ firstpos(node.left)
+ when Nodes::Terminal
+ nullable?(node) ? [] : [node]
+ else
+ raise ArgumentError, 'unknown firstpos: %s' % node.class.name
+ end
+ end
+
+ def lastpos(node)
+ case node
+ when Nodes::Star
+ firstpos(node.left)
+ when Nodes::Or
+ node.children.map { |c| lastpos(c) }.flatten.uniq
+ when Nodes::Cat
+ if nullable?(node.right)
+ lastpos(node.left) | lastpos(node.right)
+ else
+ lastpos(node.right)
+ end
+ when Nodes::Terminal
+ nullable?(node) ? [] : [node]
+ when Nodes::Unary
+ lastpos(node.left)
+ else
+ raise ArgumentError, 'unknown lastpos: %s' % node.class.name
+ end
+ end
+
+ def followpos(node)
+ followpos_table[node]
+ end
+
+ private
+
+ def followpos_table
+ @followpos ||= build_followpos
+ end
+
+ def build_followpos
+ table = Hash.new { |h, k| h[k] = [] }
+ @ast.each do |n|
+ case n
+ when Nodes::Cat
+ lastpos(n.left).each do |i|
+ table[i] += firstpos(n.right)
+ end
+ when Nodes::Star
+ lastpos(n).each do |i|
+ table[i] += firstpos(n)
+ end
+ end
+ end
+ table
+ end
+
+ def symbol(edge)
+ case edge
+ when Journey::Nodes::Symbol
+ edge.regexp
+ else
+ edge.left
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/gtg/simulator.rb b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
new file mode 100644
index 0000000000..58ad803841
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/simulator.rb
@@ -0,0 +1,44 @@
+require 'strscan'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class MatchData # :nodoc:
+ attr_reader :memos
+
+ def initialize(memos)
+ @memos = memos
+ end
+ end
+
+ class Simulator # :nodoc:
+ attr_reader :tt
+
+ def initialize(transition_table)
+ @tt = transition_table
+ end
+
+ def simulate(string)
+ input = StringScanner.new(string)
+ state = [0]
+ while sym = input.scan(%r([/.?]|[^/.?]+))
+ state = tt.move(state, sym)
+ end
+
+ acceptance_states = state.find_all { |s|
+ tt.accepting? s
+ }
+
+ return if acceptance_states.empty?
+
+ memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
+
+ MatchData.new(memos)
+ end
+
+ alias :=~ :simulate
+ alias :match :simulate
+ end
+ 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
new file mode 100644
index 0000000000..971cb3447f
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/gtg/transition_table.rb
@@ -0,0 +1,167 @@
+require 'action_dispatch/journey/nfa/dot'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module GTG # :nodoc:
+ class TransitionTable # :nodoc:
+ include Journey::NFA::Dot
+
+ attr_reader :memos
+
+ def initialize
+ @regexp_states = {}
+ @string_states = {}
+ @accepting = {}
+ @memos = Hash.new { |h,k| h[k] = [] }
+ end
+
+ def add_accepting(state)
+ @accepting[state] = true
+ end
+
+ def accepting_states
+ @accepting.keys
+ end
+
+ def accepting?(state)
+ @accepting[state]
+ end
+
+ def add_memo(idx, memo)
+ @memos[idx] << memo
+ end
+
+ def memo(idx)
+ @memos[idx]
+ end
+
+ def eclosure(t)
+ Array(t)
+ end
+
+ def move(t, a)
+ move_string(t, a).concat(move_regexp(t, a))
+ end
+
+ def to_json
+ require 'json'
+
+ simple_regexp = Hash.new { |h,k| h[k] = {} }
+
+ @regexp_states.each do |from, hash|
+ hash.each do |re, to|
+ simple_regexp[from][re.source] = to
+ end
+ end
+
+ JSON.dump({
+ regexp_states: simple_regexp,
+ string_states: @string_states,
+ accepting: @accepting
+ })
+ end
+
+ def to_svg
+ svg = IO.popen('dot -Tsvg', 'w+') { |f|
+ f.write(to_dot)
+ f.close_write
+ f.readlines
+ }
+ 3.times { svg.shift }
+ svg.join.sub(/width="[^"]*"/, '').sub(/height="[^"]*"/, '')
+ end
+
+ def visualizer(paths, title = 'FSM')
+ viz_dir = File.join File.dirname(__FILE__), '..', 'visualizer'
+ fsm_js = File.read File.join(viz_dir, 'fsm.js')
+ fsm_css = File.read File.join(viz_dir, 'fsm.css')
+ erb = File.read File.join(viz_dir, 'index.html.erb')
+ states = "function tt() { return #{to_json}; }"
+
+ fun_routes = paths.shuffle.first(3).map do |ast|
+ ast.map { |n|
+ case n
+ when Nodes::Symbol
+ case n.left
+ when ':id' then rand(100).to_s
+ when ':format' then %w{ xml json }.shuffle.first
+ else
+ 'omg'
+ end
+ when Nodes::Terminal then n.symbol
+ else
+ nil
+ end
+ }.compact.join
+ end
+
+ stylesheets = [fsm_css]
+ svg = to_svg
+ javascripts = [states, fsm_js]
+
+ # Annoying hack for 1.9 warnings
+ fun_routes = fun_routes
+ stylesheets = stylesheets
+ svg = svg
+ javascripts = javascripts
+
+ require 'erb'
+ template = ERB.new erb
+ template.result(binding)
+ end
+
+ def []=(from, to, sym)
+ to_mappings = states_hash_for(sym)[from] ||= {}
+ to_mappings[sym] = to
+ end
+
+ def states
+ ss = @string_states.keys + @string_states.values.map(&:values).flatten
+ rs = @regexp_states.keys + @regexp_states.values.map(&:values).flatten
+ (ss + rs).uniq
+ end
+
+ def transitions
+ @string_states.map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ }.flatten(1) + @regexp_states.map { |from, hash|
+ hash.map { |s, to| [from, s, to] }
+ }.flatten(1)
+ end
+
+ private
+
+ def states_hash_for(sym)
+ case sym
+ when String
+ @string_states
+ when Regexp
+ @regexp_states
+ else
+ raise ArgumentError, 'unknown symbol: %s' % sym.class
+ end
+ end
+
+ 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
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/builder.rb b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
new file mode 100644
index 0000000000..ee6494c3e4
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/builder.rb
@@ -0,0 +1,76 @@
+require 'action_dispatch/journey/nfa/transition_table'
+require 'action_dispatch/journey/gtg/transition_table'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class Visitor < Visitors::Visitor # :nodoc:
+ def initialize(tt)
+ @tt = tt
+ @i = -1
+ end
+
+ def visit_CAT(node)
+ left = visit(node.left)
+ right = visit(node.right)
+
+ @tt.merge(left.last, right.first)
+
+ [left.first, right.last]
+ end
+
+ def visit_GROUP(node)
+ from = @i += 1
+ left = visit(node.left)
+ to = @i += 1
+
+ @tt.accepting = to
+
+ @tt[from, left.first] = nil
+ @tt[left.last, to] = nil
+ @tt[from, to] = nil
+
+ [from, to]
+ end
+
+ def visit_OR(node)
+ from = @i += 1
+ children = node.children.map { |c| visit(c) }
+ to = @i += 1
+
+ children.each do |child|
+ @tt[from, child.first] = nil
+ @tt[child.last, to] = nil
+ end
+
+ @tt.accepting = to
+
+ [from, to]
+ end
+
+ def terminal(node)
+ from_i = @i += 1 # new state
+ to_i = @i += 1 # new state
+
+ @tt[from_i, to_i] = node
+ @tt.accepting = to_i
+ @tt.add_memo(to_i, node.memo)
+
+ [from_i, to_i]
+ end
+ end
+
+ class Builder # :nodoc:
+ def initialize(ast)
+ @ast = ast
+ end
+
+ def transition_table
+ tt = TransitionTable.new
+ Visitor.new(tt).accept(@ast)
+ tt
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/dot.rb b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
new file mode 100644
index 0000000000..5c33a872e5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/dot.rb
@@ -0,0 +1,36 @@
+# encoding: utf-8
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ module Dot # :nodoc:
+ def to_dot
+ edges = transitions.map { |from, sym, to|
+ " #{from} -> #{to} [label=\"#{sym || 'ε'}\"];"
+ }
+
+ #memo_nodes = memos.values.flatten.map { |n|
+ # label = n
+ # if Journey::Route === n
+ # label = "#{n.verb.source} #{n.path.spec}"
+ # end
+ # " #{n.object_id} [label=\"#{label}\", shape=box];"
+ #}
+ #memo_edges = memos.map { |k, memos|
+ # (memos || []).map { |v| " #{k} -> #{v.object_id};" }
+ #}.flatten.uniq
+
+ <<-eodot
+digraph nfa {
+ rankdir=LR;
+ node [shape = doublecircle];
+ #{accepting_states.join ' '};
+ node [shape = circle];
+#{edges.join "\n"}
+}
+ eodot
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/simulator.rb b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
new file mode 100644
index 0000000000..5b40da6569
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/simulator.rb
@@ -0,0 +1,47 @@
+require 'strscan'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class MatchData # :nodoc:
+ attr_reader :memos
+
+ def initialize(memos)
+ @memos = memos
+ end
+ end
+
+ class Simulator # :nodoc:
+ attr_reader :tt
+
+ def initialize(transition_table)
+ @tt = transition_table
+ end
+
+ def simulate(string)
+ input = StringScanner.new(string)
+ state = tt.eclosure(0)
+ until input.eos?
+ sym = input.scan(%r([/.?]|[^/.?]+))
+
+ # FIXME: tt.eclosure is not needed for the GTG
+ state = tt.eclosure(tt.move(state, sym))
+ end
+
+ acceptance_states = state.find_all { |s|
+ tt.accepting?(tt.eclosure(s).sort.last)
+ }
+
+ return if acceptance_states.empty?
+
+ memos = acceptance_states.map { |x| tt.memo(x) }.flatten.compact
+
+ MatchData.new(memos)
+ end
+
+ alias :=~ :simulate
+ alias :match :simulate
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
new file mode 100644
index 0000000000..a3017aeea1
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nfa/transition_table.rb
@@ -0,0 +1,163 @@
+require 'action_dispatch/journey/nfa/dot'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module NFA # :nodoc:
+ class TransitionTable # :nodoc:
+ include Journey::NFA::Dot
+
+ attr_accessor :accepting
+ attr_reader :memos
+
+ def initialize
+ @table = Hash.new { |h,f| h[f] = {} }
+ @memos = {}
+ @accepting = nil
+ @inverted = nil
+ end
+
+ def accepting?(state)
+ accepting == state
+ end
+
+ def accepting_states
+ [accepting]
+ end
+
+ def add_memo(idx, memo)
+ @memos[idx] = memo
+ end
+
+ def memo(idx)
+ @memos[idx]
+ end
+
+ def []=(i, f, s)
+ @table[f][i] = s
+ end
+
+ def merge(left, right)
+ @memos[right] = @memos.delete(left)
+ @table[right] = @table.delete(left)
+ end
+
+ def states
+ (@table.keys + @table.values.map(&:keys).flatten).uniq
+ end
+
+ # Returns a generalized transition graph with reduced states. The states
+ # are reduced like a DFA, but the table must be simulated like an NFA.
+ #
+ # Edges of the GTG are regular expressions.
+ def generalized_table
+ gt = GTG::TransitionTable.new
+ marked = {}
+ state_id = Hash.new { |h,k| h[k] = h.length }
+ alphabet = self.alphabet
+
+ stack = [eclosure(0)]
+
+ until stack.empty?
+ state = stack.pop
+ next if marked[state] || state.empty?
+
+ marked[state] = true
+
+ alphabet.each do |alpha|
+ next_state = eclosure(following_states(state, alpha))
+ next if next_state.empty?
+
+ gt[state_id[state], state_id[next_state]] = alpha
+ stack << next_state
+ end
+ end
+
+ final_groups = state_id.keys.find_all { |s|
+ s.sort.last == accepting
+ }
+
+ final_groups.each do |states|
+ id = state_id[states]
+
+ gt.add_accepting(id)
+ save = states.find { |s|
+ @memos.key?(s) && eclosure(s).sort.last == accepting
+ }
+
+ gt.add_memo(id, memo(save))
+ end
+
+ gt
+ end
+
+ # 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
+ end
+
+ # Returns set of NFA states to which there is a transition on ast symbol
+ # +a+ from some state +s+ in +t+.
+ def move(t, a)
+ Array(t).map { |s|
+ inverted[s].keys.compact.find_all { |sym|
+ sym === a
+ }.map { |sym| inverted[s][sym] }
+ }.flatten.uniq
+ end
+
+ def alphabet
+ inverted.values.map(&:keys).flatten.compact.uniq.sort_by { |x| x.to_s }
+ end
+
+ # Returns a set of NFA states reachable from some NFA state +s+ in set
+ # +t+ on nil-transitions alone.
+ def eclosure(t)
+ stack = Array(t)
+ seen = {}
+ children = []
+
+ until stack.empty?
+ s = stack.pop
+ next if seen[s]
+
+ seen[s] = true
+ children << s
+
+ stack.concat(inverted[s][nil])
+ end
+
+ children.uniq
+ end
+
+ def transitions
+ @table.map { |to, hash|
+ hash.map { |from, sym| [from, sym, to] }
+ }.flatten(1)
+ end
+
+ private
+
+ def inverted
+ return @inverted if @inverted
+
+ @inverted = Hash.new { |h, from|
+ h[from] = Hash.new { |j, s| j[s] = [] }
+ }
+
+ @table.each { |to, hash|
+ hash.each { |from, sym|
+ if sym
+ sym = Nodes::Symbol === sym ? sym.regexp : sym.left
+ end
+
+ @inverted[from][sym] << to
+ }
+ }
+
+ @inverted
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/nodes/node.rb b/actionpack/lib/action_dispatch/journey/nodes/node.rb
new file mode 100644
index 0000000000..935442ef66
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/nodes/node.rb
@@ -0,0 +1,124 @@
+require 'action_dispatch/journey/visitors'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module Nodes # :nodoc:
+ class Node # :nodoc:
+ include Enumerable
+
+ attr_accessor :left, :memo
+
+ def initialize(left)
+ @left = left
+ @memo = nil
+ end
+
+ def each(&block)
+ Visitors::Each.new(block).accept(self)
+ end
+
+ def to_s
+ Visitors::String.new.accept(self)
+ end
+
+ def to_dot
+ Visitors::Dot.new.accept(self)
+ end
+
+ def to_sym
+ name.to_sym
+ end
+
+ def name
+ left.tr '*:', ''
+ end
+
+ def type
+ raise NotImplementedError
+ end
+
+ def symbol?; false; end
+ def literal?; false; end
+ end
+
+ class Terminal < Node # :nodoc:
+ alias :symbol :left
+ end
+
+ class Literal < Terminal # :nodoc:
+ def literal?; true; end
+ def type; :LITERAL; end
+ end
+
+ class Dummy < Literal # :nodoc:
+ def initialize(x = Object.new)
+ super
+ end
+
+ def literal?; false; end
+ end
+
+ %w{ Symbol Slash Dot }.each do |t|
+ class_eval <<-eoruby, __FILE__, __LINE__ + 1
+ class #{t} < Terminal;
+ def type; :#{t.upcase}; end
+ end
+ eoruby
+ end
+
+ class Symbol < Terminal # :nodoc:
+ attr_accessor :regexp
+ alias :symbol :regexp
+
+ DEFAULT_EXP = /[^\.\/\?]+/
+ def initialize(left)
+ super
+ @regexp = DEFAULT_EXP
+ end
+
+ def default_regexp?
+ regexp == DEFAULT_EXP
+ end
+
+ def symbol?; true; end
+ end
+
+ class Unary < Node # :nodoc:
+ def children; [left] end
+ end
+
+ class Group < Unary # :nodoc:
+ def type; :GROUP; end
+ end
+
+ class Star < Unary # :nodoc:
+ def type; :STAR; end
+ end
+
+ class Binary < Node # :nodoc:
+ attr_accessor :right
+
+ def initialize(left, right)
+ super(left)
+ @right = right
+ end
+
+ def children; [left, right] end
+ end
+
+ class Cat < Binary # :nodoc:
+ def type; :CAT; end
+ end
+
+ class Or < Node # :nodoc:
+ attr_reader :children
+
+ def initialize(children)
+ @children = children
+ end
+
+ def type; :OR; end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/parser.rb b/actionpack/lib/action_dispatch/journey/parser.rb
new file mode 100644
index 0000000000..bb4cbb00e2
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.rb
@@ -0,0 +1,206 @@
+#
+# DO NOT MODIFY!!!!
+# This file is automatically generated by Racc 1.4.9
+# from Racc grammer file "".
+#
+
+require 'racc/parser.rb'
+
+
+require 'action_dispatch/journey/parser_extras'
+module ActionDispatch
+ module Journey # :nodoc:
+ class Parser < Racc::Parser # :nodoc:
+##### 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 ]
+
+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 ]
+
+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 ]
+
+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 ]
+
+racc_goto_table = [
+ 18, 1, nil, nil, nil, nil, nil, nil, 20, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, 22, 18 ]
+
+racc_goto_check = [
+ 2, 1, nil, nil, nil, nil, nil, nil, 1, nil,
+ nil, nil, nil, nil, nil, nil, nil, nil, 2, 2 ]
+
+racc_goto_pointer = [
+ nil, 1, -1, nil, nil, nil, nil, nil, nil, nil,
+ nil ]
+
+racc_goto_default = [
+ nil, nil, 2, 3, 4, 5, 6, 9, 10, 11,
+ 12 ]
+
+racc_reduce_table = [
+ 0, 0, :racc_error,
+ 2, 11, :_reduce_1,
+ 1, 11, :_reduce_2,
+ 1, 11, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 1, 12, :_reduce_none,
+ 3, 15, :_reduce_7,
+ 3, 13, :_reduce_8,
+ 1, 16, :_reduce_9,
+ 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 ]
+
+racc_reduce_n = 18
+
+racc_shift_n = 24
+
+racc_token_table = {
+ false => 0,
+ :error => 1,
+ :SLASH => 2,
+ :LITERAL => 3,
+ :SYMBOL => 4,
+ :LPAREN => 5,
+ :RPAREN => 6,
+ :DOT => 7,
+ :STAR => 8,
+ :OR => 9 }
+
+racc_nt_base = 10
+
+racc_use_result_var = true
+
+Racc_arg = [
+ racc_action_table,
+ racc_action_check,
+ racc_action_default,
+ racc_action_pointer,
+ racc_goto_table,
+ racc_goto_check,
+ racc_goto_default,
+ racc_goto_pointer,
+ racc_nt_base,
+ racc_reduce_table,
+ racc_token_table,
+ racc_shift_n,
+ racc_reduce_n,
+ racc_use_result_var ]
+
+Racc_token_to_s_table = [
+ "$end",
+ "error",
+ "SLASH",
+ "LITERAL",
+ "SYMBOL",
+ "LPAREN",
+ "RPAREN",
+ "DOT",
+ "STAR",
+ "OR",
+ "$start",
+ "expressions",
+ "expression",
+ "or",
+ "terminal",
+ "group",
+ "star",
+ "symbol",
+ "literal",
+ "slash",
+ "dot" ]
+
+Racc_debug_parser = false
+
+##### State transition tables end #####
+
+# reduce 0 omitted
+
+def _reduce_1(val, _values, result)
+ result = Cat.new(val.first, val.last)
+ result
+end
+
+def _reduce_2(val, _values, result)
+ result = val.first
+ result
+end
+
+# reduce 3 omitted
+
+# reduce 4 omitted
+
+# reduce 5 omitted
+
+# reduce 6 omitted
+
+def _reduce_7(val, _values, result)
+ result = Group.new(val[1])
+ result
+end
+
+def _reduce_8(val, _values, result)
+ result = Or.new([val.first, val.last])
+ result
+end
+
+def _reduce_9(val, _values, result)
+ result = Star.new(Symbol.new(val.last))
+ result
+end
+
+# reduce 10 omitted
+
+# reduce 11 omitted
+
+# reduce 12 omitted
+
+# reduce 13 omitted
+
+def _reduce_14(val, _values, result)
+ result = Slash.new('/')
+ result
+end
+
+def _reduce_15(val, _values, result)
+ result = Symbol.new(val.first)
+ result
+end
+
+def _reduce_16(val, _values, result)
+ result = Literal.new(val.first)
+ result
+end
+
+def _reduce_17(val, _values, result)
+ result = Dot.new(val.first)
+ result
+end
+
+def _reduce_none(val, _values, result)
+ val[0]
+end
+
+ end # class Parser
+ end # module Journey
+ end # module ActionDispatch
diff --git a/actionpack/lib/action_dispatch/journey/parser.y b/actionpack/lib/action_dispatch/journey/parser.y
new file mode 100644
index 0000000000..040f8d5922
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser.y
@@ -0,0 +1,48 @@
+class ActionDispatch::Journey::Parser
+
+token SLASH LITERAL SYMBOL LPAREN RPAREN DOT STAR OR
+
+rule
+ expressions
+ : expressions expression { result = Cat.new(val.first, val.last) }
+ | expression { result = val.first }
+ | or
+ ;
+ expression
+ : terminal
+ | group
+ | star
+ ;
+ group
+ : LPAREN expressions RPAREN { result = Group.new(val[1]) }
+ ;
+ or
+ : expressions OR expression { result = Or.new([val.first, val.last]) }
+ ;
+ star
+ : STAR { result = Star.new(Symbol.new(val.last)) }
+ ;
+ terminal
+ : symbol
+ | literal
+ | slash
+ | dot
+ ;
+ slash
+ : SLASH { result = Slash.new('/') }
+ ;
+ symbol
+ : SYMBOL { result = Symbol.new(val.first) }
+ ;
+ literal
+ : LITERAL { result = Literal.new(val.first) }
+ ;
+ dot
+ : DOT { result = Dot.new(val.first) }
+ ;
+
+end
+
+---- header
+
+require 'action_dispatch/journey/parser_extras'
diff --git a/actionpack/lib/action_dispatch/journey/parser_extras.rb b/actionpack/lib/action_dispatch/journey/parser_extras.rb
new file mode 100644
index 0000000000..14892f4321
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/parser_extras.rb
@@ -0,0 +1,23 @@
+require 'action_dispatch/journey/scanner'
+require 'action_dispatch/journey/nodes/node'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Parser < Racc::Parser # :nodoc:
+ include Journey::Nodes
+
+ def initialize
+ @scanner = Scanner.new
+ end
+
+ def parse(string)
+ @scanner.scan_setup(string)
+ do_parse
+ end
+
+ def next_token
+ @scanner.next_token
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/path/pattern.rb b/actionpack/lib/action_dispatch/journey/path/pattern.rb
new file mode 100644
index 0000000000..d37aa1fbe5
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/path/pattern.rb
@@ -0,0 +1,196 @@
+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
+
+ 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
+
+ @names = nil
+ @optional_names = nil
+ @required_names = nil
+ @re = nil
+ @offsets = nil
+ end
+
+ def ast
+ @spec.grep(Nodes::Symbol).each do |node|
+ re = @requirements[node.to_sym]
+ node.regexp = re if re
+ end
+
+ @spec.grep(Nodes::Star).each do |node|
+ node = node.left
+ node.regexp = @requirements[node.to_sym] || /(.+)/
+ end
+
+ @spec
+ end
+
+ def names
+ @names ||= spec.grep(Nodes::Symbol).map { |n| n.name }
+ end
+
+ def required_names
+ @required_names ||= names - optional_names
+ end
+
+ def optional_names
+ @optional_names ||= spec.grep(Nodes::Group).map { |group|
+ group.grep(Nodes::Symbol)
+ }.flatten.map { |n| n.name }.uniq
+ end
+
+ class RegexpOffsets < Journey::Visitors::Visitor # :nodoc:
+ attr_reader :offsets
+
+ def initialize(matchers)
+ @matchers = matchers
+ @capture_count = [0]
+ end
+
+ def visit(node)
+ super
+ @capture_count
+ end
+
+ def visit_SYMBOL(node)
+ node = node.to_sym
+
+ if @matchers.key?(node)
+ re = /#{@matchers[node]}|/
+ @capture_count.push((re.match('').length - 1) + (@capture_count.last || 0))
+ else
+ @capture_count << (@capture_count.last || 0)
+ end
+ end
+ end
+
+ class AnchoredRegexp < Journey::Visitors::Visitor # :nodoc:
+ def initialize(separator, matchers)
+ @separator = separator
+ @matchers = matchers
+ @separator_re = "([^#{separator}]+)"
+ super()
+ end
+
+ def accept(node)
+ %r{\A#{visit node}\Z}
+ end
+
+ def visit_CAT(node)
+ [visit(node.left), visit(node.right)].join
+ end
+
+ def visit_SYMBOL(node)
+ node = node.to_sym
+
+ return @separator_re unless @matchers.key?(node)
+
+ re = @matchers[node]
+ "(#{re})"
+ end
+
+ def visit_GROUP(node)
+ "(?:#{visit node.left})?"
+ end
+
+ def visit_LITERAL(node)
+ Regexp.escape(node.left)
+ end
+ alias :visit_DOT :visit_LITERAL
+
+ def visit_SLASH(node)
+ node.left
+ end
+
+ def visit_STAR(node)
+ re = @matchers[node.left.to_sym] || '.+'
+ "(#{re})"
+ end
+ end
+
+ class UnanchoredRegexp < AnchoredRegexp # :nodoc:
+ def accept(node)
+ %r{\A#{visit node}}
+ end
+ end
+
+ class MatchData # :nodoc:
+ attr_reader :names
+
+ def initialize(names, offsets, match)
+ @names = names
+ @offsets = offsets
+ @match = match
+ end
+
+ def captures
+ (length - 1).times.map { |i| self[i + 1] }
+ end
+
+ def [](x)
+ idx = @offsets[x - 1] + x
+ @match[idx]
+ end
+
+ def length
+ @offsets.length
+ end
+
+ def post_match
+ @match.post_match
+ end
+
+ def to_s
+ @match.to_s
+ end
+ end
+
+ def match(other)
+ return unless match = to_regexp.match(other)
+ MatchData.new(names, offsets, match)
+ end
+ alias :=~ :match
+
+ def source
+ to_regexp.source
+ end
+
+ def to_regexp
+ @re ||= regexp_visitor.new(@separators, @requirements).accept spec
+ end
+
+ private
+
+ def regexp_visitor
+ @anchored ? AnchoredRegexp : UnanchoredRegexp
+ end
+
+ def offsets
+ return @offsets if @offsets
+
+ viz = RegexpOffsets.new(@requirements)
+ @offsets = viz.accept(spec)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/route.rb b/actionpack/lib/action_dispatch/journey/route.rb
new file mode 100644
index 0000000000..c8eb0f6f2d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/route.rb
@@ -0,0 +1,136 @@
+module ActionDispatch
+ module Journey # :nodoc:
+ class Route # :nodoc:
+ attr_reader :app, :path, :defaults, :name
+
+ attr_reader :constraints
+ alias :conditions :constraints
+
+ attr_accessor :precedence
+
+ ##
+ # +path+ is a path constraint.
+ # +constraints+ is a hash of constraints to be applied to this route.
+ def initialize(name, app, path, constraints, defaults = {})
+ @name = name
+ @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
+ @required_parts = nil
+ @parts = nil
+ @decorated_ast = nil
+ @precedence = 0
+ end
+
+ def ast
+ @decorated_ast ||= begin
+ decorated_ast = path.ast
+ decorated_ast.grep(Nodes::Terminal).each { |n| n.memo = self }
+ decorated_ast
+ end
+ end
+
+ def requirements # :nodoc:
+ # needed for rails `rake routes`
+ path.requirements.merge(@defaults).delete_if { |_,v|
+ /.+?/ == v
+ }
+ end
+
+ def segments
+ path.names
+ end
+
+ def required_keys
+ required_parts + required_defaults.keys
+ end
+
+ def score(constraints)
+ required_keys = path.required_names
+ supplied_keys = constraints.map { |k,v| v && k.to_s }.compact
+
+ return -1 unless (required_keys - supplied_keys).empty?
+
+ score = (supplied_keys & path.names).length
+ score + (required_defaults.length * 2)
+ end
+
+ def parts
+ @parts ||= segments.map { |n| n.to_sym }
+ end
+ 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)
+ end
+
+ def optional_parts
+ path.optional_names.map { |n| n.to_sym }
+ end
+
+ def required_parts
+ @required_parts ||= path.required_names.map { |n| n.to_sym }
+ end
+
+ def required_default?(key)
+ (constraints[:required_defaults] || []).include?(key)
+ end
+
+ def required_defaults
+ @required_defaults ||= @defaults.dup.delete_if do |k,_|
+ parts.include?(k) || !required_default?(k)
+ end
+ end
+
+ def dispatcher?
+ @dispatcher
+ end
+
+ def matches?(request)
+ constraints.all? do |method, value|
+ next true unless request.respond_to?(method)
+
+ case value
+ when Regexp, String
+ value === request.send(method).to_s
+ when Array
+ value.include?(request.send(method))
+ when TrueClass
+ request.send(method).present?
+ when FalseClass
+ request.send(method).blank?
+ else
+ value === request.send(method)
+ end
+ end
+ end
+
+ def ip
+ constraints[:ip] || //
+ end
+
+ def verb
+ constraints[:request_method] || //
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/router.rb b/actionpack/lib/action_dispatch/journey/router.rb
new file mode 100644
index 0000000000..da32f1bfe7
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router.rb
@@ -0,0 +1,172 @@
+require 'action_dispatch/journey/router/utils'
+require 'action_dispatch/journey/router/strexp'
+require 'action_dispatch/journey/routes'
+require 'action_dispatch/journey/formatter'
+
+before = $-w
+$-w = false
+require 'action_dispatch/journey/parser'
+$-w = before
+
+require 'action_dispatch/journey/route'
+require 'action_dispatch/journey/path/pattern'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class RoutingError < ::StandardError # :nodoc:
+ end
+
+ # :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
+ end
+
+ def call(env)
+ env['PATH_INFO'] = 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)
+
+ unless route.path.anchored
+ env['SCRIPT_NAME'] = (script_name.to_s + match.to_s).chomp('/')
+ env['PATH_INFO'] = match.post_match
+ end
+
+ env[@params_key] = (set_params || {}).merge parameters
+
+ status, headers, body = route.app.call(env)
+
+ if 'pass' == headers['X-Cascade']
+ env['SCRIPT_NAME'] = script_name
+ env['PATH_INFO'] = path_info
+ env[@params_key] = set_params
+ next
+ end
+
+ return [status, headers, body]
+ end
+
+ return [404, {'X-Cascade' => 'pass'}, ['Not Found']]
+ end
+
+ def recognize(req)
+ find_routes(req.env).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')
+ end
+
+ yield(route, nil, parameters)
+ end
+ end
+
+ def visualizer
+ tt = GTG::Builder.new(ast).transition_table
+ groups = partitioned_routes.first.map(&:ast).group_by { |a| a.to_s }
+ asts = groups.values.map { |v| v.first }
+ tt.visualizer(asts)
+ end
+
+ private
+
+ def normalize_path(path)
+ path = "/#{path}"
+ path.squeeze!('/')
+ path
+ end
+
+ def partitioned_routes
+ routes.partitioned_routes
+ end
+
+ def ast
+ routes.ast
+ end
+
+ def simulator
+ routes.simulator
+ end
+
+ def custom_routes
+ partitioned_routes.last
+ end
+
+ def filter_routes(path)
+ return [] unless ast
+ data = simulator.match(path)
+ data ? data.memos : []
+ end
+
+ def find_routes env
+ req = request_class.new(env)
+
+ 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) }
+
+ 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]
+ }
+ end
+
+ def get_routes_as_head(routes)
+ precedence = (routes.map(&:precedence).max || 0) + 1
+ routes = routes.select { |r|
+ r.verb === "GET" && !(r.verb === "HEAD")
+ }.map! { |r|
+ Route.new(r.name,
+ r.app,
+ r.path,
+ r.conditions.merge(request_method: "HEAD"),
+ r.defaults).tap do |route|
+ route.precedence = r.precedence + precedence
+ end
+ }
+ routes.flatten!
+ routes
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/router/strexp.rb b/actionpack/lib/action_dispatch/journey/router/strexp.rb
new file mode 100644
index 0000000000..f97f1a223e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router/strexp.rb
@@ -0,0 +1,24 @@
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class Strexp # :nodoc:
+ class << self
+ alias :compile :new
+ end
+
+ attr_reader :path, :requirements, :separators, :anchor
+
+ def initialize(path, requirements, separators, anchor = true)
+ @path = path
+ @requirements = requirements
+ @separators = separators
+ @anchor = anchor
+ end
+
+ def names
+ @path.scan(/:\w+/).map { |s| s.tr(':', '') }
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/router/utils.rb b/actionpack/lib/action_dispatch/journey/router/utils.rb
new file mode 100644
index 0000000000..d1a004af50
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/router/utils.rb
@@ -0,0 +1,57 @@
+require 'uri'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Router # :nodoc:
+ class Utils # :nodoc:
+ # Normalizes URI path.
+ #
+ # Strips off trailing slash and ensures there is a leading slash.
+ # Also converts downcase url encoded string to uppercase.
+ #
+ # normalize_path("/foo") # => "/foo"
+ # normalize_path("/foo/") # => "/foo"
+ # normalize_path("foo") # => "/foo"
+ # normalize_path("") # => "/"
+ # normalize_path("/%ab") # => "/%AB"
+ def self.normalize_path(path)
+ path = "/#{path}"
+ path.squeeze!('/')
+ path.sub!(%r{/+\Z}, '')
+ path.gsub!(/(%[a-f0-9]{2})/) { $1.upcase }
+ path = '/' if path == ''
+ path
+ end
+
+ # URI path and fragment escaping
+ # http://tools.ietf.org/html/rfc3986
+ module UriEscape # :nodoc:
+ # Symbol captures can generate multiple path segments, so include /.
+ reserved_segment = '/'
+ reserved_fragment = '/?'
+ reserved_pchar = ':@&=+$,;%'
+
+ safe_pchar = "#{URI::REGEXP::PATTERN::UNRESERVED}#{reserved_pchar}"
+ safe_segment = "#{safe_pchar}#{reserved_segment}"
+ safe_fragment = "#{safe_pchar}#{reserved_fragment}"
+ UNSAFE_SEGMENT = Regexp.new("[^#{safe_segment}]", false).freeze
+ UNSAFE_FRAGMENT = Regexp.new("[^#{safe_fragment}]", false).freeze
+ end
+
+ Parser = URI::Parser.new
+
+ def self.escape_path(path)
+ Parser.escape(path.to_s, UriEscape::UNSAFE_SEGMENT)
+ end
+
+ def self.escape_fragment(fragment)
+ Parser.escape(fragment.to_s, UriEscape::UNSAFE_FRAGMENT)
+ end
+
+ def self.unescape_uri(uri)
+ Parser.unescape(uri)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/routes.rb b/actionpack/lib/action_dispatch/journey/routes.rb
new file mode 100644
index 0000000000..80e3818ccd
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/routes.rb
@@ -0,0 +1,76 @@
+module ActionDispatch
+ module Journey # :nodoc:
+ # The Routing table. Contains all routes for a system. Routes can be
+ # added to the table by calling Routes#add_route.
+ class Routes # :nodoc:
+ include Enumerable
+
+ attr_reader :routes, :named_routes
+
+ def initialize
+ @routes = []
+ @named_routes = {}
+ @ast = nil
+ @partitioned_routes = nil
+ @simulator = nil
+ end
+
+ def length
+ routes.length
+ end
+ alias :size :length
+
+ def last
+ routes.last
+ end
+
+ def each(&block)
+ routes.each(&block)
+ end
+
+ def clear
+ routes.clear
+ named_routes.clear
+ end
+
+ def partitioned_routes
+ @partitioned_routes ||= routes.partition do |r|
+ r.path.anchored && r.ast.grep(Nodes::Symbol).all?(&:default_regexp?)
+ end
+ end
+
+ def ast
+ @ast ||= begin
+ asts = partitioned_routes.first.map(&:ast)
+ Nodes::Or.new(asts) unless asts.empty?
+ end
+ end
+
+ def simulator
+ @simulator ||= begin
+ gtg = GTG::Builder.new(ast).transition_table
+ GTG::Simulator.new(gtg)
+ end
+ end
+
+ # Add a route to the routing table.
+ def add_route(app, path, conditions, defaults, name = nil)
+ route = Route.new(name, app, path, conditions, defaults)
+
+ route.precedence = routes.length
+ routes << route
+ named_routes[name] = route if name && !named_routes[name]
+ clear_cache!
+ route
+ end
+
+ private
+
+ def clear_cache!
+ @ast = nil
+ @partitioned_routes = nil
+ @simulator = nil
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/scanner.rb b/actionpack/lib/action_dispatch/journey/scanner.rb
new file mode 100644
index 0000000000..633be11a2d
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/scanner.rb
@@ -0,0 +1,61 @@
+require 'strscan'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ class Scanner # :nodoc:
+ def initialize
+ @ss = nil
+ end
+
+ def scan_setup(str)
+ @ss = StringScanner.new(str)
+ end
+
+ def eos?
+ @ss.eos?
+ end
+
+ def pos
+ @ss.pos
+ end
+
+ def pre_match
+ @ss.pre_match
+ end
+
+ def next_token
+ return if @ss.eos?
+
+ until token = scan || @ss.eos?; end
+ token
+ end
+
+ private
+
+ def scan
+ case
+ # /
+ when text = @ss.scan(/\//)
+ [:SLASH, text]
+ when text = @ss.scan(/\*\w+/)
+ [:STAR, text]
+ when text = @ss.scan(/\(/)
+ [:LPAREN, text]
+ when text = @ss.scan(/\)/)
+ [:RPAREN, text]
+ when text = @ss.scan(/\|/)
+ [:OR, text]
+ when text = @ss.scan(/\./)
+ [:DOT, text]
+ when text = @ss.scan(/:\w+/)
+ [:SYMBOL, text]
+ when text = @ss.scan(/[\w%\-~]+/)
+ [:LITERAL, text]
+ # any char
+ when text = @ss.scan(/./)
+ [:LITERAL, text]
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/visitors.rb b/actionpack/lib/action_dispatch/journey/visitors.rb
new file mode 100644
index 0000000000..9e66cab052
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visitors.rb
@@ -0,0 +1,199 @@
+# encoding: utf-8
+
+require 'thread_safe'
+
+module ActionDispatch
+ module Journey # :nodoc:
+ module Visitors # :nodoc:
+ class Visitor # :nodoc:
+ DISPATCH_CACHE = ThreadSafe::Cache.new { |h,k|
+ h[k] = :"visit_#{k}"
+ }
+
+ def accept(node)
+ visit(node)
+ end
+
+ private
+
+ def visit node
+ send(DISPATCH_CACHE[node.type], node)
+ end
+
+ def binary(node)
+ visit(node.left)
+ visit(node.right)
+ end
+ def visit_CAT(n); binary(n); end
+
+ def nary(node)
+ node.children.each { |c| visit(c) }
+ end
+ def visit_OR(n); nary(n); end
+
+ def unary(node)
+ visit(node.left)
+ end
+ def visit_GROUP(n); unary(n); end
+ 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__
+ end
+ end
+
+ # Loop through the requirements AST
+ class Each < Visitor # :nodoc:
+ attr_reader :block
+
+ def initialize(block)
+ @block = block
+ end
+
+ def visit(node)
+ super
+ block.call(node)
+ end
+ end
+
+ class String < Visitor # :nodoc:
+ private
+
+ def binary(node)
+ [visit(node.left), visit(node.right)].join
+ end
+
+ def nary(node)
+ node.children.map { |c| visit(c) }.join '|'
+ end
+
+ def terminal(node)
+ node.left
+ end
+
+ def visit_GROUP(node)
+ "(#{visit(node.left)})"
+ end
+ end
+
+ class OptimizedPath < String # :nodoc:
+ private
+
+ def visit_GROUP(node)
+ ""
+ 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 = []
+ @edges = []
+ end
+
+ def accept(node)
+ super
+ <<-eodot
+ digraph parse_tree {
+ size="8,5"
+ node [shape = none];
+ edge [dir = none];
+ #{@nodes.join "\n"}
+ #{@edges.join("\n")}
+ }
+ eodot
+ end
+
+ private
+
+ def binary(node)
+ node.children.each do |c|
+ @edges << "#{node.object_id} -> #{c.object_id};"
+ end
+ super
+ end
+
+ def nary(node)
+ node.children.each do |c|
+ @edges << "#{node.object_id} -> #{c.object_id};"
+ end
+ super
+ end
+
+ def unary(node)
+ @edges << "#{node.object_id} -> #{node.left.object_id};"
+ super
+ end
+
+ def visit_GROUP(node)
+ @nodes << "#{node.object_id} [label=\"()\"];"
+ super
+ end
+
+ def visit_CAT(node)
+ @nodes << "#{node.object_id} [label=\"â—‹\"];"
+ super
+ end
+
+ def visit_STAR(node)
+ @nodes << "#{node.object_id} [label=\"*\"];"
+ super
+ end
+
+ def visit_OR(node)
+ @nodes << "#{node.object_id} [label=\"|\"];"
+ super
+ end
+
+ def terminal(node)
+ value = node.left
+
+ @nodes << "#{node.object_id} [label=\"#{value}\"];"
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.css b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
new file mode 100644
index 0000000000..50caebaa18
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.css
@@ -0,0 +1,34 @@
+body {
+ font-family: "Helvetica Neue", Helvetica, Arial, Sans-Serif;
+ margin: 0;
+}
+
+h1 {
+ font-size: 2.0em; font-weight: bold; text-align: center;
+ color: white; background-color: black;
+ padding: 5px 0;
+ margin: 0 0 20px;
+}
+
+h2 {
+ text-align: center;
+ display: none;
+ font-size: 0.5em;
+}
+
+div#chart-2 {
+ height: 350px;
+}
+
+.clearfix {display: inline-block; }
+.input { overflow: show;}
+.instruction { color: #666; padding: 0 30px 20px; font-size: 0.9em}
+.instruction p { padding: 0 0 5px; }
+.instruction li { padding: 0 10px 5px; }
+
+.form { background: #EEE; padding: 20px 30px; border-radius: 5px; margin-left: auto; margin-right: auto; width: 500px; margin-bottom: 20px}
+.form p, .form form { text-align: center }
+.form form {padding: 0 10px 5px; }
+.form .fun_routes { font-size: 0.9em;}
+.form .fun_routes a { margin: 0 5px 0 0; }
+
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/fsm.js b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js
new file mode 100644
index 0000000000..d9bcaef928
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/fsm.js
@@ -0,0 +1,134 @@
+function tokenize(input, callback) {
+ while(input.length > 0) {
+ callback(input.match(/^[\/\.\?]|[^\/\.\?]+/)[0]);
+ input = input.replace(/^[\/\.\?]|[^\/\.\?]+/, '');
+ }
+}
+
+var graph = d3.select("#chart-2 svg");
+var svg_edges = {};
+var svg_nodes = {};
+
+graph.selectAll("g.edge").each(function() {
+ var node = d3.select(this);
+ var index = node.select("title").text().split("->");
+ var left = parseInt(index[0]);
+ var right = parseInt(index[1]);
+
+ if(!svg_edges[left]) { svg_edges[left] = {} }
+ svg_edges[left][right] = node;
+});
+
+graph.selectAll("g.node").each(function() {
+ var node = d3.select(this);
+ var index = parseInt(node.select("title").text());
+ svg_nodes[index] = node;
+});
+
+function reset_graph() {
+ for(var key in svg_edges) {
+ for(var mkey in svg_edges[key]) {
+ var node = svg_edges[key][mkey];
+ var path = node.select("path");
+ var arrow = node.select("polygon");
+ path.style("stroke", "black");
+ arrow.style("stroke", "black").style("fill", "black");
+ }
+ }
+
+ for(var key in svg_nodes) {
+ var node = svg_nodes[key];
+ node.select('ellipse').style("fill", "white");
+ node.select('polygon').style("fill", "white");
+ }
+ return false;
+}
+
+function highlight_edge(from, to) {
+ var node = svg_edges[from][to];
+ var path = node.select("path");
+ var arrow = node.select("polygon");
+
+ path
+ .transition().duration(500)
+ .style("stroke", "green");
+
+ arrow
+ .transition().duration(500)
+ .style("stroke", "green").style("fill", "green");
+}
+
+function highlight_state(index, color) {
+ if(!color) { color = "green"; }
+
+ svg_nodes[index].select('ellipse')
+ .style("fill", "white")
+ .transition().duration(500)
+ .style("fill", color);
+}
+
+function highlight_finish(index) {
+ svg_nodes[index].select('polygon')
+ .style("fill", "while")
+ .transition().duration(500)
+ .style("fill", "blue");
+}
+
+function match(input) {
+ reset_graph();
+ var table = tt();
+ var states = [0];
+ var regexp_states = table['regexp_states'];
+ var string_states = table['string_states'];
+ var accepting = table['accepting'];
+
+ highlight_state(0);
+
+ tokenize(input, function(token) {
+ var new_states = [];
+ for(var key in states) {
+ var state = states[key];
+
+ if(string_states[state] && string_states[state][token]) {
+ var new_state = string_states[state][token];
+ highlight_edge(state, new_state);
+ highlight_state(new_state);
+ new_states.push(new_state);
+ }
+
+ if(regexp_states[state]) {
+ for(var key in regexp_states[state]) {
+ var re = new RegExp("^" + key + "$");
+ if(re.test(token)) {
+ var new_state = regexp_states[state][key];
+ highlight_edge(state, new_state);
+ highlight_state(new_state);
+ new_states.push(new_state);
+ }
+ }
+ }
+ }
+
+ if(new_states.length == 0) {
+ return;
+ }
+ states = new_states;
+ });
+
+ for(var key in states) {
+ var state = states[key];
+ if(accepting[state]) {
+ for(var mkey in svg_edges[state]) {
+ if(!regexp_states[mkey] && !string_states[mkey]) {
+ highlight_edge(state, mkey);
+ highlight_finish(mkey);
+ }
+ }
+ } else {
+ highlight_state(state, "red");
+ }
+ }
+
+ return false;
+}
+
diff --git a/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
new file mode 100644
index 0000000000..6aff10956a
--- /dev/null
+++ b/actionpack/lib/action_dispatch/journey/visualizer/index.html.erb
@@ -0,0 +1,52 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title><%= title %></title>
+ <link rel="stylesheet" href="https://raw.github.com/gist/1706081/af944401f75ea20515a02ddb3fb43d23ecb8c662/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>
+ </head>
+ <body>
+ <div id="wrapper">
+ <h1>Routes FSM with NFA simulation</h1>
+ <div class="instruction form">
+ <p>
+ Type a route in to the box and click "simulate".
+ </p>
+ <form onsubmit="return match(this.route.value);">
+ <input type="text" size="30" name="route" value="/articles/new" />
+ <button>simulate</button>
+ <input type="reset" value="reset" onclick="return reset_graph();"/>
+ </form>
+ <p class="fun_routes">
+ Some fun routes to try:
+ <% fun_routes.each do |path| %>
+ <a href="#" onclick="document.forms[0].elements[0].value=this.text.replace(/^\s+|\s+$/g,''); return match(this.text.replace(/^\s+|\s+$/g,''));">
+ <%= path %>
+ </a>
+ <% end %>
+ </p>
+ </div>
+ <div class='chart' id='chart-2'>
+ <%= svg %>
+ </div>
+ <div class="instruction">
+ <p>
+ This is a FSM for a system that has the following routes:
+ </p>
+ <ul>
+ <% paths.each do |route| %>
+ <li><%= route %></li>
+ <% end %>
+ </ul>
+ </div>
+ </div>
+ <% javascripts.each do |js| %>
+ <script><%= js %></script>
+ <% end %>
+ </body>
+</html>
diff --git a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb b/actionpack/lib/action_dispatch/middleware/best_standards_support.rb
deleted file mode 100644
index 69adcc419f..0000000000
--- a/actionpack/lib/action_dispatch/middleware/best_standards_support.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module ActionDispatch
- class BestStandardsSupport
- def initialize(app, type = true)
- @app = app
-
- @header = case type
- when true
- "IE=Edge,chrome=1"
- when :builtin
- "IE=Edge"
- when false
- nil
- end
- end
-
- def call(env)
- status, headers, body = @app.call(env)
- headers["X-UA-Compatible"] = @header
- [status, headers, body]
- end
- end
-end
diff --git a/actionpack/lib/action_dispatch/middleware/cookies.rb b/actionpack/lib/action_dispatch/middleware/cookies.rb
index ba5d332d49..3ccd0c9ee8 100644
--- a/actionpack/lib/action_dispatch/middleware/cookies.rb
+++ b/actionpack/lib/action_dispatch/middleware/cookies.rb
@@ -1,5 +1,8 @@
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/module/attribute_accessors'
+require 'active_support/core_ext/object/blank'
+require 'active_support/key_generator'
+require 'active_support/message_verifier'
module ActionDispatch
class Request < Rack::Request
@@ -14,7 +17,7 @@ module ActionDispatch
# being written will be sent out with the response. Reading a cookie does not get
# the cookie object itself back, just the value it holds.
#
- # Examples for writing:
+ # Examples of writing:
#
# # Sets a simple session cookie.
# # This cookie will be deleted when the user's browser is closed.
@@ -24,11 +27,11 @@ module ActionDispatch
# cookies[:lat_lon] = [47.68, -122.37]
#
# # Sets a cookie that expires in 1 hour.
- # cookies[:login] = { :value => "XJ-122", :expires => 1.hour.from_now }
+ # cookies[:login] = { value: "XJ-122", expires: 1.hour.from_now }
#
# # Sets a signed cookie, which prevents users from tampering with its value.
- # # The cookie is signed by your app's <tt>config.secret_token</tt> value.
- # # It can be read using the signed method <tt>cookies.signed[:key]</tt>
+ # # The cookie is signed by your app's <tt>config.secret_key_base</tt> value.
+ # # It can be read using the signed method <tt>cookies.signed[:name]</tt>
# cookies.signed[:user_id] = current_user.id
#
# # Sets a "permanent" cookie (which expires in 20 years from now).
@@ -37,7 +40,7 @@ module ActionDispatch
# # You can also chain these methods:
# cookies.permanent.signed[:login] = "XJ-122"
#
- # Examples for reading:
+ # Examples of reading:
#
# cookies[:user_name] # => "david"
# cookies.size # => 2
@@ -50,13 +53,13 @@ module ActionDispatch
#
# Please note that if you specify a :domain when setting a cookie, you must also specify the domain when deleting the cookie:
#
- # cookies[:key] = {
- # :value => 'a yummy cookie',
- # :expires => 1.year.from_now,
- # :domain => 'domain.com'
+ # cookies[:name] = {
+ # value: 'a yummy cookie',
+ # expires: 1.year.from_now,
+ # domain: 'domain.com'
# }
#
- # cookies.delete(:key, :domain => 'domain.com')
+ # cookies.delete(:name, domain: 'domain.com')
#
# The option symbols for setting cookies are:
#
@@ -67,26 +70,125 @@ module ActionDispatch
# restrict to the domain level. If you use a schema like www.example.com
# and want to share session with user.example.com set <tt>:domain</tt>
# to <tt>:all</tt>. Make sure to specify the <tt>:domain</tt> option with
- # <tt>:all</tt> again when deleting keys.
+ # <tt>:all</tt> again when deleting cookies.
#
- # :domain => nil # Does not sets cookie domain. (default)
- # :domain => :all # Allow the cookie for the top most level
+ # domain: nil # Does not sets cookie domain. (default)
+ # domain: :all # Allow the cookie for the top most level
# domain and subdomains.
#
# * <tt>:expires</tt> - The time at which this cookie expires, as a \Time object.
- # * <tt>:secure</tt> - Whether this cookie is a only transmitted to HTTPS servers.
+ # * <tt>:secure</tt> - Whether this cookie is only transmitted to HTTPS servers.
# Default is +false+.
# * <tt>:httponly</tt> - Whether this cookie is accessible via scripting or
# only HTTP. Defaults to +false+.
class Cookies
- HTTP_HEADER = "Set-Cookie".freeze
- TOKEN_KEY = "action_dispatch.secret_token".freeze
+ HTTP_HEADER = "Set-Cookie".freeze
+ GENERATOR_KEY = "action_dispatch.key_generator".freeze
+ SIGNED_COOKIE_SALT = "action_dispatch.signed_cookie_salt".freeze
+ ENCRYPTED_COOKIE_SALT = "action_dispatch.encrypted_cookie_salt".freeze
+ ENCRYPTED_SIGNED_COOKIE_SALT = "action_dispatch.encrypted_signed_cookie_salt".freeze
+ SECRET_TOKEN = "action_dispatch.secret_token".freeze
+ SECRET_KEY_BASE = "action_dispatch.secret_key_base".freeze
+
+ # Cookies can typically store 4096 bytes.
+ MAX_COOKIE_SIZE = 4096
# Raised when storing more than 4K of session data.
CookieOverflow = Class.new StandardError
+ # Include in a cookie jar to allow chaining, e.g. cookies.permanent.signed
+ module ChainedCookieJars
+ # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
+ #
+ # cookies.permanent[:prefers_open_id] = true
+ # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ #
+ # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
+ #
+ # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
+ #
+ # cookies.permanent.signed[:remember_me] = current_user.id
+ # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
+ def permanent
+ @permanent ||= PermanentCookieJar.new(self, @key_generator, @options)
+ end
+
+ # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
+ # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
+ # cookie was tampered with by the user (or a 3rd party), nil will be returned.
+ #
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # legacy cookies signed with the old key generator will be transparently upgraded.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
+ #
+ # Example:
+ #
+ # cookies.signed[:discount] = 45
+ # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
+ #
+ # cookies.signed[:discount] # => 45
+ def signed
+ @signed ||=
+ if @options[:upgrade_legacy_signed_cookies]
+ UpgradeLegacySignedCookieJar.new(self, @key_generator, @options)
+ else
+ SignedCookieJar.new(self, @key_generator, @options)
+ end
+ end
+
+ # Returns a jar that'll automatically encrypt cookie values before sending them to the client and will decrypt them for read.
+ # If the cookie was tampered with by the user (or a 3rd party), nil will be returned.
+ #
+ # If +config.secret_key_base+ and +config.secret_token+ (deprecated) are both set,
+ # legacy cookies signed with the old key generator will be transparently upgraded.
+ #
+ # This jar requires that you set a suitable secret for the verification on your app's +config.secret_key_base+.
+ #
+ # Example:
+ #
+ # cookies.encrypted[:discount] = 45
+ # # => Set-Cookie: discount=ZS9ZZ1R4cG1pcUJ1bm80anhQang3dz09LS1mbDZDSU5scGdOT3ltQ2dTdlhSdWpRPT0%3D--ab54663c9f4e3bc340c790d6d2b71e92f5b60315; path=/
+ #
+ # cookies.encrypted[:discount] # => 45
+ def encrypted
+ @encrypted ||=
+ if @options[:upgrade_legacy_signed_cookies]
+ UpgradeLegacyEncryptedCookieJar.new(self, @key_generator, @options)
+ else
+ EncryptedCookieJar.new(self, @key_generator, @options)
+ end
+ end
+
+ # Returns the +signed+ or +encrypted+ jar, preferring +encrypted+ if +secret_key_base+ is set.
+ # Used by ActionDispatch::Session::CookieStore to avoid the need to introduce new cookie stores.
+ def signed_or_encrypted
+ @signed_or_encrypted ||=
+ if @options[:secret_key_base].present?
+ encrypted
+ else
+ signed
+ end
+ end
+ end
+
+ module VerifyAndUpgradeLegacySignedMessage
+ def initialize(*args)
+ super
+ @legacy_verifier = ActiveSupport::MessageVerifier.new(@options[:secret_token])
+ end
+
+ def verify_and_upgrade_legacy_signed_message(name, signed_message)
+ @legacy_verifier.verify(signed_message).tap do |value|
+ self[name] = value
+ end
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ end
+ end
+
class CookieJar #:nodoc:
- include Enumerable
+ include Enumerable, ChainedCookieJars
# This regular expression is used to split the levels of a domain.
# The top level domain can be any string without a period or
@@ -102,22 +204,36 @@ module ActionDispatch
# $& => example.local
DOMAIN_REGEXP = /[^.]*\.([^.]*|..\...|...\...)$/
+ def self.options_for_env(env) #:nodoc:
+ { signed_cookie_salt: env[SIGNED_COOKIE_SALT] || '',
+ encrypted_cookie_salt: env[ENCRYPTED_COOKIE_SALT] || '',
+ encrypted_signed_cookie_salt: env[ENCRYPTED_SIGNED_COOKIE_SALT] || '',
+ secret_token: env[SECRET_TOKEN],
+ secret_key_base: env[SECRET_KEY_BASE],
+ upgrade_legacy_signed_cookies: env[SECRET_TOKEN].present? && env[SECRET_KEY_BASE].present?
+ }
+ end
+
def self.build(request)
- secret = request.env[TOKEN_KEY]
+ env = request.env
+ key_generator = env[GENERATOR_KEY]
+ options = options_for_env env
+
host = request.host
secure = request.ssl?
- new(secret, host, secure).tap do |hash|
+ new(key_generator, host, secure, options).tap do |hash|
hash.update(request.cookies)
end
end
- def initialize(secret = nil, host = nil, secure = false)
- @secret = secret
+ def initialize(key_generator, host = nil, secure = false, options = {})
+ @key_generator = key_generator
@set_cookies = {}
@delete_cookies = {}
@host = host
@secure = secure
+ @options = options
@cookies = {}
end
@@ -130,6 +246,10 @@ module ActionDispatch
@cookies[name.to_s]
end
+ def fetch(name, *args, &block)
+ @cookies.fetch(name.to_s, *args, &block)
+ end
+
def key?(name)
@cookies.key?(name.to_s)
end
@@ -160,7 +280,7 @@ module ActionDispatch
# Sets the cookie named +name+. The second argument may be the very cookie
# value, or a hash of options as documented above.
- def []=(key, options)
+ def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
value = options[:value]
@@ -171,36 +291,36 @@ module ActionDispatch
handle_options(options)
- if @cookies[key.to_s] != value or options[:expires]
- @cookies[key.to_s] = value
- @set_cookies[key.to_s] = options
- @delete_cookies.delete(key.to_s)
+ if @cookies[name.to_s] != value or options[:expires]
+ @cookies[name.to_s] = value
+ @set_cookies[name.to_s] = options
+ @delete_cookies.delete(name.to_s)
end
value
end
# Removes the cookie on the client machine by setting the value to an empty string
- # and setting its expiration date into the past. Like <tt>[]=</tt>, you can pass in
+ # and the expiration date in the past. Like <tt>[]=</tt>, you can pass in
# an options hash to delete cookies with extra data such as a <tt>:path</tt>.
- def delete(key, options = {})
- return unless @cookies.has_key? key.to_s
+ def delete(name, options = {})
+ return unless @cookies.has_key? name.to_s
options.symbolize_keys!
handle_options(options)
- value = @cookies.delete(key.to_s)
- @delete_cookies[key.to_s] = options
+ value = @cookies.delete(name.to_s)
+ @delete_cookies[name.to_s] = options
value
end
# Whether the given cookie is to be deleted by this CookieJar.
# Like <tt>[]=</tt>, you can pass in an options hash to test if a
# deletion applies to a specific <tt>:path</tt>, <tt>:domain</tt> etc.
- def deleted?(key, options = {})
+ def deleted?(name, options = {})
options.symbolize_keys!
handle_options(options)
- @delete_cookies[key.to_s] == options
+ @delete_cookies[name.to_s] == options
end
# Removes all cookies on the client machine by calling <tt>delete</tt> for each cookie
@@ -208,38 +328,6 @@ module ActionDispatch
@cookies.each_key{ |k| delete(k, options) }
end
- # Returns a jar that'll automatically set the assigned cookies to have an expiration date 20 years from now. Example:
- #
- # cookies.permanent[:prefers_open_id] = true
- # # => Set-Cookie: prefers_open_id=true; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
- #
- # This jar is only meant for writing. You'll read permanent cookies through the regular accessor.
- #
- # This jar allows chaining with the signed jar as well, so you can set permanent, signed cookies. Examples:
- #
- # cookies.permanent.signed[:remember_me] = current_user.id
- # # => Set-Cookie: remember_me=BAhU--848956038e692d7046deab32b7131856ab20e14e; path=/; expires=Sun, 16-Dec-2029 03:24:16 GMT
- def permanent
- @permanent ||= PermanentCookieJar.new(self, @secret)
- end
-
- # Returns a jar that'll automatically generate a signed representation of cookie value and verify it when reading from
- # the cookie again. This is useful for creating cookies with values that the user is not supposed to change. If a signed
- # cookie was tampered with by the user (or a 3rd party), an ActiveSupport::MessageVerifier::InvalidSignature exception will
- # be raised.
- #
- # This jar requires that you set a suitable secret for the verification on your app's +config.secret_token+.
- #
- # Example:
- #
- # cookies.signed[:discount] = 45
- # # => Set-Cookie: discount=BAhpMg==--2c1c6906c90a3bc4fd54a51ffb41dffa4bf6b5f7; path=/
- #
- # cookies.signed[:discount] # => 45
- def signed
- @signed ||= SignedCookieJar.new(self, @secret)
- end
-
def write(headers)
@set_cookies.each { |k, v| ::Rack::Utils.set_cookie_header!(headers, k, v) if write_cookie?(v) }
@delete_cookies.each { |k, v| ::Rack::Utils.delete_cookie_header!(headers, k, v) }
@@ -254,18 +342,25 @@ module ActionDispatch
self.always_write_cookie = false
private
-
def write_cookie?(cookie)
@secure || !cookie[:secure] || always_write_cookie
end
end
- class PermanentCookieJar < CookieJar #:nodoc:
- def initialize(parent_jar, secret)
- @parent_jar, @secret = parent_jar, secret
+ class PermanentCookieJar #:nodoc:
+ include ChainedCookieJars
+
+ def initialize(parent_jar, key_generator, options = {})
+ @parent_jar = parent_jar
+ @key_generator = key_generator
+ @options = options
+ end
+
+ def [](name)
+ @parent_jar[name.to_s]
end
- def []=(key, options)
+ def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
else
@@ -273,33 +368,27 @@ module ActionDispatch
end
options[:expires] = 20.years.from_now
- @parent_jar[key] = options
- end
-
- def method_missing(method, *arguments, &block)
- @parent_jar.send(method, *arguments, &block)
+ @parent_jar[name] = options
end
end
- class SignedCookieJar < CookieJar #:nodoc:
- MAX_COOKIE_SIZE = 4096 # Cookies can typically store 4096 bytes.
- SECRET_MIN_LENGTH = 30 # Characters
+ class SignedCookieJar #:nodoc:
+ include ChainedCookieJars
- def initialize(parent_jar, secret)
- ensure_secret_secure(secret)
+ def initialize(parent_jar, key_generator, options = {})
@parent_jar = parent_jar
+ @options = options
+ secret = key_generator.generate_key(@options[:signed_cookie_salt])
@verifier = ActiveSupport::MessageVerifier.new(secret)
end
def [](name)
if signed_message = @parent_jar[name]
- @verifier.verify(signed_message)
+ verify(signed_message)
end
- rescue ActiveSupport::MessageVerifier::InvalidSignature
- nil
end
- def []=(key, options)
+ def []=(name, options)
if options.is_a?(Hash)
options.symbolize_keys!
options[:value] = @verifier.generate(options[:value])
@@ -308,31 +397,83 @@ module ActionDispatch
end
raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
- @parent_jar[key] = options
+ @parent_jar[name] = options
end
- def method_missing(method, *arguments, &block)
- @parent_jar.send(method, *arguments, &block)
+ private
+ def verify(signed_message)
+ @verifier.verify(signed_message)
+ rescue ActiveSupport::MessageVerifier::InvalidSignature
+ nil
+ end
+ end
+
+ # UpgradeLegacySignedCookieJar is used instead of SignedCookieJar if
+ # config.secret_token and config.secret_key_base are both set. It reads
+ # legacy cookies signed with the old dummy key generator and re-saves
+ # them using the new key generator to provide a smooth upgrade path.
+ class UpgradeLegacySignedCookieJar < SignedCookieJar #:nodoc:
+ include VerifyAndUpgradeLegacySignedMessage
+
+ def [](name)
+ if signed_message = @parent_jar[name]
+ verify(signed_message) || verify_and_upgrade_legacy_signed_message(name, signed_message)
+ end
end
+ end
+
+ class EncryptedCookieJar #:nodoc:
+ include ChainedCookieJars
+
+ def initialize(parent_jar, key_generator, options = {})
+ if ActiveSupport::LegacyKeyGenerator === key_generator
+ raise "You didn't set config.secret_key_base, which is required for this cookie jar. " +
+ "Read the upgrade documentation to learn more about this new config option."
+ end
- protected
+ @parent_jar = parent_jar
+ @options = options
+ secret = key_generator.generate_key(@options[:encrypted_cookie_salt])
+ sign_secret = key_generator.generate_key(@options[:encrypted_signed_cookie_salt])
+ @encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+ end
- # To prevent users from using something insecure like "Password" we make sure that the
- # secret they've provided is at least 30 characters in length.
- def ensure_secret_secure(secret)
- if secret.blank?
- raise ArgumentError, "A secret is required to generate an " +
- "integrity hash for cookie session data. Use " +
- "config.secret_token = \"some secret phrase of at " +
- "least #{SECRET_MIN_LENGTH} characters\"" +
- "in config/initializers/secret_token.rb"
+ def [](name)
+ if encrypted_message = @parent_jar[name]
+ decrypt_and_verify(encrypted_message)
end
+ end
- if secret.length < SECRET_MIN_LENGTH
- raise ArgumentError, "Secret should be something secure, " +
- "like \"#{SecureRandom.hex(16)}\". The value you " +
- "provided, \"#{secret}\", is shorter than the minimum length " +
- "of #{SECRET_MIN_LENGTH} characters"
+ def []=(name, options)
+ if options.is_a?(Hash)
+ options.symbolize_keys!
+ else
+ options = { :value => options }
+ end
+ options[:value] = @encryptor.encrypt_and_sign(options[:value])
+
+ raise CookieOverflow if options[:value].size > MAX_COOKIE_SIZE
+ @parent_jar[name] = options
+ end
+
+ private
+ def decrypt_and_verify(encrypted_message)
+ @encryptor.decrypt_and_verify(encrypted_message)
+ rescue ActiveSupport::MessageVerifier::InvalidSignature, ActiveSupport::MessageEncryptor::InvalidMessage
+ nil
+ end
+ end
+
+ # UpgradeLegacyEncryptedCookieJar is used by ActionDispatch::Session::CookieStore
+ # instead of EncryptedCookieJar if config.secret_token and config.secret_key_base
+ # are both set. It reads legacy cookies signed with the old dummy key generator and
+ # encrypts and re-saves them using the new key generator to provide a smooth upgrade path.
+ class UpgradeLegacyEncryptedCookieJar < EncryptedCookieJar #:nodoc:
+ include VerifyAndUpgradeLegacySignedMessage
+
+ def [](name)
+ if encrypted_or_signed_message = @parent_jar[name]
+ decrypt_and_verify(encrypted_or_signed_message) || verify_and_upgrade_legacy_signed_message(name, encrypted_or_signed_message)
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
index 0f0589a844..0ca1a87645 100644
--- a/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/debug_exceptions.rb
@@ -2,12 +2,11 @@ require 'action_dispatch/http/request'
require 'action_dispatch/middleware/exception_wrapper'
require 'action_dispatch/routing/inspector'
-
module ActionDispatch
# This middleware is responsible for logging exceptions and
# showing a debugging page in case the request is local.
class DebugExceptions
- RESCUES_TEMPLATE_PATH = File.join(File.dirname(__FILE__), 'templates')
+ RESCUES_TEMPLATE_PATH = File.expand_path('../templates', __FILE__)
def initialize(app, routes_app = nil)
@app = app
@@ -15,19 +14,17 @@ module ActionDispatch
end
def call(env)
- begin
- response = @app.call(env)
+ _, headers, body = response = @app.call(env)
- if response[1]['X-Cascade'] == 'pass'
- body = response[2]
- body.close if body.respond_to?(:close)
- raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
- end
- rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
+ if headers['X-Cascade'] == 'pass'
+ body.close if body.respond_to?(:close)
+ raise ActionController::RoutingError, "No route matches [#{env['REQUEST_METHOD']}] #{env['PATH_INFO'].inspect}"
end
- exception ? render_exception(env, exception) : response
+ response
+ rescue Exception => exception
+ raise exception if env['action_dispatch.show_exceptions'] == false
+ render_exception(env, exception)
end
private
@@ -37,25 +34,35 @@ module ActionDispatch
log_error(env, wrapper)
if env['action_dispatch.show_detailed_exceptions']
+ request = Request.new(env)
template = ActionView::Base.new([RESCUES_TEMPLATE_PATH],
- :request => Request.new(env),
- :exception => wrapper.exception,
- :application_trace => wrapper.application_trace,
- :framework_trace => wrapper.framework_trace,
- :full_trace => wrapper.full_trace,
- :routes => formatted_routes(exception)
+ request: request,
+ exception: wrapper.exception,
+ application_trace: wrapper.application_trace,
+ framework_trace: wrapper.framework_trace,
+ full_trace: wrapper.full_trace,
+ routes_inspector: routes_inspector(exception),
+ source_extract: wrapper.source_extract,
+ line_number: wrapper.line_number,
+ file: wrapper.file
)
-
file = "rescues/#{wrapper.rescue_template}"
- body = template.render(:template => file, :layout => 'rescues/layout')
- render(wrapper.status_code, body)
+
+ if request.xhr?
+ body = template.render(template: file, layout: false, formats: [:text])
+ format = "text/plain"
+ else
+ body = template.render(template: file, layout: 'rescues/layout')
+ format = "text/html"
+ end
+ render(wrapper.status_code, body, format)
else
raise exception
end
end
- def render(status, body)
- [status, {'Content-Type' => "text/html; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
+ def render(status, body, format)
+ [status, {'Content-Type' => "#{format}; charset=#{Response.default_charset}", 'Content-Length' => body.bytesize.to_s}, [body]]
end
def log_error(env, wrapper)
@@ -83,11 +90,9 @@ module ActionDispatch
@stderr_logger ||= ActiveSupport::Logger.new($stderr)
end
- def formatted_routes(exception)
- return false unless @routes_app.respond_to?(:routes)
- if exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error)
- inspector = ActionDispatch::Routing::RoutesInspector.new
- inspector.format(@routes_app.routes.routes).join("\n")
+ def routes_inspector(exception)
+ if @routes_app.respond_to?(:routes) && (exception.is_a?(ActionController::RoutingError) || exception.is_a?(ActionView::Template::Error))
+ ActionDispatch::Routing::RoutesInspector.new(@routes_app.routes.routes)
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
index ae38c56a67..daddaccadd 100644
--- a/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
+++ b/actionpack/lib/action_dispatch/middleware/exception_wrapper.rb
@@ -1,5 +1,4 @@
require 'action_controller/metal/exceptions'
-require 'active_support/core_ext/exception'
require 'active_support/core_ext/class/attribute_accessors'
module ActionDispatch
@@ -10,10 +9,14 @@ module ActionDispatch
'ActionController::RoutingError' => :not_found,
'AbstractController::ActionNotFound' => :not_found,
'ActionController::MethodNotAllowed' => :method_not_allowed,
+ 'ActionController::UnknownHttpMethod' => :method_not_allowed,
'ActionController::NotImplemented' => :not_implemented,
'ActionController::UnknownFormat' => :not_acceptable,
'ActionController::InvalidAuthenticityToken' => :unprocessable_entity,
- 'ActionController::BadRequest' => :bad_request
+ 'ActionDispatch::ParamsParser::ParseError' => :bad_request,
+ 'ActionController::BadRequest' => :bad_request,
+ 'ActionController::ParameterMissing' => :bad_request,
+ 'ActionController::EmptyParameter' => :bad_request
)
cattr_accessor :rescue_templates
@@ -25,7 +28,7 @@ module ActionDispatch
'ActionView::Template::Error' => 'template_error'
)
- attr_reader :env, :exception
+ attr_reader :env, :exception, :line_number, :file
def initialize(env, exception)
@env = env
@@ -56,6 +59,15 @@ module ActionDispatch
Rack::Utils.status_code(@@rescue_responses[class_name])
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
+ end
+
private
def original_exception(exception)
@@ -81,5 +93,17 @@ module ActionDispatch
def backtrace_cleaner
@backtrace_cleaner ||= @env['action_dispatch.backtrace_cleaner']
end
+
+ def source_fragment(path, line)
+ return unless Rails.respond_to?(:root) && Rails.root
+ full_path = Rails.root.join(path)
+ if File.exist?(full_path)
+ File.open(full_path, "r") do |file|
+ start = [line - 3, 0].max
+ lines = file.each_line.drop(start).take(6)
+ Hash[*(start+1..(lines.count+start)).zip(lines).flatten]
+ end
+ end
+ end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/flash.rb b/actionpack/lib/action_dispatch/middleware/flash.rb
index 9928b7cc3a..89003e7a5e 100644
--- a/actionpack/lib/action_dispatch/middleware/flash.rb
+++ b/actionpack/lib/action_dispatch/middleware/flash.rb
@@ -4,7 +4,7 @@ module ActionDispatch
# read a notice you put there or <tt>flash["notice"] = "hello"</tt>
# to put a new one.
def flash
- @env[Flash::KEY] ||= (session["flash"] || Flash::FlashHash.new).tap(&:sweep)
+ @env[Flash::KEY] ||= Flash::FlashHash.from_session_value(session["flash"])
end
end
@@ -59,27 +59,41 @@ module ActionDispatch
@flash[k]
end
- # Convenience accessor for flash.now[:alert]=
+ # Convenience accessor for <tt>flash.now[:alert]=</tt>.
def alert=(message)
self[:alert] = message
end
- # Convenience accessor for flash.now[:notice]=
+ # Convenience accessor for <tt>flash.now[:notice]=</tt>.
def notice=(message)
self[:notice] = message
end
end
- # Implementation detail: please do not change the signature of the
- # FlashHash class. Doing that will likely affect all Rails apps in
- # production as the FlashHash currently stored in their sessions will
- # become invalid.
class FlashHash
include Enumerable
- def initialize #:nodoc:
- @discard = Set.new
- @flashes = {}
+ def self.from_session_value(value)
+ flash = case value
+ when FlashHash # Rails 3.1, 3.2
+ new(value.instance_variable_get(:@flashes), value.instance_variable_get(:@used))
+ when Hash # Rails 4.0
+ new(value['flashes'], value['discard'])
+ else
+ new
+ end
+
+ flash.tap(&:sweep)
+ end
+
+ def to_session_value
+ return nil if empty?
+ {'discard' => @discard.to_a, 'flashes' => @flashes}
+ end
+
+ def initialize(flashes = {}, discard = []) #:nodoc:
+ @discard = Set.new(discard)
+ @flashes = flashes
@now = nil
end
@@ -91,7 +105,7 @@ module ActionDispatch
super
end
- def []=(k, v) #:nodoc:
+ def []=(k, v)
@discard.delete k
@flashes[k] = v
end
@@ -155,6 +169,14 @@ module ActionDispatch
# vanish when the current action is done.
#
# Entries set via <tt>now</tt> are accessed the same way as standard entries: <tt>flash['my-key']</tt>.
+ #
+ # Also, brings two convenience accessors:
+ #
+ # flash.now.alert = "Beware now!"
+ # # Equivalent to flash.now[:alert] = "Beware now!"
+ #
+ # flash.now.notice = "Good luck now!"
+ # # Equivalent to flash.now[:notice] = "Good luck now!"
def now
@now ||= FlashNow.new(self)
end
@@ -185,22 +207,22 @@ module ActionDispatch
@discard.replace @flashes.keys
end
- # Convenience accessor for flash[:alert]
+ # Convenience accessor for <tt>flash[:alert]</tt>.
def alert
self[:alert]
end
- # Convenience accessor for flash[:alert]=
+ # Convenience accessor for <tt>flash[:alert]=</tt>.
def alert=(message)
self[:alert] = message
end
- # Convenience accessor for flash[:notice]
+ # Convenience accessor for <tt>flash[:notice]</tt>.
def notice
self[:notice]
end
- # Convenience accessor for flash[:notice]=
+ # Convenience accessor for <tt>flash[:notice]=</tt>.
def notice=(message)
self[:notice] = message
end
@@ -221,19 +243,13 @@ module ActionDispatch
session = Request::Session.find(env) || {}
flash_hash = env[KEY]
- if flash_hash
- if !flash_hash.empty? || session.key?('flash')
- session["flash"] = flash_hash
- new_hash = flash_hash.dup
- else
- new_hash = flash_hash
- end
-
- env[KEY] = new_hash
+ if flash_hash && (flash_hash.present? || session.key?('flash'))
+ session["flash"] = flash_hash.to_session_value
+ env[KEY] = flash_hash.dup
end
if (!session.respond_to?(:loaded?) || session.loaded?) && # (reset_session uses {}, which doesn't implement #loaded?)
- session.key?('flash') && session['flash'].empty?
+ session.key?('flash') && session['flash'].nil?
session.delete('flash')
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/params_parser.rb b/actionpack/lib/action_dispatch/middleware/params_parser.rb
index 2c98ca03a8..b426183488 100644
--- a/actionpack/lib/action_dispatch/middleware/params_parser.rb
+++ b/actionpack/lib/action_dispatch/middleware/params_parser.rb
@@ -13,10 +13,7 @@ module ActionDispatch
end
end
- DEFAULT_PARSERS = {
- Mime::XML => :xml_simple,
- Mime::JSON => :json
- }
+ DEFAULT_PARSERS = { Mime::JSON => :json }
def initialize(app, parsers = {})
@app, @parsers = app, DEFAULT_PARSERS.merge(parsers)
@@ -36,45 +33,26 @@ module ActionDispatch
return false if request.content_length.zero?
- mime_type = content_type_from_legacy_post_data_format_header(env) ||
- request.content_mime_type
-
- strategy = @parsers[mime_type]
+ strategy = @parsers[request.content_mime_type]
return false unless strategy
case strategy
when Proc
strategy.call(request.raw_post)
- when :xml_simple, :xml_node
- data = Hash.from_xml(request.raw_post) || {}
- data.with_indifferent_access
- when :yaml
- YAML.load(request.raw_post)
when :json
data = ActiveSupport::JSON.decode(request.raw_post)
data = {:_json => data} unless data.is_a?(Hash)
- data.with_indifferent_access
+ Request::Utils.deep_munge(data).with_indifferent_access
else
false
end
- rescue Exception => e # YAML, XML or Ruby code block errors
+ rescue Exception => e # JSON or Ruby code block errors
logger(env).debug "Error occurred while parsing request parameters.\nContents:\n\n#{request.raw_post}"
raise ParseError.new(e.message, e)
end
- def content_type_from_legacy_post_data_format_header(env)
- if x_post_format = env['HTTP_X_POST_DATA_FORMAT']
- case x_post_format.to_s.downcase
- when 'yaml' then return Mime::YAML
- when 'xml' then return Mime::XML
- end
- end
-
- nil
- end
-
def logger(env)
env['action_dispatch.logger'] || ActiveSupport::Logger.new($stderr)
end
diff --git a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
index 53bedaa40a..cbb2d475b1 100644
--- a/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/public_exceptions.rb
@@ -7,11 +7,10 @@ module ActionDispatch
end
def call(env)
- exception = env["action_dispatch.exception"]
status = env["PATH_INFO"][1..-1]
request = ActionDispatch::Request.new(env)
content_type = request.formats.first
- body = { :status => status, :error => exception.message }
+ body = { :status => status, :error => Rack::Utils::HTTP_STATUS_CODES.fetch(status.to_i, Rack::Utils::HTTP_STATUS_CODES[500]) }
render(status, content_type, body)
end
@@ -19,7 +18,7 @@ module ActionDispatch
private
def render(status, content_type, body)
- format = content_type && "to_#{content_type.to_sym}"
+ format = "to_#{content_type.to_sym}" if content_type
if format && body.respond_to?(format)
render_format(status, content_type, body.public_send(format))
else
diff --git a/actionpack/lib/action_dispatch/middleware/remote_ip.rb b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
index ec15a2a715..57bc6d5cd0 100644
--- a/actionpack/lib/action_dispatch/middleware/remote_ip.rb
+++ b/actionpack/lib/action_dispatch/middleware/remote_ip.rb
@@ -1,23 +1,59 @@
module ActionDispatch
+ # This middleware calculates the IP address of the remote client that is
+ # making the request. It does this by checking various headers that could
+ # contain the address, and then picking the last-set address that is not
+ # on the list of trusted IPs. This follows the precedent set by e.g.
+ # {the Tomcat server}[https://issues.apache.org/bugzilla/show_bug.cgi?id=50453],
+ # with {reasoning explained at length}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection]
+ # by @gingerlime. A more detailed explanation of the algorithm is given
+ # at GetIp#calculate_ip.
+ #
+ # 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)
+ # 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.
+ # This middleware assumes that there is at least one proxy sitting around
+ # and setting headers with the client's remote IP address. If you don't use
+ # a proxy, because you are hosted on e.g. Heroku without SSL, any client can
+ # claim to have any IP address by setting the X-Forwarded-For header. If you
+ # care about that, then you need to explicitly drop or ignore those headers
+ # sometime before this middleware runs.
class RemoteIp
- class IpSpoofAttackError < StandardError ; end
+ class IpSpoofAttackError < StandardError; end
- # IP addresses that are "trusted proxies" that can be stripped from
- # the comma-delimited list in the X-Forwarded-For header. See also:
- # http://en.wikipedia.org/wiki/Private_network#Private_IPv4_address_spaces
- # http://en.wikipedia.org/wiki/Private_network#Private_IPv6_addresses.
+ # The default trusted IPs list simply includes IP addresses that are
+ # guaranteed by the IP specification to be private addresses. Those will
+ # not be the ultimate client IP in production, and so are discarded. See
+ # http://en.wikipedia.org/wiki/Private_network for details.
TRUSTED_PROXIES = %r{
- ^127\.0\.0\.1$ | # localhost
- ^::1$ |
- ^(10 | # private IP 10.x.x.x
- 172\.(1[6-9]|2[0-9]|3[0-1]) | # private IP in the range 172.16.0.0 .. 172.31.255.255
- 192\.168 | # private IP 192.168.x.x
- fc00:: # private IP fc00
- )\.
+ ^127\.0\.0\.1$ | # localhost IPv4
+ ^::1$ | # localhost IPv6
+ ^fc00: | # private IPv6 range fc00
+ ^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
}x
attr_reader :check_ip, :proxies
+ # Create a new +RemoteIp+ middleware instance.
+ #
+ # The +check_ip_spoofing+ option is on by default. When on, an exception
+ # is raised if it looks like the client is trying to lie about its own IP
+ # address. It makes sense to turn off this check on sites aimed at non-IP
+ # 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
+ # 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
+ # IP addresses, and return the one that you want.
def initialize(app, check_ip_spoofing = true, custom_proxies = nil)
@app = app
@check_ip = check_ip_spoofing
@@ -31,15 +67,23 @@ module ActionDispatch
end
end
+ # Since the IP address may not be needed, we store the object here
+ # without calculating the IP to keep from slowing down the majority of
+ # requests. For those requests that do need to know the IP, the
+ # GetIp#calculate_ip method will calculate the memoized client IP address.
def call(env)
env["action_dispatch.remote_ip"] = GetIp.new(env, self)
@app.call(env)
end
+ # The GetIp class exists as a way to defer processing of the request data
+ # into an actual IP address. If the ActionDispatch::Request#remote_ip method
+ # is called, this class will calculate the value and then memoize it.
class GetIp
- # IP v4 and v6 (with compression) validation regexp
- # https://gist.github.com/1289635
+ # This constant contains a regular expression that validates every known
+ # form of IP v4 and v6 address, with or without abbreviations, adapted
+ # from {this gist}[https://gist.github.com/gazay/1289635].
VALID_IP = %r{
(^(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[0-9]{1,2})){3}$) | # ip v4
(^(
@@ -57,70 +101,84 @@ module ActionDispatch
(([0-9A-Fa-f]{1,4}:){0,4}:([0-9A-Fa-f]{1,4}:){1}((\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
(::([0-9A-Fa-f]{1,4}:){0,5}((\b((25[0-5])|(1\d{2})|(2[0-4]\d) |(\d{1,2}))\b)\.){3}(\b((25[0-5])|(1\d{2})|(2[0-4]\d)|(\d{1,2}))\b)) | # ip v6 with compatible to v4
([0-9A-Fa-f]{1,4}::([0-9A-Fa-f]{1,4}:){0,5}[0-9A-Fa-f]{1,4}) | # ip v6 with compatible to v4
- (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the begining
+ (::([0-9A-Fa-f]{1,4}:){0,6}[0-9A-Fa-f]{1,4}) | # ip v6 with double colon at the beginning
(([0-9A-Fa-f]{1,4}:){1,7}:) # ip v6 without ending
)$)
}x
def initialize(env, middleware)
- @env = env
- @middleware = middleware
- @calculated_ip = false
+ @env = env
+ @check_ip = middleware.check_ip
+ @proxies = middleware.proxies
end
- # Determines originating IP address. REMOTE_ADDR is the standard
- # but will be wrong if the user is behind a proxy. Proxies will set
- # HTTP_CLIENT_IP and/or HTTP_X_FORWARDED_FOR, so we prioritize those.
- # HTTP_X_FORWARDED_FOR may be a comma-delimited list in the case of
- # multiple chained proxies. The first address which is in this list
- # if it's not a known proxy will be the originating IP.
- # Format of HTTP_X_FORWARDED_FOR:
- # client_ip, proxy_ip1, proxy_ip2...
- # http://en.wikipedia.org/wiki/X-Forwarded-For
+ # Sort through the various IP address headers, looking for the IP most
+ # likely to be the address of the actual remote client making this
+ # request.
+ #
+ # 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
+ # 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.
+ #
+ # As discussed in {this post about Rails IP Spoofing}[http://blog.gingerlime.com/2012/rails-ip-spoofing-vulnerabilities-and-protection/],
+ # while the first IP in the list is likely to be the "originating" IP,
+ # it could also have been set by the client maliciously.
+ #
+ # In order to find the first address that is (probably) accurate, we
+ # take the list of IPs, remove known and trusted proxies, and then take
+ # the last address left, which was presumably set by one of those proxies.
def calculate_ip
- client_ip = @env['HTTP_CLIENT_IP']
- forwarded_ip = ips_from('HTTP_X_FORWARDED_FOR').first
- remote_addrs = ips_from('REMOTE_ADDR')
-
- check_ip = client_ip && @middleware.check_ip
- if check_ip && forwarded_ip != client_ip
+ # Set by the Rack web server, this is a single value.
+ remote_addr = ips_from('REMOTE_ADDR').last
+
+ # Could be a CSV list and/or repeated headers that were concatenated.
+ client_ips = ips_from('HTTP_CLIENT_IP').reverse
+ forwarded_ips = ips_from('HTTP_X_FORWARDED_FOR').reverse
+
+ # +Client-Ip+ and +X-Forwarded-For+ should not, generally, both be set.
+ # If they are both set, it means that this request passed through two
+ # proxies with incompatible IP header conventions, and there is no way
+ # for us to determine which header is the right one after the fact.
+ # Since we have no idea, we give up and explode.
+ should_check_ip = @check_ip && client_ips.last && forwarded_ips.last
+ if should_check_ip && !forwarded_ips.include?(client_ips.last)
# We don't know which came from the proxy, and which from the user
- raise IpSpoofAttackError, "IP spoofing attack?!" \
- "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect}" \
+ raise IpSpoofAttackError, "IP spoofing attack?! " +
+ "HTTP_CLIENT_IP=#{@env['HTTP_CLIENT_IP'].inspect} " +
"HTTP_X_FORWARDED_FOR=#{@env['HTTP_X_FORWARDED_FOR'].inspect}"
end
- client_ips = remove_proxies [client_ip, forwarded_ip, remote_addrs].flatten
- if client_ips.present?
- client_ips.first
- else
- # If there is no client ip we can return first valid proxy ip from REMOTE_ADDR
- remote_addrs.find { |ip| valid_ip? ip }
- end
+ # We assume these things about the IP headers:
+ #
+ # - X-Forwarded-For will be a list of IPs, one per proxy, or blank
+ # - Client-Ip is propagated from the outermost proxy, or is blank
+ # - REMOTE_ADDR will be the IP that made the request to Rack
+ ips = [forwarded_ips, client_ips, remote_addr].flatten.compact
+
+ # If every single IP option is in the trusted list, just return REMOTE_ADDR
+ filter_proxies(ips).first || remote_addr
end
+ # Memoizes the value returned by #calculate_ip and returns it for
+ # ActionDispatch::Request to use.
def to_s
- return @ip if @calculated_ip
- @calculated_ip = true
- @ip = calculate_ip
+ @ip ||= calculate_ip
end
- private
+ protected
def ips_from(header)
- @env[header] ? @env[header].strip.split(/[,\s]+/) : []
- end
-
- def valid_ip?(ip)
- ip =~ VALID_IP
- end
-
- def not_a_proxy?(ip)
- ip !~ @middleware.proxies
+ # Split the comma-separated list into an array of strings
+ ips = @env[header] ? @env[header].strip.split(/[,\s]+/) : []
+ # Only return IPs that are valid according to the regex
+ ips.select{ |ip| ip =~ VALID_IP }
end
- def remove_proxies(ips)
- ips.select { |ip| valid_ip?(ip) && not_a_proxy?(ip) }
+ def filter_proxies(ips)
+ ips.reject { |ip| ip =~ @proxies }
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/request_id.rb b/actionpack/lib/action_dispatch/middleware/request_id.rb
index 44290445d4..5d1740d0d4 100644
--- a/actionpack/lib/action_dispatch/middleware/request_id.rb
+++ b/actionpack/lib/action_dispatch/middleware/request_id.rb
@@ -18,7 +18,7 @@ module ActionDispatch
def call(env)
env["action_dispatch.request_id"] = external_request_id(env) || internal_request_id
- @app.call(env).tap { |status, headers, body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
+ @app.call(env).tap { |_status, headers, _body| headers["X-Request-Id"] = env["action_dispatch.request_id"] }
end
private
diff --git a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
index 7c12590c49..84df55fd5a 100644
--- a/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/abstract_store.rb
@@ -26,7 +26,7 @@ module ActionDispatch
def generate_sid
sid = SecureRandom.hex(16)
- sid.encode!('UTF-8')
+ sid.encode!(Encoding::UTF_8)
sid
end
diff --git a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
index 019849ef95..b9eb8036e9 100644
--- a/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
+++ b/actionpack/lib/action_dispatch/middleware/session/cookie_store.rb
@@ -4,61 +4,89 @@ require 'rack/session/cookie'
module ActionDispatch
module Session
- # This cookie-based session store is the Rails default. Sessions typically
- # contain at most a user_id and flash message; both fit within the 4K cookie
- # size limit. Cookie-based sessions are dramatically faster than the
- # alternatives.
+ # This cookie-based session store is the Rails default. It is
+ # dramatically faster than the alternatives.
#
- # If you have more than 4K of session data or don't want your data to be
- # visible to the user, pick another session store.
+ # Sessions typically contain at most a user_id and flash message; both fit
+ # within the 4K cookie size limit. A CookieOverflow exception is raised if
+ # you attempt to store more than 4K of data.
#
- # CookieOverflow is raised if you attempt to store more than 4K of data.
+ # The cookie jar used for storage is automatically configured to be the
+ # best possible option given your application's configuration.
#
- # A message digest is included with the cookie to ensure data integrity:
- # a user cannot alter his +user_id+ without knowing the secret key
- # included in the hash. New apps are generated with a pregenerated secret
- # in config/environment.rb. Set your own for old apps you're upgrading.
+ # If you only have secret_token set, your cookies will be signed, but
+ # not encrypted. This means a user cannot alter his +user_id+ without
+ # knowing your app's secret key, but can easily read his +user_id+. This
+ # was the default for Rails 3 apps.
#
- # Session options:
+ # If you have secret_key_base set, your cookies will be encrypted. This
+ # goes a step further than signed cookies in that encrypted cookies cannot
+ # be altered or read by users. This is the default starting in Rails 4.
#
- # * <tt>:secret</tt>: An application-wide key string or block returning a
- # string called per generated digest. The block is called with the
- # CGI::Session instance as an argument. It's important that the secret
- # is not vulnerable to a dictionary attack. Therefore, you should choose
- # a secret consisting of random numbers and letters and more than 30
- # characters.
+ # If you have both secret_token and secret_key base set, your cookies will
+ # be encrypted, and signed cookies generated by Rails 3 will be
+ # transparently read and encrypted to provide a smooth upgrade path.
#
- # :secret => '449fe2e7daee471bffae2fd8dc02313d'
- # :secret => Proc.new { User.current_user.secret_key }
+ # Configure your session store in config/initializers/session_store.rb:
#
- # * <tt>:digest</tt>: The message digest algorithm used to verify session
- # integrity defaults to 'SHA1' but may be any digest provided by OpenSSL,
- # such as 'MD5', 'RIPEMD160', 'SHA256', etc.
+ # Myapp::Application.config.session_store :cookie_store, key: '_your_app_session'
#
- # To generate a secret key for an existing application, run
- # "rake secret" and set the key in config/initializers/secret_token.rb.
+ # Configure your secret key in config/initializers/secret_token.rb:
+ #
+ # Myapp::Application.config.secret_key_base 'secret key'
+ #
+ # To generate a secret key for an existing application, run `rake secret`.
+ #
+ # If you are upgrading an existing Rails 3 app, you should leave your
+ # existing secret_token in place and simply add the new secret_key_base.
+ # Note that you should wait to set secret_key_base until you have 100% of
+ # your userbase on Rails 4 and are reasonably sure you will not need to
+ # rollback to Rails 3. This is because cookies signed based on the new
+ # secret_key_base in Rails 4 are not backwards compatible with Rails 3.
+ # You are free to leave your existing secret_token in place, not set the
+ # new secret_key_base, and ignore the deprecation warnings until you are
+ # 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.
#
# Note that changing digest or secret invalidates all existing sessions!
- class CookieStore < Rack::Session::Cookie
+ class CookieStore < Rack::Session::Abstract::ID
include Compatibility
include StaleSessionCheck
include SessionObject
- # Override rack's method
+ def initialize(app, options={})
+ super(app, options.merge!(:cookie_only => true))
+ end
+
def destroy_session(env, session_id, options)
- new_sid = super
+ new_sid = generate_sid unless options[:drop]
# Reset hash and Assign the new session id
env["action_dispatch.request.unsigned_session_cookie"] = new_sid ? { "session_id" => new_sid } : {}
new_sid
end
+ def load_session(env)
+ stale_session_check! do
+ data = unpacked_cookie_data(env)
+ data = persistent_session_id!(data)
+ [data["session_id"], data]
+ end
+ end
+
private
+ def extract_session_id(env)
+ stale_session_check! do
+ unpacked_cookie_data(env)["session_id"]
+ end
+ end
+
def unpacked_cookie_data(env)
env["action_dispatch.request.unsigned_session_cookie"] ||= begin
stale_session_check! do
- request = ActionDispatch::Request.new(env)
- if data = request.cookie_jar.signed[@key]
+ if data = get_cookie(env)
data.stringify_keys!
end
data || {}
@@ -66,14 +94,28 @@ module ActionDispatch
end
end
+ def persistent_session_id!(data, sid=nil)
+ data ||= {}
+ data["session_id"] ||= sid || generate_sid
+ data
+ end
+
def set_session(env, sid, session_data, options)
session_data["session_id"] = sid
session_data
end
def set_cookie(env, session_id, cookie)
+ cookie_jar(env)[@key] = cookie
+ end
+
+ def get_cookie(env)
+ cookie_jar(env)[@key]
+ end
+
+ def cookie_jar(env)
request = ActionDispatch::Request.new(env)
- request.cookie_jar.signed[@key] = cookie
+ request.cookie_jar.signed_or_encrypted
end
end
end
diff --git a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
index 0de10695e0..1d4f0f89a6 100644
--- a/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
+++ b/actionpack/lib/action_dispatch/middleware/show_exceptions.rb
@@ -16,9 +16,9 @@ module ActionDispatch
# catches the exceptions and returns a FAILSAFE_RESPONSE.
class ShowExceptions
FAILSAFE_RESPONSE = [500, { 'Content-Type' => 'text/plain' },
- ["500 Internal Server Error\n" <<
- "If you are the administrator of this website, then please read this web " <<
- "application's log file and/or the web server's log file to find out what " <<
+ ["500 Internal Server Error\n" \
+ "If you are the administrator of this website, then please read this web " \
+ "application's log file and/or the web server's log file to find out what " \
"went wrong."]]
def initialize(app, exceptions_app)
@@ -27,13 +27,13 @@ module ActionDispatch
end
def call(env)
- begin
- response = @app.call(env)
- rescue Exception => exception
- raise exception if env['action_dispatch.show_exceptions'] == false
+ @app.call(env)
+ rescue Exception => exception
+ if env['action_dispatch.show_exceptions'] == false
+ raise exception
+ else
+ render_exception(env, exception)
end
-
- response || render_exception(env, exception)
end
private
diff --git a/actionpack/lib/action_dispatch/middleware/ssl.rb b/actionpack/lib/action_dispatch/middleware/ssl.rb
index 9098f4e170..999c022535 100644
--- a/actionpack/lib/action_dispatch/middleware/ssl.rb
+++ b/actionpack/lib/action_dispatch/middleware/ssl.rb
@@ -36,8 +36,7 @@ module ActionDispatch
url.scheme = "https"
url.host = @host if @host
url.port = @port if @port
- headers = hsts_headers.merge('Content-Type' => 'text/html',
- 'Location' => url.to_s)
+ headers = { 'Content-Type' => 'text/html', 'Location' => url.to_s }
[301, headers, []]
end
@@ -45,7 +44,7 @@ module ActionDispatch
# http://tools.ietf.org/html/draft-hodges-strict-transport-sec-02
def hsts_headers
if @hsts
- value = "max-age=#{@hsts[:expires]}"
+ value = "max-age=#{@hsts[:expires].to_i}"
value += "; includeSubDomains" if @hsts[:subdomains]
{ 'Strict-Transport-Security' => value }
else
@@ -58,7 +57,7 @@ module ActionDispatch
cookies = cookies.split("\n")
headers['Set-Cookie'] = cookies.map { |cookie|
- if cookie !~ /;\s+secure(;|$)/
+ if cookie !~ /;\s*secure\s*(;|$)/i
"#{cookie}; secure"
else
cookie
diff --git a/actionpack/lib/action_dispatch/middleware/static.rb b/actionpack/lib/action_dispatch/middleware/static.rb
index e3b15b43b9..c6a7d9c415 100644
--- a/actionpack/lib/action_dispatch/middleware/static.rb
+++ b/actionpack/lib/action_dispatch/middleware/static.rb
@@ -6,7 +6,8 @@ module ActionDispatch
def initialize(root, cache_control)
@root = root.chomp('/')
@compiled_root = /^#{Regexp.escape(root)}/
- @file_server = ::Rack::File.new(@root, cache_control)
+ headers = cache_control && { 'Cache-Control' => cache_control }
+ @file_server = ::Rack::File.new(@root, headers)
end
def match?(path)
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
deleted file mode 100644
index 823f5d25b6..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.erb
+++ /dev/null
@@ -1,31 +0,0 @@
-<% unless @exception.blamed_files.blank? %>
- <% if (hide = @exception.blamed_files.length > 8) %>
- <a href="#" onclick="document.getElementById('blame_trace').style.display='block'; return false;">Show blamed files</a>
- <% end %>
- <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%=h @exception.describe_blame %></code></pre>
-<% end %>
-
-<%
- clean_params = @request.filtered_parameters.clone
- clean_params.delete("action")
- clean_params.delete("controller")
-
- request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
-
- def debug_hash(object)
- object.to_hash.sort_by { |k, v| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
- end unless self.class.method_defined?(:debug_hash)
-%>
-
-<h2 style="margin-top: 30px">Request</h2>
-<p><b>Parameters</b>: <pre><%=h request_dump %></pre></p>
-
-<p><a href="#" onclick="document.getElementById('session_dump').style.display='block'; return false;">Show session dump</a></p>
-<div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
-
-<p><a href="#" onclick="document.getElementById('env_dump').style.display='block'; return false;">Show env dump</a></p>
-<div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
-
-
-<h2 style="margin-top: 30px">Response</h2>
-<p><b>Headers</b>: <pre><%=h defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
new file mode 100644
index 0000000000..db219c8fa9
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.html.erb
@@ -0,0 +1,34 @@
+<% unless @exception.blamed_files.blank? %>
+ <% if (hide = @exception.blamed_files.length > 8) %>
+ <a href="#" onclick="return toggleTrace()">Toggle blamed files</a>
+ <% end %>
+ <pre id="blame_trace" <%='style="display:none"' if hide %>><code><%= @exception.describe_blame %></code></pre>
+<% end %>
+
+<%
+ clean_params = @request.filtered_parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end unless self.class.method_defined?(:debug_hash)
+%>
+
+<h2 style="margin-top: 30px">Request</h2>
+<p><b>Parameters</b>:</p> <pre><%= request_dump %></pre>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleSessionDump()">Toggle session dump</a></div>
+ <div id="session_dump" style="display:none"><pre><%= debug_hash @request.session %></pre></div>
+</div>
+
+<div class="details">
+ <div class="summary"><a href="#" onclick="return toggleEnvDump()">Toggle env dump</a></div>
+ <div id="env_dump" style="display:none"><pre><%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %></pre></div>
+</div>
+
+<h2 style="margin-top: 30px">Response</h2>
+<p><b>Headers</b>:</p> <pre><%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %></pre>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
new file mode 100644
index 0000000000..396768ecee
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_request_and_response.text.erb
@@ -0,0 +1,23 @@
+<%
+ clean_params = @request.filtered_parameters.clone
+ clean_params.delete("action")
+ clean_params.delete("controller")
+
+ request_dump = clean_params.empty? ? 'None' : clean_params.inspect.gsub(',', ",\n")
+
+ def debug_hash(object)
+ object.to_hash.sort_by { |k, _| k.to_s }.map { |k, v| "#{k}: #{v.inspect rescue $!.message}" }.join("\n")
+ end unless self.class.method_defined?(:debug_hash)
+%>
+
+Request parameters
+<%= request_dump %>
+
+Session dump
+<%= debug_hash @request.session %>
+
+Env dump
+<%= debug_hash @request.env.slice(*@request.class::ENV_METHODS) %>
+
+Response headers
+<%= defined?(@response) ? @response.headers.inspect.gsub(',', ",\n") : 'None' %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
new file mode 100644
index 0000000000..38429cb78e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_source.erb
@@ -0,0 +1,25 @@
+<% 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| %>
+<span><%= line_number -%></span>
+ <% end %>
+ </pre>
+ </td>
+<td width="100%">
+<pre>
+<% @source_extract.each do |line, source| -%><div class="line<%= " active" if line == @line_number -%>"><%= source -%></div><% end -%>
+</pre>
+</td>
+ </tr>
+ </table>
+</div>
+</div>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
deleted file mode 100644
index 8771b5fd6d..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.erb
+++ /dev/null
@@ -1,26 +0,0 @@
-<%
- traces = [
- ["Application Trace", @application_trace],
- ["Framework Trace", @framework_trace],
- ["Full Trace", @full_trace]
- ]
- names = traces.collect {|name, trace| name}
-%>
-
-<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
-
-<div id="traces">
- <% names.each do |name| %>
- <%
- show = "document.getElementById('#{name.gsub(/\s/, '-')}').style.display='block';"
- hide = (names - [name]).collect {|hide_name| "document.getElementById('#{hide_name.gsub(/\s/, '-')}').style.display='none';"}
- %>
- <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
- <% end %>
-
- <% traces.each do |name, trace| %>
- <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;">
- <pre><code><%=h trace.join "\n" %></code></pre>
- </div>
- <% end %>
-</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
new file mode 100644
index 0000000000..b181909bff
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.html.erb
@@ -0,0 +1,24 @@
+<%
+ traces = { "Application Trace" => @application_trace,
+ "Framework Trace" => @framework_trace,
+ "Full Trace" => @full_trace }
+ names = traces.keys
+%>
+
+<p><code>Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %></code></p>
+
+<div id="traces">
+ <% names.each do |name| %>
+ <%
+ show = "show('#{name.gsub(/\s/, '-')}');"
+ hide = (names - [name]).collect {|hide_name| "hide('#{hide_name.gsub(/\s/, '-')}');"}
+ %>
+ <a href="#" onclick="<%= hide.join %><%= show %>; return false;"><%= name %></a> <%= '|' unless names.last == name %>
+ <% end %>
+
+ <% traces.each do |name, trace| %>
+ <div id="<%= name.gsub(/\s/, '-') %>" style="display: <%= (name == "Application Trace") ? 'block' : 'none' %>;">
+ <pre><code><%= trace.join "\n" %></code></pre>
+ </div>
+ <% end %>
+</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
new file mode 100644
index 0000000000..d4af5c9b06
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/_trace.text.erb
@@ -0,0 +1,15 @@
+<%
+ traces = { "Application Trace" => @application_trace,
+ "Framework Trace" => @framework_trace,
+ "Full Trace" => @full_trace }
+%>
+
+Rails.root: <%= defined?(Rails) && Rails.respond_to?(:root) ? Rails.root : "unset" %>
+
+<% traces.each do |name, trace| %>
+<% if trace.any? %>
+<%= name %>
+<%= trace.join("\n") %>
+
+<% end %>
+<% end %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
index 4b9d3141d5..f154021ae6 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/diagnostics.erb
@@ -1,10 +1,16 @@
-<h1>
- <%=h @exception.class.to_s %>
- <% if @request.parameters['controller'] %>
- in <%=h @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%=h @request.parameters['action'] %><% end %>
- <% end %>
-</h1>
-<pre><%=h @exception.message %></pre>
+<header>
+ <h1>
+ <%= @exception.class.to_s %>
+ <% if @request.parameters['controller'] %>
+ in <%= @request.parameters['controller'].camelize %>Controller<% if @request.parameters['action'] %>#<%= @request.parameters['action'] %><% end %>
+ <% end %>
+ </h1>
+</header>
-<%= render :template => "rescues/_trace" %>
-<%= render :template => "rescues/_request_and_response" %>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+
+ <%= render template: "rescues/_source" %>
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
index 1a308707d1..bc5d03dc10 100644
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/layout.erb
@@ -4,7 +4,11 @@
<meta charset="utf-8" />
<title>Action Controller: Exception caught</title>
<style>
- body { background-color: #fff; color: #333; }
+ body {
+ background-color: #FAFAFA;
+ color: #333;
+ margin: 0px;
+ }
body, p, ol, ul, td {
font-family: helvetica, verdana, arial, sans-serif;
@@ -13,16 +17,134 @@
}
pre {
- background-color: #eee;
- padding: 10px;
font-size: 11px;
white-space: pre-wrap;
}
- a { color: #000; }
+ pre.box {
+ border: 1px solid #EEE;
+ padding: 10px;
+ margin: 0px;
+ width: 958px;
+ }
+
+ header {
+ color: #F0F0F0;
+ background: #C52F24;
+ padding: 0.5em 1.5em;
+ }
+
+ h1 {
+ margin: 0.2em 0;
+ line-height: 1.1em;
+ font-size: 2em;
+ }
+
+ h2 {
+ color: #C52F24;
+ line-height: 25px;
+ }
+
+ .details {
+ border: 1px solid #D0D0D0;
+ border-radius: 4px;
+ margin: 1em 0px;
+ display: block;
+ width: 978px;
+ }
+
+ .summary {
+ padding: 8px 15px;
+ border-bottom: 1px solid #D0D0D0;
+ display: block;
+ }
+
+ .details pre {
+ margin: 5px;
+ border: none;
+ }
+
+ #container {
+ box-sizing: border-box;
+ width: 100%;
+ padding: 0 1.5em;
+ }
+
+ .source * {
+ margin: 0px;
+ padding: 0px;
+ }
+
+ .source {
+ border: 1px solid #D9D9D9;
+ background: #ECECEC;
+ width: 978px;
+ }
+
+ .source pre {
+ padding: 10px 0px;
+ border: none;
+ }
+
+ .source .data {
+ font-size: 80%;
+ overflow: auto;
+ background-color: #FFF;
+ }
+
+ .info {
+ padding: 0.5em;
+ }
+
+ .source .data .line_numbers {
+ background-color: #ECECEC;
+ color: #AAA;
+ padding: 1em .5em;
+ border-right: 1px solid #DDD;
+ text-align: right;
+ }
+
+ .line {
+ padding-left: 10px;
+ }
+
+ .line:hover {
+ background-color: #F6F6F6;
+ }
+
+ .line.active {
+ background-color: #FFCCCC;
+ }
+
+ a { color: #980905; }
a:visited { color: #666; }
- a:hover { color: #fff; background-color:#000; }
+ a:hover { color: #C52F24; }
+
+ <%= yield :style %>
</style>
+
+ <script>
+ var toggle = function(id) {
+ var s = document.getElementById(id).style;
+ s.display = s.display == 'none' ? 'block' : 'none';
+ return false;
+ }
+ var show = function(id) {
+ document.getElementById(id).style.display = 'block';
+ }
+ var hide = function(id) {
+ document.getElementById(id).style.display = 'none';
+ }
+ var toggleTrace = function() {
+ return toggle('blame_trace');
+ }
+ var toggleSessionDump = function() {
+ return toggle('session_dump');
+ }
+ var toggleEnvDump = function() {
+ return toggle('env_dump');
+ }
+ </script>
</head>
<body>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb
deleted file mode 100644
index dbfdf76947..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1>Template is missing</h1>
-<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
new file mode 100644
index 0000000000..5c016e544e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.html.erb
@@ -0,0 +1,7 @@
+<header>
+ <h1>Template is missing</h1>
+</header>
+
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
new file mode 100644
index 0000000000..ae62d9eb02
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/missing_template.text.erb
@@ -0,0 +1,3 @@
+Template is missing
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
deleted file mode 100644
index 8c594c1523..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.erb
+++ /dev/null
@@ -1,23 +0,0 @@
-<h1>Routing Error</h1>
-<p><pre><%=h @exception.message %></pre></p>
-<% unless @exception.failures.empty? %>
- <p>
- <h2>Failure reasons:</h2>
- <ol>
- <% @exception.failures.each do |route, reason| %>
- <li><code><%=h route.inspect.gsub('\\', '') %></code> failed because <%=h reason.downcase %></li>
- <% end %>
- </ol>
- </p>
-<% end %>
-<%= render :template => "rescues/_trace" %>
-
-<h2>
- Routes
-</h2>
-
-<p>
- Routes match in priority from top to bottom
-</p>
-
-<p><pre><%= @routes %></pre></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
new file mode 100644
index 0000000000..7e9cedb95e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.html.erb
@@ -0,0 +1,30 @@
+<header>
+ <h1>Routing Error</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+ <% unless @exception.failures.empty? %>
+ <p>
+ <h2>Failure reasons:</h2>
+ <ol>
+ <% @exception.failures.each do |route, reason| %>
+ <li><code><%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %></li>
+ <% end %>
+ </ol>
+ </p>
+ <% end %>
+
+ <%= render template: "rescues/_trace" %>
+
+ <% if @routes_inspector %>
+ <h2>
+ Routes
+ </h2>
+
+ <p>
+ Routes match in priority from top to bottom
+ </p>
+
+ <%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
+ <% end %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
new file mode 100644
index 0000000000..f6e4dac1f3
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/routing_error.text.erb
@@ -0,0 +1,11 @@
+Routing Error
+
+<%= @exception.message %>
+<% unless @exception.failures.empty? %>
+Failure reasons:
+<% @exception.failures.each do |route, reason| %>
+ - <%= route.inspect.delete('\\') %></code> failed because <%= reason.downcase %>
+<% end %>
+<% end %>
+
+<%= render template: "rescues/_trace", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
deleted file mode 100644
index c658559be9..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.erb
+++ /dev/null
@@ -1,17 +0,0 @@
-<h1>
- <%=h @exception.original_exception.class.to_s %> in
- <%=h @request.parameters["controller"].capitalize if @request.parameters["controller"]%>#<%=h @request.parameters["action"] %>
-</h1>
-
-<p>
- Showing <i><%=h @exception.file_name %></i> where line <b>#<%=h @exception.line_number %></b> raised:
- <pre><code><%=h @exception.message %></code></pre>
-</p>
-
-<p>Extracted source (around line <b>#<%=h @exception.line_number %></b>):
-<pre><code><%=h @exception.source_extract %></code></pre></p>
-
-<p><%=h @exception.sub_template_message %></p>
-
-<%= render :template => "rescues/_trace" %>
-<%= render :template => "rescues/_request_and_response" %>
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
new file mode 100644
index 0000000000..027a0f5b3e
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.html.erb
@@ -0,0 +1,43 @@
+<% @source_extract = @exception.source_extract(0, :html) %>
+<header>
+ <h1>
+ <%= @exception.original_exception.class.to_s %> in
+ <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+ </h1>
+</header>
+
+<div id="container">
+ <p>
+ Showing <i><%= @exception.file_name %></i> where line <b>#<%= @exception.line_number %></b> raised:
+ </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>
+
+ <p><%= @exception.sub_template_message %></p>
+
+ <%= render template: "rescues/_trace" %>
+ <%= render template: "rescues/_request_and_response" %>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
new file mode 100644
index 0000000000..5da21d9784
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/template_error.text.erb
@@ -0,0 +1,8 @@
+<% @source_extract = @exception.source_extract(0, :html) %>
+<%= @exception.original_exception.class.to_s %> in <%= @request.parameters["controller"].camelize if @request.parameters["controller"] %>#<%= @request.parameters["action"] %>
+
+Showing <%= @exception.file_name %> where line #<%= @exception.line_number %> raised:
+<%= @exception.message %>
+<%= @exception.sub_template_message %>
+<%= render template: "rescues/_trace", format: :text %>
+<%= render template: "rescues/_request_and_response", format: :text %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb
deleted file mode 100644
index 683379da10..0000000000
--- a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.erb
+++ /dev/null
@@ -1,2 +0,0 @@
-<h1>Unknown action</h1>
-<p><%=h @exception.message %></p>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
new file mode 100644
index 0000000000..259fb2bb3b
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.html.erb
@@ -0,0 +1,6 @@
+<header>
+ <h1>Unknown action</h1>
+</header>
+<div id="container">
+ <h2><%= h @exception.message %></h2>
+</div>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
new file mode 100644
index 0000000000..83973addcb
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/rescues/unknown_action.text.erb
@@ -0,0 +1,3 @@
+Unknown action
+
+<%= @exception.message %>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
new file mode 100644
index 0000000000..24e44f31ac
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_route.html.erb
@@ -0,0 +1,16 @@
+<tr class='route_row' data-helper='path'>
+ <td data-route-name='<%= route[:name] %>'>
+ <% if route[:name].present? %>
+ <%= route[:name] %><span class='helper'>_path</span>
+ <% end %>
+ </td>
+ <td data-route-verb='<%= route[:verb] %>'>
+ <%= route[:verb] %>
+ </td>
+ <td data-route-path='<%= route[:path] %>' data-regexp='<%= route[:regexp] %>'>
+ <%= route[:path] %>
+ </td>
+ <td data-route-reqs='<%= route[:reqs] %>'>
+ <%= route[:reqs] %>
+ </td>
+</tr>
diff --git a/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
new file mode 100644
index 0000000000..95461fa693
--- /dev/null
+++ b/actionpack/lib/action_dispatch/middleware/templates/routes/_table.html.erb
@@ -0,0 +1,144 @@
+<% content_for :style do %>
+ #route_table {
+ margin: 0 auto 0;
+ border-collapse: collapse;
+ }
+
+ #route_table td {
+ padding: 0 30px;
+ }
+
+ #route_table tr.bottom th {
+ padding-bottom: 10px;
+ line-height: 15px;
+ }
+
+ #route_table .matched_paths {
+ background-color: LightGoldenRodYellow;
+ }
+
+ #route_table .matched_paths {
+ border-bottom: solid 3px SlateGrey;
+ }
+
+ #path_search {
+ width: 80%;
+ font-size: inherit;
+ }
+<% end %>
+
+<table id='route_table' class='route_table'>
+ <thead>
+ <tr>
+ <th>Helper</th>
+ <th>HTTP Verb</th>
+ <th>Path</th>
+ <th>Controller#Action</th>
+ </tr>
+ <tr class='bottom'>
+ <th><%# Helper %>
+ <%= link_to "Path", "#", 'data-route-helper' => '_path',
+ title: "Returns a relative path (without the http or domain)" %> /
+ <%= link_to "Url", "#", 'data-route-helper' => '_url',
+ title: "Returns an absolute url (with the http and domain)" %>
+ </th>
+ <th><%# HTTP Verb %>
+ </th>
+ <th><%# Path %>
+ <%= search_field(:path, nil, id: 'path_search', placeholder: "Path Match") %>
+ </th>
+ <th><%# Controller#action %>
+ </th>
+ </tr>
+ </thead>
+ <tbody class='matched_paths' id='matched_paths'>
+ </tbody>
+ <tbody>
+ <%= yield %>
+ </tbody>
+</table>
+
+<script type='text/javascript'>
+ function each(elems, func) {
+ if (!elems instanceof Array) { elems = [elems]; }
+ for (var i = 0, len = elems.length; i < len; i++) {
+ func(elems[i]);
+ }
+ }
+
+ function setValOn(elems, val) {
+ each(elems, function(elem) {
+ elem.innerHTML = val;
+ });
+ }
+
+ function onClick(elems, func) {
+ each(elems, function(elem) {
+ elem.onclick = func;
+ });
+ }
+
+ // 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);
+ });
+ }
+
+ // takes an array of elements with a data-regexp attribute and
+ // passes their their parent <tr> into the callback function
+ // if the regexp matchs a given path
+ function eachElemsForPath(elems, path, func) {
+ each(elems, function(e){
+ var reg = e.getAttribute("data-regexp");
+ if (path.match(RegExp(reg))) {
+ func(e.parentNode.cloneNode(true));
+ }
+ })
+ }
+
+ // 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(/\#.*|\?.*/, '');
+ }
+
+ // 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>';
+
+
+ // Remove matches if no path is present
+ pathElem.onblur = function(e) {
+ if (pathElem.value === "") selectedSection.innerHTML = "";
+ }
+
+ // 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>';
+
+ // Clear out results section
+ selectedSection.innerHTML= defaultText;
+
+ // Display matches if they exist
+ eachElemsForPath(regexpElems, path, function(e){
+ selectedSection.appendChild(e);
+ });
+
+ // If no match present, tell the user
+ if (selectedSection.innerHTML === defaultText) {
+ selectedSection.innerHTML = selectedSection.innerHTML + noMatchText;
+ }
+ }
+ }
+
+ setupMatchPaths();
+ setupRouteToggleHelperLinks();
+</script>
diff --git a/actionpack/lib/action_dispatch/railtie.rb b/actionpack/lib/action_dispatch/railtie.rb
index 284dd180db..2dfaab3587 100644
--- a/actionpack/lib/action_dispatch/railtie.rb
+++ b/actionpack/lib/action_dispatch/railtie.rb
@@ -1,18 +1,21 @@
require "action_dispatch"
module ActionDispatch
- class Railtie < Rails::Railtie
+ class Railtie < Rails::Railtie # :nodoc:
config.action_dispatch = ActiveSupport::OrderedOptions.new
config.action_dispatch.x_sendfile_header = nil
config.action_dispatch.ip_spoofing_check = true
config.action_dispatch.show_exceptions = true
- config.action_dispatch.best_standards_support = true
config.action_dispatch.tld_length = 1
config.action_dispatch.ignore_accept_header = false
config.action_dispatch.rescue_templates = { }
config.action_dispatch.rescue_responses = { }
config.action_dispatch.default_charset = nil
config.action_dispatch.rack_cache = false
+ config.action_dispatch.http_auth_salt = 'http authentication'
+ config.action_dispatch.signed_cookie_salt = 'signed cookie'
+ config.action_dispatch.encrypted_cookie_salt = 'encrypted cookie'
+ config.action_dispatch.encrypted_signed_cookie_salt = 'signed encrypted cookie'
config.action_dispatch.default_headers = {
'X-Frame-Options' => 'SAMEORIGIN',
diff --git a/actionpack/lib/action_dispatch/request/session.rb b/actionpack/lib/action_dispatch/request/session.rb
index a05a23d953..6d911a75f1 100644
--- a/actionpack/lib/action_dispatch/request/session.rb
+++ b/actionpack/lib/action_dispatch/request/session.rb
@@ -63,6 +63,10 @@ module ActionDispatch
@exists = nil # we haven't checked yet
end
+ def id
+ options[:id]
+ end
+
def options
Options.find @env
end
@@ -123,6 +127,18 @@ module ActionDispatch
@delegate.delete key.to_s
end
+ def fetch(key, default=nil)
+ if self.key?(key)
+ self[key]
+ elsif default
+ self[key] = default
+ elsif block_given?
+ self[key] = yield(key)
+ else
+ raise KeyError
+ end
+ end
+
def inspect
if loaded?
super
diff --git a/actionpack/lib/action_dispatch/request/utils.rb b/actionpack/lib/action_dispatch/request/utils.rb
new file mode 100644
index 0000000000..8b43cdada8
--- /dev/null
+++ b/actionpack/lib/action_dispatch/request/utils.rb
@@ -0,0 +1,24 @@
+module ActionDispatch
+ class Request < Rack::Request
+ class Utils # :nodoc:
+ class << self
+ # Remove nils from the params hash
+ def deep_munge(hash)
+ hash.each do |k, v|
+ case v
+ when Array
+ v.grep(Hash) { |x| deep_munge(x) }
+ v.compact!
+ hash[k] = nil if v.empty?
+ when Hash
+ deep_munge(v)
+ end
+ end
+
+ hash
+ end
+ end
+ end
+ end
+end
+
diff --git a/actionpack/lib/action_dispatch/routing.rb b/actionpack/lib/action_dispatch/routing.rb
index 29090882a5..a9ac2bce1d 100644
--- a/actionpack/lib/action_dispatch/routing.rb
+++ b/actionpack/lib/action_dispatch/routing.rb
@@ -61,7 +61,7 @@ module ActionDispatch
# directory by using +scope+. +scope+ takes additional options which
# apply to all enclosed routes.
#
- # scope :path => "/cpanel", :as => 'admin' do
+ # scope path: "/cpanel", as: 'admin' do
# resources :posts, :comments
# end
#
@@ -69,6 +69,22 @@ module ActionDispatch
# <tt>Routing::Mapper::Scoping#namespace</tt>, and
# <tt>Routing::Mapper::Scoping#scope</tt>.
#
+ # == Non-resourceful routes
+ #
+ # For routes that don't fit the <tt>resources</tt> mold, you can use the HTTP helper
+ # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
+ #
+ # get 'post/:id' => 'posts#show'
+ # post 'post/:id' => 'posts#create_comment'
+ #
+ # If your route needs to respond to more than one HTTP method (or all methods) then using the
+ # <tt>:via</tt> option on <tt>match</tt> is preferable.
+ #
+ # match 'post/:id' => 'posts#show', via: [:get, :post]
+ #
+ # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
+ # URL will route to the <tt>show</tt> action.
+ #
# == Named routes
#
# Routes can be named by passing an <tt>:as</tt> option,
@@ -78,22 +94,22 @@ module ActionDispatch
# Example:
#
# # In routes.rb
- # match '/login' => 'accounts#login', :as => 'login'
+ # get '/login' => 'accounts#login', as: 'login'
#
# # With render, redirect_to, tests, etc.
# redirect_to login_url
#
# Arguments can be passed as well.
#
- # redirect_to show_item_path(:id => 25)
+ # redirect_to show_item_path(id: 25)
#
# Use <tt>root</tt> as a shorthand to name a route for the root path "/".
#
# # In routes.rb
- # root :to => 'blogs#index'
+ # root to: 'blogs#index'
#
# # would recognize http://www.example.com/ as
- # params = { :controller => 'blogs', :action => 'index' }
+ # params = { controller: 'blogs', action: 'index' }
#
# # and provide these named routes
# root_url # => 'http://www.example.com/'
@@ -104,126 +120,70 @@ module ActionDispatch
#
# # In routes.rb
# controller :blog do
- # match 'blog/show' => :list
- # match 'blog/delete' => :delete
- # match 'blog/edit/:id' => :edit
+ # get 'blog/show' => :list
+ # get 'blog/delete' => :delete
+ # get 'blog/edit/:id' => :edit
# end
#
# # provides named routes for show, delete, and edit
- # link_to @article.title, show_path(:id => @article.id)
+ # link_to @article.title, show_path(id: @article.id)
#
# == Pretty URLs
#
# Routes can generate pretty URLs. For example:
#
- # match '/articles/:year/:month/:day' => 'articles#find_by_id', :constraints => {
- # :year => /\d{4}/,
- # :month => /\d{1,2}/,
- # :day => /\d{1,2}/
+ # get '/articles/:year/:month/:day' => 'articles#find_by_id', constraints: {
+ # year: /\d{4}/,
+ # month: /\d{1,2}/,
+ # day: /\d{1,2}/
# }
#
# Using the route above, the URL "http://localhost:3000/articles/2005/11/06"
# maps to
#
- # params = {:year => '2005', :month => '11', :day => '06'}
+ # params = {year: '2005', month: '11', day: '06'}
#
# == Regular Expressions and parameters
# You can specify a regular expression to define a format for a parameter.
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, :constraints => {
- # :postalcode => /\d{5}(-\d{4})?/
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /\d{5}(-\d{4})?/
# }
#
# Constraints can include the 'ignorecase' and 'extended syntax' regular
# expression modifiers:
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, :constraints => {
- # :postalcode => /hx\d\d\s\d[a-z]{2}/i
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /hx\d\d\s\d[a-z]{2}/i
# }
# end
#
# controller 'geocode' do
- # match 'geocode/:postalcode' => :show, :constraints => {
- # :postalcode => /# Postcode format
+ # get 'geocode/:postalcode' => :show, constraints: {
+ # postalcode: /# Postcode format
# \d{5} #Prefix
# (-\d{4})? #Suffix
# /x
# }
# end
#
- # Using the multiline match modifier will raise an +ArgumentError+.
+ # Using the multiline modifier will raise an +ArgumentError+.
# Encoding regular expression modifiers are silently ignored. The
# match will always use the default encoding or ASCII.
#
- # == Default route
- #
- # Consider the following route, which you will find commented out at the
- # bottom of your generated <tt>config/routes.rb</tt>:
- #
- # match ':controller(/:action(/:id))(.:format)'
- #
- # This route states that it expects requests to consist of a
- # <tt>:controller</tt> followed optionally by an <tt>:action</tt> that in
- # turn is followed optionally by an <tt>:id</tt>, which in turn is followed
- # optionally by a <tt>:format</tt>.
- #
- # Suppose you get an incoming request for <tt>/blog/edit/22</tt>, you'll end
- # up with:
- #
- # params = { :controller => 'blog',
- # :action => 'edit',
- # :id => '22'
- # }
- #
- # By not relying on default routes, you improve the security of your
- # application since not all controller actions, which includes actions you
- # might add at a later time, are exposed by default.
- #
- # == HTTP Methods
- #
- # Using the <tt>:via</tt> option when specifying a route allows you to
- # restrict it to a specific HTTP method. Possible values are <tt>:post</tt>,
- # <tt>:get</tt>, <tt>:patch</tt>, <tt>:put</tt>, <tt>:delete</tt> and
- # <tt>:any</tt>. If your route needs to respond to more than one method you
- # can use an array, e.g. <tt>[ :get, :post ]</tt>. The default value is
- # <tt>:any</tt> which means that the route will respond to any of the HTTP
- # methods.
- #
- # Examples:
- #
- # match 'post/:id' => 'posts#show', :via => :get
- # match 'post/:id' => 'posts#create_comment', :via => :post
- #
- # Now, if you POST to <tt>/posts/:id</tt>, it will route to the <tt>create_comment</tt> action. A GET on the same
- # URL will route to the <tt>show</tt> action.
- #
- # === HTTP helper methods
- #
- # An alternative method of specifying which HTTP method a route should respond to is to use the helper
- # methods <tt>get</tt>, <tt>post</tt>, <tt>patch</tt>, <tt>put</tt> and <tt>delete</tt>.
- #
- # Examples:
- #
- # get 'post/:id' => 'posts#show'
- # post 'post/:id' => 'posts#create_comment'
- #
- # This syntax is less verbose and the intention is more apparent to someone else reading your code,
- # however if your route needs to respond to more than one HTTP method (or all methods) then using the
- # <tt>:via</tt> option on <tt>match</tt> is preferable.
- #
# == External redirects
#
# You can redirect any path to another path using the redirect helper in your router:
#
- # match "/stories" => redirect("/posts")
+ # get "/stories" => redirect("/posts")
#
# == Unicode character routes
#
# You can specify unicode character routes in your router:
#
- # match "ã“ã‚“ã«ã¡ã¯" => "welcome#index"
+ # get "ã“ã‚“ã«ã¡ã¯" => "welcome#index"
#
# == Routing to Rack Applications
#
@@ -231,7 +191,7 @@ module ActionDispatch
# index action in the PostsController, you can specify any Rack application
# as the endpoint for a matcher:
#
- # match "/application.js" => Sprockets
+ # get "/application.js" => Sprockets
#
# == Reloading routes
#
@@ -249,7 +209,7 @@ module ActionDispatch
# === +assert_routing+
#
# def test_movie_route_properly_splits
- # opts = {:controller => "plugin", :action => "checkout", :id => "2"}
+ # opts = {controller: "plugin", action: "checkout", id: "2"}
# assert_routing "plugin/checkout/2", opts
# end
#
@@ -258,7 +218,7 @@ module ActionDispatch
# === +assert_recognizes+
#
# def test_route_has_options
- # opts = {:controller => "plugin", :action => "show", :id => "12"}
+ # opts = {controller: "plugin", action: "show", id: "12"}
# assert_recognizes opts, "/plugins/show/12"
# end
#
@@ -286,11 +246,13 @@ module ActionDispatch
# Target specific controllers by prefixing the command with <tt>CONTROLLER=x</tt>.
#
module Routing
- autoload :Mapper, 'action_dispatch/routing/mapper'
- autoload :RouteSet, 'action_dispatch/routing/route_set'
- autoload :RoutesProxy, 'action_dispatch/routing/routes_proxy'
- autoload :UrlFor, 'action_dispatch/routing/url_for'
- autoload :PolymorphicRoutes, 'action_dispatch/routing/polymorphic_routes'
+ extend ActiveSupport::Autoload
+
+ autoload :Mapper
+ autoload :RouteSet
+ autoload :RoutesProxy
+ autoload :UrlFor
+ autoload :PolymorphicRoutes
SEPARATORS = %w( / . ? ) #:nodoc:
HTTP_METHODS = [:get, :head, :post, :patch, :put, :delete, :options] #:nodoc:
diff --git a/actionpack/lib/action_dispatch/routing/inspector.rb b/actionpack/lib/action_dispatch/routing/inspector.rb
index c18dc94d4f..cffb814e1e 100644
--- a/actionpack/lib/action_dispatch/routing/inspector.rb
+++ b/actionpack/lib/action_dispatch/routing/inspector.rb
@@ -1,4 +1,5 @@
require 'delegate'
+require 'active_support/core_ext/string/strip'
module ActionDispatch
module Routing
@@ -34,6 +35,23 @@ module ActionDispatch
super.to_s
end
+ def regexp
+ __getobj__.path.to_regexp
+ end
+
+ def json_regexp
+ str = regexp.inspect.
+ sub('\\A' , '^').
+ sub('\\Z' , '$').
+ sub('\\z' , '$').
+ sub(/^\// , '').
+ sub(/\/[a-z]*$/ , '').
+ gsub(/\(\?#.+\)/ , '').
+ gsub(/\(\?-\w+:/ , '(').
+ gsub(/\s/ , '')
+ Regexp.new(str).source
+ end
+
def reqs
@reqs ||= begin
reqs = endpoint
@@ -51,7 +69,7 @@ module ActionDispatch
end
def internal?
- path =~ %r{/rails/info.*|^#{Rails.application.config.assets.prefix}}
+ controller.to_s =~ %r{\Arails/(info|welcome)} || path =~ %r{\A#{Rails.application.config.assets.prefix}}
end
def engine?
@@ -61,32 +79,58 @@ module ActionDispatch
##
# This class is just used for displaying route information when someone
- # executes `rake routes`. People should not use this class.
+ # executes `rake routes` or looks at the RoutingError page.
+ # People should not use this class.
class RoutesInspector # :nodoc:
- def initialize
- @engines = Hash.new
+ def initialize(routes)
+ @engines = {}
+ @routes = routes
end
- def format(all_routes, filter = nil)
- if filter
- all_routes = all_routes.select{ |route| route.defaults[:controller] == filter }
+ def format(formatter, filter = nil)
+ routes_to_display = filter_routes(filter)
+
+ routes = collect_routes(routes_to_display)
+
+ if routes.none?
+ formatter.no_routes
+ return formatter.result
+ end
+
+ formatter.header routes
+ formatter.section routes
+
+ @engines.each do |name, engine_routes|
+ formatter.section_title "Routes for #{name}"
+ formatter.section engine_routes
end
- routes = collect_routes(all_routes)
+ formatter.result
+ end
+
+ private
- formatted_routes(routes) +
- formatted_routes_for_engines
+ def filter_routes(filter)
+ if filter
+ @routes.select { |route| route.defaults[:controller] == filter }
+ else
+ @routes
+ end
end
def collect_routes(routes)
- routes = routes.collect do |route|
+ routes.collect do |route|
RouteWrapper.new(route)
end.reject do |route|
route.internal?
end.collect do |route|
collect_engine_routes(route)
- {:name => route.name, :verb => route.verb, :path => route.path, :reqs => route.reqs }
+ { name: route.name,
+ verb: route.verb,
+ path: route.path,
+ reqs: route.reqs,
+ regexp: route.json_regexp }
end
end
@@ -100,21 +144,96 @@ module ActionDispatch
@engines[name] = collect_routes(routes.routes)
end
end
+ end
+
+ class ConsoleFormatter
+ def initialize
+ @buffer = []
+ end
+
+ def result
+ @buffer.join("\n")
+ end
- def formatted_routes_for_engines
- @engines.map do |name, routes|
- ["\nRoutes for #{name}:"] + formatted_routes(routes)
- end.flatten
+ def section_title(title)
+ @buffer << "\n#{title}:"
end
- def formatted_routes(routes)
- name_width = routes.map{ |r| r[:name].length }.max
- verb_width = routes.map{ |r| r[:verb].length }.max
- path_width = routes.map{ |r| r[:path].length }.max
+ def section(routes)
+ @buffer << draw_section(routes)
+ end
+
+ def header(routes)
+ @buffer << draw_header(routes)
+ end
- routes.map do |r|
- "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ def no_routes
+ @buffer << <<-MESSAGE.strip_heredoc
+ You don't have any routes defined!
+
+ Please add some routes in config/routes.rb.
+
+ For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
+ MESSAGE
+ end
+
+ private
+ def draw_section(routes)
+ name_width, verb_width, path_width = widths(routes)
+
+ routes.map do |r|
+ "#{r[:name].rjust(name_width)} #{r[:verb].ljust(verb_width)} #{r[:path].ljust(path_width)} #{r[:reqs]}"
+ end
end
+
+ def draw_header(routes)
+ name_width, verb_width, path_width = widths(routes)
+
+ "#{"Prefix".rjust(name_width)} #{"Verb".ljust(verb_width)} #{"URI Pattern".ljust(path_width)} Controller#Action"
+ end
+
+ def widths(routes)
+ [routes.map { |r| r[:name].length }.max,
+ routes.map { |r| r[:verb].length }.max,
+ routes.map { |r| r[:path].length }.max]
+ end
+ end
+
+ class HtmlTableFormatter
+ def initialize(view)
+ @view = view
+ @buffer = []
+ end
+
+ def section_title(title)
+ @buffer << %(<tr><th colspan="4">#{title}</th></tr>)
+ end
+
+ def section(routes)
+ @buffer << @view.render(partial: "routes/route", collection: routes)
+ end
+
+ # the header is part of the HTML page, so we don't construct it here.
+ def header(routes)
+ end
+
+ def no_routes
+ @buffer << <<-MESSAGE.strip_heredoc
+ <p>You don't have any routes defined!</p>
+ <ul>
+ <li>Please add some routes in <tt>config/routes.rb</tt>.</li>
+ <li>
+ For more information about routes, please see the Rails guide
+ <a href="http://guides.rubyonrails.org/routing.html">Rails Routing from the Outside In</a>.
+ </li>
+ </ul>
+ MESSAGE
+ end
+
+ def result
+ @view.raw @view.render(layout: "routes/table") {
+ @view.raw @buffer.join("\n")
+ }
end
end
end
diff --git a/actionpack/lib/action_dispatch/routing/mapper.rb b/actionpack/lib/action_dispatch/routing/mapper.rb
index c5cf413c8f..db9c993590 100644
--- a/actionpack/lib/action_dispatch/routing/mapper.rb
+++ b/actionpack/lib/action_dispatch/routing/mapper.rb
@@ -2,12 +2,18 @@ require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/reverse_merge'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/enumerable'
+require 'active_support/core_ext/array/extract_options'
require 'active_support/inflector'
require 'action_dispatch/routing/redirection'
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?
@@ -26,15 +32,10 @@ module ActionDispatch
def matches?(env)
req = @request.new(env)
- @constraints.each { |constraint|
- if constraint.respond_to?(:matches?) && !constraint.matches?(req)
- return false
- elsif constraint.respond_to?(:call) && !constraint.call(*constraint_args(constraint, req))
- return false
- end
- }
-
- return true
+ @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
@@ -50,126 +51,157 @@ module ActionDispatch
end
class Mapping #:nodoc:
- IGNORE_OPTIONS = [:to, :as, :via, :on, :constraints, :defaults, :only, :except, :anchor, :shallow, :shallow_path, :shallow_prefix]
+ 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}
- SHORTHAND_REGEX = %r{/[\w/]+$}
WILDCARD_PATH = %r{\*([^/\)]+)\)?$}
- def initialize(set, scope, path, options)
- @set, @scope = set, scope
- @segment_keys = nil
- @options = (@scope[:options] || {}).merge(options)
- @path = normalize_path(path)
- normalize_options!
+ attr_reader :scope, :path, :options, :requirements, :conditions, :defaults
- via_all = @options.delete(:via) if @options[:via] == :all
+ def initialize(set, scope, path, options)
+ @set, @scope, @path, @options = set, scope, path, options
+ @requirements, @conditions, @defaults = {}, {}, {}
- if !via_all && request_method_condition.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 GET, use `get` in the router:\n\n" \
- " Instead of: match \"controller#action\"\n" \
- " Do: get \"controller#action\""
- raise msg
- end
+ normalize_options!
+ normalize_path!
+ normalize_requirements!
+ normalize_conditions!
+ normalize_defaults!
end
def to_route
- [ app, conditions, requirements, defaults, @options[:as], @options[:anchor] ]
+ [ app, conditions, requirements, defaults, options[:as], options[:anchor] ]
end
private
- def normalize_options!
- path_without_format = @path.sub(/\(\.:format\)$/, '')
+ def normalize_path!
+ raise ArgumentError, "path is required" if @path.blank?
+ @path = Mapper.normalize_path(@path)
- if using_match_shorthand?(path_without_format, @options)
- to_shorthand = @options[:to].blank?
- @options[:to] ||= path_without_format.gsub(/\(.*\)/, "")[1..-1].sub(%r{/([^/]*)$}, '#\1')
- end
-
- @options.merge!(default_controller_and_action(to_shorthand))
-
- requirements.each do |name, requirement|
- # segment_keys.include?(k.to_s) || k == :controller
- next unless Regexp === requirement && !constraints[name]
-
- if requirement.source =~ ANCHOR_CHARACTERS_REGEX
- raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
- end
- if requirement.multiline?
- raise ArgumentError, "Regexp multiline option not allowed in routing requirements: #{requirement.inspect}"
- end
+ if required_format?
+ @path = "#{@path}.:format"
+ elsif optional_format?
+ @path = "#{@path}(.:format)"
end
+ end
- if @options[:constraints].is_a?(Hash)
- (@options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(@options[:constraints]))
- end
+ def required_format?
+ options[:format] == true
end
- # match "account/overview"
- def using_match_shorthand?(path, options)
- path && (options[:to] || options[:action]).nil? && path =~ SHORTHAND_REGEX
+ def optional_format?
+ options[:format] != false && !path.include?(':format') && !path.end_with?('/')
end
- def normalize_path(path)
- raise ArgumentError, "path is required" if path.blank?
- path = Mapper.normalize_path(path)
+ def normalize_options!
+ @options.reverse_merge!(scope[:options]) if scope[:options]
+ path_without_format = path.sub(/\(\.:format\)$/, '')
- if path.match(':controller')
- raise ArgumentError, ":controller segment is not allowed within a namespace block" if @scope[:module]
+ # 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] ||= /.+?/
+ end
+
+ if path_without_format.match(':controller')
+ raise ArgumentError, ":controller segment is not allowed within a namespace block" if scope[:module]
# 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' }
+ # => { controller: 'admin/products', action: 'show', id: '1' }
@options[:controller] ||= /.+?/
end
- # Add a constraint for wildcard route to make it non-greedy and match the
- # optional format part of the route by default
- if path.match(WILDCARD_PATH) && @options[:format] != false
- @options[$1.to_sym] ||= /.+?/
+ @options.merge!(default_controller_and_action)
+ 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
end
- if @options[:format] == false
- @options.delete(:format)
- path
- elsif path.include?(":format") || path.end_with?('/')
- path
- elsif @options[:format] == true
- "#{path}.:format"
- else
- "#{path}(.:format)"
+ if options[:format] == true
+ @requirements[:format] ||= /.+/
+ elsif Regexp === options[:format]
+ @requirements[:format] = options[:format]
+ elsif String === options[:format]
+ @requirements[:format] = Regexp.compile(options[:format])
end
end
- def app
- Constraints.new(
- to.respond_to?(:call) ? to : Routing::RouteSet::Dispatcher.new(:defaults => defaults),
- blocks,
- @set.request_class
- )
- end
+ def verify_regexp_requirement(requirement)
+ if requirement.source =~ ANCHOR_CHARACTERS_REGEX
+ raise ArgumentError, "Regexp anchor characters are not allowed in routing requirements: #{requirement.inspect}"
+ end
- def conditions
- { :path_info => @path }.merge(constraints).merge(request_method_condition)
+ if requirement.multiline?
+ raise ArgumentError, "Regexp multiline option is not allowed in routing requirements: #{requirement.inspect}"
+ end
end
- def requirements
- @requirements ||= (@options[:constraints].is_a?(Hash) ? @options[:constraints] : {}).tap do |requirements|
- requirements.reverse_merge!(@scope[:constraints]) if @scope[:constraints]
- @options.each { |k, v| requirements[k] = v if v.is_a?(Regexp) }
+ def normalize_defaults!
+ @defaults.merge!(scope[:defaults]) if scope[:defaults]
+ @defaults.merge!(options[:defaults]) if options[:defaults]
+
+ options.each do |key, default|
+ next if Regexp === default || IGNORE_OPTIONS.include?(key)
+ @defaults[key] = default
+ end
+
+ if options[:constraints].is_a?(Hash)
+ options[:constraints].each do |key, default|
+ next unless URL_OPTIONS.include?(key) && (String === default || Fixnum === default)
+ @defaults[key] ||= default
+ end
+ end
+
+ if Regexp === options[:format]
+ @defaults[:format] = nil
+ elsif String === options[:format]
+ @defaults[:format] = options[:format]
end
end
- def defaults
- @defaults ||= (@options[:defaults] || {}).tap do |defaults|
- defaults.reverse_merge!(@scope[:defaults]) if @scope[:defaults]
- @options.each { |k, v| defaults[k] = v unless v.is_a?(Regexp) || IGNORE_OPTIONS.include?(k.to_sym) }
+ def normalize_conditions!
+ @conditions.merge!(:path_info => path)
+
+ constraints.each do |key, condition|
+ next if segment_keys.include?(key) || key == :controller
+ @conditions[key] = condition
+ end
+
+ @conditions[:required_defaults] = []
+ options.each do |key, required_default|
+ next if segment_keys.include?(key) || IGNORE_OPTIONS.include?(key)
+ next if Regexp === required_default
+ @conditions[:required_defaults] << key
+ end
+
+ via_all = options.delete(:via) if options[:via] == :all
+
+ if !via_all && options[:via].blank?
+ 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
+ end
+
+ if via = options[:via]
+ list = Array(via).map { |m| m.to_s.dasherize.upcase }
+ @conditions.merge!(:request_method => list)
end
end
- def default_controller_and_action(to_shorthand=nil)
+ def app
+ Constraints.new(endpoint, blocks, @set.request_class)
+ end
+
+ def default_controller_and_action
if to.respond_to?(:call)
{ }
else
@@ -193,14 +225,20 @@ module ActionDispatch
controller = controller.to_s unless controller.is_a?(Regexp)
action = action.to_s unless action.is_a?(Regexp)
- if controller.blank? && segment_keys.exclude?("controller")
+ if controller.blank? && segment_keys.exclude?(:controller)
raise ArgumentError, "missing :controller"
end
- if action.blank? && segment_keys.exclude?("action")
+ if action.blank? && segment_keys.exclude?(:action)
raise ArgumentError, "missing :action"
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"
+ raise ArgumentError, message
+ end
+
hash = {}
hash[:controller] = controller unless controller.blank?
hash[:action] = action unless action.blank?
@@ -209,54 +247,59 @@ module ActionDispatch
end
def blocks
- constraints = @options[:constraints]
- if constraints.present? && !constraints.is_a?(Hash)
- [constraints]
+ if options[:constraints].present? && !options[:constraints].is_a?(Hash)
+ [options[:constraints]]
else
- @scope[:blocks] || []
+ scope[:blocks] || []
end
end
def constraints
- @constraints ||= requirements.reject { |k, v| segment_keys.include?(k.to_s) || k == :controller }
- end
+ @constraints ||= {}.tap do |constraints|
+ constraints.merge!(scope[:constraints]) if scope[:constraints]
- def request_method_condition
- if via = @options[:via]
- list = Array(via).map { |m| m.to_s.dasherize.upcase }
- { :request_method => list }
- else
- { }
+ options.except(*IGNORE_OPTIONS).each do |key, option|
+ constraints[key] = option if Regexp === option
+ end
+
+ constraints.merge!(options[:constraints]) if options[:constraints].is_a?(Hash)
end
end
def segment_keys
- return @segment_keys if @segment_keys
+ @segment_keys ||= path_pattern.names.map{ |s| s.to_sym }
+ end
- @segment_keys = Journey::Path::Pattern.new(
- Journey::Router::Strexp.compile(@path, requirements, SEPARATORS)
- ).names
+ def path_pattern
+ Journey::Path::Pattern.new(strexp)
+ end
+
+ def strexp
+ Journey::Router::Strexp.compile(path, requirements, SEPARATORS)
+ end
+
+ def endpoint
+ to.respond_to?(:call) ? to : dispatcher
+ end
+
+ def dispatcher
+ Routing::RouteSet::Dispatcher.new(:defaults => defaults)
end
def to
- @options[:to]
+ options[:to]
end
def default_controller
- @options[:controller] || @scope[:controller]
+ options[:controller] || scope[:controller]
end
def default_action
- @options[:action] || @scope[:action]
- end
-
- def defaults_from_constraints(constraints)
- url_keys = [:protocol, :subdomain, :domain, :host, :port]
- constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
+ options[:action] || scope[:action]
end
end
- # Invokes Rack::Mount::Utils.normalize path and ensure that
+ # Invokes Journey::Router::Utils.normalize_path and ensure that
# (:locale) becomes (/:locale) instead of /(:locale). Except
# for root cases, where the latter is the correct one.
def self.normalize_path(path)
@@ -272,7 +315,7 @@ module ActionDispatch
module Base
# You can specify what Rails should route "/" to with the root method:
#
- # root :to => 'pages#main'
+ # root to: 'pages#main'
#
# For options, see +match+, as +root+ uses it internally.
#
@@ -284,8 +327,7 @@ module ActionDispatch
# because this means it will be matched first. As this is the most popular route
# of most Rails applications, this is beneficial.
def root(options = {})
- options = { :to => options } if options.is_a?(String)
- match '/', { :as => :root, :via => :get }.merge(options)
+ match '/', { :as => :root, :via => :get }.merge!(options)
end
# Matches a url pattern to one or more routes. Any symbols in a pattern
@@ -299,7 +341,7 @@ module ActionDispatch
# and +:action+ to the controller's action. A pattern can also map
# wildcard segments (globs) to params:
#
- # match 'songs/*category/:title' => 'songs#show'
+ # match 'songs/*category/:title', to: 'songs#show'
#
# # 'songs/rock/classic/stairway-to-heaven' sets
# # params[:category] = 'rock/classic'
@@ -309,16 +351,21 @@ module ActionDispatch
# +: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', to: 'photos#show'
+ # match 'photos/:id', controller: 'photos', action: 'show'
#
# A pattern can also point to a +Rack+ endpoint i.e. anything that
# responds to +call+:
#
- # match 'photos/:id' => lambda {|hash| [200, {}, "Coming soon"] }
- # match 'photos/:id' => PhotoRackApp
+ # match 'photos/:id', to: lambda {|hash| [200, {}, ["Coming soon"]] }
+ # match 'photos/:id', to: PhotoRackApp
# # Yes, controller actions are just rack endpoints
- # match 'photos/:id' => PhotosController.action(:show)
+ # match 'photos/:id', to: PhotosController.action(:show)
+ #
+ # Because requesting various HTTP verbs with a single action has security
+ # implications, you must either specify the actions in
+ # the via options or use one of the HtttpHelpers[rdoc-ref:HttpHelpers]
+ # instead +match+
#
# === Options
#
@@ -336,7 +383,7 @@ module ActionDispatch
# [:module]
# The namespace for :controller.
#
- # match 'path' => 'c#a', :module => 'sekret', :controller => 'posts'
+ # match 'path', to: 'c#a', module: 'sekret', controller: 'posts'
# #=> Sekret::PostsController
#
# See <tt>Scoping#namespace</tt> for its scope equivalent.
@@ -347,16 +394,17 @@ module ActionDispatch
# [:via]
# Allowed HTTP verb(s) for route.
#
- # match 'path' => 'c#a', :via => :get
- # match 'path' => 'c#a', :via => [:get, :post]
+ # match 'path', to: 'c#a', via: :get
+ # match 'path', to: 'c#a', via: [:get, :post]
+ # match 'path', to: 'c#a', via: :all
#
# [:to]
# 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'
+ # match 'path', to: lambda { |env| [200, {}, ["Success!"]] }
+ # match 'path', to: RackApp
#
# [:on]
# Shorthand for wrapping routes in a specific RESTful context. Valid
@@ -364,27 +412,31 @@ module ActionDispatch
# <tt>resource(s)</tt> block. For example:
#
# resource :bar do
- # match 'foo' => 'c#a', :on => :member, :via => [:get, :post]
+ # match 'foo', to: 'c#a', on: :member, via: [:get, :post]
# end
#
# Is equivalent to:
#
# resource :bar do
# member do
- # match 'foo' => 'c#a', :via => [:get, :post]
+ # match 'foo', to: 'c#a', via: [:get, :post]
# end
# end
#
# [:constraints]
- # Constrains parameters with a hash of regular expressions or an
- # object that responds to <tt>matches?</tt>
+ # Constrains parameters with a hash of regular expressions
+ # or an object that responds to <tt>matches?</tt>. In addition, constraints
+ # 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}/ }
#
- # class Blacklist
+ # match 'json_only', constraints: { format: 'json' }
+ #
+ # class Whitelist
# def matches?(request) request.remote_ip == '1.2.3.4' end
# end
- # match 'path' => 'c#a', :constraints => Blacklist.new
+ # match 'path', to: 'c#a', constraints: Whitelist.new
#
# See <tt>Scoping#constraints</tt> for more examples with its scope
# equivalent.
@@ -393,7 +445,7 @@ module ActionDispatch
# Sets defaults for parameters
#
# # Sets params[:format] to 'jpg' by default
- # match 'path' => 'c#a', :defaults => { :format => 'jpg' }
+ # match 'path', to: 'c#a', defaults: { format: 'jpg' }
#
# See <tt>Scoping#defaults</tt> for its scope equivalent.
#
@@ -402,7 +454,7 @@ module ActionDispatch
# false, the pattern matches any request prefixed with the given path.
#
# # Matches any request starting with 'path'
- # match 'path' => 'c#a', :anchor => false
+ # match 'path', to: 'c#a', anchor: false
#
# [:format]
# Allows you to specify the default value for optional +format+
@@ -412,7 +464,7 @@ module ActionDispatch
# Mount a Rack-based application to be used within the application.
#
- # mount SomeRackApp, :at => "some_route"
+ # mount SomeRackApp, at: "some_route"
#
# Alternatively:
#
@@ -425,7 +477,7 @@ module ActionDispatch
# the helper is either +some_rack_app_path+ or +some_rack_app_url+.
# To customize this helper's name, use the +:as+ option:
#
- # mount(SomeRackApp => "some_route", :as => "exciting")
+ # mount(SomeRackApp => "some_route", as: "exciting")
#
# This will generate the +exciting_path+ and +exciting_url+ helpers
# which can be used to navigate to this mounted app.
@@ -438,7 +490,7 @@ module ActionDispatch
end
options = app
- app, path = options.find { |k, v| k.respond_to?(:call) }
+ app, path = options.find { |k, _| k.respond_to?(:call) }
options.delete(app) if app
end
@@ -464,6 +516,11 @@ module ActionDispatch
end
end
+ # Query if the following named route was already defined.
+ def has_named_route?(name)
+ @set.named_routes.routes[name.to_sym]
+ end
+
private
def app_name(app)
return unless app.respond_to?(:routes)
@@ -491,9 +548,7 @@ module ActionDispatch
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) }
- prefix = _routes.url_helpers.send("#{name}_path", prefix_options)
- prefix = '' if prefix == '/'
- prefix
+ _routes.url_helpers.send("#{name}_path", prefix_options)
end
end
end
@@ -501,41 +556,41 @@ module ActionDispatch
module HttpHelpers
# Define a route that only recognizes HTTP GET.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # get 'bacon', :to => 'food#bacon'
+ # get 'bacon', to: 'food#bacon'
def get(*args, &block)
map_method(:get, args, &block)
end
# Define a route that only recognizes HTTP POST.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # post 'bacon', :to => 'food#bacon'
+ # post 'bacon', to: 'food#bacon'
def post(*args, &block)
map_method(:post, args, &block)
end
# Define a route that only recognizes HTTP PATCH.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # patch 'bacon', :to => 'food#bacon'
+ # patch 'bacon', to: 'food#bacon'
def patch(*args, &block)
map_method(:patch, args, &block)
end
# Define a route that only recognizes HTTP PUT.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # put 'bacon', :to => 'food#bacon'
+ # put 'bacon', to: 'food#bacon'
def put(*args, &block)
map_method(:put, args, &block)
end
# Define a route that only recognizes HTTP DELETE.
- # For supported arguments, see <tt>Base#match</tt>.
+ # For supported arguments, see match[rdoc-ref:Base#match]
#
- # delete 'broccoli', :to => 'food#broccoli'
+ # delete 'broccoli', to: 'food#broccoli'
def delete(*args, &block)
map_method(:delete, args, &block)
end
@@ -543,8 +598,7 @@ module ActionDispatch
private
def map_method(method, args, &block)
options = args.extract_options!
- options[:via] = method
- options[:path] ||= args.first if args.first.is_a?(String)
+ options[:via] = method
match(*args, options, &block)
self
end
@@ -574,13 +628,13 @@ module ActionDispatch
# If you want to route /posts (without the prefix /admin) to
# <tt>Admin::PostsController</tt>, you could use
#
- # scope :module => "admin" do
+ # scope module: "admin" do
# resources :posts
# end
#
# or, for a single case
#
- # resources :posts, :module => "admin"
+ # resources :posts, module: "admin"
#
# If you want to route /admin/posts to +PostsController+
# (without the Admin:: module prefix), you could use
@@ -591,7 +645,7 @@ module ActionDispatch
#
# or, for a single case
#
- # resources :posts, :path => "/admin/posts"
+ # resources :posts, path: "/admin/posts"
#
# In each of these cases, the named routes remain the same as if you did
# not use scope. In the last case, the following paths map to
@@ -609,7 +663,7 @@ module ActionDispatch
#
# Take the following route definition as an example:
#
- # scope :path => ":account_id", :as => "account" do
+ # scope path: ":account_id", as: "account" do
# resources :projects
# end
#
@@ -621,66 +675,62 @@ module ActionDispatch
#
# Takes same options as <tt>Base#match</tt> and <tt>Resources#resources</tt>.
#
- # === Examples
- #
# # route /posts (without the prefix /admin) to <tt>Admin::PostsController</tt>
- # scope :module => "admin" do
+ # scope module: "admin" do
# resources :posts
# end
#
# # prefix the posts resource's requests with '/admin'
- # scope :path => "/admin" do
+ # scope path: "/admin" do
# resources :posts
# end
#
# # prefix the routing helper name: +sekret_posts_path+ instead of +posts_path+
- # scope :as => "sekret" do
+ # scope as: "sekret" do
# resources :posts
# end
def scope(*args)
- options = args.extract_options!
- options = options.dup
-
- options[:path] = args.first if args.first.is_a?(String)
+ options = args.extract_options!.dup
recover = {}
+ options[:path] = args.flatten.join('/') if args.any?
options[:constraints] ||= {}
- unless options[:constraints].is_a?(Hash)
- block, options[:constraints] = options[:constraints], {}
- end
if options[:constraints].is_a?(Hash)
- (options[:defaults] ||= {}).reverse_merge!(defaults_from_constraints(options[:constraints]))
+ defaults = options[:constraints].select do
+ |k, v| URL_OPTIONS.include?(k) && (v.is_a?(String) || v.is_a?(Fixnum))
+ end
+
+ (options[:defaults] ||= {}).reverse_merge!(defaults)
+ else
+ block, options[:constraints] = options[:constraints], {}
end
- scope_options.each do |option|
- if value = options.delete(option)
+ SCOPE_OPTIONS.each do |option|
+ if option == :blocks
+ value = block
+ elsif option == :options
+ value = options
+ else
+ value = options.delete(option)
+ end
+
+ if value
recover[option] = @scope[option]
@scope[option] = send("merge_#{option}_scope", @scope[option], value)
end
end
- recover[:block] = @scope[:blocks]
- @scope[:blocks] = merge_blocks_scope(@scope[:blocks], block)
-
- recover[:options] = @scope[:options]
- @scope[:options] = merge_options_scope(@scope[:options], options)
-
yield
self
ensure
- scope_options.each do |option|
- @scope[option] = recover[option] if recover.has_key?(option)
- end
-
- @scope[:options] = recover[:options]
- @scope[:blocks] = recover[:block]
+ @scope.merge!(recover)
end
# Scopes routes to a specific controller
#
# controller "food" do
- # match "bacon", :action => "bacon"
+ # match "bacon", action: "bacon"
# end
def controller(controller, options={})
options[:controller] = controller
@@ -711,20 +761,18 @@ module ActionDispatch
# For options, see <tt>Base#match</tt>. For +:shallow_path+ option, see
# <tt>Resources#resources</tt>.
#
- # === Examples
- #
# # accessible through /sekret/posts rather than /admin/posts
- # namespace :admin, :path => "sekret" do
+ # namespace :admin, path: "sekret" do
# resources :posts
# end
#
# # maps to <tt>Sekret::PostsController</tt> rather than <tt>Admin::PostsController</tt>
- # namespace :admin, :module => "sekret" do
+ # namespace :admin, module: "sekret" do
# resources :posts
# end
#
# # generates +sekret_posts_path+ rather than +admin_posts_path+
- # namespace :admin, :as => "sekret" do
+ # namespace :admin, as: "sekret" do
# resources :posts
# end
def namespace(path, options = {})
@@ -738,7 +786,7 @@ module ActionDispatch
# Allows you to constrain the nested routes based on a set of rules.
# For instance, in order to change the routes to allow for a dot character in the +id+ parameter:
#
- # constraints(:id => /\d+\.\d+/) do
+ # constraints(id: /\d+\.\d+/) do
# resources :posts
# end
#
@@ -748,7 +796,7 @@ module ActionDispatch
# You may use this to also restrict other parameters:
#
# resources :posts do
- # constraints(:post_id => /\d+\.\d+/) do
+ # constraints(post_id: /\d+\.\d+/) do
# resources :comments
# end
# end
@@ -757,7 +805,7 @@ module ActionDispatch
#
# Routes can also be constrained to an IP or a certain range of IP addresses:
#
- # constraints(:ip => /192\.168\.\d+\.\d+/) do
+ # constraints(ip: /192\.168\.\d+\.\d+/) do
# resources :posts
# end
#
@@ -794,8 +842,8 @@ module ActionDispatch
end
# Allows you to set default parameters for a route, such as this:
- # defaults :id => 'home' do
- # match 'scoped_pages/(:id)', :to => 'pages#show'
+ # defaults id: 'home' do
+ # match 'scoped_pages/(:id)', to: 'pages#show'
# end
# Using this, the +:id+ parameter here will default to 'home'.
def defaults(defaults = {})
@@ -803,10 +851,6 @@ module ActionDispatch
end
private
- def scope_options #:nodoc:
- @scope_options ||= private_methods.grep(/^merge_(.+)_scope$/) { $1.to_sym }
- end
-
def merge_path_scope(parent, child) #:nodoc:
Mapper.normalize_path("#{parent}/#{child}")
end
@@ -831,6 +875,10 @@ module ActionDispatch
child
end
+ def merge_action_scope(parent, child) #:nodoc:
+ child
+ end
+
def merge_path_names_scope(parent, child) #:nodoc:
merge_options_scope(parent, child)
end
@@ -850,7 +898,7 @@ module ActionDispatch
end
def merge_options_scope(parent, child) #:nodoc:
- (parent || {}).except(*override_keys(child)).merge(child)
+ (parent || {}).except(*override_keys(child)).merge!(child)
end
def merge_shallow_scope(parent, child) #:nodoc:
@@ -860,11 +908,6 @@ module ActionDispatch
def override_keys(child) #:nodoc:
child.key?(:only) || child.key?(:except) ? [:only, :except] : []
end
-
- def defaults_from_constraints(constraints)
- url_keys = [:protocol, :subdomain, :domain, :host, :port]
- constraints.slice(*url_keys).select{ |k, v| v.is_a?(String) || v.is_a?(Fixnum) }
- end
end
# Resource routing allows you to quickly declare all of the common routes
@@ -902,7 +945,7 @@ module ActionDispatch
# use dots as part of the +:id+ parameter add a constraint which
# overrides this restriction, e.g:
#
- # resources :articles, :id => /[^\/]+/
+ # resources :articles, id: /[^\/]+/
#
# This allows any character other than a slash as part of your +:id+.
#
@@ -912,6 +955,8 @@ 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
@@ -1022,18 +1067,18 @@ module ActionDispatch
# a singular resource to map /profile (rather than /profile/:id) to
# the show action:
#
- # resource :geocoder
+ # resource :profile
#
# creates six different routes in your application, all mapping to
- # the +GeoCoders+ controller (note that the controller is named after
+ # the +Profiles+ controller (note that the controller is named after
# the plural):
#
- # GET /geocoder/new
- # POST /geocoder
- # GET /geocoder
- # GET /geocoder/edit
- # PATCH/PUT /geocoder
- # DELETE /geocoder
+ # GET /profile/new
+ # POST /profile
+ # GET /profile
+ # GET /profile/edit
+ # PATCH/PUT /profile
+ # DELETE /profile
#
# === Options
# Takes same options as +resources+.
@@ -1057,15 +1102,7 @@ module ActionDispatch
get :new
end if parent_resource.actions.include?(:new)
- member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
- if parent_resource.actions.include?(:update)
- patch :update
- put :update
- end
- delete :destroy if parent_resource.actions.include?(:destroy)
- end
+ set_member_mappings_for_resource
end
self
@@ -1112,43 +1149,43 @@ module ActionDispatch
# Allows you to change the segment component of the +edit+ and +new+ actions.
# Actions not specified are not changed.
#
- # resources :posts, :path_names => { :new => "brand_new" }
+ # resources :posts, path_names: { new: "brand_new" }
#
# The above example will now change /posts/new to /posts/brand_new
#
# [:path]
# Allows you to change the path prefix for the resource.
#
- # resources :posts, :path => 'postings'
+ # resources :posts, path: 'postings'
#
# The resource and all segments will now route to /postings instead of /posts
#
# [:only]
# Only generate routes for the given actions.
#
- # resources :cows, :only => :show
- # resources :cows, :only => [:show, :index]
+ # resources :cows, only: :show
+ # resources :cows, only: [:show, :index]
#
# [:except]
# Generate all routes except for the given actions.
#
- # resources :cows, :except => :show
- # resources :cows, :except => [:show, :index]
+ # resources :cows, except: :show
+ # resources :cows, except: [:show, :index]
#
# [:shallow]
# Generates shallow routes for nested resource(s). When placed on a parent resource,
# generates shallow routes for all nested resources.
#
- # resources :posts, :shallow => true do
+ # resources :posts, shallow: true do
# resources :comments
# end
#
# Is the same as:
#
# resources :posts do
- # resources :comments, :except => [:show, :edit, :update, :destroy]
+ # resources :comments, except: [:show, :edit, :update, :destroy]
# end
- # resources :comments, :only => [:show, :edit, :update, :destroy]
+ # resources :comments, only: [:show, :edit, :update, :destroy]
#
# This allows URLs for resources that otherwise would be deeply nested such
# as a comment on a blog post like <tt>/posts/a-long-permalink/comments/1234</tt>
@@ -1157,9 +1194,9 @@ module ActionDispatch
# [:shallow_path]
# Prefixes nested shallow routes with the specified path.
#
- # scope :shallow_path => "sekret" do
+ # scope shallow_path: "sekret" do
# resources :posts do
- # resources :comments, :shallow => true
+ # resources :comments, shallow: true
# end
# end
#
@@ -1176,9 +1213,9 @@ module ActionDispatch
# [:shallow_prefix]
# Prefixes nested shallow route names with specified prefix.
#
- # scope :shallow_prefix => "sekret" do
+ # scope shallow_prefix: "sekret" do
# resources :posts do
- # resources :comments, :shallow => true
+ # resources :comments, shallow: true
# end
# end
#
@@ -1199,10 +1236,10 @@ module ActionDispatch
# === Examples
#
# # routes call <tt>Admin::PostsController</tt>
- # resources :posts, :module => "admin"
+ # resources :posts, module: "admin"
#
# # resource actions are at /admin/posts.
- # resources :posts, :path => "admin/posts"
+ # resources :posts, path: "admin/posts"
def resources(*resources, &block)
options = resources.extract_options!.dup
@@ -1224,15 +1261,7 @@ module ActionDispatch
get :new
end if parent_resource.actions.include?(:new)
- member do
- get :edit if parent_resource.actions.include?(:edit)
- get :show if parent_resource.actions.include?(:show)
- if parent_resource.actions.include?(:update)
- patch :update
- put :update
- end
- delete :destroy if parent_resource.actions.include?(:destroy)
- end
+ set_member_mappings_for_resource
end
self
@@ -1344,7 +1373,7 @@ module ActionDispatch
def match(path, *rest)
if rest.empty? && Hash === path
options = path
- path, to = options.find { |name, value| name.is_a?(String) }
+ path, to = options.find { |name, _value| name.is_a?(String) }
options[:to] = to
options.delete(path)
paths = [path]
@@ -1359,10 +1388,28 @@ module ActionDispatch
raise ArgumentError, "Unknown scope #{on.inspect} given to :on"
end
- paths.each { |_path| decomposed_match(_path, options.dup) }
+ if @scope[:controller] && @scope[:action]
+ options[:to] ||= "#{@scope[:controller]}##{@scope[:action]}"
+ end
+
+ paths.each do |_path|
+ route_options = options.dup
+ route_options[:path] ||= _path if _path.is_a?(String)
+
+ path_without_format = _path.to_s.sub(/\(\.:format\)$/, '')
+ if using_match_shorthand?(path_without_format, route_options)
+ route_options[:to] ||= path_without_format.gsub(%r{^/}, "").sub(%r{/([^/]*)$}, '#\1')
+ end
+
+ decomposed_match(_path, route_options)
+ end
self
end
+ def using_match_shorthand?(path, options)
+ path && (options[:to] || options[:action]).nil? && path =~ %r{/[\w/]+$}
+ end
+
def decomposed_match(path, options) # :nodoc:
if on = options.delete(:on)
send(on) { decomposed_match(path, options) }
@@ -1380,9 +1427,10 @@ module ActionDispatch
def add_route(action, options) # :nodoc:
path = path_for_action(action, options.delete(:path))
+ action = action.to_s.dup
- if action.to_s =~ /^[\w\/]+$/
- options[:action] ||= action unless action.to_s.include?("/")
+ if action =~ /^[\w\/]+$/
+ options[:action] ||= action unless action.include?("/")
else
action = nil
end
@@ -1398,7 +1446,15 @@ module ActionDispatch
@set.add_route(app, conditions, requirements, defaults, as, anchor)
end
- def root(options={})
+ def root(path, options={})
+ if path.is_a?(String)
+ options[:to] = path
+ elsif path.is_a?(Hash) and options.empty?
+ options = path
+ else
+ raise ArgumentError, "must be called with a path and/or options"
+ end
+
if @scope[:scope_level] == :resources
with_scope_level(:root) do
scope(parent_resource.path) do
@@ -1459,11 +1515,11 @@ module ActionDispatch
end
def resource_scope? #:nodoc:
- [:resource, :resources].include? @scope[:scope_level]
+ RESOURCE_SCOPES.include? @scope[:scope_level]
end
def resource_method_scope? #:nodoc:
- [:collection, :member, :new].include? @scope[:scope_level]
+ RESOURCE_METHOD_SCOPES.include? @scope[:scope_level]
end
def with_exclusive_scope
@@ -1583,6 +1639,18 @@ module ActionDispatch
end
end
end
+
+ def set_member_mappings_for_resource
+ member do
+ get :edit if parent_resource.actions.include?(:edit)
+ get :show if parent_resource.actions.include?(:show)
+ if parent_resource.actions.include?(:update)
+ patch :update
+ put :update
+ end
+ delete :destroy if parent_resource.actions.include?(:destroy)
+ end
+ end
end
# Routing Concerns allow you to declare common routes that can be reused
diff --git a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
index 3d7b8878b8..2fb03f2712 100644
--- a/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
+++ b/actionpack/lib/action_dispatch/routing/polymorphic_routes.rb
@@ -34,7 +34,7 @@ module ActionDispatch
# == Prefixed polymorphic helpers
#
# In addition to <tt>polymorphic_url</tt> and <tt>polymorphic_path</tt> methods, a
- # number of prefixed helpers are available as a shorthand to <tt>:action => "..."</tt>
+ # number of prefixed helpers are available as a shorthand to <tt>action: "..."</tt>
# in options. Those are:
#
# * <tt>edit_polymorphic_url</tt>, <tt>edit_polymorphic_path</tt>
@@ -43,7 +43,7 @@ module ActionDispatch
# Example usage:
#
# edit_polymorphic_path(@post) # => "/posts/1/edit"
- # polymorphic_path(@post, :format => :pdf) # => "/posts/1.pdf"
+ # polymorphic_path(@post, format: :pdf) # => "/posts/1.pdf"
#
# == Usage with mounted engines
#
@@ -74,7 +74,18 @@ module ActionDispatch
# * <tt>:routing_type</tt> - Allowed values are <tt>:path</tt> or <tt>:url</tt>.
# Default is <tt>:url</tt>.
#
- # ==== Examples
+ # Also includes all the options from <tt>url_for</tt>. These include such
+ # things as <tt>:anchor</tt> or <tt>:trailing_slash</tt>. Example usage
+ # is given below:
+ #
+ # polymorphic_url([blog, post], anchor: 'my_anchor')
+ # # => "http://example.com/blogs/1/posts/1#my_anchor"
+ # polymorphic_url([blog, post], anchor: 'my_anchor', script_name: "/my_app")
+ # # => "http://example.com/my_app/blogs/1/posts/1#my_anchor"
+ #
+ # For all of these options, see the documentation for <tt>url_for</tt>.
+ #
+ # ==== Functionality
#
# # an Article record
# polymorphic_url(record) # same as article_url(record)
@@ -132,7 +143,7 @@ module ActionDispatch
end
# Returns the path component of a URL for the given record. It uses
- # <tt>polymorphic_url</tt> with <tt>:routing_type => :path</tt>.
+ # <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))
end
diff --git a/actionpack/lib/action_dispatch/routing/redirection.rb b/actionpack/lib/action_dispatch/routing/redirection.rb
index 205ff44b1c..3e54c7e71c 100644
--- a/actionpack/lib/action_dispatch/routing/redirection.rb
+++ b/actionpack/lib/action_dispatch/routing/redirection.rb
@@ -17,7 +17,7 @@ module ActionDispatch
def call(env)
req = Request.new(env)
- # If any of the path parameters has a invalid encoding then
+ # If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
req.symbolized_path_parameters.each do |key, value|
unless value.valid_encoding?
@@ -30,6 +30,10 @@ module ActionDispatch
uri.host ||= req.host
uri.port ||= req.port unless req.standard_port?
+ if relative_path?(uri.path)
+ uri.path = "#{req.script_name}/#{uri.path}"
+ end
+
body = %(<html><body>You are being <a href="#{ERB::Util.h(uri.to_s)}">redirected</a>.</body></html>)
headers = {
@@ -48,6 +52,11 @@ module ActionDispatch
def inspect
"redirect(#{status})"
end
+
+ private
+ def relative_path?(path)
+ path && !path.empty? && path[0] != '/'
+ end
end
class PathRedirect < Redirect
@@ -75,12 +84,17 @@ module ActionDispatch
:port => request.optional_port,
:path => request.path,
:params => request.query_parameters
- }.merge options
+ }.merge! options
if !params.empty? && url_options[:path].match(/%\{\w*\}/)
url_options[:path] = (url_options[:path] % escape_path(params))
end
+ if relative_path?(url_options[:path])
+ url_options[:path] = "/#{url_options[:path]}"
+ url_options[:script_name] = request.script_name
+ end
+
ActionDispatch::Http::URL.url_for url_options
end
@@ -98,11 +112,15 @@ module ActionDispatch
# Redirect any path to another path:
#
- # match "/stories" => redirect("/posts")
+ # get "/stories" => redirect("/posts")
#
# You can also use interpolation in the supplied redirect argument:
#
- # match 'docs/:article', :to => redirect('/wiki/%{article}')
+ # get 'docs/:article', to: redirect('/wiki/%{article}')
+ #
+ # Note that if you return a path without a leading slash then the url is prefixed with the
+ # current SCRIPT_NAME environment variable. This is typically '/' but may be different in
+ # a mounted engine or where the application is deployed to a subdirectory of a website.
#
# Alternatively you can use one of the other syntaxes:
#
@@ -111,25 +129,25 @@ module ActionDispatch
# params, depending of how many arguments your block accepts. A string is required as a
# return value.
#
- # match 'jokes/:number', :to => redirect { |params, request|
+ # get 'jokes/:number', to: redirect { |params, request|
# path = (params[:number].to_i.even? ? "wheres-the-beef" : "i-love-lamp")
# "http://#{request.host_with_port}/#{path}"
# }
#
# Note that the +do end+ syntax for the redirect block wouldn't work, as Ruby would pass
- # the block to +match+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
+ # the block to +get+ instead of +redirect+. Use <tt>{ ... }</tt> instead.
#
# The options version of redirect allows you to supply only the parts of the url which need
# to change, it also supports interpolation of the path similar to the first example.
#
- # match 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
- # match 'stores/:name(*all)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{all}')
+ # get 'stores/:name', to: redirect(subdomain: 'stores', path: '/%{name}')
+ # get 'stores/:name(*all)', to: redirect(subdomain: 'stores', path: '/%{name}%{all}')
#
# Finally, an object which responds to call can be supplied to redirect, allowing you to reuse
# common redirect routes. The call method must accept two arguments, params and request, and return
# a string.
#
- # match 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
+ # get 'accounts/:name' => redirect(SubdomainRedirector.new('api'))
#
def redirect(*args, &block)
options = args.extract_options!
diff --git a/actionpack/lib/action_dispatch/routing/route_set.rb b/actionpack/lib/action_dispatch/routing/route_set.rb
index 060d0bfa2f..b8abdabca5 100644
--- a/actionpack/lib/action_dispatch/routing/route_set.rb
+++ b/actionpack/lib/action_dispatch/routing/route_set.rb
@@ -1,8 +1,10 @@
-require 'journey'
+require 'action_dispatch/journey'
require 'forwardable'
+require 'thread_safe'
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'
module ActionDispatch
@@ -20,15 +22,17 @@ module ActionDispatch
def initialize(options={})
@defaults = options[:defaults]
@glob_param = options.delete(:glob)
- @controllers = {}
+ @controller_class_names = ThreadSafe::Cache.new
end
def call(env)
params = env[PARAMETERS_KEY]
- # If any of the path parameters has a invalid encoding then
+ # If any of the path parameters has an invalid encoding then
# raise since it's likely to trigger errors further on.
params.each do |key, value|
+ next unless value.respond_to?(:valid_encoding?)
+
unless value.valid_encoding?
raise ActionController::BadRequest, "Invalid parameter: #{key} => #{value}"
end
@@ -68,13 +72,8 @@ module ActionDispatch
private
def controller_reference(controller_param)
- controller_name = "#{controller_param.camelize}Controller"
-
- unless controller = @controllers[controller_param]
- controller = @controllers[controller_param] =
- ActiveSupport::Dependencies.reference(controller_name)
- end
- controller.get(controller_name)
+ const_name = @controller_class_names[controller_param] ||= "#{controller_param.camelize}Controller"
+ ActiveSupport::Dependencies.constantize(const_name)
end
def dispatch(controller, action, env)
@@ -104,32 +103,18 @@ module ActionDispatch
def initialize
@routes = {}
@helpers = []
- @module = Module.new do
- protected
-
- def handle_positional_args(args, options, segment_keys)
- inner_options = args.extract_options!
- result = options.dup
-
- if args.size > 0
- keys = segment_keys
- if args.size < keys.size - 1 # take format into account
- keys -= self.url_options.keys if self.respond_to?(:url_options)
- keys -= options.keys
- end
- result.merge!(Hash[keys.zip(args)])
- end
-
- result.merge!(inner_options)
- end
- end
+ @module = Module.new
end
def helper_names
- self.module.instance_methods.map(&:to_s)
+ @helpers.map(&:to_s)
end
def clear!
+ @helpers.each do |helper|
+ @module.remove_possible_method helper
+ end
+
@routes.clear
@helpers.clear
end
@@ -160,68 +145,145 @@ module ActionDispatch
routes.length
end
- private
+ class UrlHelper # :nodoc:
+ def self.create(route, options)
+ if optimize_helper?(route)
+ OptimizedUrlHelper.new(route, options)
+ else
+ new route, options
+ end
+ 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)
+ def self.optimize_helper?(route)
+ route.requirements.except(:controller, :action).empty?
end
- # Create a url helper allowing ordered parameters to be associated
- # with corresponding dynamic segments, so you can do:
- #
- # foo_url(bar, baz, bang)
- #
- # Instead of:
- #
- # foo_url(:bar => bar, :baz => baz, :bang => bang)
- #
- # Also allow options hash, so you can do:
- #
- # foo_url(bar, baz, bang, :sort_by => 'baz')
- #
- def define_url_helper(route, name, options)
- @module.module_eval <<-END_EVAL, __FILE__, __LINE__ + 1
- remove_possible_method :#{name}
- def #{name}(*args)
- if #{optimize_helper?(route)} && args.size == #{route.required_parts.size} && !args.last.is_a?(Hash) && optimize_routes_generation?
- options = #{options.inspect}
- options.merge!(url_options) if respond_to?(:url_options)
- options[:path] = "#{optimized_helper(route)}"
- ActionDispatch::Http::URL.url_for(options)
- else
- url_for(handle_positional_args(args, #{options.inspect}, #{route.segment_keys.inspect}))
+ class OptimizedUrlHelper < UrlHelper # :nodoc:
+ attr_reader :arg_size
+
+ def initialize(route, options)
+ super
+ @path_parts = @route.required_parts
+ @arg_size = @path_parts.size
+ @string_route = @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)
+ options[:path] = optimized_helper(args)
+ ActionDispatch::Http::URL.url_for(options)
+ else
+ super
+ end
+ end
+
+ private
+
+ def optimized_helper(args)
+ path = @string_route.dup
+ klass = Journey::Router::Utils
+
+ @path_parts.zip(args) do |part, arg|
+ parameterized_arg = arg.to_param
+
+ if parameterized_arg.nil? || parameterized_arg.empty?
+ raise_generation_error(args)
end
+
+ # Replace each route parameter
+ # e.g. :id for regular parameter or *path for globbing
+ # with ruby string interpolation code
+ path.gsub!(/(\*|:)#{part}/, klass.escape_fragment(parameterized_arg))
end
- END_EVAL
+ path
+ end
+
+ def optimize_routes_generation?(t)
+ t.send(:optimize_routes_generation?)
+ end
- helpers << name
+ def raise_generation_error(args)
+ parts, missing_keys = [], []
+
+ @path_parts.zip(args) do |part, arg|
+ parameterized_arg = arg.to_param
+
+ if parameterized_arg.nil? || parameterized_arg.empty?
+ missing_keys << part
+ end
+
+ parts << [part, arg]
+ end
+
+ message = "No route matches #{Hash[parts].inspect}"
+ message << " missing required keys: #{missing_keys.inspect}"
+
+ raise ActionController::UrlGenerationError, message
+ end
end
- # Clause check about when we need to generate an optimized helper.
- def optimize_helper?(route) #:nodoc:
- route.requirements.except(:controller, :action).empty?
+ def initialize(route, options)
+ @options = options
+ @segment_keys = route.segment_keys
+ @route = route
end
- # Generates the interpolation to be used in the optimized helper.
- def optimized_helper(route)
- string_route = route.ast.to_s
+ def call(t, args)
+ t.url_for(handle_positional_args(t, args, @options, @segment_keys))
+ end
- while string_route.gsub!(/\([^\)]*\)/, "")
- true
- end
+ def handle_positional_args(t, args, options, keys)
+ inner_options = args.extract_options!
+ result = options.dup
- route.required_parts.each_with_index do |part, i|
- # Replace each route parameter
- # e.g. :id for regular parameter or *path for globbing
- # with ruby string interpolation code
- string_route.gsub!(/(\*|:)#{part}/, "\#{Journey::Router::Utils.escape_fragment(args[#{i}].to_param)}")
+ 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
+ end
+ keys -= inner_options.keys
+ result.merge!(Hash[keys.zip(args)])
end
- string_route
+ result.merge!(inner_options)
end
+ end
+
+ private
+ # Create a url helper allowing ordered parameters to be associated
+ # with corresponding dynamic segments, so you can do:
+ #
+ # foo_url(bar, baz, bang)
+ #
+ # Instead of:
+ #
+ # foo_url(bar: bar, baz: baz, bang: bang)
+ #
+ # Also allow options hash, so you can do:
+ #
+ # 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
+ define_method(name) do |*args|
+ helper.call self, args
+ 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
attr_accessor :formatter, :set, :named_routes, :default_scope, :router
@@ -368,11 +430,19 @@ module ActionDispatch
def add_route(app, conditions = {}, requirements = {}, defaults = {}, name = nil, anchor = true)
raise ArgumentError, "Invalid route name: '#{name}'" unless name.blank? || name.to_s.match(/^[_a-z]\w*$/i)
+ if name && named_routes[name]
+ raise ArgumentError, "Invalid route name, already in use: '#{name}' \n" \
+ "You may have defined two routes with the same name using the `:as` option, or " \
+ "you may be overriding a route already defined by a resource with the same naming. " \
+ "For the latter, you can restrict the routes created with `resources` as explained here: \n" \
+ "http://guides.rubyonrails.org/routing.html#restricting-the-routes-created"
+ end
+
path = build_path(conditions.delete(:path_info), requirements, SEPARATORS, anchor)
conditions = build_conditions(conditions, path.names.map { |x| x.to_sym })
route = @set.add_route(app, path, conditions, defaults, name)
- named_routes[name] = route if name && !named_routes[name]
+ named_routes[name] = route if name
route
end
@@ -419,7 +489,7 @@ module ActionDispatch
end
conditions.keep_if do |k, _|
- k == :action || k == :controller ||
+ k == :action || k == :controller || k == :required_defaults ||
@request_class.public_method_defined?(k) || path_values.include?(k)
end
end
@@ -444,11 +514,12 @@ module ActionDispatch
@recall = recall.dup
@set = set
+ normalize_recall!
normalize_options!
normalize_controller_action_id!
use_relative_controller!
normalize_controller!
- handle_nil_action!
+ normalize_action!
end
def controller
@@ -467,11 +538,16 @@ module ActionDispatch
end
end
+ # Set 'index' as default action for recall
+ def normalize_recall!
+ @recall[:action] ||= 'index'
+ end
+
def normalize_options!
# If an explicit :controller was given, always make :action explicit
# too, so that action expiry works as expected for things like
#
- # generate({:controller => 'content'}, {:controller => 'content', :action => 'show'})
+ # generate({controller: 'content'}, {controller: 'content', action: 'show'})
#
# (the above is from the unit tests). In the above case, because the
# controller was explicitly given, but no action, the action is implied to
@@ -482,8 +558,8 @@ module ActionDispatch
options[:controller] = options[:controller].to_s
end
- if options[:action]
- options[:action] = options[:action].to_s
+ if options.key?(:action)
+ options[:action] = (options[:action] || 'index').to_s
end
end
@@ -493,14 +569,12 @@ module ActionDispatch
# :controller, :action or :id is not found, don't pull any
# more keys from the recall.
def normalize_controller_action_id!
- @recall[:action] ||= 'index' if current_controller
-
use_recall_for(:controller) or return
use_recall_for(:action) or return
use_recall_for(:id)
end
- # if the current controller is "foo/bar/baz" and :controller => "baz/bat"
+ # if the current controller is "foo/bar/baz" and controller: "baz/bat"
# is specified, the controller becomes "foo/baz/bat"
def use_relative_controller!
if !named_route && different_controller? && !controller.start_with?("/")
@@ -516,21 +590,17 @@ module ActionDispatch
@options[:controller] = controller.sub(%r{^/}, '') if controller
end
- # This handles the case of :action => nil being explicitly passed.
- # It is identical to :action => "index"
- def handle_nil_action!
- if options.has_key?(:action) && options[:action].nil?
- options[:action] = 'index'
+ # Move 'index' action from options to recall
+ def normalize_action!
+ if @options[:action] == 'index'
+ @recall[:action] = @options.delete(:action)
end
- recall[:action] = options.delete(:action) if options[:action] == 'index'
end
- # Generates a path from routes, returns [path, params]
- # if no path is returned the formatter will raise Journey::Router::RoutingError
+ # 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)
- rescue Journey::Router::RoutingError => e
- raise ActionController::UrlGenerationError, "No route matches #{options.inspect} #{e.message}"
end
def different_controller?
@@ -624,7 +694,7 @@ module ActionDispatch
end
req = @request_class.new(env)
- @router.recognize(req) do |route, matches, params|
+ @router.recognize(req) do |route, _matches, params|
params.merge!(extras)
params.each do |key, value|
if value.is_a?(String)
diff --git a/actionpack/lib/action_dispatch/routing/routes_proxy.rb b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
index 73af5920ed..e2393d3799 100644
--- a/actionpack/lib/action_dispatch/routing/routes_proxy.rb
+++ b/actionpack/lib/action_dispatch/routing/routes_proxy.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/array/extract_options'
+
module ActionDispatch
module Routing
class RoutesProxy #:nodoc:
diff --git a/actionpack/lib/action_dispatch/routing/url_for.rb b/actionpack/lib/action_dispatch/routing/url_for.rb
index d4cd537048..bcebe532bf 100644
--- a/actionpack/lib/action_dispatch/routing/url_for.rb
+++ b/actionpack/lib/action_dispatch/routing/url_for.rb
@@ -18,9 +18,9 @@ module ActionDispatch
# of parameters. For example, you've probably had the chance to write code
# like this in one of your views:
#
- # <%= link_to('Click here', :controller => 'users',
- # :action => 'new', :message => 'Welcome!') %>
- # # => "/users/new?message=Welcome%21"
+ # <%= link_to('Click here', controller: 'users',
+ # action: 'new', message: 'Welcome!') %>
+ # # => <a href="/users/new?message=Welcome%21">Click here</a>
#
# link_to, and all other functions that require URL generation functionality,
# actually use ActionController::UrlFor under the hood. And in particular,
@@ -28,22 +28,22 @@ module ActionDispatch
# the same path as the above example by using the following code:
#
# include UrlFor
- # url_for(:controller => 'users',
- # :action => 'new',
- # :message => 'Welcome!',
- # :only_path => true)
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # only_path: true)
# # => "/users/new?message=Welcome%21"
#
- # Notice the <tt>:only_path => true</tt> part. This is because UrlFor has no
+ # Notice the <tt>only_path: true</tt> part. This is because UrlFor has no
# information about the website hostname that your Rails app is serving. So if you
# want to include the hostname as well, then you must also pass the <tt>:host</tt>
# argument:
#
# include UrlFor
- # url_for(:controller => 'users',
- # :action => 'new',
- # :message => 'Welcome!',
- # :host => 'www.example.com')
+ # url_for(controller: 'users',
+ # action: 'new',
+ # message: 'Welcome!',
+ # host: 'www.example.com')
# # => "http://www.example.com/users/new?message=Welcome%21"
#
# By default, all controllers and views have access to a special version of url_for,
@@ -130,18 +130,23 @@ module ActionDispatch
# * <tt>:port</tt> - Optionally specify the port to connect to.
# * <tt>:anchor</tt> - An anchor name to be appended to the path.
# * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2009/"
+ # * <tt>:script_name</tt> - Specifies application path relative to domain root. If provided, prepends application path.
#
# Any other key (<tt>:controller</tt>, <tt>:action</tt>, etc.) given to
# +url_for+ is forwarded to the Routes module.
#
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :port => '8080'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', port: '8080'
# # => 'http://somehost.org:8080/tasks/testing'
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :anchor => 'ok', :only_path => true
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', anchor: 'ok', only_path: true
# # => '/tasks/testing#ok'
- # url_for :controller => 'tasks', :action => 'testing', :trailing_slash => true
+ # url_for controller: 'tasks', action: 'testing', trailing_slash: true
# # => 'http://somehost.org/tasks/testing/'
- # url_for :controller => 'tasks', :action => 'testing', :host => 'somehost.org', :number => '33'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', number: '33'
# # => 'http://somehost.org/tasks/testing?number=33'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp"
+ # # => 'http://somehost.org/myapp/tasks/testing'
+ # url_for controller: 'tasks', action: 'testing', host: 'somehost.org', script_name: "/myapp", only_path: true
+ # # => '/myapp/tasks/testing'
def url_for(options = nil)
case options
when nil
diff --git a/actionpack/lib/action_dispatch/testing/assertions/dom.rb b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
index 6c61d4e61a..241a39393a 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/dom.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/dom.rb
@@ -7,20 +7,20 @@ module ActionDispatch
#
# # assert that the referenced method generates the appropriate HTML string
# assert_dom_equal '<a href="http://www.example.com">Apples</a>', link_to("Apples", "http://www.example.com")
- def assert_dom_equal(expected, actual, message = "")
+ def assert_dom_equal(expected, actual, message = nil)
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
- assert_equal expected_dom, actual_dom
+ assert_equal expected_dom, actual_dom, message
end
# The negated form of +assert_dom_equivalent+.
#
# # assert that the referenced method does not generate the specified HTML string
# assert_dom_not_equal '<a href="http://www.example.com">Apples</a>', link_to("Oranges", "http://www.example.com")
- def assert_dom_not_equal(expected, actual, message = "")
+ def assert_dom_not_equal(expected, actual, message = nil)
expected_dom = HTML::Document.new(expected).root
actual_dom = HTML::Document.new(actual).root
- refute_equal expected_dom, actual_dom
+ assert_not_equal expected_dom, actual_dom, message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/response.rb b/actionpack/lib/action_dispatch/testing/assertions/response.rb
index b15e0446de..93f9fab9c2 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/response.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/response.rb
@@ -35,11 +35,11 @@ module ActionDispatch
end
# Assert that the redirection options passed in match those of the redirect called in the latest action.
- # This match can be partial, such that <tt>assert_redirected_to(:controller => "weblog")</tt> will also
- # match the redirection of <tt>redirect_to(:controller => "weblog", :action => "show")</tt> and so on.
+ # This match can be partial, such that <tt>assert_redirected_to(controller: "weblog")</tt> will also
+ # match the redirection of <tt>redirect_to(controller: "weblog", action: "show")</tt> and so on.
#
# # assert that the redirection was to the "index" action on the WeblogController
- # assert_redirected_to :controller => "weblog", :action => "index"
+ # assert_redirected_to controller: "weblog", action: "index"
#
# # assert that the redirection was to the named route login_url
# assert_redirected_to login_url
@@ -67,21 +67,11 @@ module ActionDispatch
end
def normalize_argument_to_redirection(fragment)
- normalized = case fragment
- when Regexp
- fragment
- when %r{^\w[A-Za-z\d+.-]*:.*}
- fragment
- when String
- @request.protocol + @request.host_with_port + fragment
- when :back
- raise RedirectBackError unless refer = @request.headers["Referer"]
- refer
- else
- @controller.url_for(fragment)
- end
-
- normalized.respond_to?(:delete) ? normalized.delete("\0\r\n") : normalized
+ if Regexp === fragment
+ fragment
+ else
+ @controller._compute_redirect_to_location(fragment)
+ end
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/routing.rb b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
index 9de545b3c5..496682e8bd 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/routing.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/routing.rb
@@ -1,6 +1,6 @@
require 'uri'
-require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/string/access'
require 'action_controller/metal/exceptions'
module ActionDispatch
@@ -15,39 +15,41 @@ module ActionDispatch
# and a :method containing the required HTTP verb.
#
# # assert that POSTing to /items will call the create action on ItemsController
- # assert_recognizes({:controller => 'items', :action => 'create'}, {:path => 'items', :method => :post})
+ # assert_recognizes({controller: 'items', action: 'create'}, {path: 'items', method: :post})
#
# You can also pass in +extras+ with a hash containing URL parameters that would normally be in the query string. This can be used
# to assert that values in the query string string will end up in the params hash correctly. To test query strings you must use the
# extras argument, appending the query string on the path directly will not work. For example:
#
# # assert that a path of '/items/list/1?view=print' returns the correct options
- # assert_recognizes({:controller => 'items', :action => 'list', :id => '1', :view => 'print'}, 'items/list/1', { :view => "print" })
+ # assert_recognizes({controller: 'items', action: 'list', id: '1', view: 'print'}, 'items/list/1', { view: "print" })
#
# The +message+ parameter allows you to pass in an error message that is displayed upon failure.
#
# # Check the default route (i.e., the index action)
- # assert_recognizes({:controller => 'items', :action => 'index'}, 'items')
+ # assert_recognizes({controller: 'items', action: 'index'}, 'items')
#
# # Test a specific action
- # assert_recognizes({:controller => 'items', :action => 'list'}, 'items/list')
+ # assert_recognizes({controller: 'items', action: 'list'}, 'items/list')
#
# # Test an action with a parameter
- # assert_recognizes({:controller => 'items', :action => 'destroy', :id => '1'}, 'items/destroy/1')
+ # assert_recognizes({controller: 'items', action: 'destroy', id: '1'}, 'items/destroy/1')
#
# # Test a custom route
- # assert_recognizes({:controller => 'items', :action => 'show', :id => '1'}, 'view/item1')
- def assert_recognizes(expected_options, path, extras={}, message=nil)
+ # assert_recognizes({controller: 'items', action: 'show', id: '1'}, 'view/item1')
+ def assert_recognizes(expected_options, path, extras={}, msg=nil)
request = recognized_request_for(path, extras)
expected_options = expected_options.clone
expected_options.stringify_keys!
- # FIXME: minitest does object diffs, do we need to have our own?
- message ||= sprintf("The recognized options <%s> did not match <%s>, difference: <%s>",
- request.path_parameters, expected_options, expected_options.diff(request.path_parameters))
- assert_equal(expected_options, request.path_parameters, message)
+ msg = message(msg, "") {
+ sprintf("The recognized options <%s> did not match <%s>, difference:",
+ request.path_parameters, expected_options)
+ }
+
+ assert_equal(expected_options, request.path_parameters, msg)
end
# Asserts that the provided options can be used to generate the provided path. This is the inverse of +assert_recognizes+.
@@ -57,16 +59,16 @@ module ActionDispatch
# The +defaults+ parameter is unused.
#
# # Asserts that the default action is generated for a route with no action
- # assert_generates "/items", :controller => "items", :action => "index"
+ # assert_generates "/items", controller: "items", action: "index"
#
# # Tests that the list action is properly routed
- # assert_generates "/items/list", :controller => "items", :action => "list"
+ # assert_generates "/items/list", controller: "items", action: "list"
#
# # Tests the generation of a route with a parameter
- # assert_generates "/items/list/1", { :controller => "items", :action => "list", :id => "1" }
+ # assert_generates "/items/list/1", { controller: "items", action: "list", id: "1" }
#
# # Asserts that the generated route gives us our custom route
- # assert_generates "changesets/12", { :controller => 'scm', :action => 'show_diff', :revision => "12" }
+ # assert_generates "changesets/12", { controller: 'scm', action: 'show_diff', revision: "12" }
def assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)
if expected_path =~ %r{://}
fail_on(URI::InvalidURIError) do
@@ -79,7 +81,7 @@ module ActionDispatch
# Load routes.rb if it hasn't been loaded.
generated_path, extra_keys = @routes.generate_extras(options, defaults)
- found_extras = options.reject {|k, v| ! extra_keys.include? k}
+ found_extras = options.reject { |k, _| ! extra_keys.include? k }
msg = message || sprintf("found extras <%s>, not <%s>", found_extras, extras)
assert_equal(extras, found_extras, msg)
@@ -97,19 +99,19 @@ module ActionDispatch
# +message+ parameter allows you to specify a custom error message to display upon failure.
#
# # Assert a basic route: a controller with the default action (index)
- # assert_routing '/home', :controller => 'home', :action => 'index'
+ # assert_routing '/home', controller: 'home', action: 'index'
#
# # Test a route generated with a specific controller, action, and parameter (id)
- # assert_routing '/entries/show/23', :controller => 'entries', :action => 'show', :id => 23
+ # assert_routing '/entries/show/23', controller: 'entries', action: 'show', id: 23
#
# # Assert a basic route (controller + default action), with an error message if it fails
- # assert_routing '/store', { :controller => 'store', :action => 'index' }, {}, {}, 'Route for store index not generated properly'
+ # assert_routing '/store', { controller: 'store', action: 'index' }, {}, {}, 'Route for store index not generated properly'
#
# # Tests a route, providing a defaults hash
- # assert_routing 'controller/action/9', {:id => "9", :item => "square"}, {:controller => "controller", :action => "action"}, {}, {:item => "square"}
+ # assert_routing 'controller/action/9', {id: "9", item: "square"}, {controller: "controller", action: "action"}, {}, {item: "square"}
#
# # Tests a route with a HTTP method
- # assert_routing({ :method => 'put', :path => '/product/321' }, { :controller => "product", :action => "update", :id => "321" })
+ # assert_routing({ method: 'put', path: '/product/321' }, { controller: "product", action: "update", id: "321" })
def assert_routing(path, options, defaults={}, extras={}, message=nil)
assert_recognizes(options, path, extras, message)
@@ -118,7 +120,7 @@ module ActionDispatch
options[:controller] = "/#{controller}"
end
- generate_options = options.dup.delete_if{ |k,v| defaults.key?(k) }
+ generate_options = options.dup.delete_if{ |k, _| defaults.key?(k) }
assert_generates(path.is_a?(Hash) ? path[:path] : path, generate_options, defaults, extras, message)
end
@@ -207,11 +209,9 @@ module ActionDispatch
end
def fail_on(exception_class)
- begin
- yield
- rescue exception_class => e
- raise MiniTest::Assertion, e.message
- end
+ yield
+ rescue exception_class => e
+ raise MiniTest::Assertion, e.message
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/selector.rb b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
index 9388d44eef..3253a3d424 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/selector.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/selector.rb
@@ -155,8 +155,6 @@ module ActionDispatch
# If the method is called with a block, once all equality tests are
# evaluated the block is called with an array of all matched elements.
#
- # ==== Examples
- #
# # At least one form element
# assert_select "form"
#
@@ -167,7 +165,7 @@ module ActionDispatch
# assert_select "title", "Welcome"
#
# # Page title is "Welcome" and there is only one title element
- # assert_select "title", {:count => 1, :text => "Welcome"},
+ # assert_select "title", {count: 1, text: "Welcome"},
# "Wrong title or more than one title element"
#
# # Page contains no forms
@@ -379,8 +377,8 @@ module ActionDispatch
node.content.gsub(/<!\[CDATA\[(.*)(\]\]>)?/m) { Rack::Utils.escapeHTML($1) }
end
- selected = elements.map do |_element|
- text = _element.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
+ selected = elements.map do |elem|
+ text = elem.children.select{ |c| not c.tag? }.map{ |c| fix_content[c] }.join
root = HTML::Document.new(CGI.unescapeHTML("<encoded>#{text}</encoded>")).root
css_select(root, "encoded:root", &block)[0]
end
diff --git a/actionpack/lib/action_dispatch/testing/assertions/tag.rb b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
index 2e38266aba..e5fe30ba82 100644
--- a/actionpack/lib/action_dispatch/testing/assertions/tag.rb
+++ b/actionpack/lib/action_dispatch/testing/assertions/tag.rb
@@ -49,44 +49,44 @@ module ActionDispatch
# * if the condition is +false+ or +nil+, the value must be +nil+.
#
# # Assert that there is a "span" tag
- # assert_tag :tag => "span"
+ # assert_tag tag: "span"
#
# # Assert that there is a "span" tag with id="x"
- # assert_tag :tag => "span", :attributes => { :id => "x" }
+ # assert_tag tag: "span", attributes: { id: "x" }
#
# # Assert that there is a "span" tag using the short-hand
# assert_tag :span
#
# # Assert that there is a "span" tag with id="x" using the short-hand
- # assert_tag :span, :attributes => { :id => "x" }
+ # assert_tag :span, attributes: { id: "x" }
#
# # Assert that there is a "span" inside of a "div"
- # assert_tag :tag => "span", :parent => { :tag => "div" }
+ # assert_tag tag: "span", parent: { tag: "div" }
#
# # Assert that there is a "span" somewhere inside a table
- # assert_tag :tag => "span", :ancestor => { :tag => "table" }
+ # assert_tag tag: "span", ancestor: { tag: "table" }
#
# # Assert that there is a "span" with at least one "em" child
- # assert_tag :tag => "span", :child => { :tag => "em" }
+ # assert_tag tag: "span", child: { tag: "em" }
#
# # Assert that there is a "span" containing a (possibly nested)
# # "strong" tag.
- # assert_tag :tag => "span", :descendant => { :tag => "strong" }
+ # assert_tag tag: "span", descendant: { tag: "strong" }
#
# # Assert that there is a "span" containing between 2 and 4 "em" tags
# # as immediate children
- # assert_tag :tag => "span",
- # :children => { :count => 2..4, :only => { :tag => "em" } }
+ # assert_tag tag: "span",
+ # children: { count: 2..4, only: { tag: "em" } }
#
# # Get funky: assert that there is a "div", with an "ul" ancestor
# # and an "li" parent (with "class" = "enum"), and containing a
# # "span" descendant that contains text matching /hello world/
- # assert_tag :tag => "div",
- # :ancestor => { :tag => "ul" },
- # :parent => { :tag => "li",
- # :attributes => { :class => "enum" } },
- # :descendant => { :tag => "span",
- # :child => /hello world/ }
+ # assert_tag tag: "div",
+ # ancestor: { tag: "ul" },
+ # parent: { tag: "li",
+ # attributes: { class: "enum" } },
+ # descendant: { tag: "span",
+ # child: /hello world/ }
#
# <b>Please note</b>: +assert_tag+ and +assert_no_tag+ only work
# with well-formed XHTML. They recognize a few tags as implicitly self-closing
@@ -103,15 +103,15 @@ module ActionDispatch
# exist. (See +assert_tag+ for a full discussion of the syntax.)
#
# # Assert that there is not a "div" containing a "p"
- # assert_no_tag :tag => "div", :descendant => { :tag => "p" }
+ # assert_no_tag tag: "div", descendant: { tag: "p" }
#
# # Assert that an unordered list is empty
- # assert_no_tag :tag => "ul", :descendant => { :tag => "li" }
+ # assert_no_tag tag: "ul", descendant: { tag: "li" }
#
# # Assert that there is not a "p" tag with between 1 to 3 "img" tags
# # as immediate children
- # assert_no_tag :tag => "p",
- # :children => { :count => 1..3, :only => { :tag => "img" } }
+ # assert_no_tag tag: "p",
+ # children: { count: 1..3, only: { tag: "img" } }
def assert_no_tag(*opts)
opts = opts.size > 1 ? opts.last.merge({ :tag => opts.first.to_s }) : opts.first
tag = find_tag(opts)
diff --git a/actionpack/lib/action_dispatch/testing/integration.rb b/actionpack/lib/action_dispatch/testing/integration.rb
index 4bd7b69642..9beb30307b 100644
--- a/actionpack/lib/action_dispatch/testing/integration.rb
+++ b/actionpack/lib/action_dispatch/testing/integration.rb
@@ -3,6 +3,7 @@ require 'uri'
require 'active_support/core_ext/kernel/singleton_class'
require 'active_support/core_ext/object/try'
require 'rack/test'
+require 'minitest'
module ActionDispatch
module Integration #:nodoc:
@@ -16,7 +17,7 @@ module ActionDispatch
# a Hash, or a String that is appropriately encoded
# (<tt>application/x-www-form-urlencoded</tt> or
# <tt>multipart/form-data</tt>).
- # - +headers+: Additional headers to pass, as a Hash. The headers will be
+ # - +headers_or_env+: Additional headers to pass, as a Hash. The headers will be
# merged into the Rack env hash.
#
# This method returns a Response object, which one can use to
@@ -27,44 +28,38 @@ module ActionDispatch
#
# You can also perform POST, PATCH, PUT, DELETE, and HEAD requests with
# +#post+, +#patch+, +#put+, +#delete+, and +#head+.
- def get(path, parameters = nil, headers = nil)
- process :get, path, parameters, headers
+ def get(path, parameters = nil, headers_or_env = nil)
+ process :get, path, parameters, headers_or_env
end
# Performs a POST request with the given parameters. See +#get+ for more
# details.
- def post(path, parameters = nil, headers = nil)
- process :post, path, parameters, headers
+ def post(path, parameters = nil, headers_or_env = nil)
+ process :post, path, parameters, headers_or_env
end
# Performs a PATCH request with the given parameters. See +#get+ for more
# details.
- def patch(path, parameters = nil, headers = nil)
- process :patch, path, parameters, headers
+ def patch(path, parameters = nil, headers_or_env = nil)
+ process :patch, path, parameters, headers_or_env
end
# Performs a PUT request with the given parameters. See +#get+ for more
# details.
- def put(path, parameters = nil, headers = nil)
- process :put, path, parameters, headers
+ def put(path, parameters = nil, headers_or_env = nil)
+ process :put, path, parameters, headers_or_env
end
# Performs a DELETE request with the given parameters. See +#get+ for
# more details.
- def delete(path, parameters = nil, headers = nil)
- process :delete, path, parameters, headers
+ def delete(path, parameters = nil, headers_or_env = nil)
+ process :delete, path, parameters, headers_or_env
end
# Performs a HEAD request with the given parameters. See +#get+ for more
# details.
- def head(path, parameters = nil, headers = nil)
- process :head, path, parameters, headers
- end
-
- # Performs a OPTIONS request with the given parameters. See +#get+ for
- # more details.
- def options(path, parameters = nil, headers = nil)
- process :options, path, parameters, headers
+ def head(path, parameters = nil, headers_or_env = nil)
+ process :head, path, parameters, headers_or_env
end
# Performs an XMLHttpRequest request with the given parameters, mirroring
@@ -73,11 +68,11 @@ module ActionDispatch
# The request_method is +:get+, +:post+, +:patch+, +:put+, +:delete+ or
# +:head+; the parameters are +nil+, a hash, or a url-encoded or multipart
# string; the headers are a hash.
- def xml_http_request(request_method, path, parameters = nil, headers = nil)
- headers ||= {}
- headers['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
- headers['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
- process(request_method, path, parameters, headers)
+ def xml_http_request(request_method, path, parameters = nil, headers_or_env = nil)
+ headers_or_env ||= {}
+ headers_or_env['HTTP_X_REQUESTED_WITH'] = 'XMLHttpRequest'
+ headers_or_env['HTTP_ACCEPT'] ||= [Mime::JS, Mime::HTML, Mime::XML, 'text/xml', Mime::ALL].join(', ')
+ process(request_method, path, parameters, headers_or_env)
end
alias xhr :xml_http_request
@@ -94,40 +89,40 @@ module ActionDispatch
# redirect. Note that the redirects are followed until the response is
# not a redirect--this means you may run into an infinite loop if your
# redirect loops back to itself.
- def request_via_redirect(http_method, path, parameters = nil, headers = nil)
- process(http_method, path, parameters, headers)
+ def request_via_redirect(http_method, path, parameters = nil, headers_or_env = nil)
+ process(http_method, path, parameters, headers_or_env)
follow_redirect! while redirect?
status
end
# Performs a GET request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def get_via_redirect(path, parameters = nil, headers = nil)
- request_via_redirect(:get, path, parameters, headers)
+ def get_via_redirect(path, parameters = nil, headers_or_env = nil)
+ request_via_redirect(:get, path, parameters, headers_or_env)
end
# Performs a POST request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def post_via_redirect(path, parameters = nil, headers = nil)
- request_via_redirect(:post, path, parameters, headers)
+ def post_via_redirect(path, parameters = nil, headers_or_env = nil)
+ request_via_redirect(:post, path, parameters, headers_or_env)
end
# Performs a PATCH request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def patch_via_redirect(path, parameters = nil, headers = nil)
- request_via_redirect(:patch, path, parameters, headers)
+ def patch_via_redirect(path, parameters = nil, headers_or_env = nil)
+ request_via_redirect(:patch, path, parameters, headers_or_env)
end
# Performs a PUT request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def put_via_redirect(path, parameters = nil, headers = nil)
- request_via_redirect(:put, path, parameters, headers)
+ def put_via_redirect(path, parameters = nil, headers_or_env = nil)
+ request_via_redirect(:put, path, parameters, headers_or_env)
end
# Performs a DELETE request, following any subsequent redirect.
# See +request_via_redirect+ for more information.
- def delete_via_redirect(path, parameters = nil, headers = nil)
- request_via_redirect(:delete, path, parameters, headers)
+ def delete_via_redirect(path, parameters = nil, headers_or_env = nil)
+ request_via_redirect(:delete, path, parameters, headers_or_env)
end
end
@@ -267,12 +262,11 @@ module ActionDispatch
end
# Performs the actual request.
- def process(method, path, parameters = nil, rack_env = nil)
- rack_env ||= {}
+ def process(method, path, parameters = nil, headers_or_env = nil)
if path =~ %r{://}
location = URI.parse(path)
https! URI::HTTPS === location if location.scheme
- host! location.host if location.host
+ host! "#{location.host}:#{location.port}" if location.host
path = location.query ? "#{location.path}?#{location.query}" : location.path
end
@@ -299,11 +293,11 @@ module ActionDispatch
"CONTENT_TYPE" => "application/x-www-form-urlencoded",
"HTTP_ACCEPT" => accept
}
+ # this modifies the passed env directly
+ Http::Headers.new(env).merge!(headers_or_env || {})
session = Rack::Test::Session.new(_mock_session)
- env.merge!(rack_env)
-
# NOTE: rack-test v0.5 doesn't build a default uri correctly
# Make sure requested path is always a full uri
uri = URI.parse('/')
@@ -340,7 +334,7 @@ module ActionDispatch
@integration_session = Integration::Session.new(app)
end
- %w(get post patch put head delete options cookies assigns
+ %w(get post patch put head delete cookies assigns
xml_http_request xhr get_via_redirect post_via_redirect).each do |method|
define_method(method) do |*args|
reset! unless integration_session
@@ -429,8 +423,8 @@ module ActionDispatch
# assert_equal 200, status
#
# # post the login and follow through to the home page
- # post "/login", :username => people(:jamis).username,
- # :password => people(:jamis).password
+ # post "/login", username: people(:jamis).username,
+ # password: people(:jamis).password
# follow_redirect!
# assert_equal 200, status
# assert_equal "/home", path
@@ -463,13 +457,13 @@ module ActionDispatch
# module CustomAssertions
# def enter(room)
# # reference a named route, for maximum internal consistency!
- # get(room_url(:id => room.id))
+ # get(room_url(id: room.id))
# assert(...)
# ...
# end
#
# def speak(room, message)
- # xml_http_request "/say/#{room.id}", :message => message
+ # xml_http_request "/say/#{room.id}", message: message
# assert(...)
# ...
# end
@@ -479,8 +473,8 @@ module ActionDispatch
# open_session do |sess|
# sess.extend(CustomAssertions)
# who = people(who)
- # sess.post "/login", :username => who.username,
- # :password => who.password
+ # sess.post "/login", username: who.username,
+ # password: who.password
# assert(...)
# end
# end
@@ -490,17 +484,9 @@ module ActionDispatch
include ActionController::TemplateAssertions
include ActionDispatch::Routing::UrlFor
- # Use AD::IntegrationTest for acceptance tests
- register_spec_type(/(Acceptance|Integration) ?Test\z/i, self)
-
@@app = nil
def self.app
- if !@@app && !ActionDispatch.test_app
- ActiveSupport::Deprecation.warn "Rails application fallback is deprecated " \
- "and no longer works, please set ActionDispatch.test_app", caller
- end
-
@@app || ActionDispatch.test_app
end
diff --git a/actionpack/lib/action_dispatch/testing/performance_test.rb b/actionpack/lib/action_dispatch/testing/performance_test.rb
deleted file mode 100644
index 13fe693c32..0000000000
--- a/actionpack/lib/action_dispatch/testing/performance_test.rb
+++ /dev/null
@@ -1,10 +0,0 @@
-require 'active_support/testing/performance'
-
-module ActionDispatch
- # An integration test that runs a code profiler on your test methods.
- # Profiling output for combinations of each test method, measurement, and
- # output format are written to your tmp/performance directory.
- class PerformanceTest < ActionDispatch::IntegrationTest
- include ActiveSupport::Testing::Performance
- end
-end
diff --git a/actionpack/lib/action_dispatch/testing/test_process.rb b/actionpack/lib/action_dispatch/testing/test_process.rb
index 3a6d081721..630e6a9b78 100644
--- a/actionpack/lib/action_dispatch/testing/test_process.rb
+++ b/actionpack/lib/action_dispatch/testing/test_process.rb
@@ -6,7 +6,7 @@ module ActionDispatch
module TestProcess
def assigns(key = nil)
assigns = {}.with_indifferent_access
- @controller.view_assigns.each {|k, v| assigns.regular_writer(k, v)}
+ @controller.view_assigns.each { |k, v| assigns.regular_writer(k, v) }
key.nil? ? assigns : assigns[key]
end
@@ -26,17 +26,19 @@ module ActionDispatch
@response.redirect_url
end
- # Shortcut for <tt>Rack::Test::UploadedFile.new(ActionController::TestCase.fixture_path + path, type)</tt>:
+ # Shortcut for <tt>Rack::Test::UploadedFile.new(File.join(ActionController::TestCase.fixture_path, path), type)</tt>:
#
- # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png')
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png')
#
# To upload binary files on Windows, pass <tt>:binary</tt> as the last parameter.
# This will not affect other platforms:
#
- # post :change_avatar, :avatar => fixture_file_upload('/files/spongebob.png', 'image/png', :binary)
+ # post :change_avatar, avatar: fixture_file_upload('files/spongebob.png', 'image/png', :binary)
def fixture_file_upload(path, mime_type = nil, binary = false)
- fixture_path = self.class.fixture_path if self.class.respond_to?(:fixture_path)
- Rack::Test::UploadedFile.new("#{fixture_path}#{path}", mime_type, binary)
+ if self.class.respond_to?(:fixture_path) && self.class.fixture_path
+ path = File.join(self.class.fixture_path, path)
+ end
+ Rack::Test::UploadedFile.new(path, mime_type, binary)
end
end
end
diff --git a/actionpack/lib/action_dispatch/testing/test_request.rb b/actionpack/lib/action_dispatch/testing/test_request.rb
index c63778f870..57c678843b 100644
--- a/actionpack/lib/action_dispatch/testing/test_request.rb
+++ b/actionpack/lib/action_dispatch/testing/test_request.rb
@@ -3,7 +3,11 @@ require 'rack/utils'
module ActionDispatch
class TestRequest < Request
- DEFAULT_ENV = Rack::MockRequest.env_for('/')
+ DEFAULT_ENV = Rack::MockRequest.env_for('/',
+ 'HTTP_HOST' => 'test.host',
+ 'REMOTE_ADDR' => '0.0.0.0',
+ 'HTTP_USER_AGENT' => 'Rails Testing'
+ )
def self.new(env = {})
super
@@ -12,10 +16,6 @@ module ActionDispatch
def initialize(env = {})
env = Rails.application.env_config.merge(env) if defined?(Rails.application) && Rails.application
super(default_env.merge(env))
-
- self.host = 'test.host'
- self.remote_addr = '0.0.0.0'
- self.user_agent = 'Rails Testing'
end
def request_method=(method)
diff --git a/actionpack/lib/action_pack.rb b/actionpack/lib/action_pack.rb
index 39c7faf740..ad5acd8080 100644
--- a/actionpack/lib/action_pack.rb
+++ b/actionpack/lib/action_pack.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
diff --git a/actionpack/lib/action_pack/version.rb b/actionpack/lib/action_pack/version.rb
index 94cbc8e478..fd08f392aa 100644
--- a/actionpack/lib/action_pack/version.rb
+++ b/actionpack/lib/action_pack/version.rb
@@ -1,10 +1,11 @@
module ActionPack
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActionPack as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActionPack.version.segments
+ STRING = ActionPack.version.to_s
end
end
diff --git a/actionpack/lib/action_view.rb b/actionpack/lib/action_view.rb
deleted file mode 100644
index 091b0d8cd2..0000000000
--- a/actionpack/lib/action_view.rb
+++ /dev/null
@@ -1,94 +0,0 @@
-#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
-#
-# Permission is hereby granted, free of charge, to any person obtaining
-# a copy of this software and associated documentation files (the
-# "Software"), to deal in the Software without restriction, including
-# without limitation the rights to use, copy, modify, merge, publish,
-# distribute, sublicense, and/or sell copies of the Software, and to
-# permit persons to whom the Software is furnished to do so, subject to
-# the following conditions:
-#
-# The above copyright notice and this permission notice shall be
-# included in all copies or substantial portions of the Software.
-#
-# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
-# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
-# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
-# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
-# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
-# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
-# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
-#++
-
-require 'active_support'
-require 'active_support/rails'
-require 'action_pack'
-
-module ActionView
- extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :AssetPaths
- autoload :Base
- autoload :Context
- autoload :CompiledTemplates, "action_view/context"
- autoload :Digestor
- autoload :Helpers
- autoload :LookupContext
- autoload :PathSet
- autoload :RecordIdentifier
- autoload :RoutingUrlFor
- autoload :Template
-
- autoload_under "renderer" do
- autoload :Renderer
- autoload :AbstractRenderer
- autoload :PartialRenderer
- autoload :TemplateRenderer
- autoload :StreamingTemplateRenderer
- end
-
- autoload_at "action_view/template/resolver" do
- autoload :Resolver
- autoload :PathResolver
- autoload :FileSystemResolver
- autoload :OptimizedFileSystemResolver
- autoload :FallbackFileSystemResolver
- end
-
- autoload_at "action_view/buffers" do
- autoload :OutputBuffer
- autoload :StreamingBuffer
- end
-
- autoload_at "action_view/flows" do
- autoload :OutputFlow
- autoload :StreamingFlow
- end
-
- autoload_at "action_view/template/error" do
- autoload :MissingTemplate
- autoload :ActionViewError
- autoload :EncodingError
- autoload :MissingRequestError
- autoload :TemplateError
- autoload :WrongEncodingError
- end
- end
-
- autoload :TestCase
-
- ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
-
- def self.eager_load!
- super
- ActionView::Template.eager_load!
- end
-end
-
-require 'active_support/core_ext/string/output_safety'
-
-ActiveSupport.on_load(:i18n) do
- I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml"
-end
diff --git a/actionpack/lib/action_view/asset_paths.rb b/actionpack/lib/action_view/asset_paths.rb
deleted file mode 100644
index 4bbb31b3ee..0000000000
--- a/actionpack/lib/action_view/asset_paths.rb
+++ /dev/null
@@ -1,143 +0,0 @@
-require 'zlib'
-require 'active_support/core_ext/file'
-
-module ActionView
- class AssetPaths #:nodoc:
- URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}
-
- attr_reader :config, :controller
-
- def initialize(config, controller = nil)
- @config = config
- @controller = controller
- end
-
- # Add the extension +ext+ if not present. Return full or scheme-relative URLs otherwise untouched.
- # Prefix with <tt>/dir/</tt> if lacking a leading +/+. Account for relative URL
- # roots. Rewrite the asset path for cache-busting asset ids. Include
- # asset host, if configured, with the correct request protocol.
- #
- # When :relative (default), the protocol will be determined by the client using current protocol
- # When :request, the protocol will be the request protocol
- # Otherwise, the protocol is used (E.g. :http, :https, etc)
- def compute_public_path(source, dir, options = {})
- source = source.to_s
- return source if is_uri?(source)
-
- source = rewrite_extension(source, dir, options[:ext]) if options[:ext]
- source = rewrite_asset_path(source, dir, options)
- source = rewrite_relative_url_root(source, relative_url_root)
- source = rewrite_host_and_protocol(source, options[:protocol])
- source
- end
-
- # Return the filesystem path for the source
- def compute_source_path(source, dir, ext)
- source = rewrite_extension(source, dir, ext) if ext
-
- sources = []
- sources << config.assets_dir
- sources << dir unless source[0] == ?/
- sources << source
-
- File.join(sources)
- end
-
- def is_uri?(path)
- path =~ URI_REGEXP
- end
-
- private
-
- def rewrite_extension(source, dir, ext)
- raise NotImplementedError
- end
-
- def rewrite_asset_path(source, path = nil)
- raise NotImplementedError
- end
-
- def rewrite_relative_url_root(source, relative_url_root)
- relative_url_root && !source.starts_with?("#{relative_url_root}/") ? "#{relative_url_root}#{source}" : source
- end
-
- def has_request?
- controller.respond_to?(:request)
- end
-
- def rewrite_host_and_protocol(source, protocol = nil)
- host = compute_asset_host(source)
- if host && !is_uri?(host)
- if (protocol || default_protocol) == :request && !has_request?
- host = nil
- else
- host = "#{compute_protocol(protocol)}#{host}"
- end
- end
- host ? "#{host}#{source}" : source
- end
-
- def compute_protocol(protocol)
- protocol ||= default_protocol
- case protocol
- when :relative
- "//"
- when :request
- unless @controller
- invalid_asset_host!("The protocol requested was :request. Consider using :relative instead.")
- end
- @controller.request.protocol
- else
- "#{protocol}://"
- end
- end
-
- def default_protocol
- @config.default_asset_host_protocol || (has_request? ? :request : :relative)
- end
-
- def invalid_asset_host!(help_message)
- raise ActionView::MissingRequestError, "This asset host cannot be computed without a request in scope. #{help_message}"
- end
-
- # Pick an asset host for this source. Returns +nil+ if no host is set,
- # the host if no wildcard is set, the host interpolated with the
- # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
- # or the value returned from invoking call on an object responding to call
- # (proc or otherwise).
- def compute_asset_host(source)
- if host = asset_host_config
- if host.respond_to?(:call)
- args = [source]
- arity = arity_of(host)
- if (arity > 1 || arity < -2) && !has_request?
- invalid_asset_host!("Remove the second argument to your asset_host Proc if you do not need the request, or make it optional.")
- end
- args << current_request if (arity > 1 || arity < 0) && has_request?
- host.call(*args)
- else
- (host =~ /%d/) ? host % (Zlib.crc32(source) % 4) : host
- end
- end
- end
-
- def relative_url_root
- config.relative_url_root || current_request.try(:script_name)
- end
-
- def asset_host_config
- config.asset_host
- end
-
- # Returns the current request if one exists.
- def current_request
- controller.request if has_request?
- end
-
- # Returns the arity of a callable
- def arity_of(callable)
- callable.respond_to?(:arity) ? callable.arity : callable.method(:call).arity
- end
-
- end
-end
diff --git a/actionpack/lib/action_view/base.rb b/actionpack/lib/action_view/base.rb
deleted file mode 100644
index 3464ec523e..0000000000
--- a/actionpack/lib/action_view/base.rb
+++ /dev/null
@@ -1,201 +0,0 @@
-require 'active_support/core_ext/module/attr_internal'
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/ordered_options'
-require 'action_view/log_subscriber'
-
-module ActionView #:nodoc:
- # = Action View Base
- #
- # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERb
- # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used.
- #
- # == ERB
- #
- # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
- # following loop for names:
- #
- # <b>Names of all the people</b>
- # <% @people.each do |person| %>
- # Name: <%= person.name %><br/>
- # <% end %>
- #
- # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
- # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
- #
- # <%# WRONG %>
- # Hi, Mr. <% puts "Frodo" %>
- #
- # If you absolutely must write from within a function use +concat+.
- #
- # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>.
- #
- # === Using sub templates
- #
- # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
- # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
- #
- # <%= render "shared/header" %>
- # Something really specific and terrific
- # <%= render "shared/footer" %>
- #
- # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
- # result of the rendering. The output embedding writes it to the current template.
- #
- # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
- # variables defined using the regular embedding tags. Like this:
- #
- # <% @page_title = "A Wonderful Hello" %>
- # <%= render "shared/header" %>
- #
- # Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag:
- #
- # <title><%= @page_title %></title>
- #
- # === Passing local variables to sub templates
- #
- # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
- #
- # <%= render "shared/header", { :headline => "Welcome", :person => person } %>
- #
- # These can now be accessed in <tt>shared/header</tt> with:
- #
- # 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,
- # Rails will check the file's modification time and recompile it in development mode.
- #
- # == Builder
- #
- # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object
- # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
- #
- # Here are some basic examples:
- #
- # xml.em("emphasized") # => <em>emphasized</em>
- # xml.em { xml.b("emph & bold") } # => <em><b>emph &amp; bold</b></em>
- # xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
- # xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\>
- # # NOTE: order of attributes is not specified.
- #
- # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
- #
- # xml.div do
- # xml.h1(@person.name)
- # xml.p(@person.bio)
- # end
- #
- # would produce something like:
- #
- # <div>
- # <h1>David Heinemeier Hansson</h1>
- # <p>A product of Danish Design during the Winter of '79...</p>
- # </div>
- #
- # A full-length RSS example actually used on Basecamp:
- #
- # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
- # xml.channel do
- # xml.title(@feed_title)
- # xml.link(@url)
- # xml.description "Basecamp: Recent items"
- # xml.language "en-us"
- # xml.ttl "40"
- #
- # @recent_items.each do |item|
- # xml.item do
- # xml.title(item_title(item))
- # xml.description(item_description(item)) if item_description(item)
- # xml.pubDate(item_pubDate(item))
- # xml.guid(@person.firm.account.url + @recent_items.url(item))
- # xml.link(@person.firm.account.url + @recent_items.url(item))
- #
- # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
- # end
- # end
- # end
- # end
- #
- # More builder documentation can be found at http://builder.rubyforge.org.
- class Base
- include Helpers, ::ERB::Util, Context
-
- # Specify the proc used to decorate input tags that refer to attributes with errors.
- cattr_accessor :field_error_proc
- @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
-
- # How to complete the streaming when an exception occurs.
- # This is our best guess: first try to close the attribute, then the tag.
- cattr_accessor :streaming_completion_on_exception
- @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>)
-
- # Specify whether rendering within namespaced controllers should prefix
- # the partial paths for ActiveModel objects with the namespace.
- # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
- cattr_accessor :prefix_partial_path_with_controller_namespace
- @@prefix_partial_path_with_controller_namespace = true
-
- # Specify default_formats that can be rendered.
- cattr_accessor :default_formats
-
- class_attribute :_routes
- class_attribute :logger
-
- class << self
- delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB'
-
- def cache_template_loading
- ActionView::Resolver.caching?
- end
-
- def cache_template_loading=(value)
- ActionView::Resolver.caching = value
- end
-
- def xss_safe? #:nodoc:
- true
- end
- end
-
- attr_accessor :view_renderer
- attr_internal :config, :assigns
-
- delegate :lookup_context, :to => :view_renderer
- delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context
-
- def assign(new_assigns) # :nodoc:
- @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
- end
-
- def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
- @_config = ActiveSupport::InheritableOptions.new
-
- if context.is_a?(ActionView::Renderer)
- @view_renderer = context
- else
- lookup_context = context.is_a?(ActionView::LookupContext) ?
- context : ActionView::LookupContext.new(context)
- lookup_context.formats = formats if formats
- lookup_context.prefixes = controller._prefixes if controller
- @view_renderer = ActionView::Renderer.new(lookup_context)
- end
-
- assign(assigns)
- assign_controller(controller)
- _prepare_context
- end
-
- ActiveSupport.run_load_hooks(:action_view, self)
- end
-end
diff --git a/actionpack/lib/action_view/buffers.rb b/actionpack/lib/action_view/buffers.rb
deleted file mode 100644
index 2372d3c433..0000000000
--- a/actionpack/lib/action_view/buffers.rb
+++ /dev/null
@@ -1,43 +0,0 @@
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView
- class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
- def initialize(*)
- super
- encode!
- end
-
- def <<(value)
- super(value.to_s)
- end
- alias :append= :<<
- alias :safe_append= :safe_concat
- end
-
- class StreamingBuffer #:nodoc:
- def initialize(block)
- @block = block
- end
-
- def <<(value)
- value = value.to_s
- value = ERB::Util.h(value) unless value.html_safe?
- @block.call(value)
- end
- alias :concat :<<
- alias :append= :<<
-
- def safe_concat(value)
- @block.call(value.to_s)
- end
- alias :safe_append= :safe_concat
-
- def html_safe?
- true
- end
-
- def html_safe
- self
- end
- end
-end
diff --git a/actionpack/lib/action_view/digestor.rb b/actionpack/lib/action_view/digestor.rb
deleted file mode 100644
index f5852dbe73..0000000000
--- a/actionpack/lib/action_view/digestor.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-module ActionView
- class Digestor
- EXPLICIT_DEPENDENCY = /# Template Dependency: ([^ ]+)/
-
- # Matches:
- # render partial: "comments/comment", collection: commentable.comments
- # render "comments/comments"
- # render 'comments/comments'
- # render('comments/comments')
- #
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
- RENDER_DEPENDENCY = /
- render\s* # render, followed by optional whitespace
- \(? # start an optional parenthesis for the render call
- (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture
- ([@a-z"'][@a-z_\/\."']+) # the template name itself -- 2nd capture
- /x
-
- cattr_reader(:cache)
- @@cache = Hash.new
-
- def self.digest(name, format, finder, options = {})
- cache["#{name}.#{format}"] ||= new(name, format, finder, options).digest
- end
-
- attr_reader :name, :format, :finder, :options
-
- def initialize(name, format, finder, options = {})
- @name, @format, @finder, @options = name, format, finder, options
- end
-
- def digest
- Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
- logger.try :info, "Cache digest for #{name}.#{format}: #{digest}"
- end
- rescue ActionView::MissingTemplate
- logger.try :error, "Couldn't find template for digesting: #{name}.#{format}"
- ''
- end
-
- def dependencies
- render_dependencies + explicit_dependencies
- rescue ActionView::MissingTemplate
- [] # File doesn't exist, so no dependencies
- end
-
- def nested_dependencies
- dependencies.collect do |dependency|
- dependencies = Digestor.new(dependency, format, finder, partial: true).nested_dependencies
- dependencies.any? ? { dependency => dependencies } : dependency
- end
- end
-
- private
-
- def logger
- ActionView::Base.logger
- end
-
- def logical_name
- name.gsub(%r|/_|, "/")
- end
-
- def directory
- name.split("/")[0..-2].join("/")
- end
-
- def partial?
- options[:partial] || name.include?("/_")
- end
-
- def source
- @source ||= finder.find(logical_name, [], partial?, formats: [ format ]).source
- end
-
- def dependency_digest
- dependencies.collect do |template_name|
- Digestor.digest(template_name, format, finder, partial: true)
- end.join("-")
- end
-
- def render_dependencies
- source.scan(RENDER_DEPENDENCY).
- collect(&:second).uniq.
-
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
- collect { |name| name.sub(/\A@?([a-z]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.
-
- # render("headline") => render("message/headline")
- collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.
-
- # replace quotes from string renders
- collect { |name| name.gsub(/["']/, "") }
- end
-
- def explicit_dependencies
- source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
- end
- end
-end
diff --git a/actionpack/lib/action_view/flows.rb b/actionpack/lib/action_view/flows.rb
deleted file mode 100644
index c0e458cd41..0000000000
--- a/actionpack/lib/action_view/flows.rb
+++ /dev/null
@@ -1,76 +0,0 @@
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView
- class OutputFlow #:nodoc:
- attr_reader :content
-
- def initialize
- @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
- end
-
- # Called by _layout_for to read stored values.
- def get(key)
- @content[key]
- end
-
- # Called by each renderer object to set the layout contents.
- def set(key, value)
- @content[key] = value
- end
-
- # Called by content_for
- def append(key, value)
- @content[key] << value
- end
- alias_method :append!, :append
-
- end
-
- class StreamingFlow < OutputFlow #:nodoc:
- def initialize(view, fiber)
- @view = view
- @parent = nil
- @child = view.output_buffer
- @content = view.view_flow.content
- @fiber = fiber
- @root = Fiber.current.object_id
- end
-
- # Try to get an stored content. If the content
- # is not available and we are inside the layout
- # fiber, we set that we are waiting for the given
- # key and yield.
- def get(key)
- return super if @content.key?(key)
-
- if inside_fiber?
- view = @view
-
- begin
- @waiting_for = key
- view.output_buffer, @parent = @child, view.output_buffer
- Fiber.yield
- ensure
- @waiting_for = nil
- view.output_buffer, @child = @parent, view.output_buffer
- end
- end
-
- super
- end
-
- # Appends the contents for the given key. This is called
- # by provides and resumes back to the fiber if it is
- # the key it is waiting for.
- def append!(key, value)
- super
- @fiber.resume if @waiting_for == key
- end
-
- private
-
- def inside_fiber?
- Fiber.current.object_id != @root
- end
- end
-end \ No newline at end of file
diff --git a/actionpack/lib/action_view/helpers.rb b/actionpack/lib/action_view/helpers.rb
deleted file mode 100644
index 269e78a021..0000000000
--- a/actionpack/lib/action_view/helpers.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-module ActionView #:nodoc:
- module Helpers #:nodoc:
- extend ActiveSupport::Autoload
-
- autoload :ActiveModelHelper
- autoload :AssetTagHelper
- autoload :AssetUrlHelper
- autoload :AtomFeedHelper
- autoload :BenchmarkHelper
- autoload :CacheHelper
- autoload :CaptureHelper
- autoload :ControllerHelper
- autoload :CsrfHelper
- autoload :DateHelper
- autoload :DebugHelper
- autoload :FormHelper
- autoload :FormOptionsHelper
- autoload :FormTagHelper
- autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
- autoload :NumberHelper
- autoload :OutputSafetyHelper
- autoload :RecordTagHelper
- autoload :RenderingHelper
- autoload :SanitizeHelper
- autoload :TagHelper
- autoload :TextHelper
- autoload :TranslationHelper
- autoload :UrlHelper
-
- extend ActiveSupport::Concern
-
- include ActiveModelHelper
- include AssetTagHelper
- include AssetUrlHelper
- include AtomFeedHelper
- include BenchmarkHelper
- include CacheHelper
- include CaptureHelper
- include ControllerHelper
- include CsrfHelper
- include DateHelper
- include DebugHelper
- include FormHelper
- include FormOptionsHelper
- include FormTagHelper
- include JavaScriptHelper
- include NumberHelper
- include OutputSafetyHelper
- include RecordTagHelper
- include RenderingHelper
- include SanitizeHelper
- include TagHelper
- include TextHelper
- include TranslationHelper
- include UrlHelper
- end
-end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helper.rb b/actionpack/lib/action_view/helpers/asset_tag_helper.rb
deleted file mode 100644
index 4eac6514df..0000000000
--- a/actionpack/lib/action_view/helpers/asset_tag_helper.rb
+++ /dev/null
@@ -1,293 +0,0 @@
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/hash/keys'
-require 'action_view/helpers/asset_url_helper'
-require 'action_view/helpers/tag_helper'
-
-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
- # the assets exist before linking to them:
- #
- # image_tag("rails.png")
- # # => <img alt="Rails" src="/assets/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
- #
- module AssetTagHelper
- extend ActiveSupport::Concern
-
- include AssetUrlHelper
- include TagHelper
-
- # Returns an HTML script tag for each of the +sources+ provided.
- #
- # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
- # to <tt>public/javascripts</tt>, full paths are assumed to be relative to the document
- # root. Relative paths are idiomatic, use absolute paths only when needed.
- #
- # When passing paths, the ".js" extension is optional.
- #
- # You can modify the HTML attributes of the script tag by passing a hash as the
- # last argument.
- #
- # javascript_include_tag "xmlhr"
- # # => <script src="/javascripts/xmlhr.js?1284139606"></script>
- #
- # javascript_include_tag "xmlhr.js"
- # # => <script src="/javascripts/xmlhr.js?1284139606"></script>
- #
- # javascript_include_tag "common.javascript", "/elsewhere/cools"
- # # => <script src="/javascripts/common.javascript?1284139606"></script>
- # # <script src="/elsewhere/cools.js?1423139606"></script>
- #
- # javascript_include_tag "http://www.example.com/xmlhr"
- # # => <script src="http://www.example.com/xmlhr"></script>
- #
- # javascript_include_tag "http://www.example.com/xmlhr.js"
- # # => <script src="http://www.example.com/xmlhr.js"></script>
- #
- def javascript_include_tag(*sources)
- options = sources.extract_options!.stringify_keys
- sources.uniq.map { |source|
- tag_options = {
- "src" => path_to_javascript(source)
- }.merge(options)
- content_tag(:script, "", tag_options)
- }.join("\n").html_safe
- end
-
- # Returns a stylesheet link tag for the sources specified as arguments. If
- # you don't specify an extension, <tt>.css</tt> will be appended automatically.
- # You can modify the link attributes by passing a hash as the last argument.
- # For historical reasons, the 'media' attribute will always be present and defaults
- # to "screen", so you must explicitely set it to "all" for the stylesheet(s) to
- # apply to all media types.
- #
- # stylesheet_link_tag "style" # =>
- # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" />
- #
- # stylesheet_link_tag "style.css" # =>
- # <link href="/stylesheets/style.css" media="screen" rel="stylesheet" />
- #
- # stylesheet_link_tag "http://www.example.com/style.css" # =>
- # <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
- #
- # stylesheet_link_tag "style", :media => "all" # =>
- # <link href="/stylesheets/style.css" media="all" rel="stylesheet" />
- #
- # stylesheet_link_tag "style", :media => "print" # =>
- # <link href="/stylesheets/style.css" media="print" rel="stylesheet" />
- #
- # stylesheet_link_tag "random.styles", "/css/stylish" # =>
- # <link href="/stylesheets/random.styles" media="screen" rel="stylesheet" />
- # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
- #
- def stylesheet_link_tag(*sources)
- options = sources.extract_options!.stringify_keys
- sources.uniq.map { |source|
- tag_options = {
- "rel" => "stylesheet",
- "media" => "screen",
- "href" => path_to_stylesheet(source)
- }.merge(options)
- tag(:link, tag_options)
- }.join("\n").html_safe
- end
-
- # Returns a link tag that browsers and news readers can use to auto-detect
- # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
- # <tt>:atom</tt>. Control the link options in url_for format using the
- # +url_options+. You can modify the LINK tag itself in +tag_options+.
- #
- # ==== Options
- # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
- # * <tt>:type</tt> - Override the auto-generated mime type
- # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
- #
- # ==== Examples
- # auto_discovery_link_tag
- # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
- # auto_discovery_link_tag(:atom)
- # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
- # auto_discovery_link_tag(:rss, {:action => "feed"})
- # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
- # auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})
- # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
- # auto_discovery_link_tag(:rss, {:controller => "news", :action => "feed"})
- # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
- # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "Example RSS"})
- # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
- def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
- if !(type == :rss || type == :atom) && tag_options[:type].blank?
- message = "You have passed type other than :rss or :atom to auto_discovery_link_tag and haven't supplied " +
- "the :type option key. This behavior is deprecated and will be remove in Rails 4.1. You should pass " +
- ":type option explicitly if you want to use other types, for example: " +
- "auto_discovery_link_tag(:xml, '/feed.xml', :type => 'application/xml')"
- ActiveSupport::Deprecation.warn message
- end
-
- tag(
- "link",
- "rel" => tag_options[:rel] || "alternate",
- "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
- "title" => tag_options[:title] || type.to_s.upcase,
- "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
- )
- end
-
- # <%= favicon_link_tag %>
- #
- # generates
- #
- # <link href="/assets/favicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
- #
- # You may specify a different file in the first argument:
- #
- # <%= favicon_link_tag '/myicon.ico' %>
- #
- # That's passed to +path_to_image+ as is, so it gives
- #
- # <link href="/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.icon" />
- #
- # The helper accepts an additional options hash where you can override "rel" and "type".
- #
- # For example, 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.
- # The following call would generate such a tag:
- #
- # <%= favicon_link_tag 'mb-icon.png', :rel => 'apple-touch-icon', :type => 'image/png' %>
- def favicon_link_tag(source='favicon.ico', options={})
- tag('link', {
- :rel => 'shortcut icon',
- :type => 'image/vnd.microsoft.icon',
- :href => path_to_image(source)
- }.merge(options.symbolize_keys))
- end
-
- # Returns an html image tag for the +source+. The +source+ can be a full
- # path or a file.
- #
- # ==== Options
- # You can add HTML attributes using the +options+. The +options+ supports
- # three additional keys for convenience and conformance:
- #
- # * <tt>:alt</tt> - If no alt text is given, the file name part of the
- # +source+ is used (capitalized and without the extension)
- # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
- # width="30" and height="45", and "50" becomes width="50" and height="50".
- # <tt>:size</tt> will be ignored if the value is not in the correct format.
- #
- # image_tag("icon")
- # # => <img alt="Icon" src="/assets/icon" />
- # image_tag("icon.png")
- # # => <img alt="Icon" src="/assets/icon.png" />
- # image_tag("icon.png", :size => "16x10", :alt => "Edit Entry")
- # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
- # image_tag("/icons/icon.gif", :size => "16")
- # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
- # image_tag("/icons/icon.gif", :height => '32', :width => '32')
- # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
- # image_tag("/icons/icon.gif", :class => "menu_icon")
- # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
- def image_tag(source, options={})
- options = options.symbolize_keys
-
- src = options[:src] = path_to_image(source)
-
- unless src =~ /^(?:cid|data):/ || src.blank?
- options[:alt] = options.fetch(:alt){ image_alt(src) }
- end
-
- if size = options.delete(:size)
- options[:width], options[:height] = size.split("x") if size =~ %r{\A\d+x\d+\z}
- options[:width] = options[:height] = size if size =~ %r{\A\d+\z}
- end
-
- tag("img", options)
- end
-
- def image_alt(src)
- File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').capitalize
- end
-
- # Returns an html video tag for the +sources+. If +sources+ is a string,
- # a single video tag will be returned. If +sources+ is an array, a video
- # tag with nested source tags for each source will be returned. The
- # +sources+ can be full paths or files that exists in your public videos
- # directory.
- #
- # ==== Options
- # You can add HTML attributes using the +options+. The +options+ supports
- # two additional keys for convenience and conformance:
- #
- # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
- # before the video loads. The path is calculated like the +src+ of +image_tag+.
- # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
- # width="30" and height="45". <tt>:size</tt> will be ignored if the
- # value is not in the correct format.
- #
- # video_tag("trailer")
- # # => <video src="/videos/trailer" />
- # video_tag("trailer.ogg")
- # # => <video src="/videos/trailer.ogg" />
- # video_tag("trailer.ogg", :controls => true, :autobuffer => true)
- # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" />
- # video_tag("trailer.m4v", :size => "16x10", :poster => "screenshot.png")
- # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" />
- # video_tag("/trailers/hd.avi", :size => "16x16")
- # # => <video src="/trailers/hd.avi" width="16" height="16" />
- # video_tag("/trailers/hd.avi", :height => '32', :width => '32')
- # # => <video height="32" src="/trailers/hd.avi" width="32" />
- # video_tag("trailer.ogg", "trailer.flv")
- # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></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"], :size => "160x120")
- # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
- def video_tag(*sources)
- multiple_sources_tag('video', sources) do |options|
- options[:poster] = path_to_image(options[:poster]) if options[:poster]
-
- if size = options.delete(:size)
- options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$}
- end
- end
- end
-
- # Returns an html audio tag for the +source+.
- # The +source+ can be full path or file that exists in
- # your public audios directory.
- #
- # audio_tag("sound") # =>
- # <audio src="/audios/sound" />
- # audio_tag("sound.wav") # =>
- # <audio src="/audios/sound.wav" />
- # audio_tag("sound.wav", :autoplay => true, :controls => true) # =>
- # <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" />
- # audio_tag("sound.wav", "sound.mid") # =>
- # <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
- def audio_tag(*sources)
- multiple_sources_tag('audio', sources)
- end
-
- private
- def multiple_sources_tag(type, sources)
- options = sources.extract_options!.symbolize_keys
- sources.flatten!
-
- yield options if block_given?
-
- if sources.size > 1
- content_tag(type, options) do
- safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) }
- end
- else
- options[:src] = send("path_to_#{type}", sources.first)
- content_tag(type, nil, options)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb b/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb
deleted file mode 100644
index 35f91cec18..0000000000
--- a/actionpack/lib/action_view/helpers/asset_tag_helpers/asset_paths.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'thread'
-require 'active_support/core_ext/file'
-require 'active_support/core_ext/module/attribute_accessors'
-
-module ActionView
- module Helpers
- module AssetTagHelper
-
- class AssetPaths < ::ActionView::AssetPaths #:nodoc:
- # You can enable or disable the asset tag ids cache.
- # With the cache enabled, the asset tag helper methods will make fewer
- # expensive file system calls (the default implementation checks the file
- # system timestamp). However this prevents you from modifying any asset
- # files while the server is running.
- #
- # ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false
- mattr_accessor :cache_asset_ids
-
- # Add or change an asset id in the asset id cache. This can be used
- # for SASS on Heroku.
- # :api: public
- def add_to_asset_ids_cache(source, asset_id)
- self.asset_ids_cache_guard.synchronize do
- self.asset_ids_cache[source] = asset_id
- end
- end
-
- private
-
- def rewrite_extension(source, dir, ext)
- source_ext = File.extname(source)
-
- source_with_ext = if source_ext.empty?
- "#{source}.#{ext}"
- elsif ext != source_ext[1..-1]
- with_ext = "#{source}.#{ext}"
- with_ext if File.exist?(File.join(config.assets_dir, dir, with_ext))
- end
-
- source_with_ext || source
- end
-
- # Break out the asset path rewrite in case plugins wish to put the asset id
- # someplace other than the query string.
- def rewrite_asset_path(source, dir, options = nil)
- source = "/#{dir}/#{source}" unless source[0] == ?/
- path = config.asset_path
-
- if path && path.respond_to?(:call)
- return path.call(source)
- elsif path && path.is_a?(String)
- return path % [source]
- end
-
- asset_id = rails_asset_id(source)
- if asset_id.empty?
- source
- else
- "#{source}?#{asset_id}"
- end
- end
-
- mattr_accessor :asset_ids_cache
- self.asset_ids_cache = {}
-
- mattr_accessor :asset_ids_cache_guard
- self.asset_ids_cache_guard = Mutex.new
-
- # Use the RAILS_ASSET_ID environment variable or the source's
- # modification time as its cache-busting asset id.
- def rails_asset_id(source)
- if asset_id = ENV["RAILS_ASSET_ID"]
- asset_id
- else
- if self.cache_asset_ids && (asset_id = self.asset_ids_cache[source])
- asset_id
- else
- path = File.join(config.assets_dir, source)
- asset_id = File.exist?(path) ? File.mtime(path).to_i.to_s : ''
-
- if self.cache_asset_ids
- add_to_asset_ids_cache(source, asset_id)
- end
-
- asset_id
- end
- end
- end
- end
-
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/asset_url_helper.rb b/actionpack/lib/action_view/helpers/asset_url_helper.rb
deleted file mode 100644
index 4554c0c473..0000000000
--- a/actionpack/lib/action_view/helpers/asset_url_helper.rb
+++ /dev/null
@@ -1,334 +0,0 @@
-require 'action_view/helpers/asset_tag_helpers/asset_paths'
-
-module ActionView
- # = Action View Asset URL Helpers
- module Helpers #:nodoc:
- # This module provides methods for generating asset paths and
- # urls.
- #
- # image_path("rails.png")
- # # => "/assets/rails.png"
- #
- # image_url("rails.png")
- # # => "http://www.example.com/assets/rails.png"
- #
- # === Using asset hosts
- #
- # By default, Rails links to these assets on the current host in the public
- # folder, but you can direct Rails to link to assets from a dedicated asset
- # server by setting <tt>ActionController::Base.asset_host</tt> in the application
- # configuration, typically in <tt>config/environments/production.rb</tt>.
- # For example, you'd define <tt>assets.example.com</tt> to be your asset
- # host this way, inside the <tt>configure</tt> block of your environment-specific
- # configuration files or <tt>config/application.rb</tt>:
- #
- # config.action_controller.asset_host = "assets.example.com"
- #
- # Helpers take that into account:
- #
- # image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
- #
- # Browsers typically open at most two simultaneous connections to a single
- # host, which means your assets often have to wait for other assets to finish
- # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
- # +asset_host+. For example, "assets%d.example.com". If that wildcard is
- # present Rails distributes asset requests among the corresponding four hosts
- # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
- # will open eight simultaneous connections rather than two.
- #
- # image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
- #
- # To do this, you can either setup four actual hosts, or you can use wildcard
- # DNS to CNAME the wildcard to a single asset host. You can read more about
- # setting up your DNS CNAME records from your ISP.
- #
- # Note: This is purely a browser performance optimization and is not meant
- # for server load balancing. See http://www.die.net/musings/page_load_time/
- # for background.
- #
- # Alternatively, you can exert more control over the asset host by setting
- # +asset_host+ to a proc like this:
- #
- # ActionController::Base.asset_host = Proc.new { |source|
- # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
- # }
- # image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
- #
- # The example above generates "http://assets1.example.com" and
- # "http://assets2.example.com". This option is useful for example if
- # you need fewer/more than four hosts, custom host names, etc.
- #
- # As you see the proc takes a +source+ parameter. That's a string with the
- # absolute path of the asset, for example "/assets/rails.png".
- #
- # ActionController::Base.asset_host = Proc.new { |source|
- # if source.ends_with?('.css')
- # "http://stylesheets.example.com"
- # else
- # "http://assets.example.com"
- # end
- # }
- # image_tag("rails.png")
- # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
- #
- # Alternatively you may ask for a second parameter +request+. That one is
- # particularly useful for serving assets from an SSL-protected page. The
- # example proc below disables asset hosting for HTTPS connections, while
- # 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.
- #
- # config.action_controller.asset_host = Proc.new { |source, request|
- # if request.ssl?
- # "#{request.protocol}#{request.host_with_port}"
- # else
- # "#{request.protocol}assets.example.com"
- # end
- # }
- #
- # You can also implement a custom asset host object that responds to +call+
- # and takes either one or two parameters just like the proc.
- #
- # config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
- # "http://asset%d.example.com", "https://asset1.example.com"
- # )
- #
- # === Customizing the asset path
- #
- # By default, Rails appends asset's timestamps to all asset paths. This allows
- # you to set a cache-expiration date for the asset far into the future, but
- # still be able to instantly invalidate it by simply updating the file (and
- # hence updating the timestamp, which then updates the URL as the timestamp
- # is part of that, which in turn busts the cache).
- #
- # It's the responsibility of the web server you use to set the far-future
- # expiration date on cache assets that you need to take advantage of this
- # feature. Here's an example for Apache:
- #
- # # Asset Expiration
- # ExpiresActive On
- # <FilesMatch "\.(ico|gif|jpe?g|png|js|css)$">
- # ExpiresDefault "access plus 1 year"
- # </FilesMatch>
- #
- # Also note that in order for this to work, all your application servers must
- # return the same timestamps. This means that they must have their clocks
- # synchronized. If one of them drifts out of sync, you'll see different
- # timestamps at random and the cache won't work. In that case the browser
- # will request the same assets over and over again even thought they didn't
- # change. You can use something like Live HTTP Headers for Firefox to verify
- # that the cache is indeed working.
- #
- # This strategy works well enough for most server setups and requires the
- # least configuration, but if you deploy several application servers at
- # different times - say to handle a temporary spike in load - then the
- # asset time stamps will be out of sync. In a setup like this you may want
- # to set the way that asset paths are generated yourself.
- #
- # Altering the asset paths that Rails generates can be done in two ways.
- # The easiest is to define the RAILS_ASSET_ID environment variable. The
- # contents of this variable will always be used in preference to
- # calculated timestamps. A more complex but flexible way is to set
- # <tt>ActionController::Base.config.asset_path</tt> to a proc
- # that takes the unmodified asset path and returns the path needed for
- # your asset caching to work. Typically you'd do something like this in
- # <tt>config/environments/production.rb</tt>:
- #
- # # Normally you'd calculate RELEASE_NUMBER at startup.
- # RELEASE_NUMBER = 12345
- # config.action_controller.asset_path = proc { |asset_path|
- # "/release-#{RELEASE_NUMBER}#{asset_path}"
- # }
- #
- # This example would cause the following behavior on all servers no
- # matter when they were deployed:
- #
- # image_tag("rails.png")
- # # => <img alt="Rails" src="/release-12345/images/rails.png" />
- # stylesheet_link_tag("application")
- # # => <link href="/release-12345/stylesheets/application.css?1232285206" media="screen" rel="stylesheet" />
- #
- # Changing the asset_path does require that your web servers have
- # knowledge of the asset template paths that you rewrite to so it's not
- # suitable for out-of-the-box use. To use the example given above you
- # could use something like this in your Apache VirtualHost configuration:
- #
- # <LocationMatch "^/release-\d+/(images|javascripts|stylesheets)/.*$">
- # # Some browsers still send conditional-GET requests if there's a
- # # Last-Modified header or an ETag header even if they haven't
- # # reached the expiry date sent in the Expires header.
- # Header unset Last-Modified
- # Header unset ETag
- # FileETag None
- #
- # # Assets requested using a cache-busting filename should be served
- # # only once and then cached for a really long time. The HTTP/1.1
- # # spec frowns on hugely-long expiration times though and suggests
- # # that assets which never expire be served with an expiration date
- # # 1 year from access.
- # ExpiresActive On
- # ExpiresDefault "access plus 1 year"
- # </LocationMatch>
- #
- # # We use cached-busting location names with the far-future expires
- # # headers to ensure that if a file does change it can force a new
- # # request. The actual asset filenames are still the same though so we
- # # need to rewrite the location from the cache-busting location to the
- # # real asset location so that we can serve it.
- # RewriteEngine On
- # RewriteRule ^/release-\d+/(images|javascripts|stylesheets)/(.*)$ /$1/$2 [L]
- #
- module AssetUrlHelper
- # 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.
- #
- # javascript_path "xmlhr" # => /javascripts/xmlhr.js
- # javascript_path "dir/xmlhr.js" # => /javascripts/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
- def javascript_path(source)
- asset_paths.compute_public_path(source, 'javascripts', :ext => 'js')
- 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.
- # This will use +javascript_path+ internally, so most of their behaviors will be the same.
- def javascript_url(source)
- URI.join(current_host, path_to_javascript(source)).to_s
- end
- 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).
- # 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 "/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
- def stylesheet_path(source)
- asset_paths.compute_public_path(source, 'stylesheets', :ext => 'css', :protocol => :request)
- end
- alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
-
- # Computes the full URL to a stylesheet asset in the public stylesheets directory.
- # This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
- def stylesheet_url(source)
- URI.join(current_host, path_to_stylesheet(source)).to_s
- end
- alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route
-
- # Computes the path to an image asset.
- # Full paths from the document root will be passed through.
- # Used internally by +image_tag+ to build the image path:
- #
- # image_path("edit") # => "/assets/edit"
- # image_path("edit.png") # => "/assets/edit.png"
- # image_path("icons/edit.png") # => "/assets/icons/edit.png"
- # image_path("/icons/edit.png") # => "/icons/edit.png"
- # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png"
- #
- # If you have images as application resources this method may conflict with their named routes.
- # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
- # plugin authors are encouraged to do so.
- def image_path(source)
- source.present? ? asset_paths.compute_public_path(source, 'images') : ""
- end
- alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
-
- # Computes the full URL to an image asset.
- # This will use +image_path+ internally, so most of their behaviors will be the same.
- def image_url(source)
- URI.join(current_host, path_to_image(source)).to_s
- end
- alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route
-
- # Computes the path to a video asset in the public videos directory.
- # Full paths from the document root will be passed through.
- # Used internally by +video_tag+ to build the video path.
- #
- # video_path("hd") # => /videos/hd
- # video_path("hd.avi") # => /videos/hd.avi
- # video_path("trailers/hd.avi") # => /videos/trailers/hd.avi
- # video_path("/trailers/hd.avi") # => /trailers/hd.avi
- # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi
- def video_path(source)
- asset_paths.compute_public_path(source, 'videos')
- end
- alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
-
- # Computes the full URL to a video asset in the public videos directory.
- # This will use +video_path+ internally, so most of their behaviors will be the same.
- def video_url(source)
- URI.join(current_host, path_to_video(source)).to_s
- end
- alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route
-
- # Computes the path to an audio asset in the public audios directory.
- # Full paths from the document root will be passed through.
- # Used internally by +audio_tag+ to build the audio path.
- #
- # audio_path("horse") # => /audios/horse
- # audio_path("horse.wav") # => /audios/horse.wav
- # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav
- # audio_path("/sounds/horse.wav") # => /sounds/horse.wav
- # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav
- def audio_path(source)
- asset_paths.compute_public_path(source, 'audios')
- end
- alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
-
- # Computes the full URL to an audio asset in the public audios directory.
- # This will use +audio_path+ internally, so most of their behaviors will be the same.
- def audio_url(source)
- URI.join(current_host, path_to_audio(source)).to_s
- end
- alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route
-
- # 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("/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)
- asset_paths.compute_public_path(source, 'fonts')
- end
- alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route
-
- # Computes the full URL to a font asset.
- # This will use +font_path+ internally, so most of their behaviors will be the same.
- def font_url(source)
- URI.join(current_host, path_to_font(source)).to_s
- end
- alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route
-
- private
- def asset_paths
- @asset_paths ||= AssetTagHelper::AssetPaths.new(config, controller)
- end
-
- def current_host
- url_for(:only_path => false)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/atom_feed_helper.rb b/actionpack/lib/action_view/helpers/atom_feed_helper.rb
deleted file mode 100644
index f9aa8d7cee..0000000000
--- a/actionpack/lib/action_view/helpers/atom_feed_helper.rb
+++ /dev/null
@@ -1,203 +0,0 @@
-require 'set'
-
-module ActionView
- # = Action View Atom Feed Helpers
- module Helpers #:nodoc:
- module AtomFeedHelper
- # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
- # template languages).
- #
- # Full usage example:
- #
- # config/routes.rb:
- # Basecamp::Application.routes.draw do
- # resources :posts
- # root :to => "posts#index"
- # end
- #
- # app/controllers/posts_controller.rb:
- # class PostsController < ApplicationController::Base
- # # GET /posts.html
- # # GET /posts.atom
- # def index
- # @posts = Post.all
- #
- # respond_to do |format|
- # format.html
- # format.atom
- # end
- # end
- # end
- #
- # app/views/posts/index.atom.builder:
- # atom_feed do |feed|
- # feed.title("My great blog!")
- # feed.updated(@posts[0].created_at) if @posts.length > 0
- #
- # @posts.each do |post|
- # feed.entry(post) do |entry|
- # entry.title(post.title)
- # entry.content(post.body, :type => 'html')
- #
- # entry.author do |author|
- # author.name("DHH")
- # end
- # end
- # end
- # end
- #
- # The options for atom_feed are:
- #
- # * <tt>:language</tt>: Defaults to "en-US".
- # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
- # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
- # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}"
- # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
- # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
- # 2005 is used (as an "I don't care" value).
- # * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]}
- #
- # Other namespaces can be added to the root element:
- #
- # app/views/posts/index.atom.builder:
- # atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
- # 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
- # feed.title("My great blog!")
- # feed.updated((@posts.first.created_at))
- # feed.tag!(openSearch:totalResults, 10)
- #
- # @posts.each do |post|
- # feed.entry(post) do |entry|
- # entry.title(post.title)
- # entry.content(post.body, :type => 'html')
- # entry.tag!('app:edited', Time.now)
- #
- # entry.author do |author|
- # author.name("DHH")
- # end
- # end
- # end
- # end
- #
- # The Atom spec defines five elements (content rights title subtitle
- # summary) which may directly contain xhtml content if :type => 'xhtml'
- # is specified as an attribute. If so, this helper will take care of
- # the enclosing div and xhtml namespace declaration. Example usage:
- #
- # entry.summary :type => 'xhtml' do |xhtml|
- # xhtml.p pluralize(order.line_items.count, "line item")
- # xhtml.p "Shipped to #{order.address}"
- # xhtml.p "Paid by #{order.pay_type}"
- # end
- #
- #
- # <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield
- # an +AtomBuilder+ instance.
- def atom_feed(options = {}, &block)
- if options[:schema_date]
- options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime)
- else
- options[:schema_date] = "2005" # The Atom spec copyright date
- end
-
- xml = options.delete(:xml) || eval("xml", block.binding)
- xml.instruct!
- if options[:instruct]
- options[:instruct].each do |target,attrs|
- if attrs.respond_to?(:keys)
- xml.instruct!(target, attrs)
- elsif attrs.respond_to?(:each)
- attrs.each { |attr_group| xml.instruct!(target, attr_group) }
- end
- end
- end
-
- feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
- feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
-
- xml.feed(feed_opts) do
- xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}")
- xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
- xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
-
- yield AtomFeedBuilder.new(xml, self, options)
- end
- end
-
- class AtomBuilder
- XHTML_TAG_NAMES = %w(content rights title subtitle summary).to_set
-
- def initialize(xml)
- @xml = xml
- end
-
- private
- # Delegate to xml builder, first wrapping the element in a xhtml
- # namespaced div element if the method and arguments indicate
- # that an xhtml_block? is desired.
- def method_missing(method, *arguments, &block)
- if xhtml_block?(method, arguments)
- @xml.__send__(method, *arguments) do
- @xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml|
- block.call(xhtml)
- end
- end
- else
- @xml.__send__(method, *arguments, &block)
- end
- end
-
- # True if the method name matches one of the five elements defined
- # in the Atom spec as potentially containing XHTML content and
- # if :type => 'xhtml' is, in fact, specified.
- def xhtml_block?(method, arguments)
- if XHTML_TAG_NAMES.include?(method.to_s)
- last = arguments.last
- last.is_a?(Hash) && last[:type].to_s == 'xhtml'
- end
- end
- end
-
- class AtomFeedBuilder < AtomBuilder
- def initialize(xml, view, feed_options = {})
- @xml, @view, @feed_options = xml, view, feed_options
- end
-
- # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used.
- def updated(date_or_time = nil)
- @xml.updated((date_or_time || Time.now.utc).xmlschema)
- end
-
- # Creates an entry tag for a specific record and prefills the id using class and id.
- #
- # Options:
- #
- # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
- # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
- # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
- # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
- # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
- def entry(record, options = {})
- @xml.entry do
- @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
-
- if options[:published] || (record.respond_to?(:created_at) && record.created_at)
- @xml.published((options[:published] || record.created_at).xmlschema)
- end
-
- if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at)
- @xml.updated((options[:updated] || record.updated_at).xmlschema)
- end
-
- type = options.fetch(:type, 'text/html')
-
- @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record))
-
- yield AtomBuilder.new(@xml)
- end
- end
- end
-
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/benchmark_helper.rb b/actionpack/lib/action_view/helpers/benchmark_helper.rb
deleted file mode 100644
index dfdd5a786d..0000000000
--- a/actionpack/lib/action_view/helpers/benchmark_helper.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'active_support/benchmarkable'
-
-module ActionView
- module Helpers
- module BenchmarkHelper
- include ActiveSupport::Benchmarkable
-
- def benchmark(*)
- capture { super }
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/cache_helper.rb b/actionpack/lib/action_view/helpers/cache_helper.rb
deleted file mode 100644
index ddac87a37d..0000000000
--- a/actionpack/lib/action_view/helpers/cache_helper.rb
+++ /dev/null
@@ -1,147 +0,0 @@
-module ActionView
- # = Action View Cache Helper
- module Helpers
- module CacheHelper
- # This helper exposes a method for caching fragments of a view
- # rather than an entire action or page. This technique is useful
- # caching pieces like menus, lists of newstopics, static HTML
- # fragments, and so on. This method takes a block that contains
- # the content you wish to cache.
- #
- # The best way to use this is by doing key-based cache expiration
- # on top of a cache store like Memcached that'll automatically
- # kick out old entries. For more on key-based expiration, see:
- # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works
- #
- # When using this method, you list the cache dependency as the name of the cache, like so:
- #
- # <% cache project do %>
- # <b>All the topics on this project</b>
- # <%= render project.topics %>
- # <% end %>
- #
- # This approach will assume that when a new topic is added, you'll touch
- # the project. The cache key generated from this call will be something like:
- #
- # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
- # ^class ^id ^updated_at ^template tree digest
- #
- # The cache is thus automatically bumped whenever the project updated_at is touched.
- #
- # If your template cache depends on multiple sources (try to avoid this to keep things simple),
- # you can name all these dependencies as part of an array:
- #
- # <% cache [ project, current_user ] do %>
- # <b>All the topics on this project</b>
- # <%= render project.topics %>
- # <% end %>
- #
- # This will include both records as part of the cache key and updating either of them will
- # expire the cache.
- #
- # ==== Template digest
- #
- # The template digest that's added to the cache key is computed by taking an md5 of the
- # contents of the entire template file. This ensures that your caches will automatically
- # expire when you change the template file.
- #
- # Note that the md5 is taken of the entire template file, not just what's within the
- # cache do/end call. So it's possible that changing something outside of that call will
- # still expire the cache.
- #
- # Additionally, the digestor will automatically look through your template file for
- # explicit and implicit dependencies, and include those as part of the digest.
- #
- # ==== Implicit dependencies
- #
- # Most template dependencies can be derived from calls to render in the template itself.
- # Here are some examples of render calls that Cache Digests knows how to decode:
- #
- # render partial: "comments/comment", collection: commentable.comments
- # render "comments/comments"
- # render 'comments/comments'
- # render('comments/comments')
- #
- # render "header" => render("comments/header")
- #
- # render(@topic) => render("topics/topic")
- # render(topics) => render("topics/topic")
- # render(message.topics) => render("topics/topic")
- #
- # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
- #
- # render group_of_attachments
- # render @project.documents.where(published: true).order('created_at')
- #
- # You will have to rewrite those to the explicit form:
- #
- # render partial: 'attachments/attachment', collection: group_of_attachments
- # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
- #
- # === Explicit dependencies
- #
- # Some times you'll have template dependencies that can't be derived at all. This is typically
- # the case when you have template rendering that happens in helpers. Here's an example:
- #
- # <%= render_sortable_todolists @project.todolists %>
- #
- # You'll need to use a special comment format to call those out:
- #
- # <%# Template Dependency: todolists/todolist %>
- # <%= render_sortable_todolists @project.todolists %>
- #
- # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so.
- # You can only declare one template dependency per line.
- #
- # === External dependencies
- #
- # If you use a helper method, for example, inside of a cached block and you then update that helper,
- # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file
- # must change. One recommendation is to simply be explicit in a comment, like:
- #
- # <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
- # <%= some_helper_method(person) %>
- #
- # Now all you'll have to do is change that timestamp when the helper method changes.
- def cache(name = {}, options = nil, &block)
- if controller.perform_caching
- safe_concat(fragment_for(fragment_name_with_digest(name), options, &block))
- else
- yield
- end
-
- nil
- end
-
- 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)
- ]
- else
- name
- end
- end
-
- private
- # TODO: Create an object that has caching read/write on it
- def fragment_for(name = {}, options = nil, &block) #:nodoc:
- if fragment = controller.read_fragment(name, options)
- fragment
- else
- # VIEW TODO: Make #capture usable outside of ERB
- # This dance is needed because Builder can't use capture
- pos = output_buffer.length
- yield
- output_safe = output_buffer.html_safe?
- fragment = output_buffer.slice!(pos..-1)
- if output_safe
- self.output_buffer = output_buffer.class.new(output_buffer)
- end
- controller.write_fragment(name, fragment, options)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/capture_helper.rb b/actionpack/lib/action_view/helpers/capture_helper.rb
deleted file mode 100644
index c98101a195..0000000000
--- a/actionpack/lib/action_view/helpers/capture_helper.rb
+++ /dev/null
@@ -1,222 +0,0 @@
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView
- # = Action View Capture Helper
- module Helpers
- # CaptureHelper exposes methods to let you extract generated markup which
- # can be used in other parts of a template or layout file.
- #
- # It provides a method to capture blocks into variables through capture and
- # a way to capture a block of markup for use in a layout through content_for.
- module CaptureHelper
- # The capture method allows you to extract part of a template into a
- # variable. You can then use this variable anywhere in your templates or layout.
- #
- # The capture method can be used in ERB templates...
- #
- # <% @greeting = capture do %>
- # Welcome to my shiny new web page! The date and time is
- # <%= Time.now %>
- # <% end %>
- #
- # ...and Builder (RXML) templates.
- #
- # @timestamp = capture do
- # "The current timestamp is #{Time.now}."
- # end
- #
- # You can then use that variable anywhere else. For example:
- #
- # <html>
- # <head><title><%= @greeting %></title></head>
- # <body>
- # <b><%= @greeting %></b>
- # </body></html>
- #
- def capture(*args)
- value = nil
- buffer = with_output_buffer { value = yield(*args) }
- if string = buffer.presence || value and string.is_a?(String)
- ERB::Util.html_escape string
- end
- end
-
- # Calling content_for stores a block of markup in an identifier for later use.
- # You can make subsequent calls to the stored content in other templates, helper modules
- # or the layout by passing the identifier as an argument to <tt>content_for</tt>.
- #
- # Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling
- # <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does.
- #
- # ==== Examples
- #
- # <% content_for :not_authorized do %>
- # alert('You are not authorized to do that!')
- # <% end %>
- #
- # You can then use <tt>content_for :not_authorized</tt> anywhere in your templates.
- #
- # <%= content_for :not_authorized if current_user.nil? %>
- #
- # This is equivalent to:
- #
- # <%= yield :not_authorized if current_user.nil? %>
- #
- # <tt>content_for</tt>, however, can also be used in helper modules.
- #
- # module StorageHelper
- # def stored_content
- # content_for(:storage) || "Your storage is empty"
- # end
- # end
- #
- # This helper works just like normal helpers.
- #
- # <%= stored_content %>
- #
- # You can use the <tt>yield</tt> syntax alongside an existing call to <tt>yield</tt> in a layout. For example:
- #
- # <%# This is the layout %>
- # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- # <head>
- # <title>My Website</title>
- # <%= yield :script %>
- # </head>
- # <body>
- # <%= yield %>
- # </body>
- # </html>
- #
- # And now, we'll create a view that has a <tt>content_for</tt> call that
- # creates the <tt>script</tt> identifier.
- #
- # <%# This is our view %>
- # Please login!
- #
- # <% content_for :script do %>
- # <script>alert('You are not authorized to view this page!')</script>
- # <% end %>
- #
- # Then, in another view, you could to do something like this:
- #
- # <%= link_to 'Logout', :action => 'logout', :remote => true %>
- #
- # <% content_for :script do %>
- # <%= javascript_include_tag :defaults %>
- # <% end %>
- #
- # That will place +script+ tags for your default set of JavaScript files on the page;
- # this technique is useful if you'll only be using these scripts in a few views.
- #
- # Note that content_for concatenates (default) the blocks it is given for a particular
- # identifier in order. For example:
- #
- # <% content_for :navigation do %>
- # <li><%= link_to 'Home', :action => 'index' %></li>
- # <% end %>
- #
- # <%# Add some other content, or use a different template: %>
- #
- # <% content_for :navigation do %>
- # <li><%= link_to 'Login', :action => 'login' %></li>
- # <% end %>
- #
- # Then, in another template or layout, this code would render both links in order:
- #
- # <ul><%= content_for :navigation %></ul>
- #
- # If the flush parameter is true content_for replaces the blocks it is given. For example:
- #
- # <% content_for :navigation do %>
- # <li><%= link_to 'Home', :action => 'index' %></li>
- # <% end %>
- #
- # <%# Add some other content, or use a different template: %>
- #
- # <% content_for :navigation, flush: true do %>
- # <li><%= link_to 'Login', :action => 'login' %></li>
- # <% end %>
- #
- # Then, in another template or layout, this code would render only the last link:
- #
- # <ul><%= content_for :navigation %></ul>
- #
- # Lastly, simple content can be passed as a parameter:
- #
- # <% content_for :script, javascript_include_tag(:defaults) %>
- #
- # WARNING: content_for is ignored in caches. So you shouldn't use it
- # for elements that will be fragment cached.
- def content_for(name, content = nil, options = {}, &block)
- if content || block_given?
- if block_given?
- options = content if content
- content = capture(&block)
- end
- if content
- options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
- end
- nil
- else
- @view_flow.get(name)
- end
- end
-
- # The same as +content_for+ but when used with streaming flushes
- # straight back to the layout. In other words, if you want to
- # concatenate several times to the same buffer when rendering a given
- # template, you should use +content_for+, if not, use +provide+ to tell
- # the layout to stop looking for more contents.
- def provide(name, content = nil, &block)
- content = capture(&block) if block_given?
- result = @view_flow.append!(name, content) if content
- result unless content
- end
-
- # content_for? simply checks whether any content has been captured yet using content_for
- # Useful to render parts of your layout differently based on what is in your views.
- #
- # ==== Examples
- #
- # Perhaps you will use different css in you layout if no content_for :right_column
- #
- # <%# This is the layout %>
- # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
- # <head>
- # <title>My Website</title>
- # <%= yield :script %>
- # </head>
- # <body class="<%= content_for?(:right_col) ? 'one-column' : 'two-column' %>">
- # <%= yield %>
- # <%= yield :right_col %>
- # </body>
- # </html>
- def content_for?(name)
- @view_flow.get(name).present?
- end
-
- # Use an alternate output buffer for the duration of the block.
- # Defaults to a new empty string.
- def with_output_buffer(buf = nil) #:nodoc:
- unless buf
- buf = ActionView::OutputBuffer.new
- buf.force_encoding(output_buffer.encoding) if output_buffer
- end
- self.output_buffer, old_buffer = buf, output_buffer
- yield
- output_buffer
- 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/actionpack/lib/action_view/helpers/date_helper.rb b/actionpack/lib/action_view/helpers/date_helper.rb
deleted file mode 100644
index 387dfeab17..0000000000
--- a/actionpack/lib/action_view/helpers/date_helper.rb
+++ /dev/null
@@ -1,1045 +0,0 @@
-require 'date'
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/date/conversions'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/object/with_options'
-
-module ActionView
- module Helpers
- # = Action View Date Helpers
- #
- # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time
- # elements. All of the select-type methods share a number of common options that are as follows:
- #
- # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
- # would give \birthday[month] instead of \date[month] if passed to the <tt>select_month</tt> method.
- # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
- # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
- # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead
- # of \date[month].
- module DateHelper
- # 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:
- #
- # 0 <-> 29 secs # => less than a minute
- # 30 secs <-> 1 min, 29 secs # => 1 minute
- # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
- # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
- # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
- # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day
- # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
- # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month
- # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months
- # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
- # 1 yr <-> 1 yr, 3 months # => about 1 year
- # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
- # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
- # 2 yrs <-> max time or date # => (same rules as 1 yr)
- #
- # With <tt>:include_seconds => true</tt> and the difference < 1 minute 29 seconds:
- # 0-4 secs # => less than 5 seconds
- # 5-9 secs # => less than 10 seconds
- # 10-19 secs # => less than 20 seconds
- # 20-39 secs # => half a minute
- # 40-59 secs # => less than a minute
- # 60-89 secs # => 1 minute
- #
- # ==== Examples
- # from_time = Time.now
- # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
- # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
- # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
- # distance_of_time_in_words(from_time, from_time + 15.seconds, :include_seconds => true) # => less than 20 seconds
- # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
- # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days
- # distance_of_time_in_words(from_time, from_time + 45.seconds, :include_seconds => true) # => less than a minute
- # distance_of_time_in_words(from_time, from_time - 45.seconds, :include_seconds => true) # => less than a minute
- # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
- # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
- # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
- # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
- #
- # to_time = Time.now + 6.years + 19.days
- # distance_of_time_in_words(from_time, to_time, :include_seconds => true) # => about 6 years
- # distance_of_time_in_words(to_time, from_time, :include_seconds => true) # => about 6 years
- # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
- def distance_of_time_in_words(from_time, to_time = 0, include_seconds_or_options = {}, options = {})
- if include_seconds_or_options.is_a?(Hash)
- options = include_seconds_or_options
- else
- ActiveSupport::Deprecation.warn "distance_of_time_in_words and time_ago_in_words now accept :include_seconds " +
- "as a part of options hash, not a boolean argument", caller
- options[:include_seconds] ||= !!include_seconds_or_options
- end
-
- from_time = from_time.to_time if from_time.respond_to?(:to_time)
- to_time = to_time.to_time if to_time.respond_to?(:to_time)
- from_time, to_time = to_time, from_time if from_time > to_time
- distance_in_minutes = ((to_time - from_time)/60.0).round
- distance_in_seconds = (to_time - from_time).round
-
- I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
- case distance_in_minutes
- when 0..1
- return distance_in_minutes == 0 ?
- locale.t(:less_than_x_minutes, :count => 1) :
- locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds]
-
- case distance_in_seconds
- when 0..4 then locale.t :less_than_x_seconds, :count => 5
- when 5..9 then locale.t :less_than_x_seconds, :count => 10
- when 10..19 then locale.t :less_than_x_seconds, :count => 20
- when 20..39 then locale.t :half_a_minute
- when 40..59 then locale.t :less_than_x_minutes, :count => 1
- else locale.t :x_minutes, :count => 1
- end
-
- when 2...45 then locale.t :x_minutes, :count => distance_in_minutes
- when 45...90 then locale.t :about_x_hours, :count => 1
- # 90 mins up to 24 hours
- when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
- # 24 hours up to 42 hours
- when 1440...2520 then locale.t :x_days, :count => 1
- # 42 hours up to 30 days
- when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round
- # 30 days up to 60 days
- when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round
- # 60 days up to 365 days
- when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round
- else
- if from_time.acts_like?(:time) && to_time.acts_like?(:time)
- fyear = from_time.year
- fyear += 1 if from_time.month >= 3
- tyear = to_time.year
- tyear -= 1 if to_time.month < 3
- leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)}
- minute_offset_for_leap_year = leap_years * 1440
- # Discount the leap year days when calculating year distance.
- # e.g. if there are 20 leap year days between 2 dates having the same day
- # and month then the based on 365 days calculation
- # the distance in years will come out to over 80 years when in written
- # english it would read better as about 80 years.
- minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
- else
- minutes_with_offset = distance_in_minutes
- end
- remainder = (minutes_with_offset % 525600)
- distance_in_years = (minutes_with_offset / 525600)
- if remainder < 131400
- locale.t(:about_x_years, :count => distance_in_years)
- elsif remainder < 394200
- locale.t(:over_x_years, :count => distance_in_years)
- else
- locale.t(:almost_x_years, :count => distance_in_years + 1)
- end
- end
- end
- end
-
- # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
- #
- # time_ago_in_words(3.minutes.from_now) # => 3 minutes
- # time_ago_in_words(3.minutes.ago) # => 3 minutes
- # time_ago_in_words(Time.now - 15.hours) # => about 15 hours
- # time_ago_in_words(Time.now) # => less than a minute
- # time_ago_in_words(Time.now, :include_seconds => true) # => less than 5 seconds
- #
- # from_time = Time.now - 3.days - 14.minutes - 25.seconds
- # time_ago_in_words(from_time) # => 3 days
- #
- # from_time = (3.days + 14.minutes + 25.seconds).ago
- # time_ago_in_words(from_time) # => 3 days
- #
- # 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)
- end
-
- alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
-
- # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
- # attribute (identified by +method+) on an object assigned to the template (identified by +object+).
- #
- #
- # ==== Options
- # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
- # "2" instead of "February").
- # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g.
- # "02" instead of "February" and "08" instead of "8").
- # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full
- # month names (e.g. "Feb" instead of "February").
- # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g.
- # "2 - February" instead of "February").
- # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
- # Note: You can also use Rails' i18n functionality for this.
- # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
- # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
- # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
- # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
- # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
- # first of the given month in order to not create invalid dates like 31 February.
- # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
- # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
- # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
- # as a hidden field instead of showing a select field.
- # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to
- # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
- # select will not be shown (like when you set <tt>:discard_xxx => true</tt>. Defaults to the order defined in
- # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
- # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
- # dates.
- # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
- # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
- # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
- # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
- # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
- # or the given prompt string.
- #
- # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
- #
- # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute.
- # date_select("article", "written_on")
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
- # # with the year in the year drop down box starting at 1995.
- # date_select("article", "written_on", :start_year => 1995)
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
- # # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
- # # and without a day select box.
- # date_select("article", "written_on", :start_year => 1995, :use_month_numbers => true,
- # :discard_day => true, :include_blank => true)
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
- # # with two digit numbers used for months and days.
- # date_select("article", "written_on", :use_two_digit_numbers => true)
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
- # # with the fields ordered as day, month, year rather than month, day, year.
- # date_select("article", "written_on", :order => [:day, :month, :year])
- #
- # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
- # # lacking a year field.
- # date_select("user", "birthday", :order => [:month, :day])
- #
- # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
- # # which is initially set to the date 3 days from the current date
- # date_select("article", "written_on", :default => 3.days.from_now)
- #
- # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
- # # that will have a default day of 20.
- # date_select("credit_card", "bill_due", :default => { :day => 20 })
- #
- # # Generates a date select with custom prompts.
- # date_select("article", "written_on", :prompt => { :day => 'Select day', :month => 'Select month', :year => 'Select year' })
- #
- # The selects are prepared for multi-parameter assignment to an Active Record object.
- #
- # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
- # all month choices are valid.
- def date_select(object_name, method, options = {}, html_options = {})
- Tags::DateSelect.new(object_name, method, self, options, html_options).render
- end
-
- # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
- # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
- # +object+). You can include the seconds with <tt>:include_seconds</tt>. You can get hours in the AM/PM format
- # with <tt>:ampm</tt> option.
- #
- # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
- # <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a
- # +date_select+ on the same method within the form otherwise an exception will be raised.
- #
- # If anything is passed in the html_options hash it will be applied to every select tag in the set.
- #
- # # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute.
- # time_select("article", "sunrise")
- #
- # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in
- # # the sunrise attribute.
- # time_select("article", "start_time", :include_seconds => true)
- #
- # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45.
- # time_select 'game', 'game_time', {:minute_step => 15}
- #
- # # Creates a time select tag with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts.
- # time_select("article", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
- # time_select("article", "written_on", :prompt => {:hour => true}) # generic prompt for hours
- # time_select("article", "written_on", :prompt => true) # generic prompts for all
- #
- # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
- # time_select 'game', 'game_time', {:ampm => true}
- #
- # The selects are prepared for multi-parameter assignment to an Active Record object.
- #
- # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
- # all month choices are valid.
- def time_select(object_name, method, options = {}, html_options = {})
- Tags::TimeSelect.new(object_name, method, self, options, html_options).render
- end
-
- # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
- # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
- # by +object+).
- #
- # If anything is passed in the html_options hash it will be applied to every select tag in the set.
- #
- # # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on
- # # attribute.
- # datetime_select("article", "written_on")
- #
- # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
- # # article variable in the written_on attribute.
- # datetime_select("article", "written_on", :start_year => 1995)
- #
- # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
- # # be stored in the trip variable in the departing attribute.
- # datetime_select("trip", "departing", :default => 3.days.from_now)
- #
- # # Generate a datetime select with hours in the AM/PM format
- # datetime_select("article", "written_on", :ampm => true)
- #
- # # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable
- # # as the written_on attribute.
- # datetime_select("article", "written_on", :discard_type => true)
- #
- # # Generates a datetime select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts.
- # datetime_select("article", "written_on", :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
- # datetime_select("article", "written_on", :prompt => {:hour => true}) # generic prompt for hours
- # datetime_select("article", "written_on", :prompt => true) # generic prompts for all
- #
- # The selects are prepared for multi-parameter assignment to an Active Record object.
- def datetime_select(object_name, method, options = {}, html_options = {})
- Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
- end
-
- # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
- # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
- # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
- # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
- # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
- # control visual display of the elements.
- #
- # If anything is passed in the html_options hash it will be applied to every select tag in the set.
- #
- # my_date_time = Time.now + 4.days
- #
- # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today).
- # select_datetime(my_date_time)
- #
- # # Generates a datetime select that defaults to today (no specified datetime)
- # select_datetime()
- #
- # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
- # # with the fields ordered year, month, day rather than month, day, year.
- # select_datetime(my_date_time, :order => [:year, :month, :day])
- #
- # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
- # # with a '/' between each date field.
- # select_datetime(my_date_time, :date_separator => '/')
- #
- # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
- # # with a date fields separated by '/', time fields separated by '' and the date and time fields
- # # separated by a comma (',').
- # select_datetime(my_date_time, :date_separator => '/', :time_separator => '', :datetime_separator => ',')
- #
- # # Generates a datetime select that discards the type of the field and defaults to the datetime in
- # # my_date_time (four days after today)
- # select_datetime(my_date_time, :discard_type => true)
- #
- # # Generate a datetime field with hours in the AM/PM format
- # select_datetime(my_date_time, :ampm => true)
- #
- # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
- # # prefixed with 'payday' rather than 'date'
- # select_datetime(my_date_time, :prefix => 'payday')
- #
- # # Generates a datetime select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts.
- # select_datetime(my_date_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
- # select_datetime(my_date_time, :prompt => {:hour => true}) # generic prompt for hours
- # select_datetime(my_date_time, :prompt => true) # generic prompts for all
- def select_datetime(datetime = Time.current, options = {}, html_options = {})
- DateTimeSelector.new(datetime, options, html_options).select_datetime
- end
-
- # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
- # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
- # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order.
- # If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden.
- #
- # If anything is passed in the html_options hash it will be applied to every select tag in the set.
- #
- # my_date = Time.now + 6.days
- #
- # # Generates a date select that defaults to the date in my_date (six days after today).
- # select_date(my_date)
- #
- # # Generates a date select that defaults to today (no specified date).
- # select_date()
- #
- # # Generates a date select that defaults to the date in my_date (six days after today)
- # # with the fields ordered year, month, day rather than month, day, year.
- # select_date(my_date, :order => [:year, :month, :day])
- #
- # # Generates a date select that discards the type of the field and defaults to the date in
- # # my_date (six days after today).
- # select_date(my_date, :discard_type => true)
- #
- # # Generates a date select that defaults to the date in my_date,
- # # which has fields separated by '/'.
- # select_date(my_date, :date_separator => '/')
- #
- # # Generates a date select that defaults to the datetime in my_date (six days after today)
- # # prefixed with 'payday' rather than 'date'.
- # select_date(my_date, :prefix => 'payday')
- #
- # # Generates a date select with a custom prompt. Use <tt>:prompt => true</tt> for generic prompts.
- # select_date(my_date, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
- # select_date(my_date, :prompt => {:hour => true}) # generic prompt for hours
- # select_date(my_date, :prompt => true) # generic prompts for all
- def select_date(date = Date.current, options = {}, html_options = {})
- DateTimeSelector.new(date, options, html_options).select_date
- end
-
- # Returns a set of html select-tags (one for hour and minute).
- # You can set <tt>:time_separator</tt> key to format the output, and
- # the <tt>:include_seconds</tt> option to include an input for seconds.
- #
- # If anything is passed in the html_options hash it will be applied to every select tag in the set.
- #
- # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
- #
- # # Generates a time select that defaults to the time in my_time.
- # select_time(my_time)
- #
- # # Generates a time select that defaults to the current time (no specified time).
- # select_time()
- #
- # # Generates a time select that defaults to the time in my_time,
- # # which has fields separated by ':'.
- # select_time(my_time, :time_separator => ':')
- #
- # # Generates a time select that defaults to the time in my_time,
- # # that also includes an input for seconds.
- # select_time(my_time, :include_seconds => true)
- #
- # # Generates a time select that defaults to the time in my_time, that has fields
- # # separated by ':' and includes an input for seconds.
- # select_time(my_time, :time_separator => ':', :include_seconds => true)
- #
- # # Generate a time select field with hours in the AM/PM format
- # select_time(my_time, :ampm => true)
- #
- # # Generates a time select field with hours that range from 2 to 14
- # select_time(my_time, :start_hour => 2, :end_hour => 14)
- #
- # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
- # select_time(my_time, :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year'})
- # select_time(my_time, :prompt => {:hour => true}) # generic prompt for hours
- # select_time(my_time, :prompt => true) # generic prompts for all
- def select_time(datetime = Time.current, options = {}, html_options = {})
- DateTimeSelector.new(datetime, options, html_options).select_time
- end
-
- # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
- # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
- # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
- #
- # my_time = Time.now + 16.minutes
- #
- # # Generates a select field for seconds that defaults to the seconds for the time in my_time.
- # select_second(my_time)
- #
- # # Generates a select field for seconds that defaults to the number given.
- # select_second(33)
- #
- # # Generates a select field for seconds that defaults to the seconds for the time in my_time
- # # that is named 'interval' rather than 'second'.
- # select_second(my_time, :field_name => 'interval')
- #
- # # Generates a select field for seconds with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_second(14, :prompt => 'Choose seconds')
- def select_second(datetime, options = {}, html_options = {})
- DateTimeSelector.new(datetime, options, html_options).select_second
- end
-
- # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
- # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
- # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
- # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
- #
- # my_time = Time.now + 6.hours
- #
- # # Generates a select field for minutes that defaults to the minutes for the time in my_time.
- # select_minute(my_time)
- #
- # # Generates a select field for minutes that defaults to the number given.
- # select_minute(14)
- #
- # # Generates a select field for minutes that defaults to the minutes for the time in my_time
- # # that is named 'moment' rather than 'minute'.
- # select_minute(my_time, :field_name => 'moment')
- #
- # # Generates a select field for minutes with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_minute(14, :prompt => 'Choose minutes')
- def select_minute(datetime, options = {}, html_options = {})
- DateTimeSelector.new(datetime, options, html_options).select_minute
- end
-
- # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
- # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
- # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
- #
- # my_time = Time.now + 6.hours
- #
- # # Generates a select field for hours that defaults to the hour for the time in my_time.
- # select_hour(my_time)
- #
- # # Generates a select field for hours that defaults to the number given.
- # select_hour(13)
- #
- # # Generates a select field for hours that defaults to the hour for the time in my_time
- # # that is named 'stride' rather than 'hour'.
- # select_hour(my_time, :field_name => 'stride')
- #
- # # Generates a select field for hours with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_hour(13, :prompt => 'Choose hour')
- #
- # # Generate a select field for hours in the AM/PM format
- # select_hour(my_time, :ampm => true)
- #
- # # Generates a select field that includes options for hours from 2 to 14.
- # select_hour(my_time, :start_hour => 2, :end_hour => 14)
- def select_hour(datetime, options = {}, html_options = {})
- DateTimeSelector.new(datetime, options, html_options).select_hour
- end
-
- # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
- # The <tt>date</tt> can also be substituted for a day number.
- # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
- # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
- #
- # my_date = Time.now + 2.days
- #
- # # Generates a select field for days that defaults to the day for the date in my_date.
- # select_day(my_time)
- #
- # # Generates a select field for days that defaults to the number given.
- # select_day(5)
- #
- # # Generates a select field for days that defaults to the number given, but displays it with two digits.
- # select_day(5, :use_two_digit_numbers => true)
- #
- # # Generates a select field for days that defaults to the day for the date in my_date
- # # that is named 'due' rather than 'day'.
- # select_day(my_time, :field_name => 'due')
- #
- # # Generates a select field for days with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_day(5, :prompt => 'Choose day')
- def select_day(date, options = {}, html_options = {})
- DateTimeSelector.new(date, options, html_options).select_day
- end
-
- # Returns a select tag with options for each of the months January through December with the current month
- # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
- # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
- # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
- # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
- # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
- # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
- # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
- # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys like "January", "March".
- # select_month(Date.today)
- #
- # # Generates a select field for months that defaults to the current month that
- # # is named "start" rather than "month".
- # select_month(Date.today, :field_name => 'start')
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys like "1", "3".
- # select_month(Date.today, :use_month_numbers => true)
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys like "1 - January", "3 - March".
- # select_month(Date.today, :add_month_numbers => true)
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys like "Jan", "Mar".
- # select_month(Date.today, :use_short_month => true)
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys like "Januar", "Marts."
- # select_month(Date.today, :use_month_names => %w(Januar Februar Marts ...))
- #
- # # Generates a select field for months that defaults to the current month that
- # # will use keys with two digit numbers like "01", "03".
- # select_month(Date.today, :use_two_digit_numbers => true)
- #
- # # Generates a select field for months with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_month(14, :prompt => 'Choose month')
- def select_month(date, options = {}, html_options = {})
- DateTimeSelector.new(date, options, html_options).select_month
- end
-
- # Returns a select tag with options for each of the five years on each side of the current, which is selected.
- # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
- # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
- # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
- # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
- #
- # # Generates a select field for years that defaults to the current year that
- # # has ascending year values.
- # select_year(Date.today, :start_year => 1992, :end_year => 2007)
- #
- # # Generates a select field for years that defaults to the current year that
- # # is named 'birth' rather than 'year'.
- # select_year(Date.today, :field_name => 'birth')
- #
- # # Generates a select field for years that defaults to the current year that
- # # has descending year values.
- # select_year(Date.today, :start_year => 2005, :end_year => 1900)
- #
- # # Generates a select field for years that defaults to the year 2006 that
- # # has ascending year values.
- # select_year(2006, :start_year => 2000, :end_year => 2010)
- #
- # # Generates a select field for years with a custom prompt. Use <tt>:prompt => true</tt> for a
- # # generic prompt.
- # select_year(14, :prompt => 'Choose year')
- def select_year(date, options = {}, html_options = {})
- DateTimeSelector.new(date, options, html_options).select_year
- end
-
- # Returns an html time tag for the given date or time.
- #
- # time_tag Date.today # =>
- # <time datetime="2010-11-04">November 04, 2010</time>
- # time_tag Time.now # =>
- # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
- # time_tag Date.yesterday, 'Yesterday' # =>
- # <time datetime="2010-11-03">Yesterday</time>
- # time_tag Date.today, :pubdate => true # =>
- # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time>
- #
- # <%= time_tag Time.now do %>
- # <span>Right now</span>
- # <% end %>
- # # => <time datetime="2010-11-04T17:55:45+01:00"><span>Right now</span></time>
- def time_tag(date_or_time, *args, &block)
- options = args.extract_options!
- format = options.delete(:format) || :long
- content = args.first || I18n.l(date_or_time, :format => format)
- datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.rfc3339
-
- content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
- end
- end
-
- class DateTimeSelector #:nodoc:
- include ActionView::Helpers::TagHelper
-
- DEFAULT_PREFIX = 'date'.freeze
- POSITION = {
- :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
- }.freeze
-
- AMPM_TRANSLATION = Hash[
- [[0, "12 AM"], [1, "01 AM"], [2, "02 AM"], [3, "03 AM"],
- [4, "04 AM"], [5, "05 AM"], [6, "06 AM"], [7, "07 AM"],
- [8, "08 AM"], [9, "09 AM"], [10, "10 AM"], [11, "11 AM"],
- [12, "12 PM"], [13, "01 PM"], [14, "02 PM"], [15, "03 PM"],
- [16, "04 PM"], [17, "05 PM"], [18, "06 PM"], [19, "07 PM"],
- [20, "08 PM"], [21, "09 PM"], [22, "10 PM"], [23, "11 PM"]]
- ].freeze
-
- def initialize(datetime, options = {}, html_options = {})
- @options = options.dup
- @html_options = html_options.dup
- @datetime = datetime
- @options[:datetime_separator] ||= ' &mdash; '
- @options[:time_separator] ||= ' : '
- end
-
- def select_datetime
- order = date_order.dup
- order -= [:hour, :minute, :second]
- @options[:discard_year] ||= true unless order.include?(:year)
- @options[:discard_month] ||= true unless order.include?(:month)
- @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
- @options[:discard_minute] ||= true if @options[:discard_hour]
- @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
-
- set_day_if_discarded
-
- if @options[:tag] && @options[:ignore_date]
- select_time
- else
- [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
- order += [:hour, :minute, :second] unless @options[:discard_hour]
-
- build_selects_from_types(order)
- end
- end
-
- def select_date
- order = date_order.dup
-
- @options[:discard_hour] = true
- @options[:discard_minute] = true
- @options[:discard_second] = true
-
- @options[:discard_year] ||= true unless order.include?(:year)
- @options[:discard_month] ||= true unless order.include?(:month)
- @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
-
- set_day_if_discarded
-
- [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
-
- build_selects_from_types(order)
- end
-
- def select_time
- order = []
-
- @options[:discard_month] = true
- @options[:discard_year] = true
- @options[:discard_day] = true
- @options[:discard_second] ||= true unless @options[:include_seconds]
-
- order += [:year, :month, :day] unless @options[:ignore_date]
-
- order += [:hour, :minute]
- order << :second if @options[:include_seconds]
-
- build_selects_from_types(order)
- end
-
- def select_second
- if @options[:use_hidden] || @options[:discard_second]
- build_hidden(:second, sec) if @options[:include_seconds]
- else
- build_options_and_select(:second, sec)
- end
- end
-
- def select_minute
- if @options[:use_hidden] || @options[:discard_minute]
- build_hidden(:minute, min)
- else
- build_options_and_select(:minute, min, :step => @options[:minute_step])
- end
- end
-
- def select_hour
- if @options[:use_hidden] || @options[:discard_hour]
- build_hidden(:hour, hour)
- else
- options = {}
- options[:ampm] = @options[:ampm] || false
- options[:start] = @options[:start_hour] || 0
- options[:end] = @options[:end_hour] || 23
- build_options_and_select(:hour, hour, options)
- end
- end
-
- def select_day
- if @options[:use_hidden] || @options[:discard_day]
- build_hidden(:day, day || 1)
- else
- build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers])
- end
- end
-
- def select_month
- if @options[:use_hidden] || @options[:discard_month]
- build_hidden(:month, month || 1)
- else
- month_options = []
- 1.upto(12) do |month_number|
- options = { :value => month_number }
- options[:selected] = "selected" if month == month_number
- month_options << content_tag(:option, month_name(month_number), options) + "\n"
- end
- build_select(:month, month_options.join)
- end
- end
-
- def select_year
- if !@datetime || @datetime == 0
- val = '1'
- middle_year = Date.today.year
- else
- val = middle_year = year
- end
-
- if @options[:use_hidden] || @options[:discard_year]
- build_hidden(:year, val)
- else
- options = {}
- options[:start] = @options[:start_year] || middle_year - 5
- options[:end] = @options[:end_year] || middle_year + 5
- options[:step] = options[:start] < options[:end] ? 1 : -1
- options[:leading_zeros] = false
- options[:max_years_allowed] = @options[:max_years_allowed] || 1000
-
- if (options[:end] - options[:start]).abs > options[:max_years_allowed]
- raise ArgumentError, "There're too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter"
- end
-
- build_options_and_select(:year, val, options)
- end
- end
-
- private
- %w( sec min hour day month year ).each do |method|
- define_method(method) do
- @datetime.kind_of?(Numeric) ? @datetime : @datetime.send(method) if @datetime
- end
- end
-
- # If the day is hidden, the day should be set to the 1st so all month and year choices are
- # valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid.
- def set_day_if_discarded
- if @datetime && @options[:discard_day]
- @datetime = @datetime.change(:day => 1)
- end
- end
-
- # Returns translated month names, but also ensures that a custom month
- # name array has a leading nil element.
- def month_names
- @month_names ||= begin
- month_names = @options[:use_month_names] || translated_month_names
- month_names.unshift(nil) if month_names.size < 13
- month_names
- end
- end
-
- # Returns translated month names.
- # => [nil, "January", "February", "March",
- # "April", "May", "June", "July",
- # "August", "September", "October",
- # "November", "December"]
- #
- # If <tt>:use_short_month</tt> option is set
- # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
- # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
- def translated_month_names
- key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
- I18n.translate(key, :locale => @options[:locale])
- end
-
- # Lookup month name for number.
- # month_name(1) => "January"
- #
- # If <tt>:use_month_numbers</tt> option is passed
- # month_name(1) => 1
- #
- # If <tt>:use_two_month_numbers</tt> option is passed
- # month_name(1) => '01'
- #
- # If <tt>:add_month_numbers</tt> option is passed
- # month_name(1) => "1 - January"
- def month_name(number)
- if @options[:use_month_numbers]
- number
- elsif @options[:use_two_digit_numbers]
- sprintf "%02d", number
- elsif @options[:add_month_numbers]
- "#{number} - #{month_names[number]}"
- else
- month_names[number]
- end
- end
-
- def date_order
- @date_order ||= @options[:order] || translated_date_order
- end
-
- def translated_date_order
- date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => [])
-
- forbidden_elements = date_order - [:year, :month, :day]
- if forbidden_elements.any?
- raise StandardError,
- "#{@options[:locale]}.date.order only accepts :year, :month and :day"
- end
-
- date_order
- end
-
- # Build full select tag from date type and options.
- def build_options_and_select(type, selected, options = {})
- build_select(type, build_options(selected, options))
- end
-
- # Build select option html from date value and options.
- # build_options(15, :start => 1, :end => 31)
- # => "<option value="1">1</option>
- # <option value="2">2</option>
- # <option value="3">3</option>..."
- #
- # If <tt>:use_two_digit_numbers => true</tt> option is passed
- # build_options(15, :start => 1, :end => 31, :use_two_digit_numbers => true)
- # => "<option value="1">01</option>
- # <option value="2">02</option>
- # <option value="3">03</option>..."
- #
- # If <tt>:step</tt> options is passed
- # build_options(15, :start => 1, :end => 31, :step => 2)
- # => "<option value="1">1</option>
- # <option value="3">3</option>
- # <option value="5">5</option>..."
- def build_options(selected, options = {})
- options = {
- leading_zeros: true, ampm: false, use_two_digit_numbers: false
- }.merge!(options)
-
- start = options.delete(:start) || 0
- stop = options.delete(:end) || 59
- step = options.delete(:step) || 1
- leading_zeros = options.delete(:leading_zeros)
-
- select_options = []
- start.step(stop, step) do |i|
- value = leading_zeros ? sprintf("%02d", i) : i
- tag_options = { :value => value }
- tag_options[:selected] = "selected" if selected == i
- text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
- text = options[:ampm] ? AMPM_TRANSLATION[i] : text
- select_options << content_tag(:option, text, tag_options)
- end
-
- (select_options.join("\n") + "\n").html_safe
- end
-
- # Builds select tag from date type and html select options.
- # build_select(:month, "<option value="1">January</option>...")
- # => "<select id="post_written_on_2i" name="post[written_on(2i)]">
- # <option value="1">January</option>...
- # </select>"
- def build_select(type, select_options_as_html)
- select_options = {
- :id => input_id_from_type(type),
- :name => input_name_from_type(type)
- }.merge!(@html_options)
- select_options[:disabled] = 'disabled' if @options[:disabled]
-
- select_html = "\n"
- select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
- select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
- select_html << select_options_as_html
-
- (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
- end
-
- # Builds a prompt option tag with supplied options or from default options.
- # prompt_option_tag(:month, :prompt => 'Select month')
- # => "<option value="">Select month</option>"
- def prompt_option_tag(type, options)
- prompt = case options
- when Hash
- default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
- default_options.merge!(options)[type.to_sym]
- when String
- options
- else
- I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
- end
-
- prompt ? content_tag(:option, prompt, :value => '') : ''
- end
-
- # Builds hidden input tag for date part and value.
- # build_hidden(:year, 2008)
- # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
- def build_hidden(type, value)
- select_options = {
- :type => "hidden",
- :id => input_id_from_type(type),
- :name => input_name_from_type(type),
- :value => value
- }.merge!(@html_options.slice(:disabled))
- select_options[:disabled] = 'disabled' if @options[:disabled]
-
- tag(:input, select_options) + "\n".html_safe
- end
-
- # Returns the name attribute for the input tag.
- # => post[written_on(1i)]
- def input_name_from_type(type)
- prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
- prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
-
- field_name = @options[:field_name] || type
- if @options[:include_position]
- field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
- end
-
- @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
- end
-
- # Returns the id attribute for the input tag.
- # => "post_written_on_1i"
- def input_id_from_type(type)
- id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
- id = @options[:namespace] + '_' + id if @options[:namespace]
-
- id
- end
-
- # Given an ordering of datetime components, create the selection HTML
- # and join them with their appropriate separators.
- def build_selects_from_types(order)
- select = ''
- first_visible = order.find { |type| !@options[:"discard_#{type}"] }
- order.reverse.each do |type|
- separator = separator(type) unless type == first_visible # don't add before first visible field
- select.insert(0, separator.to_s + send("select_#{type}").to_s)
- end
- select.html_safe
- end
-
- # Returns the separator for a given datetime component.
- def separator(type)
- return "" if @options[:use_hidden]
-
- case type
- when :year, :month, :day
- @options[:"discard_#{type}"] ? "" : @options[:date_separator]
- when :hour
- (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
- when :minute, :second
- @options[:"discard_#{type}"] ? "" : @options[:time_separator]
- end
- end
- end
-
- class FormBuilder
- def date_select(method, options = {}, html_options = {})
- @template.date_select(@object_name, method, objectify_options(options), html_options)
- end
-
- def time_select(method, options = {}, html_options = {})
- @template.time_select(@object_name, method, objectify_options(options), html_options)
- end
-
- def datetime_select(method, options = {}, html_options = {})
- @template.datetime_select(@object_name, method, objectify_options(options), html_options)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/debug_helper.rb b/actionpack/lib/action_view/helpers/debug_helper.rb
deleted file mode 100644
index d8b92c5cab..0000000000
--- a/actionpack/lib/action_view/helpers/debug_helper.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module ActionView
- # = Action View Debug Helper
- #
- # Provides a set of methods for making it easier to debug Rails objects.
- module Helpers
- module DebugHelper
-
- include TagHelper
-
- # Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
- # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
- # Useful for inspecting an object at the time of rendering.
- #
- # @user = User.new({ :username => 'testing', :password => 'xyz', :age => 42}) %>
- # debug(@user)
- # # =>
- # <pre class='debug_dump'>--- !ruby/object:User
- # attributes:
- # &nbsp; updated_at:
- # &nbsp; username: testing
- #
- # &nbsp; age: 42
- # &nbsp; password: xyz
- # &nbsp; created_at:
- # attributes_cache: {}
- #
- # new_record: true
- # </pre>
- def debug(object)
- begin
- Marshal::dump(object)
- object = ERB::Util.html_escape(object.to_yaml).gsub(" ", "&nbsp; ").html_safe
- 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
- content_tag(:code, object.to_yaml, :class => "debug_dump")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/form_helper.rb b/actionpack/lib/action_view/helpers/form_helper.rb
deleted file mode 100644
index b87c2e936f..0000000000
--- a/actionpack/lib/action_view/helpers/form_helper.rb
+++ /dev/null
@@ -1,1416 +0,0 @@
-require 'cgi'
-require 'action_view/helpers/date_helper'
-require 'action_view/helpers/tag_helper'
-require 'action_view/helpers/form_tag_helper'
-require 'action_view/helpers/active_model_helper'
-require 'action_view/helpers/tags'
-require 'action_view/model_naming'
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/core_ext/hash/slice'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/array/extract_options'
-require 'active_support/core_ext/string/inflections'
-
-module ActionView
- # = Action View Form Helpers
- module Helpers
- # Form helpers are designed to make working with resources much easier
- # compared to using vanilla HTML.
- #
- # Typically, a form designed to create or update a resource reflects the
- # identity of the resource in several ways: (i) the url that the form is
- # sent to (the form element's +action+ attribute) should result in a request
- # being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
- # parameter in the case of an existing resource), (ii) input fields should
- # be named in such a way that in the controller their values appear in the
- # appropriate places within the +params+ hash, and (iii) for an existing record,
- # when the form is initially displayed, input fields corresponding to attributes
- # of the resource should show the current values of those attributes.
- #
- # In Rails, this is usually achieved by creating the form using +form_for+ and
- # a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt>
- # tag and yields a form builder object that knows the model the form is about.
- # Input fields are created by calling methods defined on the form builder, which
- # means they are able to generate the appropriate names and default values
- # corresponding to the model attributes, as well as convenient IDs, etc.
- # Conventions in the generated field names allow controllers to receive form data
- # nicely structured in +params+ with no effort on your side.
- #
- # For example, to create a new person you typically set up a new instance of
- # +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and
- # in the view template pass that object to +form_for+:
- #
- # <%= form_for @person do |f| %>
- # <%= f.label :first_name %>:
- # <%= f.text_field :first_name %><br />
- #
- # <%= f.label :last_name %>:
- # <%= f.text_field :last_name %><br />
- #
- # <%= f.submit %>
- # <% end %>
- #
- # The HTML generated for this would be (modulus formatting):
- #
- # <form action="/people" class="new_person" id="new_person" method="post">
- # <div style="margin:0;padding:0;display:inline">
- # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
- # </div>
- # <label for="person_first_name">First name</label>:
- # <input id="person_first_name" name="person[first_name]" type="text" /><br />
- #
- # <label for="person_last_name">Last name</label>:
- # <input id="person_last_name" name="person[last_name]" type="text" /><br />
- #
- # <input name="commit" type="submit" value="Create Person" />
- # </form>
- #
- # As you see, the HTML reflects knowledge about the resource in several spots,
- # like the path the form should be submitted to, or the names of the input fields.
- #
- # In particular, thanks to the conventions followed in the generated field names, the
- # controller gets a nested hash <tt>params[:person]</tt> with the person attributes
- # set in the form. That hash is ready to be passed to <tt>Person.create</tt>:
- #
- # if @person = Person.create(params[:person])
- # # success
- # else
- # # error handling
- # end
- #
- # Interestingly, the exact same view code in the previous example can be used to edit
- # a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256,
- # the code above as is would yield instead:
- #
- # <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
- # <div style="margin:0;padding:0;display:inline">
- # <input name="_method" type="hidden" value="put" />
- # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
- # </div>
- # <label for="person_first_name">First name</label>:
- # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br />
- #
- # <label for="person_last_name">Last name</label>:
- # <input id="person_last_name" name="person[last_name]" type="text" value="Smith" /><br />
- #
- # <input name="commit" type="submit" value="Update Person" />
- # </form>
- #
- # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>.
- # That works that way because the involved helpers know whether the resource is a new record or not,
- # and generate HTML accordingly.
- #
- # The controller would receive the form data again in <tt>params[:person]</tt>, ready to be
- # passed to <tt>Person#update_attributes</tt>:
- #
- # if @person.update_attributes(params[:person])
- # # success
- # else
- # # error handling
- # end
- #
- # That's how you typically work with resources.
- module FormHelper
- extend ActiveSupport::Concern
-
- include FormTagHelper
- include UrlHelper
- include ModelNaming
-
- # Creates a form that allows the user to create or update the attributes
- # of a specific model object.
- #
- # The method can be used in several slightly different ways, depending on
- # how much you wish to rely on Rails to infer automatically from the model
- # how the form should be constructed. For a generic model object, a form
- # can be created by passing +form_for+ a string or symbol representing
- # the object we are concerned with:
- #
- # <%= form_for :person do |f| %>
- # First name: <%= f.text_field :first_name %><br />
- # Last name : <%= f.text_field :last_name %><br />
- # Biography : <%= f.text_area :biography %><br />
- # Admin? : <%= f.check_box :admin %><br />
- # <%= f.submit %>
- # <% end %>
- #
- # The variable +f+ yielded to the block is a FormBuilder object that
- # incorporates the knowledge about the model object represented by
- # <tt>:person</tt> passed to +form_for+. Methods defined on the FormBuilder
- # are used to generate fields bound to this model. Thus, for example,
- #
- # <%= f.text_field :first_name %>
- #
- # will get expanded to
- #
- # <%= text_field :person, :first_name %>
- # which results in an html <tt><input></tt> tag whose +name+ attribute is
- # <tt>person[first_name]</tt>. This means that when the form is submitted,
- # the value entered by the user will be available in the controller as
- # <tt>params[:person][:first_name]</tt>.
- #
- # For fields generated in this way using the FormBuilder,
- # if <tt>:person</tt> also happens to be the name of an instance variable
- # <tt>@person</tt>, the default value of the field shown when the form is
- # initially displayed (e.g. in the situation where you are editing an
- # existing record) will be the value of the corresponding attribute of
- # <tt>@person</tt>.
- #
- # The rightmost argument to +form_for+ is an
- # optional hash of options -
- #
- # * <tt>:url</tt> - The URL the form is to be submitted to. This may be
- # represented in the same way as values passed to +url_for+ or +link_to+.
- # So for example you may use a named route directly. When the model is
- # represented by a string or symbol, as in the example above, if the
- # <tt>:url</tt> option is not specified, by default the form will be
- # sent back to the current url (We will describe below an alternative
- # resource-oriented usage of +form_for+ in which the URL does not need
- # to be specified explicitly).
- # * <tt>:namespace</tt> - A namespace 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.
- # * <tt>:html</tt> - Optional HTML attributes for the form tag.
- #
- # Also note that +form_for+ doesn't create an exclusive scope. It's still
- # possible to use both the stand-alone FormHelper methods and methods
- # from FormTagHelper. For example:
- #
- # <%= form_for :person do |f| %>
- # First name: <%= f.text_field :first_name %>
- # Last name : <%= f.text_field :last_name %>
- # Biography : <%= text_area :person, :biography %>
- # Admin? : <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
- # <%= f.submit %>
- # <% end %>
- #
- # This also works for the methods in FormOptionHelper and DateHelper that
- # are designed to work with an object as base, like
- # FormOptionHelper#collection_select and DateHelper#datetime_select.
- #
- # === #form_for with a model object
- #
- # In the examples above, the object to be created or edited was
- # represented by a symbol passed to +form_for+, and we noted that
- # a string can also be used equivalently. It is also possible, however,
- # to pass a model object itself to +form_for+. For example, if <tt>@post</tt>
- # is an existing record you wish to edit, you can create the form using
- #
- # <%= form_for @post do |f| %>
- # ...
- # <% end %>
- #
- # This behaves in almost the same way as outlined previously, with a
- # couple of small exceptions. First, the prefix used to name the input
- # elements within the form (hence the key that denotes them in the +params+
- # hash) is actually derived from the object's _class_, e.g. <tt>params[:post]</tt>
- # if the object's class is +Post+. However, this can be overwritten using
- # the <tt>:as</tt> option, e.g. -
- #
- # <%= form_for(@person, as: :client) do |f| %>
- # ...
- # <% end %>
- #
- # would result in <tt>params[:client]</tt>.
- #
- # Secondly, the field values shown when the form is initially displayed
- # are taken from the attributes of the object passed to +form_for+,
- # regardless of whether the object is an instance
- # variable. So, for example, if we had a _local_ variable +post+
- # representing an existing record,
- #
- # <%= form_for post do |f| %>
- # ...
- # <% end %>
- #
- # would produce a form with fields whose initial state reflect the current
- # values of the attributes of +post+.
- #
- # === Resource-oriented style
- #
- # In the examples just shown, although not indicated explicitly, we still
- # need to use the <tt>:url</tt> option in order to specify where the
- # form is going to be sent. However, further simplification is possible
- # if the record passed to +form_for+ is a _resource_, i.e. it corresponds
- # to a set of RESTful routes, e.g. defined using the +resources+ method
- # in <tt>config/routes.rb</tt>. In this case Rails will simply infer the
- # appropriate URL from the record itself. For example,
- #
- # <%= form_for @post do |f| %>
- # ...
- # <% end %>
- #
- # is then equivalent to something like:
- #
- # <%= form_for @post, as: :post, url: post_path(@post), method: :put, html: { class: "edit_post", id: "edit_post_45" } do |f| %>
- # ...
- # <% end %>
- #
- # And for a new record
- #
- # <%= form_for(Post.new) do |f| %>
- # ...
- # <% end %>
- #
- # is equivalent to something like:
- #
- # <%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %>
- # ...
- # <% end %>
- #
- # However you can still overwrite individual conventions, such as:
- #
- # <%= form_for(@post, url: super_posts_path) do |f| %>
- # ...
- # <% end %>
- #
- # You can also set the answer format, like this:
- #
- # <%= form_for(@post, format: :json) do |f| %>
- # ...
- # <% end %>
- #
- # For namespaced routes, like +admin_post_url+:
- #
- # <%= form_for([:admin, @post]) do |f| %>
- # ...
- # <% end %>
- #
- # If your resource has associations defined, for example, you want to add comments
- # to the document given that the routes are set correctly:
- #
- # <%= form_for([@document, @comment]) do |f| %>
- # ...
- # <% end %>
- #
- # Where <tt>@document = Document.find(params[:id])</tt> and
- # <tt>@comment = Comment.new</tt>.
- #
- # === Setting the method
- #
- # You can force the form to use the full array of HTTP verbs by setting
- #
- # method: (:get|:post|:patch|:put|:delete)
- #
- # in the options hash. If the verb is not GET or POST, which are natively
- # supported by HTML forms, the form will be set to POST and a hidden input
- # called _method will carry the intended verb for the server to interpret.
- #
- # === Unobtrusive JavaScript
- #
- # Specifying:
- #
- # remote: true
- #
- # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its
- # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular
- # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor.
- # Even though it's using JavaScript to serialize the form elements, the form submission will work just like
- # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>).
- #
- # Example:
- #
- # <%= form_for(@post, remote: true) do |f| %>
- # ...
- # <% end %>
- #
- # The HTML generated for this would be:
- #
- # <form action='http://www.example.com' method='post' data-remote='true'>
- # <div style='margin:0;padding:0;display:inline'>
- # <input name='_method' type='hidden' value='put' />
- # </div>
- # ...
- # </form>
- #
- # === Setting HTML options
- #
- # You can set data attributes directly by passing in a data hash, but all other HTML options must be wrapped in
- # the HTML key. Example:
- #
- # <%= form_for(@post, data: { behavior: "autosave" }, html: { name: "go" }) do |f| %>
- # ...
- # <% end %>
- #
- # The HTML generated for this would be:
- #
- # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
- # <div style='margin:0;padding:0;display:inline'>
- # <input name='_method' type='hidden' value='put' />
- # </div>
- # ...
- # </form>
- #
- # === Removing hidden model id's
- #
- # The form_for method automatically includes the model id as a hidden field in the form.
- # This is used to maintain the correlation between the form data and its associated model.
- # Some ORM systems do not use IDs on nested models so in this case you want to be able
- # to disable the hidden id.
- #
- # In the following example the Post model has many Comments stored within it in a NoSQL database,
- # thus there is no primary key for comments.
- #
- # Example:
- #
- # <%= form_for(@post) do |f| %>
- # <%= f.fields_for(:comments, include_id: false) do |cf| %>
- # ...
- # <% end %>
- # <% end %>
- #
- # === Customized form builders
- #
- # You can also build forms using a customized FormBuilder class. Subclass
- # FormBuilder and override or define some more helpers, then use your
- # custom builder. For example, let's say you made a helper to
- # automatically add labels to form inputs.
- #
- # <%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %>
- # <%= f.text_field :first_name %>
- # <%= f.text_field :last_name %>
- # <%= f.text_area :biography %>
- # <%= f.check_box :admin %>
- # <%= f.submit %>
- # <% end %>
- #
- # In this case, if you use this:
- #
- # <%= render f %>
- #
- # The rendered template is <tt>people/_labelling_form</tt> and the local
- # variable referencing the form builder is called
- # <tt>labelling_form</tt>.
- #
- # The custom FormBuilder class is automatically merged with the options
- # of a nested fields_for call, unless it's explicitly set.
- #
- # In many cases you will want to wrap the above in another helper, so you
- # could do something like the following:
- #
- # def labelled_form_for(record_or_name_or_array, *args, &proc)
- # options = args.extract_options!
- # form_for(record_or_name_or_array, *(args << options.merge(builder: LabellingFormBuilder)), &proc)
- # end
- #
- # If you don't need to attach a form to a model instance, then check out
- # FormTagHelper#form_tag.
- #
- # === Form to external resources
- #
- # When you build forms to external resources sometimes you need to set an authenticity token or just render a form
- # without it, for example when you submit data to a payment gateway number and types of fields could be limited.
- #
- # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
- #
- # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f|
- # ...
- # <% end %>
- #
- # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
- #
- # <%= form_for @invoice, url: external_url, authenticity_token: false do |f|
- # ...
- # <% end %>
- def form_for(record, options = {}, &proc)
- raise ArgumentError, "Missing block" unless block_given?
-
- options[:html] ||= {}
-
- case record
- when String, Symbol
- object_name = record
- object = nil
- else
- object = record.is_a?(Array) ? record.last : record
- raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
- object_name = options[:as] || model_name_from_record_or_class(object).param_key
- apply_form_for_options!(record, object, options)
- end
-
- options[:html][:data] = options.delete(:data) if options.has_key?(:data)
- options[:html][:remote] = options.delete(:remote) if options.has_key?(:remote)
- options[:html][:method] = options.delete(:method) if options.has_key?(:method)
- options[:html][:authenticity_token] = options.delete(:authenticity_token)
-
- builder = options[:parent_builder] = instantiate_builder(object_name, object, options)
- fields_for = fields_for(object_name, object, options, &proc)
- default_options = builder.multipart? ? { multipart: true } : {}
- default_options.merge!(options.delete(:html))
-
- form_tag(options.delete(:url) || {}, default_options) { fields_for }
- end
-
- def apply_form_for_options!(record, object, options) #:nodoc:
- object = convert_to_model(object)
-
- as = options[:as]
- action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
- options[:html].reverse_merge!(
- class: as ? "#{action}_#{as}" : dom_class(object, action),
- id: as ? "#{action}_#{as}" : [options[:namespace], dom_id(object, action)].compact.join("_").presence,
- method: method
- )
-
- options[:url] ||= polymorphic_path(record, format: options.delete(:format))
- end
- private :apply_form_for_options!
-
- # Creates a scope around a specific model object like form_for, but
- # doesn't create the form tags themselves. This makes fields_for suitable
- # for specifying additional model objects in the same form.
- #
- # === Generic Examples
- #
- # Although the usage and purpose of +field_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
- # generate fields associated with the model object. Fields may reflect
- # a model object in two ways - how they are named (hence how submitted
- # values appear within the +params+ hash in the controller) and what
- # default values are shown when the form the fields appear in is first
- # displayed. In order for both of these features to be specified independently,
- # both an object name (represented by either a symbol or string) and the
- # object itself can be passed to the method separately -
- #
- # <%= form_for @person do |person_form| %>
- # First name: <%= person_form.text_field :first_name %>
- # Last name : <%= person_form.text_field :last_name %>
- #
- # <%= fields_for :permission, @person.permission do |permission_fields| %>
- # Admin? : <%= permission_fields.check_box :admin %>
- # <% end %>
- #
- # <%= f.submit %>
- # <% end %>
- #
- # In this case, the checkbox field will be represented by an HTML +input+
- # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
- # value will appear in the controller as <tt>params[:permission][:admin]</tt>.
- # If <tt>@person.permission</tt> is an existing record with an attribute
- # +admin+, the initial state of the checkbox when first displayed will
- # reflect the value of <tt>@person.permission.admin</tt>.
- #
- # Often this can be simplified by passing just the name of the model
- # object to +fields_for+ -
- #
- # <%= fields_for :permission do |permission_fields| %>
- # Admin?: <%= permission_fields.check_box :admin %>
- # <% end %>
- #
- # ...in which case, if <tt>:permission</tt> also happens to be the name of an
- # instance variable <tt>@permission</tt>, the initial state of the input
- # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
- #
- # Alternatively, you can pass just the model object itself (if the first
- # argument isn't a string or symbol +fields_for+ will realize that the
- # name has been omitted) -
- #
- # <%= fields_for @person.permission do |permission_fields| %>
- # Admin?: <%= permission_fields.check_box :admin %>
- # <% end %>
- #
- # and +fields_for+ will derive the required name of the field from the
- # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
- # of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
- #
- # Note: This also works for the methods in FormOptionHelper and
- # DateHelper that are designed to work with an object as base, like
- # FormOptionHelper#collection_select and DateHelper#datetime_select.
- #
- # === Nested Attributes Examples
- #
- # When the object belonging to the current scope has a nested attribute
- # writer for a certain attribute, fields_for will yield a new scope
- # for that attribute. This allows you to create forms that set or change
- # the attributes of a parent object and its associations in one go.
- #
- # Nested attribute writers are normal setter methods named after an
- # association. The most common way of defining these writers is either
- # with +accepts_nested_attributes_for+ in a model definition or by
- # defining a method with the proper name. For example: the attribute
- # writer for the association <tt>:address</tt> is called
- # <tt>address_attributes=</tt>.
- #
- # Whether a one-to-one or one-to-many style form builder will be yielded
- # depends on whether the normal reader method returns a _single_ object
- # or an _array_ of objects.
- #
- # ==== One-to-one
- #
- # Consider a Person class which returns a _single_ Address from the
- # <tt>address</tt> reader method and responds to the
- # <tt>address_attributes=</tt> writer method:
- #
- # class Person
- # def address
- # @address
- # end
- #
- # def address_attributes=(attributes)
- # # Process the attributes hash
- # end
- # end
- #
- # This model can now be used with a nested fields_for, like so:
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :address do |address_fields| %>
- # Street : <%= address_fields.text_field :street %>
- # Zip code: <%= address_fields.text_field :zip_code %>
- # <% end %>
- # ...
- # <% end %>
- #
- # When address is already an association on a Person you can use
- # +accepts_nested_attributes_for+ to define the writer method for you:
- #
- # class Person < ActiveRecord::Base
- # has_one :address
- # accepts_nested_attributes_for :address
- # end
- #
- # If you want to destroy the associated model through the form, you have
- # to enable it first using the <tt>:allow_destroy</tt> option for
- # +accepts_nested_attributes_for+:
- #
- # class Person < ActiveRecord::Base
- # has_one :address
- # accepts_nested_attributes_for :address, allow_destroy: true
- # end
- #
- # Now, when you use a form element with the <tt>_destroy</tt> parameter,
- # with a value that evaluates to +true+, you will destroy the associated
- # model (eg. 1, '1', true, or 'true'):
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :address do |address_fields| %>
- # ...
- # Delete: <%= address_fields.check_box :_destroy %>
- # <% end %>
- # ...
- # <% end %>
- #
- # ==== One-to-many
- #
- # Consider a Person class which returns an _array_ of Project instances
- # from the <tt>projects</tt> reader method and responds to the
- # <tt>projects_attributes=</tt> writer method:
- #
- # class Person
- # def projects
- # [@project1, @project2]
- # end
- #
- # def projects_attributes=(attributes)
- # # Process the attributes hash
- # end
- # end
- #
- # Note that the <tt>projects_attributes=</tt> writer method is in fact
- # required for fields_for to correctly identify <tt>:projects</tt> as a
- # collection, and the correct indices to be set in the form markup.
- #
- # When projects is already an association on Person you can use
- # +accepts_nested_attributes_for+ to define the writer method for you:
- #
- # class Person < ActiveRecord::Base
- # has_many :projects
- # accepts_nested_attributes_for :projects
- # end
- #
- # This model can now be used with a nested fields_for. The block given to
- # the nested fields_for call will be repeated for each instance in the
- # collection:
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :projects do |project_fields| %>
- # <% if project_fields.object.active? %>
- # Name: <%= project_fields.text_field :name %>
- # <% end %>
- # <% end %>
- # ...
- # <% end %>
- #
- # It's also possible to specify the instance to be used:
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <% @person.projects.each do |project| %>
- # <% if project.active? %>
- # <%= person_form.fields_for :projects, project do |project_fields| %>
- # Name: <%= project_fields.text_field :name %>
- # <% end %>
- # <% end %>
- # <% end %>
- # ...
- # <% end %>
- #
- # Or a collection to be used:
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
- # Name: <%= project_fields.text_field :name %>
- # <% end %>
- # ...
- # <% end %>
- #
- # When projects is already an association on Person you can use
- # +accepts_nested_attributes_for+ to define the writer method for you:
- #
- # class Person < ActiveRecord::Base
- # has_many :projects
- # accepts_nested_attributes_for :projects
- # end
- #
- # If you want to destroy any of the associated models through the
- # form, you have to enable it first using the <tt>:allow_destroy</tt>
- # option for +accepts_nested_attributes_for+:
- #
- # class Person < ActiveRecord::Base
- # has_many :projects
- # accepts_nested_attributes_for :projects, allow_destroy: true
- # end
- #
- # This will allow you to specify which models to destroy in the
- # attributes hash by adding a form element for the <tt>_destroy</tt>
- # parameter with a value that evaluates to +true+
- # (eg. 1, '1', true, or 'true'):
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :projects do |project_fields| %>
- # Delete: <%= project_fields.check_box :_destroy %>
- # <% end %>
- # ...
- # <% end %>
- #
- # When a collection is used you might want to know the index of each
- # object into the array. For this purpose, the <tt>index</tt> method
- # is available in the FormBuilder object.
- #
- # <%= form_for @person do |person_form| %>
- # ...
- # <%= person_form.fields_for :projects do |project_fields| %>
- # Project #<%= project_fields.index %>
- # ...
- # <% end %>
- # ...
- # <% end %>
- def fields_for(record_name, record_object = nil, options = {}, &block)
- builder = instantiate_builder(record_name, record_object, options)
- output = capture(builder, &block)
- output.concat builder.hidden_field(:id) if output && options[:hidden_field_id] && !builder.emitted_hidden_id?
- output
- end
-
- # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
- # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
- # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
- # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
- # target labels for radio_button tags (where the value is used in the ID of the input tag).
- #
- # ==== Examples
- # label(:post, :title)
- # # => <label for="post_title">Title</label>
- #
- # You can localize your labels based on model and attribute names.
- # For example you can define the following in your locale (e.g. en.yml)
- #
- # helpers:
- # label:
- # post:
- # body: "Write your entire text here"
- #
- # Which then will result in
- #
- # label(:post, :body)
- # # => <label for="post_body">Write your entire text here</label>
- #
- # Localization can also be based purely on the translation of the attribute-name
- # (if you are using ActiveRecord):
- #
- # activerecord:
- # attributes:
- # post:
- # cost: "Total cost"
- #
- # label(:post, :cost)
- # # => <label for="post_cost">Total cost</label>
- #
- # label(:post, :title, "A short title")
- # # => <label for="post_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(:post, :privacy, "Public Post", value: "public")
- # # => <label for="post_privacy_public">Public Post</label>
- #
- # label(:post, :terms) do
- # 'Accept <a href="/terms">Terms</a>.'.html_safe
- # end
- 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
-
- # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
- # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
- # shown.
- #
- # ==== Examples
- # text_field(:post, :title, size: 20)
- # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
- #
- # text_field(:post, :title, class: "create_input")
- # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
- #
- # text_field(:session, :user, onchange: "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }")
- # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange = "if $('session[user]').value == 'admin' { alert('Your login can not be admin!'); }"/>
- #
- # text_field(:snippet, :code, size: 20, class: 'code_input')
- # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
- def text_field(object_name, method, options = {})
- Tags::TextField.new(object_name, method, self, options).render
- end
-
- # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
- # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
- # shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired.
- #
- # ==== Examples
- # password_field(:login, :pass, size: 20)
- # # => <input type="password" id="login_pass" name="login[pass]" size="20" />
- #
- # password_field(:account, :secret, class: "form_input", value: @account.secret)
- # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
- #
- # password_field(:user, :password, onchange: "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }")
- # # => <input type="password" id="user_password" name="user[password]" onchange = "if $('user[password]').length > 30 { alert('Your password needs to be shorter!'); }"/>
- #
- # password_field(:account, :pin, size: 20, class: 'form_input')
- # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
- def password_field(object_name, method, options = {})
- Tags::PasswordField.new(object_name, method, self, options).render
- end
-
- # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
- # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
- # shown.
- #
- # ==== Examples
- # hidden_field(:signup, :pass_confirm)
- # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
- #
- # hidden_field(:post, :tag_list)
- # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
- #
- # hidden_field(:user, :token)
- # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
- def hidden_field(object_name, method, options = {})
- Tags::HiddenField.new(object_name, method, self, options).render
- end
-
- # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
- # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
- # shown.
- #
- # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
- #
- # ==== Examples
- # file_field(:user, :avatar)
- # # => <input type="file" id="user_avatar" name="user[avatar]" />
- #
- # file_field(:post, :attached, accept: 'text/html')
- # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
- #
- # file_field(:attachment, :file, class: 'file_input')
- # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
- def file_field(object_name, method, options = {})
- Tags::FileField.new(object_name, method, self, options).render
- end
-
- # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
- # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
- # hash with +options+.
- #
- # ==== Examples
- # text_area(:post, :body, cols: 20, rows: 40)
- # # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
- # # #{@post.body}
- # # </textarea>
- #
- # text_area(:comment, :text, size: "20x30")
- # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
- # # #{@comment.text}
- # # </textarea>
- #
- # text_area(:application, :notes, cols: 40, rows: 15, class: 'app_input')
- # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
- # # #{@application.notes}
- # # </textarea>
- #
- # text_area(:entry, :body, size: "20x20", disabled: 'disabled')
- # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
- # # #{@entry.body}
- # # </textarea>
- def text_area(object_name, method, options = {})
- Tags::TextArea.new(object_name, method, self, options).render
- end
-
- # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
- # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
- # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
- # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
- #
- # ==== Gotcha
- #
- # The HTML specification says unchecked check boxes are not successful, and
- # thus web browsers do not send them. Unfortunately this introduces a gotcha:
- # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
- # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
- # any mass-assignment idiom like
- #
- # @invoice.update_attributes(params[:invoice])
- #
- # wouldn't update the flag.
- #
- # To prevent this the helper generates an auxiliary hidden field before
- # the very check box. The hidden field has the same name and its
- # attributes mimic an unchecked check box.
- #
- # This way, the client either sends only the hidden field (representing
- # the check box is unchecked), or both fields. Since the HTML specification
- # says key/value pairs have to be sent in the same order they appear in the
- # form, and parameters extraction gets the last occurrence of any repeated
- # key in the query string, that works for ordinary forms.
- #
- # Unfortunately that workaround does not work when the check box goes
- # within an array-like parameter, as in
- #
- # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
- # <%= form.check_box :paid %>
- # ...
- # <% end %>
- #
- # because parameter name repetition is precisely what Rails seeks to distinguish
- # the elements of the array. For each item with a checked check box you
- # get an extra ghost item with only that attribute, assigned to "0".
- #
- # In that case it is preferable to either use +check_box_tag+ or to use
- # hashes instead of arrays.
- #
- # # Let's say that @post.validated? is 1:
- # check_box("post", "validated")
- # # => <input name="post[validated]" type="hidden" value="0" />
- # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
- #
- # # Let's say that @puppy.gooddog is "no":
- # check_box("puppy", "gooddog", {}, "yes", "no")
- # # => <input name="puppy[gooddog]" type="hidden" value="no" />
- # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
- #
- # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
- # # => <input name="eula[accepted]" type="hidden" value="no" />
- # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
- def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
- Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render
- end
-
- # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
- # radio button will be checked.
- #
- # To force the radio button to be checked pass <tt>checked: true</tt> in the
- # +options+ hash. You may pass HTML options there as well.
- #
- # # 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" />
- #
- # radio_button("user", "receive_newsletter", "yes")
- # radio_button("user", "receive_newsletter", "no")
- # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
- # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
- def radio_button(object_name, method, tag_value, options = {})
- Tags::RadioButton.new(object_name, method, self, tag_value, options).render
- end
-
- # Returns a text_field of type "color".
- #
- # color_field("car", "color")
- # # => <input id="car_color" name="car[color]" type="color" value="#000000" />
- def color_field(object_name, method, options = {})
- Tags::ColorField.new(object_name, method, self, options).render
- end
-
- # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object
- # assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by
- # some browsers.
- #
- # search_field(:user, :name)
- # # => <input id="user_name" name="user[name]" type="search" />
- # search_field(:user, :name, autosave: false)
- # # => <input autosave="false" id="user_name" name="user[name]" type="search" />
- # search_field(:user, :name, results: 3)
- # # => <input id="user_name" name="user[name]" results="3" type="search" />
- # # Assume request.host returns "www.example.com"
- # search_field(:user, :name, autosave: true)
- # # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />
- # search_field(:user, :name, onsearch: true)
- # # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
- # search_field(:user, :name, autosave: false, onsearch: true)
- # # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
- # search_field(:user, :name, autosave: true, onsearch: true)
- # # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" type="search" />
- def search_field(object_name, method, options = {})
- Tags::SearchField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "tel".
- #
- # telephone_field("user", "phone")
- # # => <input id="user_phone" name="user[phone]" type="tel" />
- #
- def telephone_field(object_name, method, options = {})
- Tags::TelField.new(object_name, method, self, options).render
- end
- # aliases telephone_field
- alias phone_field telephone_field
-
- # Returns a text_field of type "date".
- #
- # date_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="date" />
- #
- # The default value is generated by trying to call "to_date"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone. You can still override that
- # by passing the "value" option explicitly, e.g.
- #
- # @user.born_on = Date.new(1984, 1, 27)
- # date_field("user", "born_on", value: "1984-05-12")
- # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" />
- #
- def date_field(object_name, method, options = {})
- Tags::DateField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "time".
- #
- # The default value is generated by trying to call +strftime+ with "%T.%L"
- # on the objects's value. It is still possible to override that
- # by passing the "value" option.
- #
- # === Options
- # * Accepts same options as time_field_tag
- #
- # === Example
- # time_field("task", "started_at")
- # # => <input id="task_started_at" name="task[started_at]" type="time" />
- #
- def time_field(object_name, method, options = {})
- Tags::TimeField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "datetime".
- #
- # datetime_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" />
- #
- # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone.
- #
- # @user.born_on = Date.new(1984, 1, 12)
- # datetime_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" />
- #
- def datetime_field(object_name, method, options = {})
- Tags::DatetimeField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "datetime-local".
- #
- # datetime_local_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" />
- #
- # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone.
- #
- # @user.born_on = Date.new(1984, 1, 12)
- # datetime_local_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
- #
- def datetime_local_field(object_name, method, options = {})
- Tags::DatetimeLocalField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "month".
- #
- # month_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="month" />
- #
- # The default value is generated by trying to call +strftime+ with "%Y-%m"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone.
- #
- # @user.born_on = Date.new(1984, 1, 27)
- # month_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" />
- #
- def month_field(object_name, method, options = {})
- Tags::MonthField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "week".
- #
- # week_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="week" />
- #
- # The default value is generated by trying to call +strftime+ with "%Y-W%W"
- # on the object's value, which makes it behave as expected for instances
- # of DateTime and ActiveSupport::TimeWithZone.
- #
- # @user.born_on = Date.new(1984, 5, 12)
- # week_field("user", "born_on")
- # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" />
- #
- def week_field(object_name, method, options = {})
- Tags::WeekField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "url".
- #
- # url_field("user", "homepage")
- # # => <input id="user_homepage" name="user[homepage]" type="url" />
- #
- def url_field(object_name, method, options = {})
- Tags::UrlField.new(object_name, method, self, options).render
- end
-
- # Returns a text_field of type "email".
- #
- # email_field("user", "address")
- # # => <input id="user_address" name="user[address]" type="email" />
- #
- def email_field(object_name, method, options = {})
- Tags::EmailField.new(object_name, method, self, options).render
- end
-
- # Returns an input tag of type "number".
- #
- # ==== Options
- # * Accepts same options as number_field_tag
- def number_field(object_name, method, options = {})
- Tags::NumberField.new(object_name, method, self, options).render
- end
-
- # Returns an input tag of type "range".
- #
- # ==== Options
- # * Accepts same options as range_field_tag
- def range_field(object_name, method, options = {})
- Tags::RangeField.new(object_name, method, self, options).render
- end
-
- private
-
- def instantiate_builder(record_name, record_object, options)
- case record_name
- when String, Symbol
- object = record_object
- object_name = record_name
- else
- object = record_name
- object_name = model_name_from_record_or_class(object).param_key
- end
-
- builder = options[:builder] || default_form_builder
- builder.new(object_name, object, self, options)
- end
-
- def default_form_builder
- builder = ActionView::Base.default_form_builder
- builder.respond_to?(:constantize) ? builder.constantize : builder
- end
- end
-
- class FormBuilder
- include ModelNaming
-
- # The methods which wrap a form helper call.
- class_attribute :field_helpers
- self.field_helpers = FormHelper.instance_methods - [:form_for, :convert_to_model, :model_name_from_record_or_class]
-
- attr_accessor :object_name, :object, :options
-
- attr_reader :multipart, :parent_builder, :index
- alias :multipart? :multipart
-
- def multipart=(multipart)
- @multipart = multipart
- parent_builder.multipart = multipart if parent_builder
- end
-
- def self._to_partial_path
- @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '')
- end
-
- def to_partial_path
- self.class._to_partial_path
- end
-
- def to_model
- self
- end
-
- def initialize(object_name, object, template, options, block=nil)
- if block
- ActiveSupport::Deprecation.warn(
- "Giving a block to FormBuilder is deprecated and has no effect anymore.")
- end
-
- @nested_child_index = {}
- @object_name, @object, @template, @options = object_name, object, template, options
- @parent_builder = options[:parent_builder]
- @default_options = @options ? @options.slice(:index, :namespace) : {}
- if @object_name.to_s.match(/\[\]$/)
- if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
- @auto_index = object.to_param
- else
- raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
- end
- end
- @multipart = nil
- @index = options[:index] || options[:child_index]
- end
-
- (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{selector}(method, options = {}) # def text_field(method, options = {})
- @template.send( # @template.send(
- #{selector.inspect}, # "text_field",
- @object_name, # @object_name,
- method, # method,
- objectify_options(options)) # objectify_options(options))
- end # end
- RUBY_EVAL
- end
-
- def fields_for(record_name, record_object = nil, fields_options = {}, &block)
- fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
- fields_options[:builder] ||= options[:builder]
- fields_options[:parent_builder] = self
- fields_options[:namespace] = options[:namespace]
-
- case record_name
- when String, Symbol
- if nested_attributes_association?(record_name)
- return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
- end
- else
- record_object = record_name.is_a?(Array) ? record_name.last : record_name
- record_name = model_name_from_record_or_class(record_object).param_key
- end
-
- index = if options.has_key?(:index)
- options[:index]
- elsif defined?(@auto_index)
- self.object_name = @object_name.to_s.sub(/\[\]$/,"")
- @auto_index
- end
-
- record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
- fields_options[:child_index] = index
-
- @template.fields_for(record_name, record_object, fields_options, &block)
- end
-
- def label(method, text = nil, options = {}, &block)
- @template.label(@object_name, method, text, objectify_options(options), &block)
- end
-
- def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
- @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
- end
-
- def radio_button(method, tag_value, options = {})
- @template.radio_button(@object_name, method, tag_value, objectify_options(options))
- end
-
- def hidden_field(method, options = {})
- @emitted_hidden_id = true if method == :id
- @template.hidden_field(@object_name, method, objectify_options(options))
- end
-
- def file_field(method, options = {})
- self.multipart = true
- @template.file_field(@object_name, method, objectify_options(options))
- end
-
- # Add the submit button for the given form. When no value is given, it checks
- # if the object is a new resource or not to create the proper label:
- #
- # <%= form_for @post do |f| %>
- # <%= f.submit %>
- # <% end %>
- #
- # In the example above, if @post is a new record, it will use "Create Post" as
- # submit button label, otherwise, it uses "Update Post".
- #
- # Those labels can be customized using I18n, under the helpers.submit key and accept
- # the %{model} as translation interpolation:
- #
- # en:
- # helpers:
- # submit:
- # create: "Create a %{model}"
- # update: "Confirm changes to %{model}"
- #
- # It also searches for a key specific for the given object:
- #
- # en:
- # helpers:
- # submit:
- # post:
- # create: "Add %{model}"
- #
- def submit(value=nil, options={})
- value, options = nil, value if value.is_a?(Hash)
- value ||= submit_default_value
- @template.submit_tag(value, options)
- end
-
- # Add the submit button for the given form. When no value is given, it checks
- # if the object is a new resource or not to create the proper label:
- #
- # <%= form_for @post do |f| %>
- # <%= f.button %>
- # <% end %>
- #
- # In the example above, if @post is a new record, it will use "Create Post" as
- # button label, otherwise, it uses "Update Post".
- #
- # Those labels can be customized using I18n, under the helpers.submit key
- # (the same as submit helper) and accept the %{model} as translation interpolation:
- #
- # en:
- # helpers:
- # submit:
- # create: "Create a %{model}"
- # update: "Confirm changes to %{model}"
- #
- # It also searches for a key specific for the given object:
- #
- # en:
- # helpers:
- # submit:
- # post:
- # create: "Add %{model}"
- #
- # ==== Examples
- # button("Create a post")
- # # => <button name='button' type='submit'>Create post</button>
- #
- # button do
- # content_tag(:strong, 'Ask me!')
- # end
- # # => <button name='button' type='submit'>
- # # <strong>Ask me!</strong>
- # # </button>
- #
- def button(value = nil, options = {}, &block)
- value, options = nil, value if value.is_a?(Hash)
- value ||= submit_default_value
- @template.button_tag(value, options, &block)
- end
-
- def emitted_hidden_id?
- @emitted_hidden_id ||= nil
- end
-
- private
- def objectify_options(options)
- @default_options.merge(options.merge(object: @object))
- end
-
- def submit_default_value
- object = convert_to_model(@object)
- key = object ? (object.persisted? ? :update : :create) : :submit
-
- model = if object.class.respond_to?(:model_name)
- object.class.model_name.human
- else
- @object_name.to_s.humanize
- end
-
- defaults = []
- defaults << :"helpers.submit.#{object_name}.#{key}"
- defaults << :"helpers.submit.#{key}"
- defaults << "#{key.to_s.humanize} #{model}"
-
- I18n.t(defaults.shift, model: model, default: defaults)
- end
-
- def nested_attributes_association?(association_name)
- @object.respond_to?("#{association_name}_attributes=")
- end
-
- def fields_for_with_nested_attributes(association_name, association, options, block)
- name = "#{object_name}[#{association_name}_attributes]"
- association = convert_to_model(association)
-
- if association.respond_to?(:persisted?)
- association = [association] if @object.send(association_name).is_a?(Array)
- elsif !association.respond_to?(:to_ary)
- association = @object.send(association_name)
- end
-
- if association.respond_to?(:to_ary)
- explicit_child_index = options[:child_index]
- output = ActiveSupport::SafeBuffer.new
- association.each do |child|
- options[:child_index] = nested_child_index(name) unless explicit_child_index
- output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
- end
- output
- elsif association
- fields_for_nested_model(name, association, options, block)
- end
- end
-
- def fields_for_nested_model(name, object, options, block)
- object = convert_to_model(object)
-
- parent_include_id = self.options.fetch(:include_id, true)
- include_id = options.fetch(:include_id, parent_include_id)
- options[:hidden_field_id] = object.persisted? && include_id
- @template.fields_for(name, object, options, &block)
- end
-
- def nested_child_index(name)
- @nested_child_index[name] ||= -1
- @nested_child_index[name] += 1
- end
- end
- end
-
- ActiveSupport.on_load(:action_view) do
- cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder }
- end
-end
diff --git a/actionpack/lib/action_view/helpers/form_options_helper.rb b/actionpack/lib/action_view/helpers/form_options_helper.rb
deleted file mode 100644
index 2bb526a539..0000000000
--- a/actionpack/lib/action_view/helpers/form_options_helper.rb
+++ /dev/null
@@ -1,788 +0,0 @@
-require 'cgi'
-require 'erb'
-require 'action_view/helpers/form_helper'
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView
- # = Action View Form Option Helpers
- module Helpers
- # Provides a number of methods for turning different kinds of containers into a set of option tags.
- # == Options
- # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
- #
- # * <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.
- #
- # For example,
- #
- # select("post", "category", Post::CATEGORIES, {:include_blank => true})
- #
- # could become:
- #
- # <select name="post[category]">
- # <option></option>
- # <option>joke</option>
- # <option>poem</option>
- # </select>
- #
- # Another common case is a select tag for an <tt>belongs_to</tt>-associated object.
- #
- # Example with @post.person_id => 2:
- #
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:include_blank => 'None'})
- #
- # 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>
- #
- # * <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.
- #
- # Example:
- #
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {:prompt => 'Select Person'})
- #
- # 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>
- #
- # 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.
- #
- # Example:
- #
- # select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
- #
- # becomes:
- #
- # <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.
- #
- # Example:
- #
- # select("post", "category", Post::CATEGORIES, {:disabled => 'restricted'})
- #
- # could become:
- #
- # <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.
- #
- # Example:
- #
- # 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>
- #
- module FormOptionsHelper
- # ERB::Util can mask some helpers like textilize. Make sure to include them.
- include TextHelper
-
- # Create a select tag and a series of contained option tags for the provided object and method.
- # The option currently held by the object will be selected, provided that the object is available.
- #
- # There are two possible formats for the choices parameter, corresponding to other helpers' output:
- # * A flat collection: see options_for_select
- # * A nested collection: see grouped_options_for_select
- #
- # Example with @post.person_id => 1:
- # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { :include_blank => true })
- #
- # could become:
- #
- # <select name="post[person_id]">
- # <option value=""></option>
- # <option value="1" selected="selected">David</option>
- # <option value="2">Sam</option>
- # <option value="3">Tobias</option>
- # </select>
- #
- # This can be used to provide a default set of options in the standard way: before rendering the create form, a
- # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
- # to the database. Instead, a second model object is created when the create request is received.
- # This allows the user to submit a form page more than once with the expected results of creating multiple records.
- # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
- #
- # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>:selected => value</tt> to use a different selection
- # or <tt>:selected => nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
- # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
- #
- # ==== Gotcha
- #
- # The HTML specification says when +multiple+ parameter passed to select and all options got deselected
- # web browsers do not send any value to server. Unfortunately this introduces a gotcha:
- # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
- # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
- # any mass-assignment idiom like
- #
- # @user.update_attributes(params[:user])
- #
- # wouldn't update roles.
- #
- # 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.
- #
- # In case if you don't want the helper to generate this hidden field you can specify <tt>:include_blank => false</tt> option.
- #
- def select(object, method, choices, options = {}, html_options = {})
- Tags::Select.new(object, method, self, choices, options, html_options).render
- end
-
- # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
- # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
- # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
- # or <tt>:include_blank</tt> in the +options+ hash.
- #
- # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
- # of +collection+. The return values are used as the +value+ attribute and contents of each
- # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
- # as a +proc+, that will be called for each member of the +collection+ to
- # retrieve the value/text.
- #
- # Example object structure for use with this method:
- # class Post < ActiveRecord::Base
- # belongs_to :author
- # end
- # class Author < ActiveRecord::Base
- # has_many :posts
- # def name_with_initial
- # "#{first_name.first}. #{last_name}"
- # end
- # end
- #
- # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
- # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, :prompt => true)
- #
- # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
- # <select name="post[author_id]">
- # <option value="">Please select</option>
- # <option value="1" selected="selected">D. Heinemeier Hansson</option>
- # <option value="2">D. Thomas</option>
- # <option value="3">M. Clark</option>
- # </select>
- def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
- Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
- end
-
- # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
- # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
- # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
- # or <tt>:include_blank</tt> in the +options+ hash.
- #
- # Parameters:
- # * +object+ - The instance of the class to be used for the select tag
- # * +method+ - The attribute of +object+ corresponding to the select tag
- # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
- # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
- # array of child objects representing the <tt><option></tt> tags.
- # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
- # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
- # * +option_key_method+ - The name of a method which, when called on a child object of a member of
- # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
- # * +option_value_method+ - The name of a method which, when called on a child object of a member of
- # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
- #
- # Example object structure for use with this method:
- # class Continent < ActiveRecord::Base
- # has_many :countries
- # # attribs: id, name
- # end
- # class Country < ActiveRecord::Base
- # belongs_to :continent
- # # attribs: id, name, continent_id
- # end
- # class City < ActiveRecord::Base
- # belongs_to :country
- # # attribs: id, name, country_id
- # end
- #
- # Sample usage:
- # grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
- #
- # Possible output:
- # <select name="city[country_id]">
- # <optgroup label="Africa">
- # <option value="1">South Africa</option>
- # <option value="3">Somalia</option>
- # </optgroup>
- # <optgroup label="Europe">
- # <option value="7" selected="selected">Denmark</option>
- # <option value="2">Ireland</option>
- # </optgroup>
- # </select>
- #
- def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
- Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
- end
-
- # Return select and option tags for the given object and method, using
- # #time_zone_options_for_select to generate the list of option tags.
- #
- # In addition to the <tt>:include_blank</tt> option documented above,
- # this method also supports a <tt>:model</tt> option, which defaults
- # to ActiveSupport::TimeZone. This may be used by users to specify a
- # different time zone model object. (See +time_zone_options_for_select+
- # for more information.)
- #
- # You can also supply an array of ActiveSupport::TimeZone objects
- # as +priority_zones+, so that they will be listed above the rest of the
- # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience
- # for obtaining a list of the US time zones, or a Regexp to select the zones
- # of your choice)
- #
- # Finally, this method supports a <tt>:default</tt> option, which selects
- # a default ActiveSupport::TimeZone if the object's time zone is +nil+.
- #
- # time_zone_select( "user", "time_zone", nil, :include_blank => true)
- #
- # time_zone_select( "user", "time_zone", nil, :default => "Pacific Time (US & Canada)" )
- #
- # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, :default => "Pacific Time (US & Canada)")
- #
- # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
- #
- # time_zone_select( "user", 'time_zone', /Australia/)
- #
- # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, :model => ActiveSupport::TimeZone)
- def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
- Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
- end
-
- # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
- # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
- # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
- # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
- # may also be an array of values to be selected when using a multiple select.
- #
- # Examples (call, result):
- # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
- # # <option value="$">Dollar</option>
- # # <option value="DKK">Kroner</option>
- #
- # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
- # # <option>VISA</option>
- # # <option selected="selected">MasterCard</option>
- #
- # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
- # # <option value="$20">Basic</option>
- # # <option value="$40" selected="selected">Plus</option>
- #
- # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
- # # <option selected="selected">VISA</option>
- # # <option>MasterCard</option>
- # # <option selected="selected">Discover</option>
- #
- # You can optionally provide html attributes as the last element of the array.
- #
- # Examples:
- # options_for_select([ "Denmark", ["USA", {:class => 'bold'}], "Sweden" ], ["USA", "Sweden"])
- # # <option value="Denmark">Denmark</option>
- # # <option value="USA" class="bold" selected="selected">USA</option>
- # # <option value="Sweden" selected="selected">Sweden</option>
- #
- # options_for_select([["Dollar", "$", {:class => "bold"}], ["Kroner", "DKK", {:onclick => "alert('HI');"}]])
- # # <option value="$" class="bold">Dollar</option>
- # # <option value="DKK" onclick="alert('HI');">Kroner</option>
- #
- # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
- # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
- #
- # Examples:
- # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => "Super Platinum")
- # # <option value="Free">Free</option>
- # # <option value="Basic">Basic</option>
- # # <option value="Advanced">Advanced</option>
- # # <option value="Super Platinum" disabled="disabled">Super Platinum</option>
- #
- # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :disabled => ["Advanced", "Super Platinum"])
- # # <option value="Free">Free</option>
- # # <option value="Basic">Basic</option>
- # # <option value="Advanced" disabled="disabled">Advanced</option>
- # # <option value="Super Platinum" disabled="disabled">Super Platinum</option>
- #
- # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], :selected => "Free", :disabled => "Super Platinum")
- # # <option value="Free" selected="selected">Free</option>
- # # <option value="Basic">Basic</option>
- # # <option value="Advanced">Advanced</option>
- # # <option value="Super Platinum" disabled="disabled">Super Platinum</option>
- #
- # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
- def options_for_select(container, selected = nil)
- return container if String === container
-
- selected, disabled = extract_selected_and_disabled(selected).map do |r|
- Array(r).map { |item| item.to_s }
- end
-
- container.map do |element|
- html_attributes = option_html_attributes(element)
- text, value = option_text_and_value(element).map { |item| item.to_s }
-
- html_attributes[:selected] = 'selected' if option_value_selected?(value, selected)
- html_attributes[:disabled] = 'disabled' if disabled && option_value_selected?(value, disabled)
- html_attributes[:value] = value
-
- content_tag_string(:option, text, html_attributes)
- end.join("\n").html_safe
- end
-
- # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning
- # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
- # Example:
- # options_from_collection_for_select(@people, 'id', 'name')
- # This will output the same HTML as if you did this:
- # <option value="#{person.id}">#{person.name}</option>
- #
- # This is more often than not used inside a #select_tag like this example:
- # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
- #
- # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
- # will be selected option tag(s).
- #
- # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
- # function are the selected values.
- #
- # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
- #
- # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
- # Failure to do this will produce undesired results. Example:
- # options_from_collection_for_select(@people, 'id', 'name', '1')
- # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
- # options_from_collection_for_select(@people, 'id', 'name', 1)
- # should produce the desired results.
- def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
- options = collection.map do |element|
- [value_for_collection(element, text_method), value_for_collection(element, value_method)]
- end
- selected, disabled = extract_selected_and_disabled(selected)
- select_deselect = {
- :selected => extract_values_from_collection(collection, value_method, selected),
- :disabled => extract_values_from_collection(collection, value_method, disabled)
- }
-
- options_for_select(options, select_deselect)
- end
-
- # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
- # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
- #
- # Parameters:
- # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
- # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
- # array of child objects representing the <tt><option></tt> tags.
- # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
- # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
- # * +option_key_method+ - The name of a method which, when called on a child object of a member of
- # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
- # * +option_value_method+ - The name of a method which, when called on a child object of a member of
- # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
- # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
- # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
- # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
- # to be specified.
- #
- # Example object structure for use with this method:
- # class Continent < ActiveRecord::Base
- # has_many :countries
- # # attribs: id, name
- # end
- # class Country < ActiveRecord::Base
- # belongs_to :continent
- # # attribs: id, name, continent_id
- # end
- #
- # Sample usage:
- # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
- #
- # Possible output:
- # <optgroup label="Africa">
- # <option value="1">Egypt</option>
- # <option value="4">Rwanda</option>
- # ...
- # </optgroup>
- # <optgroup label="Asia">
- # <option value="3" selected="selected">China</option>
- # <option value="12">India</option>
- # <option value="5">Japan</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.
- def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
- collection.map do |group|
- option_tags = options_from_collection_for_select(
- group.send(group_method), option_key_method, option_value_method, selected_key)
-
- content_tag(:optgroup, option_tags, :label => group.send(group_label_method))
- end.join.html_safe
- 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.
- #
- # Sample usage (Array):
- # grouped_options = [
- # ['North America',
- # [['United States','US'],'Canada']],
- # ['Europe',
- # ['Denmark','Germany','France']]
- # ]
- # grouped_options_for_select(grouped_options)
- #
- # Sample usage (Hash):
- # grouped_options = {
- # 'North America' => [['United States','US'], 'Canada'],
- # 'Europe' => ['Denmark','Germany','France']
- # }
- # grouped_options_for_select(grouped_options)
- #
- # Possible output:
- # <optgroup label="Europe">
- # <option value="Denmark">Denmark</option>
- # <option value="Germany">Germany</option>
- # <option value="France">France</option>
- # </optgroup>
- # <optgroup label="North America">
- # <option value="US">United States</option>
- # <option value="Canada">Canada</option>
- # </optgroup>
- #
- # Sample usage (divider):
- # 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.
- def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
- if options.is_a?(Hash)
- prompt = options[:prompt]
- divider = options[:divider]
- else
- prompt = options
- options = {}
- ActiveSupport::Deprecation.warn "Passing the prompt to grouped_options_for_select as an argument is deprecated. Please use an options hash like `{ prompt: #{prompt.inspect} }`."
- end
-
- body = "".html_safe
-
- if prompt
- body.safe_concat content_tag(:option, prompt_text(prompt), :value => "")
- end
-
- grouped_options = grouped_options.sort if grouped_options.is_a?(Hash)
-
- grouped_options.each do |container|
- if divider
- label = divider
- else
- label, container = container
- end
- body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), :label => label)
- end
-
- body
- end
-
- # Returns a string of option tags for pretty much any time zone in the
- # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
- # marked as the selected option tag. You can also supply an array of
- # ActiveSupport::TimeZone objects as +priority_zones+, so that they will
- # be listed above the rest of the (long) list. (You can use
- # ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list
- # of the US time zones, or a Regexp to select the zones of your choice)
- #
- # The +selected+ parameter must be either +nil+, or a string that names
- # a ActiveSupport::TimeZone.
- #
- # By default, +model+ is the ActiveSupport::TimeZone constant (which can
- # be obtained in Active Record as a value object). The only requirement
- # is that the +model+ parameter be an object that responds to +all+, and
- # returns an array of objects that represent time zones.
- #
- # NOTE: Only the option tags are returned, you have to wrap this call in
- # a regular HTML select tag.
- def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
- zone_options = "".html_safe
-
- zones = model.all
- convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
-
- if priority_zones
- if priority_zones.is_a?(Regexp)
- priority_zones = zones.select { |z| z =~ priority_zones }
- end
-
- zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
- zone_options.safe_concat content_tag(:option, '-------------', :value => '', :disabled => 'disabled')
- zone_options.safe_concat "\n"
-
- zones.reject! { |z| priority_zones.include?(z) }
- end
-
- zone_options.safe_concat options_for_select(convert_zones[zones], selected)
- end
-
- # Returns radio button tags for the collection of existing return values
- # of +method+ for +object+'s class. The value returned from calling
- # +method+ on the instance +object+ will be selected. If calling +method+
- # returns +nil+, no selection is made.
- #
- # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
- # methods to be called on each member of +collection+. The return values
- # are used as the +value+ attribute and contents of each radio button tag,
- # respectively. They can also be any object that responds to +call+, such
- # as a +proc+, that will be called for each member of the +collection+ to
- # retrieve the value/text.
- #
- # Example object structure for use with this method:
- # class Post < ActiveRecord::Base
- # belongs_to :author
- # end
- # class Author < ActiveRecord::Base
- # has_many :posts
- # def name_with_initial
- # "#{first_name.first}. #{last_name}"
- # end
- # end
- #
- # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
- # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
- #
- # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
- # <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>
- #
- # It is also possible to customize the way the elements will be shown by
- # giving a block to the method:
- # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
- # b.label { b.radio_button }
- # end
- #
- # The argument passed to the block is a special kind of builder for this
- # collection, which has the ability to generate the label and radio button
- # for the current item in the collection, with proper text and value.
- # Using it, you can change the label and radio button display order or
- # even use the label as wrapper, as in the example above.
- #
- # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
- # extra html options:
- # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
- # b.label(:class => "radio_button") { b.radio_button(:class => "radio_button") }
- # end
- #
- # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
- # <tt>value</tt>, which are the current item being rendered, its text and value methods,
- # respectively. You can use them like this:
- # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
- # b.label(:"data-value" => b.value) { b.radio_button + b.text }
- # end
- def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
- Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
- end
-
- # Returns check box tags for the collection of existing return values of
- # +method+ for +object+'s class. The value returned from calling +method+
- # on the instance +object+ will be selected. If calling +method+ returns
- # +nil+, no selection is made.
- #
- # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
- # methods to be called on each member of +collection+. The return values
- # are used as the +value+ attribute and contents of each check box tag,
- # respectively. They can also be any object that responds to +call+, such
- # as a +proc+, that will be called for each member of the +collection+ to
- # retrieve the value/text.
- #
- # Example object structure for use with this method:
- # class Post < ActiveRecord::Base
- # has_and_belongs_to_many :author
- # end
- # class Author < ActiveRecord::Base
- # has_and_belongs_to_many :posts
- # def name_with_initial
- # "#{first_name.first}. #{last_name}"
- # end
- # end
- #
- # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
- # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
- #
- # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
- # <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="" />
- #
- # It is also possible to customize the way the elements will be shown by
- # giving a block to the method:
- # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
- # b.label { b.check_box }
- # end
- #
- # The argument passed to the block is a special kind of builder for this
- # collection, which has the ability to generate the label and check box
- # for the current item in the collection, with proper text and value.
- # Using it, you can change the label and check box display order or even
- # use the label as wrapper, as in the example above.
- #
- # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
- # extra html options:
- # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
- # b.label(:class => "check_box") { b.check_box(:class => "check_box") }
- # end
- #
- # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
- # <tt>value</tt>, which are the current item being rendered, its text and value methods,
- # respectively. You can use them like this:
- # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
- # b.label(:"data-value" => b.value) { b.check_box + b.text }
- # end
- def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
- Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
- end
-
- private
- def option_html_attributes(element)
- if Array === element
- element.select { |e| Hash === e }.reduce({}, :merge!)
- else
- {}
- end
- end
-
- def option_text_and_value(option)
- # Options are [text, value] pairs or strings used for both.
- if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
- option = option.reject { |e| Hash === e } if Array === option
- [option.first, option.last]
- else
- [option, option]
- end
- end
-
- def option_value_selected?(value, selected)
- Array(selected).include? value
- end
-
- def extract_selected_and_disabled(selected)
- if selected.is_a?(Proc)
- [selected, nil]
- else
- selected = Array.wrap(selected)
- options = selected.extract_options!.symbolize_keys
- selected_items = options.fetch(:selected, selected)
- [selected_items, options[:disabled]]
- end
- end
-
- def extract_values_from_collection(collection, value_method, selected)
- if selected.is_a?(Proc)
- collection.map do |element|
- element.send(value_method) if selected.call(element)
- end.compact
- else
- selected
- end
- end
-
- def value_for_collection(item, value)
- value.respond_to?(:call) ? value.call(item) : item.send(value)
- end
-
- def prompt_text(prompt)
- prompt = prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', :default => 'Please select')
- end
- end
-
- class FormBuilder
- def select(method, choices, options = {}, html_options = {})
- @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options))
- end
-
- def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
- @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
- end
-
- def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
- @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
- end
-
- def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
- @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
- end
-
- def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {})
- @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
- end
-
- def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {})
- @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/form_tag_helper.rb b/actionpack/lib/action_view/helpers/form_tag_helper.rb
deleted file mode 100644
index f16e33d08d..0000000000
--- a/actionpack/lib/action_view/helpers/form_tag_helper.rb
+++ /dev/null
@@ -1,773 +0,0 @@
-require 'cgi'
-require 'action_view/helpers/tag_helper'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/core_ext/module/attribute_accessors'
-
-module ActionView
- # = Action View Form Tag Helpers
- module Helpers
- # Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like
- # FormHelper does. Instead, you provide the names and values manually.
- #
- # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying
- # <tt>:disabled => true</tt> will give <tt>disabled="disabled"</tt>.
- module FormTagHelper
- extend ActiveSupport::Concern
-
- include UrlHelper
- include TextHelper
-
- mattr_accessor :embed_authenticity_token_in_remote_forms
- self.embed_authenticity_token_in_remote_forms = false
-
- # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
- # ActionController::Base#url_for. The method for the form defaults to POST.
- #
- # ==== Options
- # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
- # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
- # If "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
- # is added to simulate the verb over post.
- # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
- # pass custom authenticity token string, or to not add authenticity_token field at all
- # (by passing <tt>false</tt>). Remote forms may omit the embedded authenticity token
- # by setting <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
- # This is helpful when you're fragment-caching the form. Remote forms get the
- # authenticity from the <tt>meta</tt> tag, so embedding is unnecessary unless you
- # support browsers without JavaScript.
- # * A list of parameters to feed to the URL the form will be posted to.
- # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
- # submit behavior. By default this behavior is an ajax submit.
- #
- # ==== Examples
- # form_tag('/posts')
- # # => <form action="/posts" method="post">
- #
- # form_tag('/posts/1', :method => :put)
- # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ...
- #
- # form_tag('/upload', :multipart => true)
- # # => <form action="/upload" method="post" enctype="multipart/form-data">
- #
- # <%= form_tag('/posts') do -%>
- # <div><%= submit_tag 'Save' %></div>
- # <% end -%>
- # # => <form action="/posts" method="post"><div><input type="submit" name="submit" value="Save" /></div></form>
- #
- # <%= form_tag('/posts', :remote => true) %>
- # # => <form action="/posts" method="post" data-remote="true">
- #
- # form_tag('http://far.away.com/form', :authenticity_token => false)
- # # form without authenticity token
- #
- # form_tag('http://far.away.com/form', :authenticity_token => "cf50faa3fe97702ca1ae")
- # # form with custom authenticity token
- #
- 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)
- else
- form_tag_html(html_options)
- end
- end
-
- # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
- # choice selection box.
- #
- # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or
- # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
- #
- # ==== 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 create
- # * <tt>:prompt</tt> - Create a prompt option with blank value and the text asking user to select something
- # * 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", "<option>David</option>".html_safe
- # # => <select id="people" name="people"><option>David</option></select>
- #
- # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe
- # # => <select id="count" name="count"><option>1</option><option>2</option>
- # # <option>3</option><option>4</option></select>
- #
- # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, :multiple => true
- # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
- # # <option>Green</option><option>Blue</option></select>
- #
- # select_tag "locations", "<option>Home</option><option selected='selected'>Work</option><option>Out</option>".html_safe
- # # => <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>
- # # <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"), :prompt => "Select something"
- # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
- #
- # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, :disabled => true
- # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
- # # <option>Paris</option><option>Rome</option></select>
- #
- # select_tag "credit_card", options_for_select([ "VISA", "MasterCard" ], "MasterCard")
- # # => <select id="credit_card" name="credit_card"><option>VISA</option>
- # # <option selected="selected">MasterCard</option></select>
- def select_tag(name, option_tags = nil, options = {})
- option_tags ||= ""
- html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
-
- if options.delete(:include_blank)
- option_tags = content_tag(:option, '', :value => '').safe_concat(option_tags)
- end
-
- if prompt = options.delete(:prompt)
- option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags)
- end
-
- content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
- end
-
- # Creates a standard text field; use these text fields to input smaller chunks of text like a username
- # or a search query.
- #
- # ==== Options
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * <tt>:size</tt> - The number of visible characters that will fit in the input.
- # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
- # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus.
- # * Any other key creates standard HTML attributes for the tag.
- #
- # ==== Examples
- # text_field_tag 'name'
- # # => <input id="name" name="name" type="text" />
- #
- # text_field_tag 'query', 'Enter your search query here'
- # # => <input id="query" name="query" type="text" value="Enter your search query here" />
- #
- # text_field_tag 'search', nil, :placeholder => 'Enter search term...'
- # # => <input id="search" name="search" placeholder="Enter search term..." type="text" />
- #
- # text_field_tag 'request', nil, :class => 'special_input'
- # # => <input class="special_input" id="request" name="request" type="text" />
- #
- # text_field_tag 'address', '', :size => 75
- # # => <input id="address" name="address" size="75" type="text" value="" />
- #
- # text_field_tag 'zip', nil, :maxlength => 5
- # # => <input id="zip" maxlength="5" name="zip" type="text" />
- #
- # text_field_tag 'payment_amount', '$0.00', :disabled => true
- # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />
- #
- # text_field_tag 'ip', '0.0.0.0', :maxlength => 15, :size => 20, :class => "ip-input"
- # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />
- def text_field_tag(name, value = nil, options = {})
- tag :input, { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
- end
-
- # Creates a label element. Accepts a block.
- #
- # ==== Options
- # * Creates standard HTML attributes for the tag.
- #
- # ==== Examples
- # label_tag 'name'
- # # => <label for="name">Name</label>
- #
- # label_tag 'name', 'Your name'
- # # => <label for="name">Your Name</label>
- #
- # label_tag 'name', nil, :class => 'small_label'
- # # => <label for="name" class="small_label">Name</label>
- def label_tag(name = nil, content_or_options = nil, options = nil, &block)
- if block_given? && content_or_options.is_a?(Hash)
- options = content_or_options = content_or_options.stringify_keys
- else
- options ||= {}
- options = options.stringify_keys
- end
- options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
- content_tag :label, content_or_options || name.to_s.humanize, options, &block
- end
-
- # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
- # data that should be hidden from the user.
- #
- # ==== Options
- # * Creates standard HTML attributes for the tag.
- #
- # ==== Examples
- # hidden_field_tag 'tags_list'
- # # => <input id="tags_list" name="tags_list" type="hidden" />
- #
- # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
- # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
- #
- # hidden_field_tag 'collected_input', '', :onchange => "alert('Input collected!')"
- # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
- # # type="hidden" value="" />
- def hidden_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "hidden"))
- end
-
- # Creates a file upload field. If you are using file uploads then you will also need
- # to set the multipart option for the form tag:
- #
- # <%= form_tag '/upload', :multipart => true do %>
- # <label for="file">File to Upload</label> <%= file_field_tag "file" %>
- # <%= submit_tag %>
- # <% end %>
- #
- # The specified URL will then be passed a File object containing the selected file, or if the field
- # was left blank, a StringIO object.
- #
- # ==== Options
- # * Creates standard HTML attributes for the tag.
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- #
- # ==== Examples
- # file_field_tag 'attachment'
- # # => <input id="attachment" name="attachment" type="file" />
- #
- # file_field_tag 'avatar', :class => 'profile_input'
- # # => <input class="profile_input" id="avatar" name="avatar" type="file" />
- #
- # file_field_tag 'picture', :disabled => true
- # # => <input disabled="disabled" id="picture" name="picture" type="file" />
- #
- # file_field_tag 'resume', :value => '~/resume.doc'
- # # => <input id="resume" name="resume" type="file" value="~/resume.doc" />
- #
- # file_field_tag 'user_pic', :accept => 'image/png,image/gif,image/jpeg'
- # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" />
- #
- # file_field_tag 'file', :accept => 'text/html', :class => 'upload', :value => 'index.html'
- # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
- def file_field_tag(name, options = {})
- text_field_tag(name, nil, options.update("type" => "file"))
- end
-
- # Creates a password field, a masked text field that will hide the users input behind a mask character.
- #
- # ==== Options
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * <tt>:size</tt> - The number of visible characters that will fit in the input.
- # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
- # * Any other key creates standard HTML attributes for the tag.
- #
- # ==== Examples
- # password_field_tag 'pass'
- # # => <input id="pass" name="pass" type="password" />
- #
- # password_field_tag 'secret', 'Your secret here'
- # # => <input id="secret" name="secret" type="password" value="Your secret here" />
- #
- # password_field_tag 'masked', nil, :class => 'masked_input_field'
- # # => <input class="masked_input_field" id="masked" name="masked" type="password" />
- #
- # password_field_tag 'token', '', :size => 15
- # # => <input id="token" name="token" size="15" type="password" value="" />
- #
- # password_field_tag 'key', nil, :maxlength => 16
- # # => <input id="key" maxlength="16" name="key" type="password" />
- #
- # password_field_tag 'confirm_pass', nil, :disabled => true
- # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" />
- #
- # password_field_tag 'pin', '1234', :maxlength => 4, :size => 6, :class => "pin_input"
- # # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
- def password_field_tag(name = "password", value = nil, options = {})
- text_field_tag(name, value, options.update("type" => "password"))
- end
-
- # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
- #
- # ==== Options
- # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10").
- # * <tt>:rows</tt> - Specify the number of rows in the textarea
- # * <tt>:cols</tt> - Specify the number of columns in the textarea
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
- # If you need unescaped contents, set this to false.
- # * Any other key creates standard HTML attributes for the tag.
- #
- # ==== Examples
- # text_area_tag 'post'
- # # => <textarea id="post" name="post"></textarea>
- #
- # text_area_tag 'bio', @user.bio
- # # => <textarea id="bio" name="bio">This is my biography.</textarea>
- #
- # text_area_tag 'body', nil, :rows => 10, :cols => 25
- # # => <textarea cols="25" id="body" name="body" rows="10"></textarea>
- #
- # text_area_tag 'body', nil, :size => "25x10"
- # # => <textarea name="body" id="body" cols="25" rows="10"></textarea>
- #
- # text_area_tag 'description', "Description goes here.", :disabled => true
- # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea>
- #
- # text_area_tag 'comment', nil, :class => 'comment_input'
- # # => <textarea class="comment_input" id="comment" name="comment"></textarea>
- def text_area_tag(name, content = nil, options = {})
- options = options.stringify_keys
-
- if size = options.delete("size")
- options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
- end
-
- escape = options.delete("escape") { true }
- content = ERB::Util.html_escape(content) if escape
-
- content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options)
- end
-
- # Creates a check box form input tag.
- #
- # ==== Options
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * Any other key creates standard HTML options for the tag.
- #
- # ==== Examples
- # check_box_tag 'accept'
- # # => <input id="accept" name="accept" type="checkbox" value="1" />
- #
- # check_box_tag 'rock', 'rock music'
- # # => <input id="rock" name="rock" type="checkbox" value="rock music" />
- #
- # check_box_tag 'receive_email', 'yes', true
- # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" />
- #
- # check_box_tag 'tos', 'yes', false, :class => 'accept_tos'
- # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" />
- #
- # check_box_tag 'eula', 'accepted', false, :disabled => true
- # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" />
- def check_box_tag(name, value = "1", checked = false, options = {})
- html_options = { "type" => "checkbox", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
- html_options["checked"] = "checked" if checked
- tag :input, html_options
- end
-
- # Creates a radio button; use groups of radio buttons named the same to allow users to
- # select from a group of options.
- #
- # ==== Options
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * Any other key creates standard HTML options for the tag.
- #
- # ==== Examples
- # radio_button_tag 'gender', 'male'
- # # => <input id="gender_male" name="gender" type="radio" value="male" />
- #
- # radio_button_tag 'receive_updates', 'no', true
- # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
- #
- # radio_button_tag 'time_slot', "3:00 p.m.", false, :disabled => true
- # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." />
- #
- # radio_button_tag 'color', "green", true, :class => "color_input"
- # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
- def radio_button_tag(name, value, checked = false, options = {})
- html_options = { "type" => "radio", "name" => name, "id" => "#{sanitize_to_id(name)}_#{sanitize_to_id(value)}", "value" => value }.update(options.stringify_keys)
- html_options["checked"] = "checked" if checked
- tag :input, html_options
- end
-
- # Creates a submit button with the text <tt>value</tt> as the caption.
- #
- # ==== Options
- # * <tt>:data</tt> - This option can be used to add custom data attributes.
- # * <tt>:disabled</tt> - If true, the user will not be able to use this input.
- # * Any other key creates standard HTML options for the tag.
- #
- # ==== Data attributes
- #
- # * <tt>:confirm => 'question?'</tt> - If present the unobtrusive JavaScript
- # drivers will provide a prompt with the question specified. If the user accepts,
- # the form is processed normally, otherwise no action is taken.
- # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
- # disabled version of the submit button when the form is submitted. This feature is
- # provided by the unobtrusive JavaScript driver.
- #
- # ==== Examples
- # submit_tag
- # # => <input name="commit" type="submit" value="Save changes" />
- #
- # submit_tag "Edit this article"
- # # => <input name="commit" type="submit" value="Edit this article" />
- #
- # submit_tag "Save edits", :disabled => true
- # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
- #
- # submit_tag "Complete sale", :data => { :disable_with => "Please wait..." }
- # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" />
- #
- # submit_tag nil, :class => "form_submit"
- # # => <input class="form_submit" name="commit" type="submit" />
- #
- # submit_tag "Edit", :class => "edit_button"
- # # => <input class="edit_button" name="commit" type="submit" value="Edit" />
- #
- # submit_tag "Save", :data => { :confirm => "Are you sure?" }
- # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />
- #
- def submit_tag(value = "Save changes", options = {})
- options = options.stringify_keys
-
- if disable_with = options.delete("disable_with")
- ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead"
-
- options["data-disable-with"] = disable_with
- end
-
- if confirm = options.delete("confirm")
- ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'"
-
- options["data-confirm"] = confirm
- end
-
- tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options)
- end
-
- # Creates a button element that defines a <tt>submit</tt> button,
- # <tt>reset</tt>button or a generic button which can be used in
- # JavaScript, for example. You can use the button tag as a regular
- # submit tag but it isn't supported in legacy browsers. However,
- # the button tag allows richer labels such as images and emphasis,
- # so this helper will also accept a block.
- #
- # ==== Options
- # * <tt>:data</tt> - This option can be used to add custom data attributes.
- # * <tt>:disabled</tt> - If true, the user will not be able to
- # use this input.
- # * Any other key creates standard HTML options for the tag.
- #
- # ==== Data attributes
- #
- # * <tt>:confirm => 'question?'</tt> - If present, the
- # unobtrusive JavaScript drivers will provide a prompt with
- # the question specified. If the user accepts, the form is
- # processed normally, otherwise no action is taken.
- # * <tt>:disable_with</tt> - Value of this parameter will be
- # used as the value for a disabled version of the submit
- # button when the form is submitted. This feature is provided
- # by the unobtrusive JavaScript driver.
- #
- # ==== Examples
- # button_tag
- # # => <button name="button" type="submit">Button</button>
- #
- # button_tag(:type => 'button') do
- # content_tag(:strong, 'Ask me!')
- # end
- # # => <button name="button" type="button">
- # # <strong>Ask me!</strong>
- # # </button>
- #
- # 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 disable_with = options.delete("disable_with")
- ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead"
-
- options["data-disable-with"] = disable_with
- end
-
- if confirm = options.delete("confirm")
- ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'"
-
- options["data-confirm"] = confirm
- end
-
- options.reverse_merge! 'name' => 'button', 'type' => 'submit'
-
- content_tag :button, content_or_options || 'Button', options, &block
- end
-
- # Displays an image which when clicked will submit the form.
- #
- # <tt>source</tt> is passed to AssetTagHelper#path_to_image
- #
- # ==== Options
- # * <tt>:data</tt> - This option can be used to add custom data attributes.
- # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
- # * Any other key creates standard HTML options for the tag.
- #
- # ==== Data attributes
- #
- # * <tt>:confirm => 'question?'</tt> - This will add a JavaScript confirm
- # prompt with the question specified. If the user accepts, the form is
- # processed normally, otherwise no action is taken.
- #
- # ==== Examples
- # image_submit_tag("login.png")
- # # => <input src="/images/login.png" type="image" />
- #
- # image_submit_tag("purchase.png", :disabled => true)
- # # => <input disabled="disabled" src="/images/purchase.png" type="image" />
- #
- # image_submit_tag("search.png", :class => 'search_button')
- # # => <input class="search_button" src="/images/search.png" type="image" />
- #
- # image_submit_tag("agree.png", :disabled => true, :class => "agree_disagree_button")
- # # => <input class="agree_disagree_button" disabled="disabled" src="/images/agree.png" type="image" />
- #
- # image_submit_tag("save.png", :data => { :confirm => "Are you sure?" })
- # # => <input src="/images/save.png" data-confirm="Are you sure?" type="image" />
- def image_submit_tag(source, options = {})
- options = options.stringify_keys
-
- if confirm = options.delete("confirm")
- ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead'"
-
- options["data-confirm"] = confirm
- end
-
- tag :input, { "type" => "image", "src" => path_to_image(source) }.update(options)
- end
-
- # Creates a field set for grouping HTML form elements.
- #
- # <tt>legend</tt> will become the fieldset's title (optional as per W3C).
- # <tt>options</tt> accept the same values as tag.
- #
- # ==== Examples
- # <%= field_set_tag do %>
- # <p><%= text_field_tag 'name' %></p>
- # <% end %>
- # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
- #
- # <%= field_set_tag 'Your details' do %>
- # <p><%= text_field_tag 'name' %></p>
- # <% end %>
- # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
- #
- # <%= field_set_tag nil, :class => 'format' do %>
- # <p><%= text_field_tag 'name' %></p>
- # <% end %>
- # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
- def field_set_tag(legend = nil, options = nil, &block)
- output = tag(:fieldset, options, true)
- output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
- output.concat(capture(&block)) if block_given?
- output.safe_concat("</fieldset>")
- end
-
- # Creates a text field of type "color".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def color_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "color"))
- end
-
- # Creates a text field of type "search".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def search_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "search"))
- end
-
- # Creates a text field of type "tel".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def telephone_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "tel"))
- end
- alias phone_field_tag telephone_field_tag
-
- # Creates a text field of type "date".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def date_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "date"))
- end
-
- # Creates a text field of type "time".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def time_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "time"))
- end
-
- # Creates a text field of type "datetime".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def datetime_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "datetime"))
- end
-
- # Creates a text field of type "datetime-local".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def datetime_local_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "datetime-local"))
- end
-
- # Creates a text field of type "month".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def month_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "month"))
- end
-
- # Creates a text field of type "week".
- #
- # === Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- def week_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "week"))
- end
-
- # Creates a text field of type "url".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def url_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "url"))
- end
-
- # Creates a text field of type "email".
- #
- # ==== Options
- # * Accepts the same options as text_field_tag.
- def email_field_tag(name, value = nil, options = {})
- text_field_tag(name, value, options.stringify_keys.update("type" => "email"))
- end
-
- # Creates a number field.
- #
- # ==== Options
- # * <tt>:min</tt> - The minimum acceptable value.
- # * <tt>:max</tt> - The maximum acceptable value.
- # * <tt>:in</tt> - A range specifying the <tt>:min</tt> and
- # <tt>:max</tt> values.
- # * <tt>:step</tt> - The acceptable value granularity.
- # * Otherwise accepts the same options as text_field_tag.
- #
- # ==== Examples
- # number_field_tag 'quantity', nil, :in => 1...10
- # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
- def number_field_tag(name, value = nil, options = {})
- options = options.stringify_keys
- options["type"] ||= "number"
- if range = options.delete("in") || options.delete("within")
- options.update("min" => range.min, "max" => range.max)
- end
- text_field_tag(name, value, options)
- end
-
- # Creates a range form element.
- #
- # ==== Options
- # * Accepts the same options as number_field_tag.
- def range_field_tag(name, value = nil, options = {})
- number_field_tag(name, value, options.stringify_keys.update("type" => "range"))
- end
-
- # 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)
- end
-
- private
- def html_options_for_form(url_for_options, options)
- options.stringify_keys.tap do |html_options|
- html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
- # The following URL is unescaped, this is just a hash of options, and it is the
- # responsibility of the caller to escape all the values.
- html_options["action"] = url_for(url_for_options)
- html_options["accept-charset"] = "UTF-8"
-
- html_options["data-remote"] = true if html_options.delete("remote")
-
- if html_options["data-remote"] &&
- !embed_authenticity_token_in_remote_forms &&
- html_options["authenticity_token"].blank?
- # The authenticity token is taken from the meta tag in this case
- html_options["authenticity_token"] = false
- elsif html_options["authenticity_token"] == true
- # Include the default authenticity_token, which is only generated when its set to nil,
- # but we needed the true value to override the default of no authenticity_token on data-remote.
- html_options["authenticity_token"] = nil
- end
- end
- end
-
- def extra_tags_for_form(html_options)
- authenticity_token = html_options.delete("authenticity_token")
- method = html_options.delete("method").to_s
-
- method_tag = case method
- when /^get$/i # must be case-insensitive, but can't use downcase as might be nil
- html_options["method"] = "get"
- ''
- when /^post$/i, "", nil
- html_options["method"] = "post"
- token_tag(authenticity_token)
- else
- html_options["method"] = "post"
- method_tag(method) + token_tag(authenticity_token)
- end
-
- tags = utf8_enforcer_tag << method_tag
- content_tag(:div, tags, :style => 'margin:0;padding:0;display:inline')
- end
-
- def form_tag_html(html_options)
- extra_tags = extra_tags_for_form(html_options)
- tag(:form, html_options, true) + extra_tags
- end
-
- def form_tag_in_block(html_options, &block)
- content = capture(&block)
- output = form_tag_html(html_options)
- output << content
- output.safe_concat("</form>")
- end
-
- # see http://www.w3.org/TR/html4/types.html#type-name
- def sanitize_to_id(name)
- name.to_s.gsub(']','').gsub(/[^-a-zA-Z0-9:.]/, "_")
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/javascript_helper.rb b/actionpack/lib/action_view/helpers/javascript_helper.rb
deleted file mode 100644
index 9f8cd8caaa..0000000000
--- a/actionpack/lib/action_view/helpers/javascript_helper.rb
+++ /dev/null
@@ -1,112 +0,0 @@
-require 'action_view/helpers/tag_helper'
-
-module ActionView
- module Helpers
- module JavaScriptHelper
- JS_ESCAPE_MAP = {
- '\\' => '\\\\',
- '</' => '<\/',
- "\r\n" => '\n',
- "\n" => '\n',
- "\r" => '\n',
- '"' => '\\"',
- "'" => "\\'"
- }
-
- JS_ESCAPE_MAP["\342\200\250".force_encoding('UTF-8').encode!] = '&#x2028;'
- JS_ESCAPE_MAP["\342\200\251".force_encoding('UTF-8').encode!] = '&#x2029;'
-
- # Escapes carriage returns and single and double quotes for JavaScript segments.
- #
- # Also available through the alias j(). This is particularly helpful in JavaScript responses, like:
- #
- # $('some_element').replaceWith('<%=j render 'some/element_template' %>');
- def escape_javascript(javascript)
- if javascript
- result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
- javascript.html_safe? ? result.html_safe : result
- else
- ''
- end
- end
-
- alias_method :j, :escape_javascript
-
- # Returns a JavaScript tag with the +content+ inside. Example:
- # javascript_tag "alert('All is good')"
- #
- # Returns:
- # <script>
- # //<![CDATA[
- # alert('All is good')
- # //]]>
- # </script>
- #
- # +html_options+ may be a hash of attributes for the <tt>\<script></tt>
- # tag. Example:
- # javascript_tag "alert('All is good')", :defer => 'defer'
- # # => <script defer="defer">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.
- # <%= javascript_tag :defer => 'defer' do -%>
- # alert('All is good')
- # <% end -%>
- def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
- content =
- if block_given?
- html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
- capture(&block)
- else
- content_or_options_with_block
- end
-
- content_tag(:script, javascript_cdata_section(content), html_options)
- end
-
- def javascript_cdata_section(content) #:nodoc:
- "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe
- end
-
- # Returns a button whose +onclick+ handler triggers the passed JavaScript.
- #
- # The helper receives a name, JavaScript code, and an optional hash of HTML options. The
- # name is used as button label and the JavaScript code goes into its +onclick+ attribute.
- # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+.
- #
- # button_to_function "Greeting", "alert('Hello world!')", :class => "ok"
- # # => <input class="ok" onclick="alert('Hello world!');" type="button" value="Greeting" />
- #
- def button_to_function(name, function=nil, html_options={})
- message = "button_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead."
- ActiveSupport::Deprecation.warn message
-
- onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function};"
-
- tag(:input, html_options.merge(:type => 'button', :value => name, :onclick => onclick))
- end
-
- # Returns a link whose +onclick+ handler triggers the passed JavaScript.
- #
- # The helper receives a name, JavaScript code, and an optional hash of HTML options. The
- # name is used as the link text and the JavaScript code goes into the +onclick+ attribute.
- # If +html_options+ has an <tt>:onclick</tt>, that one is put before +function+. Once all
- # the JavaScript is set, the helper appends "; return false;".
- #
- # The +href+ attribute of the tag is set to "#" unless +html_options+ has one.
- #
- # link_to_function "Greeting", "alert('Hello world!')", :class => "nav_link"
- # # => <a class="nav_link" href="#" onclick="alert('Hello world!'); return false;">Greeting</a>
- #
- def link_to_function(name, function, html_options={})
- message = "link_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead."
- ActiveSupport::Deprecation.warn message
-
- onclick = "#{"#{html_options[:onclick]}; " if html_options[:onclick]}#{function}; return false;"
- href = html_options[:href] || '#'
-
- content_tag(:a, name, html_options.merge(:href => href, :onclick => onclick))
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/number_helper.rb b/actionpack/lib/action_view/helpers/number_helper.rb
deleted file mode 100644
index 9720e90429..0000000000
--- a/actionpack/lib/action_view/helpers/number_helper.rb
+++ /dev/null
@@ -1,442 +0,0 @@
-# encoding: utf-8
-
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/output_safety'
-require 'active_support/number_helper'
-
-module ActionView
- # = Action View Number Helpers
- module Helpers #:nodoc:
-
- # Provides methods for converting numbers into formatted strings.
- # Methods are provided for phone numbers, currency, percentage,
- # precision, positional notation, file size and pretty printing.
- #
- # Most methods expect a +number+ argument, and will return it
- # unchanged if can't be converted into a valid number.
- module NumberHelper
-
- # Raised when argument +number+ param given to the helpers is invalid and
- # the option :raise is set to +true+.
- class InvalidNumberError < StandardError
- attr_accessor :number
- def initialize(number)
- @number = number
- end
- end
-
- # Formats a +number+ into a US phone number (e.g., (555)
- # 123-9876). You can customize the format in the +options+ hash.
- #
- # ==== Options
- #
- # * <tt>:area_code</tt> - Adds parentheses around the area code.
- # * <tt>:delimiter</tt> - Specifies the delimiter to use
- # (defaults to "-").
- # * <tt>:extension</tt> - Specifies an extension to add to the
- # end of the generated number.
- # * <tt>:country_code</tt> - Sets the country code for the phone
- # number.
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_to_phone(5551234) # => 555-1234
- # number_to_phone("5551234") # => 555-1234
- # number_to_phone(1235551234) # => 123-555-1234
- # number_to_phone(1235551234, :area_code => true) # => (123) 555-1234
- # number_to_phone(1235551234, :delimiter => " ") # => 123 555 1234
- # number_to_phone(1235551234, :area_code => true, :extension => 555) # => (123) 555-1234 x 555
- # number_to_phone(1235551234, :country_code => 1) # => +1-123-555-1234
- # number_to_phone("123a456") # => 123a456
- #
- # number_to_phone("1234a567", :raise => true) # => InvalidNumberError
- #
- # number_to_phone(1235551234, :country_code => 1, :extension => 1343, :delimiter => ".")
- # # => +1.123.555.1234 x 1343
- def number_to_phone(number, options = {})
- return unless number
- options = options.symbolize_keys
-
- parse_float(number, true) if options.delete(:raise)
- ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
- end
-
- # Formats a +number+ into a currency string (e.g., $13.65). You
- # can customize the format in the +options+ hash.
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:precision</tt> - Sets the level of precision (defaults
- # to 2).
- # * <tt>:unit</tt> - Sets the denomination of the currency
- # (defaults to "$").
- # * <tt>:separator</tt> - Sets the separator between the units
- # (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to ",").
- # * <tt>:format</tt> - Sets the format for non-negative numbers
- # (defaults to "%u%n"). Fields are <tt>%u</tt> for the
- # currency, and <tt>%n</tt> for the number.
- # * <tt>:negative_format</tt> - Sets the format for negative
- # numbers (defaults to prepending an hyphen to the formatted
- # number given by <tt>:format</tt>). Accepts the same fields
- # than <tt>:format</tt>, except <tt>%n</tt> is here the
- # absolute value of the number.
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_to_currency(1234567890.50) # => $1,234,567,890.50
- # number_to_currency(1234567890.506) # => $1,234,567,890.51
- # number_to_currency(1234567890.506, :precision => 3) # => $1,234,567,890.506
- # number_to_currency(1234567890.506, :locale => :fr) # => 1 234 567 890,51 €
- # number_to_currency("123a456") # => $123a456
- #
- # number_to_currency("123a456", :raise => true) # => InvalidNumberError
- #
- # number_to_currency(-1234567890.50, :negative_format => "(%u%n)")
- # # => ($1,234,567,890.50)
- # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "")
- # # => &pound;1234567890,50
- # number_to_currency(1234567890.50, :unit => "&pound;", :separator => ",", :delimiter => "", :format => "%n %u")
- # # => 1234567890,50 &pound;
- def number_to_currency(number, options = {})
- return unless number
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_currency(number, options)
- }
- end
-
- # Formats a +number+ as a percentage string (e.g., 65%). You can
- # customize the format in the +options+ hash.
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
- # digits (defaults to +false+).
- # * <tt>:separator</tt> - Sets the separator between the
- # fractional and integer digits (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to "").
- # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
- # insignificant zeros after the decimal separator (defaults to
- # +false+).
- # * <tt>:format</tt> - Specifies the format of the percentage
- # string The number field is <tt>%n</tt> (defaults to "%n%").
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage("98") # => 98.000%
- # number_to_percentage(100, :precision => 0) # => 100%
- # number_to_percentage(1000, :delimiter => '.', :separator => ',') # => 1.000,000%
- # number_to_percentage(302.24398923423, :precision => 5) # => 302.24399%
- # number_to_percentage(1000, :locale => :fr) # => 1 000,000%
- # number_to_percentage("98a") # => 98a%
- # number_to_percentage(100, :format => "%n %") # => 100 %
- #
- # number_to_percentage("98a", :raise => true) # => InvalidNumberError
- def number_to_percentage(number, options = {})
- return unless number
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_percentage(number, options)
- }
- end
-
- # Formats a +number+ with grouped thousands using +delimiter+
- # (e.g., 12,324). You can customize the format in the +options+
- # hash.
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to ",").
- # * <tt>:separator</tt> - Sets the separator between the
- # fractional and integer digits (defaults to ".").
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_with_delimiter(12345678) # => 12,345,678
- # number_with_delimiter("123456") # => 123,456
- # number_with_delimiter(12345678.05) # => 12,345,678.05
- # number_with_delimiter(12345678, :delimiter => ".") # => 12.345.678
- # number_with_delimiter(12345678, :delimiter => ",") # => 12,345,678
- # number_with_delimiter(12345678.05, :separator => " ") # => 12,345,678 05
- # number_with_delimiter(12345678.05, :locale => :fr) # => 12 345 678,05
- # number_with_delimiter("112a") # => 112a
- # number_with_delimiter(98765432.98, :delimiter => " ", :separator => ",")
- # # => 98 765 432,98
- #
- # number_with_delimiter("112a", :raise => true) # => raise InvalidNumberError
- def number_with_delimiter(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_delimited(number, options)
- }
- end
-
- # Formats a +number+ with the specified level of
- # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
- # +:significant+ is +false+, and 5 if +:significant+ is +true+).
- # You can customize the format in the +options+ hash.
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
- # digits (defaults to +false+).
- # * <tt>:separator</tt> - Sets the separator between the
- # fractional and integer digits (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to "").
- # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
- # insignificant zeros after the decimal separator (defaults to
- # +false+).
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_with_precision(111.2345) # => 111.235
- # number_with_precision(111.2345, :precision => 2) # => 111.23
- # number_with_precision(13, :precision => 5) # => 13.00000
- # number_with_precision(389.32314, :precision => 0) # => 389
- # number_with_precision(111.2345, :significant => true) # => 111
- # number_with_precision(111.2345, :precision => 1, :significant => true) # => 100
- # number_with_precision(13, :precision => 5, :significant => true) # => 13.000
- # number_with_precision(111.234, :locale => :fr) # => 111,234
- #
- # number_with_precision(13, :precision => 5, :significant => true, :strip_insignificant_zeros => true)
- # # => 13
- #
- # number_with_precision(389.32314, :precision => 4, :significant => true) # => 389.3
- # number_with_precision(1111.2345, :precision => 2, :separator => ',', :delimiter => '.')
- # # => 1.111,23
- def number_with_precision(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_rounded(number, options)
- }
- end
-
-
- # Formats the bytes in +number+ into a more understandable
- # representation (e.g., giving it 1500 yields 1.5 KB). This
- # method is useful for reporting file sizes to users. You can
- # customize the format in the +options+ hash.
- #
- # See <tt>number_to_human</tt> if you want to pretty-print a
- # generic number.
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
- # digits (defaults to +true+)
- # * <tt>:separator</tt> - Sets the separator between the
- # fractional and integer digits (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to "").
- # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
- # insignificant zeros after the decimal separator (defaults to
- # +true+)
- # * <tt>:prefix</tt> - If +:si+ formats the number using the SI
- # prefix (defaults to :binary)
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_to_human_size(123) # => 123 Bytes
- # number_to_human_size(1234) # => 1.21 KB
- # number_to_human_size(12345) # => 12.1 KB
- # number_to_human_size(1234567) # => 1.18 MB
- # number_to_human_size(1234567890) # => 1.15 GB
- # number_to_human_size(1234567890123) # => 1.12 TB
- # 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"
- def number_to_human_size(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_human_size(number, options)
- }
- end
-
- # Pretty prints (formats and approximates) a number in a way it
- # is more readable by humans (eg.: 1200000000 becomes "1.2
- # Billion"). This is useful for numbers that can get very large
- # (and too hard to read).
- #
- # See <tt>number_to_human_size</tt> if you want to print a file
- # size.
- #
- # You can also define you own unit-quantifier names if you want
- # to use other decimal units (eg.: 1500 becomes "1.5
- # kilometers", 0.150 becomes "150 milliliters", etc). You may
- # define a wide range of unit quantifiers, even fractional ones
- # (centi, deci, mili, etc).
- #
- # ==== Options
- #
- # * <tt>:locale</tt> - Sets the locale to be used for formatting
- # (defaults to current locale).
- # * <tt>:precision</tt> - Sets the precision of the number
- # (defaults to 3).
- # * <tt>:significant</tt> - If +true+, precision will be the #
- # of significant_digits. If +false+, the # of fractional
- # digits (defaults to +true+)
- # * <tt>:separator</tt> - Sets the separator between the
- # fractional and integer digits (defaults to ".").
- # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
- # to "").
- # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
- # insignificant zeros after the decimal separator (defaults to
- # +true+)
- # * <tt>:units</tt> - A Hash of unit quantifier names. Or a
- # string containing an i18n scope where to find this hash. It
- # might have the following keys:
- # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
- # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
- # *<tt>:billion</tt>, <tt>:trillion</tt>,
- # *<tt>:quadrillion</tt>
- # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
- # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
- # *<tt>:pico</tt>, <tt>:femto</tt>
- # * <tt>:format</tt> - Sets the format of the output string
- # (defaults to "%n %u"). The field types are:
- # * %u - The quantifier (ex.: 'thousand')
- # * %n - The number
- # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
- # the argument is invalid.
- #
- # ==== Examples
- #
- # number_to_human(123) # => "123"
- # number_to_human(1234) # => "1.23 Thousand"
- # number_to_human(12345) # => "12.3 Thousand"
- # number_to_human(1234567) # => "1.23 Million"
- # number_to_human(1234567890) # => "1.23 Billion"
- # number_to_human(1234567890123) # => "1.23 Trillion"
- # number_to_human(1234567890123456) # => "1.23 Quadrillion"
- # number_to_human(1234567890123456789) # => "1230 Quadrillion"
- # number_to_human(489939, :precision => 2) # => "490 Thousand"
- # number_to_human(489939, :precision => 4) # => "489.9 Thousand"
- # number_to_human(1234567, :precision => 4,
- # :significant => false) # => "1.2346 Million"
- # number_to_human(1234567, :precision => 1,
- # :separator => ',',
- # :significant => false) # => "1,2 Million"
- #
- # 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"
- #
- # ==== Custom Unit Quantifiers
- #
- # You can also use your own custom unit quantifiers:
- # number_to_human(500000, :units => {:unit => "ml", :thousand => "lt"}) # => "500 lt"
- #
- # If in your I18n locale you have:
- # distance:
- # centi:
- # one: "centimeter"
- # other: "centimeters"
- # unit:
- # one: "meter"
- # other: "meters"
- # thousand:
- # one: "kilometer"
- # other: "kilometers"
- # billion: "gazillion-distance"
- #
- # Then you could do:
- #
- # number_to_human(543934, :units => :distance) # => "544 kilometers"
- # number_to_human(54393498, :units => :distance) # => "54400 kilometers"
- # number_to_human(54393498000, :units => :distance) # => "54.4 gazillion-distance"
- # number_to_human(343, :units => :distance, :precision => 1) # => "300 meters"
- # number_to_human(1, :units => :distance) # => "1 meter"
- # number_to_human(0.34, :units => :distance) # => "34 centimeters"
- #
- def number_to_human(number, options = {})
- options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
-
- wrap_with_output_safety_handling(number, options.delete(:raise)) {
- ActiveSupport::NumberHelper.number_to_human(number, options)
- }
- end
-
- private
-
- def escape_unsafe_delimiters_and_separators(options)
- options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe?
- options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe?
- options
- 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
-
- formatted_number = yield
-
- if valid_float || number.html_safe?
- formatted_number.html_safe
- else
- formatted_number
- end
- end
-
- def valid_float?(number)
- !parse_float(number, false).nil?
- end
-
- def parse_float(number, raise_error)
- Float(number)
- rescue ArgumentError, TypeError
- raise InvalidNumberError, number if raise_error
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/output_safety_helper.rb b/actionpack/lib/action_view/helpers/output_safety_helper.rb
deleted file mode 100644
index 2e7e9dc50c..0000000000
--- a/actionpack/lib/action_view/helpers/output_safety_helper.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView #:nodoc:
- # = Action View Raw Output Helper
- module Helpers #:nodoc:
- module OutputSafetyHelper
- # This method outputs without escaping a string. Since escaping tags is
- # now default, this can be used when you don't want Rails to automatically
- # escape tags. This is not recommended if the data is coming from the user's
- # input.
- #
- # For example:
- #
- # <%=raw @user.name %>
- def raw(stringish)
- 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.
- #
- # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
- # # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
- #
- # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
- # # => "<p>foo</p><br /><p>bar</p>"
- #
- def safe_join(array, sep=$,)
- sep = ERB::Util.html_escape(sep)
-
- array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/record_tag_helper.rb b/actionpack/lib/action_view/helpers/record_tag_helper.rb
deleted file mode 100644
index dded9aab7c..0000000000
--- a/actionpack/lib/action_view/helpers/record_tag_helper.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-module ActionView
- # = Action View Record Tag Helpers
- module Helpers
- module RecordTagHelper
- include ActionView::RecordIdentifier
-
- # Produces a wrapper DIV element with id and class parameters that
- # relate to the specified Active Record object. Usage example:
- #
- # <%= div_for(@person, :class => "foo") do %>
- # <%= @person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- #
- # You can also pass an array of Active Record objects, which will then
- # get iterated over and yield each record as an argument for the block.
- # For example:
- #
- # <%= div_for(@people, :class => "foo") do |person| %>
- # <%= person.name %>
- # <% end %>
- #
- # produces:
- #
- # <div id="person_123" class="person foo"> Joe Bloggs </div>
- # <div id="person_124" class="person foo"> Jane Bloggs </div>
- #
- def div_for(record, *args, &block)
- content_tag_for(:div, record, *args, &block)
- end
-
- # content_tag_for creates an HTML element with id and class parameters
- # that relate to the specified Active Record object. For example:
- #
- # <%= content_tag_for(:tr, @person) do %>
- # <td><%= @person.first_name %></td>
- # <td><%= @person.last_name %></td>
- # <% end %>
- #
- # would produce the following HTML (assuming @person is an instance of
- # a Person object, with an id value of 123):
- #
- # <tr id="person_123" class="person">....</tr>
- #
- # If you require the HTML id attribute to have a prefix, you can specify it:
- #
- # <%= content_tag_for(:tr, @person, :foo) do %> ...
- #
- # produces:
- #
- # <tr id="foo_person_123" class="person">...
- #
- # You can also pass an array of objects which this method will loop through
- # and yield the current object to the supplied block, reducing the need for
- # having to iterate through the object (using <tt>each</tt>) beforehand.
- # For example (assuming @people is an array of Person objects):
- #
- # <%= content_tag_for(:tr, @people) do |person| %>
- # <td><%= person.first_name %></td>
- # <td><%= person.last_name %></td>
- # <% end %>
- #
- # produces:
- #
- # <tr id="person_123" class="person">...</tr>
- # <tr id="person_124" class="person">...</tr>
- #
- # content_tag_for also accepts a hash of options, which will be converted to
- # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined
- # with the default class name for your object. For example:
- #
- # <%= content_tag_for(:li, @person, :class => "bar") %>...
- #
- # produces:
- #
- # <li id="person_123" class="person bar">...
- #
- def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block)
- options, prefix = prefix, nil if prefix.is_a?(Hash)
-
- Array(single_or_multiple_records).map do |single_record|
- content_tag_for_single_record(tag_name, single_record, prefix, options, &block)
- end.join("\n").html_safe
- end
-
- private
-
- # Called by <tt>content_tag_for</tt> internally to render a content tag
- # for each record.
- def content_tag_for_single_record(tag_name, record, prefix, options, &block)
- options = options ? options.dup : {}
- options[:class] = "#{dom_class(record, prefix)} #{options[:class]}".rstrip
- options[:id] = dom_id(record, prefix)
-
- content_tag(tag_name, capture(record, &block), options)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/rendering_helper.rb b/actionpack/lib/action_view/helpers/rendering_helper.rb
deleted file mode 100644
index 626e1a1ab7..0000000000
--- a/actionpack/lib/action_view/helpers/rendering_helper.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-module ActionView
- module Helpers
- # = Action View Rendering
- #
- # Implements methods that allow rendering from a view context.
- # In order to use this module, all you need is to implement
- # view_renderer that returns an ActionView::Renderer object.
- module RenderingHelper
- # Returns the result of a render that's dictated by the options hash. The primary options are:
- #
- # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
- # * <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.
- #
- # 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.
- def render(options = {}, locals = {}, &block)
- case options
- when Hash
- if block_given?
- view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block)
- else
- view_renderer.render(self, options)
- end
- else
- view_renderer.render_partial(self, :partial => options, :locals => locals)
- end
- end
-
- # Overwrites _layout_for in the context object so it supports the case a block is
- # passed to a partial. Returns the contents that are yielded to a layout, given a
- # name or a block.
- #
- # You can think of a layout as a method that is called with a block. If the user calls
- # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
- # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
- #
- # The user can override this default by passing a block to the layout:
- #
- # # The template
- # <%= render :layout => "my_layout" do %>
- # Content
- # <% end %>
- #
- # # The layout
- # <html>
- # <%= yield %>
- # </html>
- #
- # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
- # this method returns the block that was passed in to <tt>render :layout</tt>, and the response
- # would be
- #
- # <html>
- # Content
- # </html>
- #
- # Finally, the block can take block arguments, which can be passed in by +yield+:
- #
- # # The template
- # <%= render :layout => "my_layout" do |customer| %>
- # Hello <%= customer.name %>
- # <% end %>
- #
- # # The layout
- # <html>
- # <%= yield Struct.new(:name).new("David") %>
- # </html>
- #
- # In this case, the layout would receive the block passed into <tt>render :layout</tt>,
- # and the struct specified would be passed into the block as an argument. The result
- # would be
- #
- # <html>
- # Hello David
- # </html>
- #
- def _layout_for(*args, &block)
- name = args.first
-
- if block && !name.is_a?(Symbol)
- capture(*args, &block)
- else
- super
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/sanitize_helper.rb b/actionpack/lib/action_view/helpers/sanitize_helper.rb
deleted file mode 100644
index 9c76c26ace..0000000000
--- a/actionpack/lib/action_view/helpers/sanitize_helper.rb
+++ /dev/null
@@ -1,256 +0,0 @@
-require 'active_support/core_ext/object/try'
-require 'action_view/vendor/html-scanner'
-
-module ActionView
- # = Action View Sanitize Helpers
- module Helpers #:nodoc:
- # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
- # These helper methods extend Action View making them callable within your template files.
- module SanitizeHelper
- extend ActiveSupport::Concern
- # This +sanitize+ helper will html encode all tags and strip all attributes that
- # aren't specifically allowed.
- #
- # It also strips href/src tags with invalid protocols, like javascript: especially.
- # It does its best to counter any tricks that hackers may use, like throwing in
- # unicode/ascii/hex values to get past the javascript: filters. Check out
- # the extensive test suite.
- #
- # <%= sanitize @article.body %>
- #
- # You can add or remove tags/attributes if you want to customize it a bit.
- # See ActionView::Base for full docs on the available options. You can add
- # tags/attributes for single uses of +sanitize+ by passing either the
- # <tt>:attributes</tt> or <tt>:tags</tt> options:
- #
- # Normal Use
- #
- # <%= sanitize @article.body %>
- #
- # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
- #
- # <%= sanitize @article.body, :tags => %w(table tr td), :attributes => %w(id class style) %>
- #
- # Add table tags to the default allowed tags
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
- # end
- #
- # Remove tags to the default allowed tags
- #
- # class Application < Rails::Application
- # config.after_initialize do
- # ActionView::Base.sanitized_allowed_tags.delete 'div'
- # end
- # end
- #
- # Change allowed default attributes
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
- # end
- #
- # Please note that sanitizing user-provided text does not guarantee that the
- # resulting markup is valid (conforming to a document type) or even well-formed.
- # The output may still contain e.g. unescaped '<', '>', '&' characters and
- # confuse browsers.
- #
- def sanitize(html, options = {})
- self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
- end
-
- # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
- def sanitize_css(style)
- self.class.white_list_sanitizer.sanitize_css(style)
- end
-
- # Strips all HTML tags from the +html+, including comments. This uses the
- # html-scanner tokenizer and so its HTML parsing ability is limited by
- # that of html-scanner.
- #
- # strip_tags("Strip <i>these</i> tags!")
- # # => Strip these tags!
- #
- # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
- # # => Bold no more! See more here...
- #
- # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
- # # => Welcome to my website!
- def strip_tags(html)
- self.class.full_sanitizer.sanitize(html)
- end
-
- # Strips all link tags from +text+ leaving just the link text.
- #
- # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
- # # => Ruby on Rails
- #
- # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
- # # => Please e-mail me at me@email.com.
- #
- # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
- # # => Blog: Visit.
- def strip_links(html)
- self.class.link_sanitizer.sanitize(html)
- end
-
- module ClassMethods #:nodoc:
- attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
-
- def sanitized_protocol_separator
- white_list_sanitizer.protocol_separator
- end
-
- def sanitized_uri_attributes
- white_list_sanitizer.uri_attributes
- end
-
- def sanitized_bad_tags
- white_list_sanitizer.bad_tags
- end
-
- def sanitized_allowed_tags
- white_list_sanitizer.allowed_tags
- end
-
- def sanitized_allowed_attributes
- white_list_sanitizer.allowed_attributes
- end
-
- def sanitized_allowed_css_properties
- white_list_sanitizer.allowed_css_properties
- end
-
- def sanitized_allowed_css_keywords
- white_list_sanitizer.allowed_css_keywords
- end
-
- def sanitized_shorthand_css_properties
- white_list_sanitizer.shorthand_css_properties
- end
-
- def sanitized_allowed_protocols
- white_list_sanitizer.allowed_protocols
- end
-
- def sanitized_protocol_separator=(value)
- white_list_sanitizer.protocol_separator = value
- end
-
- # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
- # any object that responds to +sanitize+.
- #
- # class Application < Rails::Application
- # config.action_view.full_sanitizer = MySpecialSanitizer.new
- # end
- #
- def full_sanitizer
- @full_sanitizer ||= HTML::FullSanitizer.new
- end
-
- # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
- # any object that responds to +sanitize+.
- #
- # class Application < Rails::Application
- # config.action_view.link_sanitizer = MySpecialSanitizer.new
- # end
- #
- def link_sanitizer
- @link_sanitizer ||= HTML::LinkSanitizer.new
- end
-
- # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
- # Replace with any object that responds to +sanitize+.
- #
- # class Application < Rails::Application
- # config.action_view.white_list_sanitizer = MySpecialSanitizer.new
- # end
- #
- def white_list_sanitizer
- @white_list_sanitizer ||= HTML::WhiteListSanitizer.new
- end
-
- # Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
- # end
- #
- def sanitized_uri_attributes=(attributes)
- HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
- end
-
- # Adds to the Set of 'bad' tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_bad_tags = 'embed', 'object'
- # end
- #
- def sanitized_bad_tags=(attributes)
- HTML::WhiteListSanitizer.bad_tags.merge(attributes)
- end
-
- # Adds to the Set of allowed tags for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
- # end
- #
- def sanitized_allowed_tags=(attributes)
- HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
- end
-
- # Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc'
- # end
- #
- def sanitized_allowed_attributes=(attributes)
- HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_properties = 'expression'
- # end
- #
- def sanitized_allowed_css_properties=(attributes)
- HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_css_keywords = 'expression'
- # end
- #
- def sanitized_allowed_css_keywords=(attributes)
- HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
- end
-
- # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_shorthand_css_properties = 'expression'
- # end
- #
- def sanitized_shorthand_css_properties=(attributes)
- HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
- end
-
- # Adds to the Set of allowed protocols for the +sanitize+ helper.
- #
- # class Application < Rails::Application
- # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
- # end
- #
- def sanitized_allowed_protocols=(attributes)
- HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tag_helper.rb b/actionpack/lib/action_view/helpers/tag_helper.rb
deleted file mode 100644
index 3327c69d61..0000000000
--- a/actionpack/lib/action_view/helpers/tag_helper.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-require 'active_support/core_ext/string/output_safety'
-require 'set'
-
-module ActionView
- # = Action View Tag Helpers
- module Helpers #:nodoc:
- # Provides methods to generate HTML tags programmatically when you can't use
- # a Builder. By default, they output XHTML compliant tags.
- module TagHelper
- extend ActiveSupport::Concern
- include CaptureHelper
-
- BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
- autoplay controls loop selected hidden scoped async
- defer reversed ismap seemless muted required
- autofocus novalidate formnovalidate open pubdate itemscope).to_set
- BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
-
- PRE_CONTENT_STRINGS = {
- :textarea => "\n"
- }
-
- # Returns an empty HTML tag of type +name+ which by default is XHTML
- # compliant. Set +open+ to true to create an open tag compatible
- # with HTML 4.0 and below. Add HTML attributes by passing an attributes
- # hash to +options+. Set +escape+ to false to disable attribute value
- # escaping.
- #
- # ==== Options
- # You can use symbols or strings for the attribute names.
- #
- # Use +true+ with boolean attributes that can render with no value, like
- # +disabled+ and +readonly+.
- #
- # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
- # pointing to a hash of sub-attributes.
- #
- # To play nicely with JavaScript conventions sub-attributes are dasherized.
- # 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.
- # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
- # from 1.4.3.
- #
- # ==== Examples
- # tag("br")
- # # => <br />
- #
- # tag("br", nil, true)
- # # => <br>
- #
- # tag("input", :type => 'text', :disabled => true)
- # # => <input type="text" disabled="disabled" />
- #
- # tag("img", :src => "open & shut.png")
- # # => <img src="open &amp; shut.png" />
- #
- # tag("img", {:src => "open &amp; shut.png"}, false, false)
- # # => <img src="open &amp; shut.png" />
- #
- # tag("div", :data => {:name => 'Stephen', :city_state => %w(Chicago IL)})
- # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
- def tag(name, options = nil, open = false, escape = true)
- "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
- end
-
- # Returns an HTML block tag of type +name+ surrounding the +content+. Add
- # HTML attributes by passing an attributes hash to +options+.
- # Instead of passing the content as an argument, you can also use a block
- # in which case, you pass your +options+ as the second parameter.
- # 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
- # <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.
- #
- # ==== Examples
- # content_tag(:p, "Hello world!")
- # # => <p>Hello world!</p>
- # content_tag(:div, content_tag(:p, "Hello world!"), :class => "strong")
- # # => <div class="strong"><p>Hello world!</p></div>
- # content_tag("select", options, :multiple => true)
- # # => <select multiple="multiple">...options...</select>
- #
- # <%= content_tag :div, :class => "strong" do -%>
- # Hello world!
- # <% end -%>
- # # => <div class="strong">Hello world!</div>
- def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
- if block_given?
- options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
- content_tag_string(name, capture(&block), options, escape)
- else
- content_tag_string(name, content_or_options_with_block, options, escape)
- end
- end
-
- # Returns a CDATA section with the given +content+. CDATA sections
- # are used to escape blocks of text containing characters which would
- # otherwise be recognized as markup. CDATA sections begin with the string
- # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
- #
- # cdata_section("<hello world>")
- # # => <![CDATA[<hello world>]]>
- #
- # cdata_section(File.read("hello_world.txt"))
- # # => <![CDATA[<hello from a text file]]>
- #
- # cdata_section("hello]]>world")
- # # => <![CDATA[hello]]]]><![CDATA[>world]]>
- def cdata_section(content)
- splitted = content.gsub(']]>', ']]]]><![CDATA[>')
- "<![CDATA[#{splitted}]]>".html_safe
- end
-
- # Returns an escaped version of +html+ without affecting existing escaped entities.
- #
- # escape_once("1 < 2 &amp; 3")
- # # => "1 &lt; 2 &amp; 3"
- #
- # escape_once("&lt;&lt; Accept & Checkout")
- # # => "&lt;&lt; Accept &amp; Checkout"
- def escape_once(html)
- ERB::Util.html_escape_once(html)
- end
-
- private
-
- def content_tag_string(name, content, options, escape = true)
- tag_options = tag_options(options, escape) if options
- content = ERB::Util.h(content) if escape
- "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
- end
-
- def tag_options(options, escape = true)
- return if options.blank?
- attrs = []
- options.each_pair do |key, value|
- if key.to_s == 'data' && value.is_a?(Hash)
- value.each_pair do |k, v|
- attrs << data_tag_option(k, v, escape)
- end
- elsif BOOLEAN_ATTRIBUTES.include?(key)
- attrs << boolean_tag_option(key) if value
- elsif !value.nil?
- attrs << tag_option(key, value, escape)
- end
- end
- " #{attrs.sort * ' '}".html_safe unless attrs.empty?
- end
-
- def data_tag_option(key, value, escape)
- key = "data-#{key.to_s.dasherize}"
- unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
- value = value.to_json
- end
- tag_option(key, value, escape)
- end
-
- def boolean_tag_option(key)
- %(#{key}="#{key}")
- end
-
- def tag_option(key, value, escape)
- value = value.join(" ") if value.is_a?(Array)
- value = ERB::Util.h(value) if escape
- %(#{key}="#{value}")
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/base.rb b/actionpack/lib/action_view/helpers/tags/base.rb
deleted file mode 100644
index 192f5eebaa..0000000000
--- a/actionpack/lib/action_view/helpers/tags/base.rb
+++ /dev/null
@@ -1,150 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class Base #:nodoc:
- include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
- include FormOptionsHelper
-
- attr_reader :object
-
- def initialize(object_name, method_name, template_object, options = {})
- @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
- @template_object = template_object
-
- @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
- @object = retrieve_object(options.delete(:object))
- @options = options
- @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
- end
-
- # This is what child classes implement.
- def render
- raise NotImplementedError, "Subclasses must implement a render method"
- end
-
- private
-
- def value(object)
- object.send @method_name if object
- end
-
- def value_before_type_cast(object)
- unless object.nil?
- method_before_type_cast = @method_name + "_before_type_cast"
-
- object.respond_to?(method_before_type_cast) ?
- object.send(method_before_type_cast) :
- value(object)
- end
- end
-
- def retrieve_object(object)
- if object
- object
- elsif @template_object.instance_variable_defined?("@#{@object_name}")
- @template_object.instance_variable_get("@#{@object_name}")
- end
- rescue NameError
- # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
- nil
- end
-
- def retrieve_autoindex(pre_match)
- object = self.object || @template_object.instance_variable_get("@#{pre_match}")
- if object && object.respond_to?(:to_param)
- object.to_param
- else
- raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
- end
- end
-
- def add_default_name_and_id_for_value(tag_value, options)
- if tag_value.nil?
- add_default_name_and_id(options)
- else
- specified_id = options["id"]
- add_default_name_and_id(options)
-
- if specified_id.blank? && options["id"].present?
- options["id"] += "_#{sanitized_value(tag_value)}"
- end
- end
- end
-
- def add_default_name_and_id(options)
- if options.has_key?("index")
- options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"]) }
- options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
- options.delete("index")
- elsif defined?(@auto_index)
- options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index) }
- options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
- else
- options["name"] ||= options.fetch("name"){ options['multiple'] ? tag_name_multiple : tag_name }
- options["id"] = options.fetch("id"){ tag_id }
- end
- options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
- end
-
- def tag_name
- "#{@object_name}[#{sanitized_method_name}]"
- end
-
- def tag_name_multiple
- "#{tag_name}[]"
- end
-
- def tag_name_with_index(index)
- "#{@object_name}[#{index}][#{sanitized_method_name}]"
- end
-
- def tag_id
- "#{sanitized_object_name}_#{sanitized_method_name}"
- end
-
- def tag_id_with_index(index)
- "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
- end
-
- def sanitized_object_name
- @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
- end
-
- def sanitized_method_name
- @sanitized_method_name ||= @method_name.sub(/\?$/,"")
- end
-
- def sanitized_value(value)
- value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
- end
-
- def select_content_tag(option_tags, options, html_options)
- html_options = html_options.stringify_keys
- add_default_name_and_id(html_options)
- options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
- select = content_tag("select", add_options(option_tags, options, value(object)), html_options)
-
- if html_options["multiple"] && options.fetch(:include_hidden, true)
- tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
- else
- select
- end
- end
-
- def select_not_required?(html_options)
- !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
- end
-
- def add_options(option_tags, options, value = nil)
- if options[:include_blank]
- option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
- end
- if value.blank? && options[:prompt]
- option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags
- end
- option_tags
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/check_box.rb b/actionpack/lib/action_view/helpers/tags/check_box.rb
deleted file mode 100644
index 9d17a1dde3..0000000000
--- a/actionpack/lib/action_view/helpers/tags/check_box.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-require 'action_view/helpers/tags/checkable'
-
-module ActionView
- module Helpers
- module Tags
- class CheckBox < Base #:nodoc:
- include Checkable
-
- def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
- @checked_value = checked_value
- @unchecked_value = unchecked_value
- super(object_name, method_name, template_object, options)
- end
-
- def render
- options = @options.stringify_keys
- options["type"] = "checkbox"
- options["value"] = @checked_value
- options["checked"] = "checked" if input_checked?(object, options)
-
- if options["multiple"]
- add_default_name_and_id_for_value(@checked_value, options)
- options.delete("multiple")
- else
- add_default_name_and_id(options)
- end
-
- include_hidden = options.delete("include_hidden") { true }
- checkbox = tag("input", options)
-
- if include_hidden
- hidden = hidden_field_for_checkbox(options)
- hidden + checkbox
- else
- checkbox
- end
- end
-
- private
-
- def checked?(value)
- case value
- when TrueClass, FalseClass
- value == !!@checked_value
- when NilClass
- false
- when String
- value == @checked_value
- when Array
- value.include?(@checked_value)
- else
- value.to_i == @checked_value.to_i
- end
- end
-
- def hidden_field_for_checkbox(options)
- @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/checkable.rb b/actionpack/lib/action_view/helpers/tags/checkable.rb
deleted file mode 100644
index b97c0c68d7..0000000000
--- a/actionpack/lib/action_view/helpers/tags/checkable.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- module Checkable
- def input_checked?(object, options)
- if options.has_key?("checked")
- checked = options.delete "checked"
- checked == true || checked == "checked"
- else
- checked?(value(object))
- end
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb b/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb
deleted file mode 100644
index e23f5113fb..0000000000
--- a/actionpack/lib/action_view/helpers/tags/collection_check_boxes.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require 'action_view/helpers/tags/collection_helpers'
-
-module ActionView
- module Helpers
- module Tags
- class CollectionCheckBoxes < Base
- include CollectionHelpers
-
- class CheckBoxBuilder < Builder
- def check_box(extra_html_options={})
- html_options = extra_html_options.merge(@input_html_options)
- @template_object.check_box(@object_name, @method_name, html_options, @value, nil)
- end
- end
-
- def render
- rendered_collection = render_collection do |item, value, text, default_html_options|
- default_html_options[:multiple] = true
- builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
-
- if block_given?
- yield builder
- else
- builder.check_box + builder.label
- end
- end
-
- # Append a hidden field to make sure something will be sent back to the
- # server if all check boxes are unchecked.
- hidden = @template_object.hidden_field_tag(tag_name_multiple, "", :id => nil)
-
- rendered_collection + hidden
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb b/actionpack/lib/action_view/helpers/tags/collection_helpers.rb
deleted file mode 100644
index 4e33e79a36..0000000000
--- a/actionpack/lib/action_view/helpers/tags/collection_helpers.rb
+++ /dev/null
@@ -1,82 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- module CollectionHelpers
- class Builder
- attr_reader :object, :text, :value
-
- def initialize(template_object, object_name, method_name, object,
- sanitized_attribute_name, text, value, input_html_options)
- @template_object = template_object
- @object_name = object_name
- @method_name = method_name
- @object = object
- @sanitized_attribute_name = sanitized_attribute_name
- @text = text
- @value = value
- @input_html_options = input_html_options
- end
-
- def label(label_html_options={}, &block)
- @template_object.label(@object_name, @sanitized_attribute_name, @text, label_html_options, &block)
- end
- end
-
- def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
- @collection = collection
- @value_method = value_method
- @text_method = text_method
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- private
-
- def instantiate_builder(builder_class, item, value, text, html_options)
- builder_class.new(@template_object, @object_name, @method_name, item,
- sanitize_attribute_name(value), text, value, html_options)
- end
-
- # Generate default options for collection helpers, such as :checked and
- # :disabled.
- def default_html_options_for_collection(item, value) #:nodoc:
- html_options = @html_options.dup
-
- [:checked, :selected, :disabled].each do |option|
- next unless current_value = @options[option]
-
- accept = if current_value.respond_to?(:call)
- current_value.call(item)
- else
- Array(current_value).map(&:to_s).include?(value.to_s)
- end
-
- if accept
- html_options[option] = true
- elsif option == :checked
- html_options[option] = false
- end
- end
-
- html_options[:object] = @object
- html_options
- end
-
- def sanitize_attribute_name(value) #:nodoc:
- "#{sanitized_method_name}_#{sanitized_value(value)}"
- end
-
- def render_collection #:nodoc:
- @collection.map do |item|
- value = value_for_collection(item, @value_method)
- text = value_for_collection(item, @text_method)
- default_html_options = default_html_options_for_collection(item, value)
-
- yield item, value, text, default_html_options
- end.join.html_safe
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb
deleted file mode 100644
index ba2035f074..0000000000
--- a/actionpack/lib/action_view/helpers/tags/collection_radio_buttons.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'action_view/helpers/tags/collection_helpers'
-
-module ActionView
- module Helpers
- module Tags
- class CollectionRadioButtons < Base
- include CollectionHelpers
-
- class RadioButtonBuilder < Builder
- def radio_button(extra_html_options={})
- html_options = extra_html_options.merge(@input_html_options)
- @template_object.radio_button(@object_name, @method_name, @value, html_options)
- end
- end
-
- def render
- render_collection do |item, value, text, default_html_options|
- builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
-
- if block_given?
- yield builder
- else
- builder.radio_button + builder.label
- end
- end
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/collection_select.rb b/actionpack/lib/action_view/helpers/tags/collection_select.rb
deleted file mode 100644
index ec78e6e5f9..0000000000
--- a/actionpack/lib/action_view/helpers/tags/collection_select.rb
+++ /dev/null
@@ -1,28 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class CollectionSelect < Base #:nodoc:
- def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
- @collection = collection
- @value_method = value_method
- @text_method = text_method
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- def render
- option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
- }
-
- select_content_tag(
- options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
- @options, @html_options
- )
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/color_field.rb b/actionpack/lib/action_view/helpers/tags/color_field.rb
deleted file mode 100644
index 6f08f8483a..0000000000
--- a/actionpack/lib/action_view/helpers/tags/color_field.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class ColorField < TextField #:nodoc:
- def render
- options = @options.stringify_keys
- options["value"] = @options.fetch("value") { validate_color_string(value(object)) }
- @options = options
- super
- end
-
- private
-
- def validate_color_string(string)
- regex = /#[0-9a-fA-F]{6}/
- if regex.match(string)
- string.downcase
- else
- "#000000"
- end
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/date_field.rb b/actionpack/lib/action_view/helpers/tags/date_field.rb
deleted file mode 100644
index 64c29dea3d..0000000000
--- a/actionpack/lib/action_view/helpers/tags/date_field.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class DateField < DatetimeField #:nodoc:
- private
-
- def format_date(value)
- value.try(:strftime, "%Y-%m-%d")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/date_select.rb b/actionpack/lib/action_view/helpers/tags/date_select.rb
deleted file mode 100644
index 5d706087b0..0000000000
--- a/actionpack/lib/action_view/helpers/tags/date_select.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class DateSelect < Base #:nodoc:
- def initialize(object_name, method_name, template_object, options, html_options)
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- def render
- error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
- end
-
- class << self
- def select_type
- @select_type ||= self.name.split("::").last.sub("Select", "").downcase
- end
- end
-
- private
-
- def select_type
- self.class.select_type
- end
-
- def datetime_selector(options, html_options)
- datetime = value(object) || default_datetime(options)
- @auto_index ||= nil
-
- options = options.dup
- options[:field_name] = @method_name
- options[:include_position] = true
- options[:prefix] ||= @object_name
- options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
-
- DateTimeSelector.new(datetime, options, html_options)
- end
-
- def default_datetime(options)
- return if options[:include_blank] || options[:prompt]
-
- case options[:default]
- when nil
- Time.current
- when Date, Time
- options[:default]
- else
- default = options[:default].dup
-
- # Rename :minute and :second to :min and :sec
- default[:min] ||= default[:minute]
- default[:sec] ||= default[:second]
-
- time = Time.current
-
- [:year, :month, :day, :hour, :min, :sec].each do |key|
- default[key] ||= time.send(key)
- end
-
- Time.utc_time(
- default[:year], default[:month], default[:day],
- default[:hour], default[:min], default[:sec]
- )
- end
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/datetime_field.rb b/actionpack/lib/action_view/helpers/tags/datetime_field.rb
deleted file mode 100644
index e407146e96..0000000000
--- a/actionpack/lib/action_view/helpers/tags/datetime_field.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class DatetimeField < TextField #:nodoc:
- def render
- options = @options.stringify_keys
- options["value"] = @options.fetch("value") { format_date(value(object)) }
- options["min"] = format_date(options["min"])
- options["max"] = format_date(options["max"])
- @options = options
- super
- end
-
- private
-
- def format_date(value)
- value.try(:strftime, "%Y-%m-%dT%T.%L%z")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb b/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb
deleted file mode 100644
index 6668d6d718..0000000000
--- a/actionpack/lib/action_view/helpers/tags/datetime_local_field.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class DatetimeLocalField < DatetimeField #:nodoc:
- class << self
- def field_type
- @field_type ||= "datetime-local"
- end
- end
-
- private
-
- def format_date(value)
- value.try(:strftime, "%Y-%m-%dT%T")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/datetime_select.rb b/actionpack/lib/action_view/helpers/tags/datetime_select.rb
deleted file mode 100644
index a32c840bce..0000000000
--- a/actionpack/lib/action_view/helpers/tags/datetime_select.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class DatetimeSelect < DateSelect #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/email_field.rb b/actionpack/lib/action_view/helpers/tags/email_field.rb
deleted file mode 100644
index 45cde507d7..0000000000
--- a/actionpack/lib/action_view/helpers/tags/email_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class EmailField < TextField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/file_field.rb b/actionpack/lib/action_view/helpers/tags/file_field.rb
deleted file mode 100644
index 59f2ff71b4..0000000000
--- a/actionpack/lib/action_view/helpers/tags/file_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class FileField < TextField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb
deleted file mode 100644
index 507ba8835f..0000000000
--- a/actionpack/lib/action_view/helpers/tags/grouped_collection_select.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class GroupedCollectionSelect < Base #:nodoc:
- def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
- @collection = collection
- @group_method = group_method
- @group_label_method = group_label_method
- @option_key_method = option_key_method
- @option_value_method = option_value_method
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- def render
- option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
- }
-
- select_content_tag(
- option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
- )
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/hidden_field.rb b/actionpack/lib/action_view/helpers/tags/hidden_field.rb
deleted file mode 100644
index a8d13dc1b1..0000000000
--- a/actionpack/lib/action_view/helpers/tags/hidden_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class HiddenField < TextField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/label.rb b/actionpack/lib/action_view/helpers/tags/label.rb
deleted file mode 100644
index 16135fcd5a..0000000000
--- a/actionpack/lib/action_view/helpers/tags/label.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class Label < Base #:nodoc:
- def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
- options ||= {}
-
- content_is_options = content_or_options.is_a?(Hash)
- if content_is_options
- options.merge! content_or_options
- @content = nil
- else
- @content = content_or_options
- end
-
- super(object_name, method_name, template_object, options)
- end
-
- def render(&block)
- options = @options.stringify_keys
- tag_value = options.delete("value")
- name_and_id = options.dup
-
- if name_and_id["for"]
- name_and_id["id"] = name_and_id["for"]
- else
- name_and_id.delete("id")
- end
-
- add_default_name_and_id_for_value(tag_value, name_and_id)
- options.delete("index")
- options.delete("namespace")
- options["for"] = name_and_id["id"] unless options.key?("for")
-
- if block_given?
- content = @template_object.capture(&block)
- else
- content = if @content.blank?
- @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
- method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
-
- if object.respond_to?(:to_model)
- key = object.class.model_name.i18n_key
- i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
- end
-
- i18n_default ||= ""
- I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
- else
- @content.to_s
- end
-
- content ||= if object && object.class.respond_to?(:human_attribute_name)
- object.class.human_attribute_name(@method_name)
- end
-
- content ||= @method_name.humanize
- end
-
- label_tag(name_and_id["id"], content, options)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/month_field.rb b/actionpack/lib/action_view/helpers/tags/month_field.rb
deleted file mode 100644
index 3d3c32d847..0000000000
--- a/actionpack/lib/action_view/helpers/tags/month_field.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class MonthField < DatetimeField #:nodoc:
- private
-
- def format_date(value)
- value.try(:strftime, "%Y-%m")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/number_field.rb b/actionpack/lib/action_view/helpers/tags/number_field.rb
deleted file mode 100644
index 9cd04434f0..0000000000
--- a/actionpack/lib/action_view/helpers/tags/number_field.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class NumberField < TextField #:nodoc:
- def render
- options = @options.stringify_keys
-
- if range = options.delete("in") || options.delete("within")
- options.update("min" => range.min, "max" => range.max)
- end
-
- @options = options
- super
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/password_field.rb b/actionpack/lib/action_view/helpers/tags/password_field.rb
deleted file mode 100644
index 6e7a4d3c36..0000000000
--- a/actionpack/lib/action_view/helpers/tags/password_field.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class PasswordField < TextField #:nodoc:
- def render
- @options = {:value => nil}.merge!(@options)
- super
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/radio_button.rb b/actionpack/lib/action_view/helpers/tags/radio_button.rb
deleted file mode 100644
index 8a0421f061..0000000000
--- a/actionpack/lib/action_view/helpers/tags/radio_button.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'action_view/helpers/tags/checkable'
-
-module ActionView
- module Helpers
- module Tags
- class RadioButton < Base #:nodoc:
- include Checkable
-
- def initialize(object_name, method_name, template_object, tag_value, options)
- @tag_value = tag_value
- super(object_name, method_name, template_object, options)
- end
-
- def render
- options = @options.stringify_keys
- options["type"] = "radio"
- options["value"] = @tag_value
- options["checked"] = "checked" if input_checked?(object, options)
- add_default_name_and_id_for_value(@tag_value, options)
- tag("input", options)
- end
-
- private
-
- def checked?(value)
- value.to_s == @tag_value.to_s
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/range_field.rb b/actionpack/lib/action_view/helpers/tags/range_field.rb
deleted file mode 100644
index 47db4680e7..0000000000
--- a/actionpack/lib/action_view/helpers/tags/range_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class RangeField < NumberField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/search_field.rb b/actionpack/lib/action_view/helpers/tags/search_field.rb
deleted file mode 100644
index 818fd4b887..0000000000
--- a/actionpack/lib/action_view/helpers/tags/search_field.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class SearchField < TextField #:nodoc:
- def render
- options = @options.stringify_keys
-
- if options["autosave"]
- if options["autosave"] == true
- options["autosave"] = request.host.split(".").reverse.join(".")
- end
- options["results"] ||= 10
- end
-
- if options["onsearch"]
- options["incremental"] = true unless options.has_key?("incremental")
- end
-
- super
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/select.rb b/actionpack/lib/action_view/helpers/tags/select.rb
deleted file mode 100644
index 53a108b7e6..0000000000
--- a/actionpack/lib/action_view/helpers/tags/select.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class Select < Base #:nodoc:
- def initialize(object_name, method_name, template_object, choices, options, html_options)
- @choices = choices
- @choices = @choices.to_a if @choices.is_a?(Range)
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- def render
- option_tags_options = {
- :selected => @options.fetch(:selected) { value(@object) },
- :disabled => @options[:disabled]
- }
-
- option_tags = if grouped_choices?
- grouped_options_for_select(@choices, option_tags_options)
- else
- options_for_select(@choices, option_tags_options)
- end
-
- select_content_tag(option_tags, @options, @html_options)
- end
-
- private
-
- # Grouped choices look like this:
- #
- # [nil, []]
- # { nil => [] }
- #
- def grouped_choices?
- !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/tel_field.rb b/actionpack/lib/action_view/helpers/tags/tel_field.rb
deleted file mode 100644
index 87c1f6b6b6..0000000000
--- a/actionpack/lib/action_view/helpers/tags/tel_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TelField < TextField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/text_area.rb b/actionpack/lib/action_view/helpers/tags/text_area.rb
deleted file mode 100644
index f74652c5e7..0000000000
--- a/actionpack/lib/action_view/helpers/tags/text_area.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TextArea < Base #:nodoc:
- def render
- options = @options.stringify_keys
- add_default_name_and_id(options)
-
- if size = options.delete("size")
- options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
- end
-
- content_tag("textarea", options.delete('value') || value_before_type_cast(object), options)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/text_field.rb b/actionpack/lib/action_view/helpers/tags/text_field.rb
deleted file mode 100644
index 024a1a8af2..0000000000
--- a/actionpack/lib/action_view/helpers/tags/text_field.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TextField < Base #:nodoc:
- def render
- options = @options.stringify_keys
- options["size"] = options["maxlength"] unless options.key?("size")
- options["type"] ||= field_type
- options["value"] = options.fetch("value"){ value_before_type_cast(object) } unless field_type == "file"
- options["value"] &&= ERB::Util.html_escape(options["value"])
- add_default_name_and_id(options)
- tag("input", options)
- end
-
- class << self
- def field_type
- @field_type ||= self.name.split("::").last.sub("Field", "").downcase
- end
- end
-
- private
-
- def field_type
- self.class.field_type
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/time_field.rb b/actionpack/lib/action_view/helpers/tags/time_field.rb
deleted file mode 100644
index a3941860c9..0000000000
--- a/actionpack/lib/action_view/helpers/tags/time_field.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TimeField < DatetimeField #:nodoc:
- private
-
- def format_date(value)
- value.try(:strftime, "%T.%L")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/time_select.rb b/actionpack/lib/action_view/helpers/tags/time_select.rb
deleted file mode 100644
index 9e97deb706..0000000000
--- a/actionpack/lib/action_view/helpers/tags/time_select.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TimeSelect < DateSelect #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/time_zone_select.rb b/actionpack/lib/action_view/helpers/tags/time_zone_select.rb
deleted file mode 100644
index 0a176157c3..0000000000
--- a/actionpack/lib/action_view/helpers/tags/time_zone_select.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class TimeZoneSelect < Base #:nodoc:
- def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
- @priority_zones = priority_zones
- @html_options = html_options
-
- super(object_name, method_name, template_object, options)
- end
-
- def render
- select_content_tag(
- time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
- )
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/url_field.rb b/actionpack/lib/action_view/helpers/tags/url_field.rb
deleted file mode 100644
index 1ffdfe0b3c..0000000000
--- a/actionpack/lib/action_view/helpers/tags/url_field.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class UrlField < TextField #:nodoc:
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/tags/week_field.rb b/actionpack/lib/action_view/helpers/tags/week_field.rb
deleted file mode 100644
index 1e13939a0a..0000000000
--- a/actionpack/lib/action_view/helpers/tags/week_field.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActionView
- module Helpers
- module Tags
- class WeekField < DatetimeField #:nodoc:
- private
-
- def format_date(value)
- value.try(:strftime, "%Y-W%W")
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/text_helper.rb b/actionpack/lib/action_view/helpers/text_helper.rb
deleted file mode 100644
index 527bfe0cab..0000000000
--- a/actionpack/lib/action_view/helpers/text_helper.rb
+++ /dev/null
@@ -1,439 +0,0 @@
-require 'active_support/core_ext/string/filters'
-require 'active_support/core_ext/array/extract_options'
-
-module ActionView
- # = Action View Text Helpers
- module Helpers #:nodoc:
- # The TextHelper module provides a set of methods for filtering, formatting
- # and transforming strings, which can reduce the amount of inline Ruby code in
- # your views. These helper methods extend Action View making them callable
- # within your template files.
- #
- # ==== Sanitization
- #
- # Most text helpers by default sanitize the given content, but do not escape it.
- # This means HTML tags will appear in the page but all malicious code will be removed.
- # Let's look at some examples using the +simple_format+ method:
- #
- # simple_format('<a href="http://example.com/">Example</a>')
- # # => "<p><a href=\"http://example.com/\">Example</a></p>"
- #
- # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
- # # => "<p><a>Example</a></p>"
- #
- # If you want to escape all content, you should invoke the +h+ method before
- # calling the text helper.
- #
- # simple_format h('<a href="http://example.com/">Example</a>')
- # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
- module TextHelper
- extend ActiveSupport::Concern
-
- include SanitizeHelper
- include TagHelper
- # The preferred method of outputting text in your views is to use the
- # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
- # do not operate as expected in an eRuby code block. If you absolutely must
- # output text within a non-output code block (i.e., <% %>), you can use the concat method.
- #
- # <%
- # concat "hello"
- # # is the equivalent of <%= "hello" %>
- #
- # if logged_in
- # concat "Logged in!"
- # else
- # concat link_to('login', :action => :login)
- # end
- # # will either display "Logged in!" or a login link
- # %>
- def concat(string)
- output_buffer << string
- end
-
- def safe_concat(string)
- output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
- end
-
- # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
- # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
- # for a total length not exceeding <tt>:length</tt>.
- #
- # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
- #
- # Pass a block if you want to show extra content when the text is truncated.
- #
- # The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is
- # +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation
- # may produce invalid HTML (such as unbalanced or incomplete tags).
- #
- # truncate("Once upon a time in a world far far away")
- # # => "Once upon a time in a world..."
- #
- # truncate("Once upon a time in a world far far away", :length => 17)
- # # => "Once upon a ti..."
- #
- # truncate("Once upon a time in a world far far away", :length => 17, :separator => ' ')
- # # => "Once upon a..."
- #
- # truncate("And they found that many people were sleeping better.", :length => 25, :omission => '... (continued)')
- # # => "And they f... (continued)"
- #
- # truncate("<p>Once upon a time in a world far far away</p>")
- # # => "<p>Once upon a time in a wo..."
- #
- # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
- # # => "Once upon a time in a wo...<a href="#">Continue</a>"
- def truncate(text, options = {}, &block)
- if text
- length = options.fetch(:length, 30)
-
- content = text.truncate(length, options)
- content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
- content << capture(&block) if block_given? && text.length > length
- content
- end
- end
-
- # 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>')
- #
- # highlight('You searched for: rails', 'rails')
- # # => You searched for: <mark>rails</mark>
- #
- # highlight('You searched for: ruby, rails, dhh', 'actionpack')
- # # => You searched for: ruby, rails, dhh
- #
- # highlight('You searched for: rails', ['for', 'rails'], :highlighter => '<em>\1</em>')
- # # => You searched <em>for</em>: <em>rails</em>
- #
- # highlight('You searched for: rails', 'rails', :highlighter => '<a href="search?q=\1">\1</a>')
- # # => You searched for: <a href="search?q=rails">rails</a>
- def highlight(text, phrases, options = {})
- highlighter = options.fetch(:highlighter, '<mark>\1</mark>')
-
- text = sanitize(text) if options.fetch(:sanitize, true)
- if text.blank? || phrases.blank?
- text
- else
- match = Array(phrases).map { |p| Regexp.escape(p) }.join('|')
- text.gsub(/(#{match})(?![^<]*?>)/i, highlighter)
- end.html_safe
- end
-
- # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
- # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
- # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
- # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. The
- # <tt>:separator</tt> enable to choose the delimation. The resulting string will be stripped in any case. If the +phrase+
- # isn't found, nil is returned.
- #
- # excerpt('This is an example', 'an', :radius => 5)
- # # => ...s is an exam...
- #
- # excerpt('This is an example', 'is', :radius => 5)
- # # => This is a...
- #
- # excerpt('This is an example', 'is')
- # # => This is an example
- #
- # excerpt('This next thing is an example', 'ex', :radius => 2)
- # # => ...next...
- #
- # excerpt('This is also an example', 'an', :radius => 8, :omission => '<chop> ')
- # # => <chop> is also an example
- #
- # excerpt('This is a very beautiful morning', 'very', :separator => ' ', :radius => 1)
- # # => ...a very beautiful...
- def excerpt(text, phrase, options = {})
- return unless text && phrase
-
- separator = options.fetch(:separator, "")
- phrase = Regexp.escape(phrase)
- regex = /#{phrase}/i
-
- return unless matches = text.match(regex)
- phrase = matches[0]
-
- text.split(separator).each do |value|
- if value.match(regex)
- regex = phrase = value
- break
- end
- end
-
- first_part, second_part = text.split(regex, 2)
-
- prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
- postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
-
- prefix + (first_part + separator + phrase + separator + second_part).strip + postfix
- end
-
- # Attempts to pluralize the +singular+ word unless +count+ is 1. If
- # +plural+ is supplied, it will use that when count is > 1, otherwise
- # it will use the Inflector to determine the plural form.
- #
- # pluralize(1, 'person')
- # # => 1 person
- #
- # pluralize(2, 'person')
- # # => 2 people
- #
- # pluralize(3, 'person', 'users')
- # # => 3 users
- #
- # pluralize(0, 'person')
- # # => 0 people
- def pluralize(count, singular, plural = nil)
- word = if (count == 1 || count =~ /^1(\.0+)?$/)
- singular
- else
- plural || singular.pluralize
- end
-
- "#{count || 0} #{word}"
- end
-
- # Wraps the +text+ into lines no longer than +line_width+ width. This method
- # breaks on the first whitespace character that does not exceed +line_width+
- # (which is 80 by default).
- #
- # word_wrap('Once upon a time')
- # # => Once upon a time
- #
- # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
- # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined...
- #
- # word_wrap('Once upon a time', :line_width => 8)
- # # => Once\nupon a\ntime
- #
- # word_wrap('Once upon a time', :line_width => 1)
- # # => Once\nupon\na\ntime
- def word_wrap(text, options = {})
- line_width = options.fetch(:line_width, 80)
-
- text.split("\n").collect do |line|
- line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
- end * "\n"
- end
-
- # Returns +text+ transformed into HTML using simple formatting rules.
- # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
- # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
- # considered as a linebreak and a <tt><br /></tt> tag is appended. This
- # method does not remove the newlines from the +text+.
- #
- # You can pass any HTML attributes into <tt>html_options</tt>. These
- # will be added to all created paragraphs.
- #
- # ==== Options
- # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
- # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
- #
- # ==== Examples
- # my_text = "Here is some basic text...\n...with a line break."
- #
- # simple_format(my_text)
- # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
- #
- # simple_format(my_text, {}, :wrapper_tag => "div")
- # # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
- #
- # more_text = "We want to put a paragraph...\n\n...right there."
- #
- # simple_format(more_text)
- # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
- #
- # simple_format("Look ma! A class!", :class => 'description')
- # # => "<p class='description'>Look ma! A class!</p>"
- #
- # simple_format("<span>I'm allowed!</span> It's true.", {}, :sanitize => false)
- # # => "<p><span>I'm allowed!</span> It's true.</p>"
- def simple_format(text, html_options = {}, options = {})
- wrapper_tag = options.fetch(:wrapper_tag, :p)
-
- text = sanitize(text) if options.fetch(:sanitize, true)
- paragraphs = split_paragraphs(text)
-
- if paragraphs.empty?
- content_tag(wrapper_tag, nil, html_options)
- else
- paragraphs.map { |paragraph|
- content_tag(wrapper_tag, paragraph, html_options, options[:sanitize])
- }.join("\n\n").html_safe
- end
- end
-
- # Creates a Cycle object whose _to_s_ method cycles through elements of an
- # array every time it is called. This can be used for example, to alternate
- # classes for table rows. You can use named cycles to allow nesting in loops.
- # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
- # named cycle. The default name for a cycle without a +:name+ key is
- # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
- # and passing the name of the cycle. The current cycle string can be obtained
- # anytime using the current_cycle method.
- #
- # # Alternate CSS classes for even and odd numbers...
- # @items = [1,2,3,4]
- # <table>
- # <% @items.each do |item| %>
- # <tr class="<%= cycle("odd", "even") -%>">
- # <td>item</td>
- # </tr>
- # <% end %>
- # </table>
- #
- #
- # # Cycle CSS classes for rows, and text colors for values within each row
- # @items = x = [{:first => 'Robert', :middle => 'Daniel', :last => 'James'},
- # {:first => 'Emily', :middle => 'Shannon', :maiden => 'Pike', :last => 'Hicks'},
- # {:first => 'June', :middle => 'Dae', :last => 'Jones'}]
- # <% @items.each do |item| %>
- # <tr class="<%= cycle("odd", "even", :name => "row_class") -%>">
- # <td>
- # <% item.values.each do |value| %>
- # <%# Create a named cycle "colors" %>
- # <span style="color:<%= cycle("red", "green", "blue", :name => "colors") -%>">
- # <%= value %>
- # </span>
- # <% end %>
- # <% reset_cycle("colors") %>
- # </td>
- # </tr>
- # <% end %>
- def cycle(first_value, *values)
- options = values.extract_options!
- name = options.fetch(:name, 'default')
-
- values.unshift(first_value)
-
- cycle = get_cycle(name)
- unless cycle && cycle.values == values
- cycle = set_cycle(name, Cycle.new(*values))
- end
- cycle.to_s
- end
-
- # Returns the current cycle string after a cycle has been started. Useful
- # for complex table highlighting or any other design need which requires
- # the current cycle string in more than one place.
- #
- # # Alternate background colors
- # @items = [1,2,3,4]
- # <% @items.each do |item| %>
- # <div style="background-color:<%= cycle("red","white","blue") %>">
- # <span style="background-color:<%= current_cycle %>"><%= item %></span>
- # </div>
- # <% end %>
- def current_cycle(name = "default")
- cycle = get_cycle(name)
- cycle.current_value if cycle
- end
-
- # Resets a cycle so that it starts from the first element the next time
- # it is called. Pass in +name+ to reset a named cycle.
- #
- # # Alternate CSS classes for even and odd numbers...
- # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
- # <table>
- # <% @items.each do |item| %>
- # <tr class="<%= cycle("even", "odd") -%>">
- # <% item.each do |value| %>
- # <span style="color:<%= cycle("#333", "#666", "#999", :name => "colors") -%>">
- # <%= value %>
- # </span>
- # <% end %>
- #
- # <% reset_cycle("colors") %>
- # </tr>
- # <% end %>
- # </table>
- def reset_cycle(name = "default")
- cycle = get_cycle(name)
- cycle.reset if cycle
- end
-
- class Cycle #:nodoc:
- attr_reader :values
-
- def initialize(first_value, *values)
- @values = values.unshift(first_value)
- reset
- end
-
- def reset
- @index = 0
- end
-
- def current_value
- @values[previous_index].to_s
- end
-
- def to_s
- value = @values[@index].to_s
- @index = next_index
- return value
- end
-
- private
-
- def next_index
- step_index(1)
- end
-
- def previous_index
- step_index(-1)
- end
-
- def step_index(n)
- (@index + n) % @values.size
- end
- end
-
- private
- # The cycle helpers need to store the cycles in a place that is
- # guaranteed to be reset every time a page is rendered, so it
- # uses an instance variable of ActionView::Base.
- def get_cycle(name)
- @_cycles = Hash.new unless defined?(@_cycles)
- return @_cycles[name]
- end
-
- def set_cycle(name, cycle_object)
- @_cycles = Hash.new unless defined?(@_cycles)
- @_cycles[name] = cycle_object
- end
-
- def split_paragraphs(text)
- return [] if text.blank?
-
- text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
- t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
- end
- end
-
- def cut_excerpt_part(part_position, part, separator, options)
- return "", "" unless part
-
- radius = options.fetch(:radius, 100)
- omission = options.fetch(:omission, "...")
-
- part = part.split(separator)
- part.delete("")
- affix = part.size > radius ? omission : ""
-
- part = if part_position == :first
- drop_index = [part.length - radius, 0].max
- part.drop(drop_index)
- else
- part.first(radius)
- end
-
- return affix, part.join(separator)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/translation_helper.rb b/actionpack/lib/action_view/helpers/translation_helper.rb
deleted file mode 100644
index 552c9ba660..0000000000
--- a/actionpack/lib/action_view/helpers/translation_helper.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-require 'action_view/helpers/tag_helper'
-require 'i18n/exceptions'
-
-module I18n
- class ExceptionHandler
- include Module.new {
- def call(exception, locale, key, options)
- exception.is_a?(MissingTranslation) && options[:rescue_format] == :html ? super.html_safe : super
- end
- }
- end
-end
-
-module ActionView
- # = Action View Translation Helpers
- module Helpers
- module TranslationHelper
- # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
- #
- # First, it'll pass the <tt>:rescue_format => :html</tt> option to I18n so that any
- # thrown +MissingTranslation+ messages will be turned into inline spans that
- #
- # * have a "translation-missing" class set,
- # * contain the missing key as a title attribute and
- # * a titleized version of the last key segment as a text.
- #
- # E.g. the value returned for a missing translation key :"blog.post.title" will be
- # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
- # This way your views will display rather reasonable strings but it will still
- # be easy to spot missing translations.
- #
- # Second, it'll scope the key by the current partial if the key starts
- # with a period. So if you call <tt>translate(".foo")</tt> from the
- # <tt>people/index.html.erb</tt> template, you'll actually be calling
- # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
- # to translate many keys within the same partials and gives you a simple framework
- # for scoping them consistently. If you don't prepend the key with a period,
- # nothing is converted.
- #
- # Third, it'll mark the translation as safe HTML if the key has the suffix
- # "_html" or the last element of the key is the word "html". For example,
- # calling translate("footer_html") or translate("footer.html") will return
- # a safe HTML string that won't be escaped by other HTML helper methods. This
- # naming convention helps to identify translations that include HTML tags so that
- # you know what kind of output to expect when you call translate in a template.
- def translate(key, options = {})
- options.merge!(:rescue_format => :html) unless options.key?(:rescue_format)
- options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
- if html_safe_translation_key?(key)
- html_safe_options = options.dup
- options.except(*I18n::RESERVED_KEYS).each do |name, value|
- unless name == :count && value.is_a?(Numeric)
- html_safe_options[name] = ERB::Util.html_escape(value.to_s)
- end
- end
- translation = I18n.translate(scope_key_by_partial(key), html_safe_options)
-
- translation.respond_to?(:html_safe) ? translation.html_safe : translation
- else
- I18n.translate(scope_key_by_partial(key), options)
- end
- end
- alias :t :translate
-
- # Delegates to <tt>I18n.localize</tt> with no additional functionality.
- #
- # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
- # for more information.
- def localize(*args)
- I18n.localize(*args)
- end
- alias :l :localize
-
- private
- def scope_key_by_partial(key)
- if key.to_s.first == "."
- if @virtual_path
- @virtual_path.gsub(%r{/_?}, ".") + key.to_s
- else
- raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
- end
- else
- key
- end
- end
-
- def html_safe_translation_key?(key)
- key.to_s =~ /(\b|_|\.)html$/
- end
-
- def wrap_translate_defaults(defaults)
- new_defaults = []
- defaults = Array(defaults)
- while key = defaults.shift
- if key.is_a?(Symbol)
- new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) }
- break
- else
- new_defaults << key
- end
- end
-
- new_defaults
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/helpers/url_helper.rb b/actionpack/lib/action_view/helpers/url_helper.rb
deleted file mode 100644
index 5105d0e585..0000000000
--- a/actionpack/lib/action_view/helpers/url_helper.rb
+++ /dev/null
@@ -1,654 +0,0 @@
-require 'action_view/helpers/javascript_helper'
-require 'active_support/core_ext/array/access'
-require 'active_support/core_ext/hash/keys'
-require 'active_support/core_ext/string/output_safety'
-
-module ActionView
- # = Action View URL Helpers
- module Helpers #:nodoc:
- # Provides a set of methods for making links and getting URLs that
- # depend on the routing subsystem (see ActionDispatch::Routing).
- # This allows you to use the same format for links in views
- # and controllers.
- module UrlHelper
- # This helper may be included in any class that includes the
- # URL helpers of a routes (routes.url_helpers). Some methods
- # provided here will only work in the context of a request
- # (link_to_unless_current, for instance), which must be provided
- # as a method called #request on the context.
-
- extend ActiveSupport::Concern
-
- include TagHelper
-
- module ClassMethods
- def _url_for_modules
- ActionView::RoutingUrlFor
- end
- end
-
- # Basic implementation of url_for to allow use helpers without routes existence
- def url_for(options = nil) # :nodoc:
- case options
- when String
- options
- when :back
- _back_url
- else
- raise ArgumentError, "arguments passed to url_for can't be handled. Please require " +
- "routes or provide your own implementation"
- end
- end
-
- def _back_url # :nodoc:
- referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"]
- referrer || 'javascript:history.back()'
- end
- protected :_back_url
-
- # Creates a link tag of the given +name+ using a URL created by the set of +options+.
- # See the valid options in the documentation for +url_for+. It's also possible to
- # pass a String instead of an options hash, which generates a link tag that uses the
- # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
- # of an options hash will generate a link to the referrer (a JavaScript back link
- # will be used in place of a referrer if none exists). If +nil+ is passed as the name
- # the value of the link itself will become the name.
- #
- # ==== Signatures
- #
- # link_to(body, url, html_options = {})
- # # url is a String; you can use URL helpers like
- # # posts_path
- #
- # link_to(body, url_options = {}, html_options = {})
- # # url_options, except :method, is passed to url_for
- #
- # link_to(options = {}, html_options = {}) do
- # # name
- # end
- #
- # link_to(url, html_options = {}) do
- # # name
- # end
- #
- # ==== Options
- # * <tt>:data</tt> - This option can be used to add custom data attributes.
- # * <tt>:method => symbol of HTTP verb</tt> - This modifier will dynamically
- # create an HTML form and immediately submit the form for processing using
- # the HTTP verb specified. Useful for having links perform a POST operation
- # in dangerous actions like deleting a record (which search bots can follow
- # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
- # Note that if the user has JavaScript disabled, the request will fall back
- # to using GET. If <tt>:href => '#'</tt> is used and the user has JavaScript
- # disabled clicking the link will have no effect. If you are relying on the
- # POST behavior, 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>.
- # * <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
- # completion of the Ajax request and performing JavaScript operations once
- # they're complete
- #
- # ==== Data attributes
- #
- # * <tt>:confirm => 'question?'</tt> - This will allow the unobtrusive JavaScript
- # driver to prompt with the question specified. If the user accepts, the link is
- # processed normally, otherwise no action is taken.
- # * <tt>:disable_with</tt> - Value of this parameter will be
- # used as the value for a disabled version of the submit
- # button when the form is submitted. This feature is provided
- # by the unobtrusive JavaScript driver.
- #
- # ==== Examples
- # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
- # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base
- # your application on resources and use
- #
- # link_to "Profile", profile_path(@profile)
- # # => <a href="/profiles/1">Profile</a>
- #
- # or the even pithier
- #
- # link_to "Profile", @profile
- # # => <a href="/profiles/1">Profile</a>
- #
- # in place of the older more verbose, non-resource-oriented
- #
- # link_to "Profile", controller: "profiles", action: "show", id: @profile
- # # => <a href="/profiles/show/1">Profile</a>
- #
- # Similarly,
- #
- # link_to "Profiles", profiles_path
- # # => <a href="/profiles">Profiles</a>
- #
- # is better than
- #
- # link_to "Profiles", controller: "profiles"
- # # => <a href="/profiles">Profiles</a>
- #
- # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
- #
- # <%= link_to(@profile) do %>
- # <strong><%= @profile.name %></strong> -- <span>Check it out!</span>
- # <% end %>
- # # => <a href="/profiles/1">
- # <strong>David</strong> -- <span>Check it out!</span>
- # </a>
- #
- # Classes and ids for CSS are easy to produce:
- #
- # link_to "Articles", articles_path, id: "news", class: "article"
- # # => <a href="/articles" class="article" id="news">Articles</a>
- #
- # Be careful when using the older argument style, as an extra literal hash is needed:
- #
- # link_to "Articles", { controller: "articles" }, id: "news", class: "article"
- # # => <a href="/articles" class="article" id="news">Articles</a>
- #
- # Leaving the hash off gives the wrong link:
- #
- # link_to "WRONG!", controller: "articles", id: "news", class: "article"
- # # => <a href="/articles/index/news?class=article">WRONG!</a>
- #
- # +link_to+ can also produce links with anchors or query strings:
- #
- # link_to "Comment wall", profile_path(@profile, anchor: "wall")
- # # => <a href="/profiles/1#wall">Comment wall</a>
- #
- # link_to "Ruby on Rails search", controller: "searches", query: "ruby on rails"
- # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
- #
- # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
- # # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
- #
- # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
- #
- # link_to("Destroy", "http://www.example.com", method: :delete)
- # # => <a href='http://www.example.com' rel="nofollow" data-method="delete">Destroy</a>
- #
- # You can also use custom data attributes using the <tt>:data</tt> option:
- #
- # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
- # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?"">Visit Other Site</a>
- def link_to(name = nil, options = nil, html_options = nil, &block)
- html_options, options = options, name if block_given?
- options ||= {}
-
- html_options = convert_options_to_data_attributes(options, html_options)
-
- url = url_for(options)
- html_options['href'] ||= url
-
- content_tag(:a, name || url, html_options, &block)
- end
-
- # Generates a form containing a single button that submits to the URL created
- # by the set of +options+. This is the safest method to ensure links that
- # cause changes to your data are not triggered by search bots or accelerators.
- # If the HTML button does not work with your layout, you can also consider
- # using the +link_to+ method with the <tt>:method</tt> modifier as described in
- # the +link_to+ documentation.
- #
- # By default, the generated form element has a class name of <tt>button_to</tt>
- # to allow styling of the form itself and its children. This can be changed
- # using the <tt>:form_class</tt> modifier within +html_options+. You can control
- # the form submission and input element behavior using +html_options+.
- # This method accepts the <tt>:method</tt> modifier described in the +link_to+ documentation.
- # If no <tt>:method</tt> modifier is given, it will default to performing a POST operation.
- # You can also disable the button by passing <tt>disabled: true</tt> in +html_options+.
- # If you are using RESTful routes, you can pass the <tt>:method</tt>
- # to change the HTTP verb used to submit the form.
- #
- # ==== Options
- # The +options+ hash accepts the same options as +url_for+.
- #
- # There are a few special +html_options+:
- # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
- # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
- # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
- # * <tt>:data</tt> - This option can be used to add custom data attributes.
- # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
- # submit behavior. By default this behavior is an ajax submit.
- # * <tt>:form</tt> - This hash will be form attributes
- # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
- # be placed
- #
- # ==== Data attributes
- #
- # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to
- # prompt with the question specified. If the user accepts, the link is
- # processed normally, otherwise no action is taken.
- # * <tt>:disable_with</tt> - Value of this parameter will be
- # used as the value for a disabled version of the submit
- # button when the form is submitted. This feature is provided
- # by the unobtrusive JavaScript driver.
- #
- # ==== Examples
- # <%= button_to "New", action: "new" %>
- # # => "<form method="post" action="/controller/new" class="button_to">
- # # <div><input value="New" type="submit" /></div>
- # # </form>"
- #
- # <%= button_to [:make_happy, @user] do %>
- # Make happy <strong><%= @user.name %></strong>
- # <% end %>
- # # => "<form method="post" action="/users/1/make_happy" class="button_to">
- # # <div>
- # # <button type="submit">
- # # Make happy <strong><%= @user.name %></strong>
- # # </button>
- # # </div>
- # # </form>"
- #
- # <%= button_to "New", action: "new", form_class: "new-thing" %>
- # # => "<form method="post" action="/controller/new" class="new-thing">
- # # <div><input value="New" type="submit" /></div>
- # # </form>"
- #
- #
- # <%= button_to "Create", action: "create", remote: true, form: { "data-type" => "json" } %>
- # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
- # # <div>
- # # <input value="Create" type="submit" />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
- # # </form>"
- #
- #
- # <%= button_to "Delete Image", { action: "delete", id: @image.id },
- # method: :delete, data: { confirm: "Are you sure?" } %>
- # # => "<form method="post" action="/images/delete/1" class="button_to">
- # # <div>
- # # <input type="hidden" name="_method" value="delete" />
- # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
- # # </form>"
- #
- #
- # <%= button_to('Destroy', 'http://www.example.com',
- # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
- # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
- # # <div>
- # # <input name='_method' value='delete' type='hidden' />
- # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
- # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
- # # </div>
- # # </form>"
- # #
- def button_to(name = nil, options = nil, html_options = nil, &block)
- html_options, options = options, name if block_given?
- options ||= {}
- html_options ||= {}
-
- html_options = html_options.stringify_keys
- convert_boolean_attributes!(html_options, %w(disabled))
-
- url = options.is_a?(String) ? options : url_for(options)
- remote = html_options.delete('remote')
-
- method = html_options.delete('method').to_s
- method_tag = %w{patch put delete}.include?(method) ? method_tag(method) : ''.html_safe
-
- form_method = method == 'get' ? 'get' : 'post'
- form_options = html_options.delete('form') || {}
- form_options[:class] ||= html_options.delete('form_class') || 'button_to'
- form_options.merge!(method: form_method, action: url)
- form_options.merge!("data-remote" => "true") if remote
-
- request_token_tag = form_method == 'post' ? token_tag : ''
-
- html_options = convert_options_to_data_attributes(options, html_options)
- html_options['type'] = 'submit'
-
- button = if block_given?
- content_tag('button', html_options, &block)
- else
- html_options['value'] = name || url
- tag('input', html_options)
- end
-
- inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
- content_tag('form', content_tag('div', inner_tags), form_options)
- end
-
- # Creates a link tag of the given +name+ using a URL created by the set of
- # +options+ unless the current request URI is the same as the links, in
- # which case only the name is returned (or the given block is yielded, if
- # one exists). You can give +link_to_unless_current+ a block which will
- # specialize the default behavior (e.g., show a "Start Here" link rather
- # than the link's text).
- #
- # ==== Examples
- # Let's say you have a navigation menu...
- #
- # <ul id="navbar">
- # <li><%= link_to_unless_current("Home", { action: "index" }) %></li>
- # <li><%= link_to_unless_current("About Us", { action: "about" }) %></li>
- # </ul>
- #
- # If in the "about" action, it will render...
- #
- # <ul id="navbar">
- # <li><a href="/controller/index">Home</a></li>
- # <li>About Us</li>
- # </ul>
- #
- # ...but if in the "index" action, it will render:
- #
- # <ul id="navbar">
- # <li>Home</li>
- # <li><a href="/controller/about">About Us</a></li>
- # </ul>
- #
- # The implicit block given to +link_to_unless_current+ is evaluated if the current
- # action is the action given. So, if we had a comments page and wanted to render a
- # "Go Back" link instead of a link to the comments page, we could do something like this...
- #
- # <%=
- # link_to_unless_current("Comment", { controller: "comments", action: "new" }) do
- # link_to("Go back", { controller: "posts", action: "index" })
- # end
- # %>
- def link_to_unless_current(name, options = {}, html_options = {}, &block)
- link_to_unless current_page?(options), name, options, html_options, &block
- end
-
- # Creates a link tag of the given +name+ using a URL created by the set of
- # +options+ unless +condition+ is true, in which case only the name is
- # returned. To specialize the default behavior (i.e., show a login link rather
- # than just the plaintext link text), you can pass a block that
- # accepts the name or the full argument list for +link_to_unless+.
- #
- # ==== Examples
- # <%= link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) %>
- # # If the user is logged in...
- # # => <a href="/controller/reply/">Reply</a>
- #
- # <%=
- # link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) do |name|
- # link_to(name, { controller: "accounts", action: "signup" })
- # end
- # %>
- # # If the user is logged in...
- # # => <a href="/controller/reply/">Reply</a>
- # # 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
- name
- end
- else
- link_to(name, options, html_options)
- end
- end
-
- # Creates a link tag of the given +name+ using a URL created by the set of
- # +options+ if +condition+ is true, otherwise only the name is
- # returned. To specialize the default behavior, you can pass a block that
- # accepts the name or the full argument list for +link_to_unless+ (see the examples
- # in +link_to_unless+).
- #
- # ==== Examples
- # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
- # # If the user isn't logged in...
- # # => <a href="/sessions/new/">Login</a>
- #
- # <%=
- # link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) do
- # link_to(@current_user.login, { controller: "accounts", action: "show", id: @current_user })
- # end
- # %>
- # # If the user isn't logged in...
- # # => <a href="/sessions/new/">Login</a>
- # # 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
- end
-
- # Creates a mailto link tag to the specified +email_address+, which is
- # also used as the name of the link unless +name+ is specified. Additional
- # HTML attributes for the link can be passed in +html_options+.
- #
- # +mail_to+ has several methods for hindering email harvesters and customizing
- # the email itself by passing special keys to +html_options+.
- #
- # ==== Options
- # * <tt>:encode</tt> - This key will accept the strings "javascript" or "hex".
- # Passing "javascript" will dynamically create and encode the mailto link then
- # eval it into the DOM of the page. This method will not show the link on
- # the page if the user has JavaScript disabled. Passing "hex" will hex
- # encode the +email_address+ before outputting the mailto link.
- # * <tt>:replace_at</tt> - When the link +name+ isn't provided, the
- # +email_address+ is used for the link label. You can use this option to
- # obfuscate the +email_address+ by substituting the @ sign with the string
- # given as the value.
- # * <tt>:replace_dot</tt> - When the link +name+ isn't provided, the
- # +email_address+ is used for the link label. You can use this option to
- # obfuscate the +email_address+ by substituting the . in the email with the
- # string given as the value.
- # * <tt>:subject</tt> - Preset the subject line of the email.
- # * <tt>:body</tt> - Preset the body of the email.
- # * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
- # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
- #
- # ==== Examples
- # mail_to "me@domain.com"
- # # => <a href="mailto:me@domain.com">me@domain.com</a>
- #
- # mail_to "me@domain.com", "My email", encode: "javascript"
- # # => <script>eval(decodeURIComponent('%64%6f%63...%27%29%3b'))</script>
- #
- # mail_to "me@domain.com", "My email", encode: "hex"
- # # => <a href="mailto:%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>
- #
- # mail_to "me@domain.com", nil, replace_at: "_at_", replace_dot: "_dot_", class: "email"
- # # => <a href="mailto:me@domain.com" class="email">me_at_domain_dot_com</a>
- #
- # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com",
- # subject: "This is an example email"
- # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
- def mail_to(email_address, name = nil, html_options = {})
- email_address = ERB::Util.html_escape(email_address)
-
- html_options = html_options.stringify_keys
- encode = html_options.delete("encode").to_s
-
- extras = %w{ cc bcc body subject }.map { |item|
- option = html_options.delete(item) || next
- "#{item}=#{Rack::Utils.escape_path(option)}"
- }.compact
- extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&'))
-
- email_address_obfuscated = email_address.to_str
- email_address_obfuscated.gsub!(/@/, html_options.delete("replace_at")) if html_options.key?("replace_at")
- email_address_obfuscated.gsub!(/\./, html_options.delete("replace_dot")) if html_options.key?("replace_dot")
- case encode
- when "javascript"
- string = ''
- html = content_tag("a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe))
- html = escape_javascript(html.to_str)
- "document.write('#{html}');".each_byte do |c|
- string << sprintf("%%%x", c)
- end
- "<script>eval(decodeURIComponent('#{string}'))</script>".html_safe
- when "hex"
- email_address_encoded = email_address_obfuscated.unpack('C*').map {|c|
- sprintf("&#%d;", c)
- }.join
-
- string = 'mailto:'.unpack('C*').map { |c|
- sprintf("&#%d;", c)
- }.join + email_address.unpack('C*').map { |c|
- char = c.chr
- char =~ /\w/ ? sprintf("%%%x", c) : char
- }.join
-
- content_tag "a", name || email_address_encoded.html_safe, html_options.merge("href" => "#{string}#{extras}".html_safe)
- else
- content_tag "a", name || email_address_obfuscated.html_safe, html_options.merge("href" => "mailto:#{email_address}#{extras}".html_safe)
- end
- end
-
- # True if the current request URI was generated by the given +options+.
- #
- # ==== Examples
- # Let's say we're in the <tt>/shop/checkout?order=desc</tt> action.
- #
- # current_page?(action: 'process')
- # # => false
- #
- # current_page?(controller: 'shop', action: 'checkout')
- # # => true
- #
- # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
- # # => false
- #
- # current_page?(action: 'checkout')
- # # => true
- #
- # current_page?(controller: 'library', action: 'checkout')
- # # => false
- #
- # Let's say we're in the <tt>/shop/checkout?order=desc&page=1</tt> action.
- #
- # current_page?(action: 'process')
- # # => false
- #
- # current_page?(controller: 'shop', action: 'checkout')
- # # => true
- #
- # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
- # # => true
- #
- # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
- # # => false
- #
- # current_page?(controller: 'shop', action: 'checkout', order: 'desc')
- # # => false
- #
- # current_page?(action: 'checkout')
- # # => true
- #
- # current_page?(controller: 'library', action: 'checkout')
- # # => false
- #
- # Let's say we're in the <tt>/products</tt> action with method POST in case of invalid product.
- #
- # current_page?(controller: 'product', action: 'index')
- # # => false
- #
- def current_page?(options)
- unless request
- raise "You cannot use helpers that need to determine the current " \
- "page unless your view context provides a Request object " \
- "in a #request method"
- end
-
- return false unless request.get?
-
- url_string = url_for(options)
-
- # We ignore any extra parameters in the request_uri if the
- # submitted url doesn't have any either. This lets the function
- # work with things like ?order=asc
- request_uri = url_string.index("?") ? request.fullpath : request.path
-
- if url_string =~ /^\w+:\/\//
- url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
- else
- url_string == request_uri
- end
- end
-
- private
- def convert_options_to_data_attributes(options, html_options)
- if html_options
- html_options = html_options.stringify_keys
- html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
-
- disable_with = html_options.delete("disable_with")
- confirm = html_options.delete('confirm')
- method = html_options.delete('method')
-
- if confirm
- ActiveSupport::Deprecation.warn ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead"
-
- html_options["data-confirm"] = confirm
- end
-
- add_method_to_attributes!(html_options, method) if method
-
- if disable_with
- ActiveSupport::Deprecation.warn ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead"
-
- html_options["data-disable-with"] = disable_with
- end
-
- html_options
- else
- link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
- end
- end
-
- def link_to_remote_options?(options)
- if options.is_a?(Hash)
- options.delete('remote') || options.delete(:remote)
- end
- end
-
- def add_method_to_attributes!(html_options, method)
- if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/
- html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip
- end
- html_options["data-method"] = method
- end
-
- # Processes the +html_options+ hash, converting the boolean
- # attributes from true/false form into the form required by
- # HTML/XHTML. (An attribute is considered to be boolean if
- # its name is listed in the given +bool_attrs+ array.)
- #
- # More specifically, for each boolean attribute in +html_options+
- # given as:
- #
- # "attr" => bool_value
- #
- # if the associated +bool_value+ evaluates to true, it is
- # replaced with the attribute's name; otherwise the attribute is
- # removed from the +html_options+ hash. (See the XHTML 1.0 spec,
- # section 4.5 "Attribute Minimization" for more:
- # http://www.w3.org/TR/xhtml1/#h-4.5)
- #
- # Returns the updated +html_options+ hash, which is also modified
- # in place.
- #
- # Example:
- #
- # convert_boolean_attributes!( html_options,
- # %w( checked disabled readonly ) )
- def convert_boolean_attributes!(html_options, bool_attrs)
- bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
- html_options
- end
-
- def token_tag(token=nil)
- if token != false && protect_against_forgery?
- token ||= form_authenticity_token
- tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
- else
- ''
- end
- end
-
- def method_tag(method)
- tag('input', type: 'hidden', name: '_method', value: method.to_s)
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/log_subscriber.rb b/actionpack/lib/action_view/log_subscriber.rb
deleted file mode 100644
index fd9a543e0a..0000000000
--- a/actionpack/lib/action_view/log_subscriber.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-module ActionView
- # = Action View Log Subscriber
- #
- # Provides functionality so that Rails can output logs from Action View.
- class LogSubscriber < ActiveSupport::LogSubscriber
- VIEWS_PATTERN = /^app\/views\//.freeze
-
- 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)
- end
- alias :render_partial :render_template
- alias :render_collection :render_template
-
- def logger
- ActionView::Base.logger
- end
-
- protected
-
- def from_rails_root(string)
- string.sub("#{Rails.root}/", "").sub(VIEWS_PATTERN, "")
- end
- end
-end
-
-ActionView::LogSubscriber.attach_to :action_view
diff --git a/actionpack/lib/action_view/lookup_context.rb b/actionpack/lib/action_view/lookup_context.rb
deleted file mode 100644
index 76f4dea7b8..0000000000
--- a/actionpack/lib/action_view/lookup_context.rb
+++ /dev/null
@@ -1,233 +0,0 @@
-require 'active_support/core_ext/module/remove_method'
-
-module ActionView
- # = Action View Lookup Context
- #
- # LookupContext is the object responsible to hold all information required to lookup
- # templates, i.e. view paths and details. The LookupContext is also responsible to
- # generate a key, given to view paths, used in the resolver cache lookup. Since
- # this key is generated just once during the request, it speeds up all cache accesses.
- class LookupContext #:nodoc:
- attr_accessor :prefixes, :rendered_format
-
- mattr_accessor :fallbacks
- @@fallbacks = FallbackFileSystemResolver.instances
-
- mattr_accessor :registered_details
- self.registered_details = []
-
- def self.register_detail(name, options = {}, &block)
- self.registered_details << name
- initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
-
- Accessors.send :define_method, :"default_#{name}", &block
- Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
- def #{name}
- @details.fetch(:#{name}, [])
- end
-
- def #{name}=(value)
- value = value.present? ? Array(value) : default_#{name}
- _set_detail(:#{name}, value) if value != @details[:#{name}]
- end
-
- remove_possible_method :initialize_details
- def initialize_details(details)
- #{initialize.join("\n")}
- end
- METHOD
- end
-
- # Holds accessors for the registered details.
- module Accessors #:nodoc:
- end
-
- register_detail(:locale) { [I18n.locale, I18n.default_locale].uniq }
- register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
- register_detail(:handlers){ Template::Handlers.extensions }
-
- class DetailsKey #:nodoc:
- alias :eql? :equal?
- alias :object_hash :hash
-
- attr_reader :hash
- @details_keys = Hash.new
-
- def self.get(details)
- @details_keys[details] ||= new
- end
-
- def self.clear
- @details_keys.clear
- end
-
- def initialize
- @hash = object_hash
- end
- end
-
- # Add caching behavior on top of Details.
- module DetailsCache
- attr_accessor :cache
-
- # Calculate the details key. Remove the handlers from calculation to improve performance
- # since the user cannot modify it explicitly.
- def details_key #:nodoc:
- @details_key ||= DetailsKey.get(@details) if @cache
- end
-
- # Temporary skip passing the details_key forward.
- def disable_cache
- old_value, @cache = @cache, false
- yield
- ensure
- @cache = old_value
- end
-
- protected
-
- def _set_detail(key, value)
- @details = @details.dup if @details_key
- @details_key = nil
- @details[key] = value
- end
- end
-
- # Helpers related to template lookup using the lookup context information.
- module ViewPaths
- attr_reader :view_paths, :html_fallback_for_js
-
- # Whenever setting view paths, makes a copy so we can manipulate then in
- # instance objects as we wish.
- def view_paths=(paths)
- @view_paths = ActionView::PathSet.new(Array(paths))
- end
-
- def find(name, prefixes = [], partial = false, keys = [], options = {})
- @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
- end
- alias :find_template :find
-
- def find_all(name, prefixes = [], partial = false, keys = [], options = {})
- @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
- end
-
- def exists?(name, prefixes = [], partial = false, keys = [], options = {})
- @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
- end
- alias :template_exists? :exists?
-
- # Add fallbacks to the view paths. Useful in cases you are rendering a :file.
- def with_fallbacks
- added_resolvers = 0
- self.class.fallbacks.each do |resolver|
- next if view_paths.include?(resolver)
- view_paths.push(resolver)
- added_resolvers += 1
- end
- yield
- ensure
- added_resolvers.times { view_paths.pop }
- end
-
- protected
-
- def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc:
- name, prefixes = normalize_name(name, prefixes)
- details, details_key = detail_args_for(details_options)
- [name, prefixes, partial || false, details, details_key, keys]
- end
-
- # Compute details hash and key according to user options (e.g. passed from #render).
- 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)]
- end
-
- # Support legacy foo.erb names even though we now ignore .erb
- # as well as incorrectly putting part of the path in the template
- # name instead of the prefix.
- def normalize_name(name, prefixes) #:nodoc:
- prefixes = prefixes.presence
- parts = name.to_s.split('/')
- parts.shift if parts.first.empty?
- name = parts.pop
-
- return name, prefixes || [""] if parts.empty?
-
- parts = parts.join('/')
- prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
-
- return name, prefixes
- end
- end
-
- include Accessors
- include DetailsCache
- include ViewPaths
-
- def initialize(view_paths, details = {}, prefixes = [])
- @details, @details_key = {}, nil
- @skip_default_locale = false
- @cache = true
- @prefixes = prefixes
- @rendered_format = nil
-
- self.view_paths = view_paths
- initialize_details(details)
- end
-
- # Override formats= to expand ["*/*"] values and automatically
- # add :html as fallback to :js.
- def formats=(values)
- if values
- values.concat(default_formats) if values.delete "*/*"
- if values == [:js]
- values << :html
- @html_fallback_for_js = true
- end
- end
- super(values)
- end
-
- # Do not use the default locale on template lookup.
- def skip_default_locale!
- @skip_default_locale = true
- self.locale = nil
- end
-
- # Override locale to return a symbol instead of array.
- def locale
- @details[:locale].first
- 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
- # acting as proxy, which we need to skip.
- def locale=(value)
- if value
- config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
- config.locale = value
- end
-
- super(@skip_default_locale ? I18n.locale : default_locale)
- end
-
- # A method which only uses the first format in the formats array for layout lookup.
- def with_layout_format
- if formats.size == 1
- yield
- else
- old_formats = formats
- _set_detail(:formats, formats[0,1])
-
- begin
- yield
- ensure
- _set_detail(:formats, old_formats)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/path_set.rb b/actionpack/lib/action_view/path_set.rb
deleted file mode 100644
index bbb1af8154..0000000000
--- a/actionpack/lib/action_view/path_set.rb
+++ /dev/null
@@ -1,89 +0,0 @@
-module ActionView #:nodoc:
- # = Action View PathSet
- class PathSet #:nodoc:
- include Enumerable
-
- attr_reader :paths
-
- def initialize(paths = [])
- @paths = typecast paths
- end
-
- def initialize_copy(other)
- @paths = other.paths.dup
- self
- end
-
- def [](i)
- paths[i]
- end
-
- def to_ary
- paths.dup
- end
-
- def include?(item)
- paths.include? item
- end
-
- def pop
- paths.pop
- end
-
- def size
- paths.size
- end
-
- def each(&block)
- paths.each(&block)
- end
-
- def compact
- PathSet.new paths.compact
- end
-
- def +(array)
- PathSet.new(paths + array)
- end
-
- %w(<< concat push insert unshift).each do |method|
- class_eval <<-METHOD, __FILE__, __LINE__ + 1
- def #{method}(*args)
- paths.#{method}(*typecast(args))
- end
- METHOD
- end
-
- def find(*args)
- find_all(*args).first || raise(MissingTemplate.new(self, *args))
- end
-
- def find_all(path, prefixes = [], *args)
- prefixes = [prefixes] if String === prefixes
- prefixes.each do |prefix|
- paths.each do |resolver|
- templates = resolver.find_all(path, prefix, *args)
- return templates unless templates.empty?
- end
- end
- []
- end
-
- def exists?(path, prefixes, *args)
- find_all(path, prefixes, *args).any?
- end
-
- private
-
- def typecast(paths)
- paths.map do |path|
- case path
- when Pathname, String
- OptimizedFileSystemResolver.new path.to_s
- else
- path
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/railtie.rb b/actionpack/lib/action_view/railtie.rb
deleted file mode 100644
index 55f6ea5522..0000000000
--- a/actionpack/lib/action_view/railtie.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require "action_view"
-require "rails"
-
-module ActionView
- # = Action View Railtie
- class Railtie < Rails::Railtie
- config.action_view = ActiveSupport::OrderedOptions.new
- config.action_view.embed_authenticity_token_in_remote_forms = false
-
- config.eager_load_namespaces << ActionView
-
- initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
- ActiveSupport.on_load(:action_view) do
- ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
- app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
- end
- end
-
- initializer "action_view.logger" do
- ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
- end
-
- initializer "action_view.cache_asset_ids" do |app|
- unless app.config.cache_classes
- ActiveSupport.on_load(:action_view) do
- ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids = false
- end
- end
- end
-
- initializer "action_view.set_configs" do |app|
- ActiveSupport.on_load(:action_view) do
- app.config.action_view.each do |k,v|
- send "#{k}=", v
- end
- end
- end
-
- initializer "action_view.caching" do |app|
- ActiveSupport.on_load(:action_view) do
- if app.config.action_view.cache_template_loading.nil?
- ActionView::Resolver.caching = app.config.cache_classes
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/record_identifier.rb b/actionpack/lib/action_view/record_identifier.rb
deleted file mode 100644
index 2953654972..0000000000
--- a/actionpack/lib/action_view/record_identifier.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-require 'active_support/core_ext/module'
-require 'action_view/model_naming'
-
-module ActionView
- # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
- # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to
- # a higher logical level.
- #
- # # routes
- # resources :posts
- #
- # # view
- # <%= div_for(post) do %> <div id="post_45" class="post">
- # <%= post.body %> What a wonderful world!
- # <% end %> </div>
- #
- # # controller
- # def update
- # post = Post.find(params[:id])
- # post.update_attributes(params[:post])
- #
- # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post)
- # end
- #
- # As the example above shows, you can stop caring to a large extent what the actual id of the post is.
- # You just know that one is being assigned and that the subsequent calls in redirect_to expect that
- # same naming convention and allows you to write less code if you follow it.
- module RecordIdentifier
- extend self
- extend ModelNaming
-
- include ModelNaming
-
- JOIN = '_'.freeze
- NEW = 'new'.freeze
-
- # The DOM class convention is to use the singular form of an object or class.
- #
- # dom_class(post) # => "post"
- # dom_class(Person) # => "person"
- #
- # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
- #
- # dom_class(post, :edit) # => "edit_post"
- # dom_class(Person, :edit) # => "edit_person"
- def dom_class(record_or_class, prefix = nil)
- singular = model_name_from_record_or_class(record_or_class).param_key
- prefix ? "#{prefix}#{JOIN}#{singular}" : singular
- end
-
- # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
- # If no id is found, prefix with "new_" instead.
- #
- # dom_id(Post.find(45)) # => "post_45"
- # dom_id(Post.new) # => "new_post"
- #
- # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
- #
- # dom_id(Post.find(45), :edit) # => "edit_post_45"
- # dom_id(Post.new, :custom) # => "custom_post"
- def dom_id(record, prefix = nil)
- if record_id = record_key_for_dom_id(record)
- "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
- else
- dom_class(record, prefix || NEW)
- end
- end
-
- protected
-
- # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
- # This can be overwritten to customize the default generated string representation if desired.
- # If you need to read back a key from a dom_id in order to query for the underlying database record,
- # you should write a helper like 'person_record_from_dom_id' that will extract the key either based
- # on the default implementation (which just joins all key attributes with '_') or on your own
- # overwritten version of the method. By default, this implementation passes the key string through a
- # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
- # make sure yourself that your dom ids are valid, in case you overwrite this method.
- def record_key_for_dom_id(record)
- key = convert_to_model(record).to_key
- key ? key.join('_') : key
- end
- end
-end
diff --git a/actionpack/lib/action_view/renderer/abstract_renderer.rb b/actionpack/lib/action_view/renderer/abstract_renderer.rb
deleted file mode 100644
index 6fb8cbb46c..0000000000
--- a/actionpack/lib/action_view/renderer/abstract_renderer.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-module ActionView
- class AbstractRenderer #:nodoc:
- delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
-
- def initialize(lookup_context)
- @lookup_context = lookup_context
- end
-
- def render
- raise NotImplementedError
- end
-
- protected
-
- def extract_details(options)
- @lookup_context.registered_details.each_with_object({}) do |key, details|
- next unless value = options[key]
- details[key] = Array(value)
- end
- end
-
- def instrument(name, options={})
- ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
- end
-
- 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
-end
diff --git a/actionpack/lib/action_view/renderer/partial_renderer.rb b/actionpack/lib/action_view/renderer/partial_renderer.rb
deleted file mode 100644
index edefeac184..0000000000
--- a/actionpack/lib/action_view/renderer/partial_renderer.rb
+++ /dev/null
@@ -1,475 +0,0 @@
-
-module ActionView
- # = Action View Partials
- #
- # There's also a convenience method for rendering sub templates within the current controller that depends on a
- # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
- # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
- # templates that could be rendered on their own.
- #
- # In a template for Advertiser#account:
- #
- # <%= render :partial => "account" %>
- #
- # This would render "advertiser/_account.html.erb".
- #
- # In another template for Advertiser#buy, we could have:
- #
- # <%= render :partial => "account", :locals => { :account => @buyer } %>
- #
- # <% @advertisements.each do |ad| %>
- # <%= render :partial => "ad", :locals => { :ad => ad } %>
- # <% end %>
- #
- # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
- # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
- #
- # == The :as and :object options
- #
- # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables.
- # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
- #
- # <%= render :partial => "account", :object => @buyer %>
- #
- # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
- # equivalent to:
- #
- # <%= render :partial => "account", :locals => { :account => @buyer } %>
- #
- # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
- # wanted it to be +user+ instead of +account+ we'd do:
- #
- # <%= render :partial => "account", :object => @buyer, :as => 'user' %>
- #
- # This is equivalent to
- #
- # <%= render :partial => "account", :locals => { :user => @buyer } %>
- #
- # == Rendering a collection of partials
- #
- # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
- # render a sub template for each of the elements. This pattern has been implemented as a single method that
- # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
- # example in "Using partials" can be rewritten with a single line:
- #
- # <%= 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+.
- #
- # The <tt>:as</tt> option may be used when rendering partials.
- #
- # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
- # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
- #
- # <%= render :partial => "ad", :collection => @advertisements, :spacer_template => "ad_divider" %>
- #
- # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you
- # to specify a text which will displayed instead by using this form:
- #
- # <%= render(:partial => "ad", :collection => @advertisements) || "There's no ad to be displayed" %>
- #
- # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
- # just keep domain objects, like Active Records, in there.
- #
- # == Rendering shared partials
- #
- # Two controllers can share a set of partials and render them like this:
- #
- # <%= render :partial => "advertisement/ad", :locals => { :ad => @advertisement } %>
- #
- # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
- #
- # == Rendering objects that respond to `to_partial_path`
- #
- # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
- # and pick the proper path by checking `to_partial_path` method.
- #
- # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
- # # <%= render :partial => "accounts/account", :locals => { :account => @account} %>
- # <%= render :partial => @account %>
- #
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
- # # that's why we can replace:
- # # <%= render :partial => "posts/post", :collection => @posts %>
- # <%= render :partial => @posts %>
- #
- # == Rendering the default case
- #
- # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
- # defaults of render to render partials. Examples:
- #
- # # Instead of <%= render :partial => "account" %>
- # <%= render "account" %>
- #
- # # Instead of <%= render :partial => "account", :locals => { :account => @buyer } %>
- # <%= render "account", :account => @buyer %>
- #
- # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
- # # <%= render :partial => "accounts/account", :locals => { :account => @account} %>
- # <%= render @account %>
- #
- # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
- # # that's why we can replace:
- # # <%= render :partial => "posts/post", :collection => @posts %>
- # <%= render @posts %>
- #
- # == Rendering partials with layouts
- #
- # Partials can have their own layouts applied to them. These layouts are different than the ones that are
- # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
- # of users:
- #
- # <%# app/views/users/index.html.erb &>
- # Here's the administrator:
- # <%= render :partial => "user", :layout => "administrator", :locals => { :user => administrator } %>
- #
- # Here's the editor:
- # <%= render :partial => "user", :layout => "editor", :locals => { :user => editor } %>
- #
- # <%# app/views/users/_user.html.erb &>
- # Name: <%= user.name %>
- #
- # <%# app/views/users/_administrator.html.erb &>
- # <div id="administrator">
- # Budget: $<%= user.budget %>
- # <%= yield %>
- # </div>
- #
- # <%# app/views/users/_editor.html.erb &>
- # <div id="editor">
- # Deadline: <%= user.deadline %>
- # <%= yield %>
- # </div>
- #
- # ...this will return:
- #
- # Here's the administrator:
- # <div id="administrator">
- # Budget: $<%= user.budget %>
- # Name: <%= user.name %>
- # </div>
- #
- # Here's the editor:
- # <div id="editor">
- # Deadline: <%= user.deadline %>
- # Name: <%= user.name %>
- # </div>
- #
- # If a collection is given, the layout will be rendered once for each item in
- # the collection. Just think these two snippets have the same output:
- #
- # <%# app/views/users/_user.html.erb %>
- # Name: <%= user.name %>
- #
- # <%# app/views/users/index.html.erb %>
- # <%# This does not use layouts %>
- # <ul>
- # <% users.each do |user| -%>
- # <li>
- # <%= render :partial => "user", :locals => { :user => user } %>
- # </li>
- # <% end -%>
- # </ul>
- #
- # <%# app/views/users/_li_layout.html.erb %>
- # <li>
- # <%= yield %>
- # </li>
- #
- # <%# app/views/users/index.html.erb %>
- # <ul>
- # <%= render :partial => "user", :layout => "li_layout", :collection => users %>
- # </ul>
- #
- # Given two users whose names are Alice and Bob, these snippets return:
- #
- # <ul>
- # <li>
- # Name: Alice
- # </li>
- # <li>
- # Name: Bob
- # </li>
- # </ul>
- #
- # The current object being rendered, as well as the object_counter, will be
- # available as local variables inside the layout template under the same names
- # as available in the partial.
- #
- # You can also apply a layout to a block within any template:
- #
- # <%# app/views/users/_chief.html.erb &>
- # <%= render(:layout => "administrator", :locals => { :user => chief }) do %>
- # Title: <%= chief.title %>
- # <% end %>
- #
- # ...this will return:
- #
- # <div id="administrator">
- # Budget: $<%= user.budget %>
- # Title: <%= chief.name %>
- # </div>
- #
- # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
- #
- # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
- # an array to layout and treat it as an enumerable.
- #
- # <%# app/views/users/_user.html.erb &>
- # <div class="user">
- # Budget: $<%= user.budget %>
- # <%= yield user %>
- # </div>
- #
- # <%# app/views/users/index.html.erb &>
- # <%= render :layout => @users do |user| %>
- # Title: <%= user.title %>
- # <% end %>
- #
- # This will render the layout for each user and yield to the block, passing the user, each time.
- #
- # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
- #
- # <%# app/views/users/_user.html.erb &>
- # <div class="user">
- # <%= yield user, :header %>
- # Budget: $<%= user.budget %>
- # <%= yield user, :footer %>
- # </div>
- #
- # <%# app/views/users/index.html.erb &>
- # <%= render :layout => @users do |user, section| %>
- # <%- case section when :header -%>
- # Title: <%= user.title %>
- # <%- when :footer -%>
- # Deadline: <%= user.deadline %>
- # <%- end -%>
- # <% end %>
- class PartialRenderer < AbstractRenderer
- PREFIXED_PARTIAL_NAMES = Hash.new { |h,k| h[k] = {} }
-
- def initialize(*)
- super
- @context_prefix = @lookup_context.prefixes.first
- end
-
- def render(context, options, block)
- setup(context, options, block)
- identifier = (@template = find_partial) ? @template.identifier : @path
-
- @lookup_context.rendered_format ||= begin
- if @template && @template.formats.present?
- @template.formats.first
- else
- formats.first
- end
- end
-
- if @collection
- instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
- render_collection
- end
- else
- instrument(:partial, :identifier => identifier) do
- render_partial
- end
- end
- end
-
- def render_collection
- return nil if @collection.blank?
-
- if @options.key?(:spacer_template)
- spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
- end
-
- result = @template ? collection_with_template : collection_without_template
- result.join(spacer).html_safe
- end
-
- def render_partial
- view, locals, block = @view, @locals, @block
- object, as = @object, @variable
-
- if !block && (layout = @options[:layout])
- layout = find_template(layout, @template_keys)
- end
-
- object ||= locals[as]
- locals[as] = object
-
- content = @template.render(view, locals) do |*name|
- view._layout_for(*name, &block)
- end
-
- content = layout.render(view, locals){ content } if layout
- content
- end
-
- private
-
- def setup(context, options, block)
- @view = context
- partial = options[:partial]
-
- @options = options
- @locals = options[:locals] || {}
- @block = block
- @details = extract_details(options)
-
- prepend_formats(options[:formats])
-
- if String === partial
- @object = options[:object]
- @path = partial
- @collection = collection
- else
- @object = partial
-
- if @collection = collection_from_object || collection
- paths = @collection_data = @collection.map { |o| partial_path(o) }
- @path = paths.uniq.size == 1 ? paths.first : nil
- else
- @path = partial_path
- end
- end
-
- if as = options[:as]
- raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/
- as = as.to_sym
- end
-
- if @path
- @variable, @variable_counter = retrieve_variable(@path, as)
- @template_keys = retrieve_template_keys
- else
- paths.map! { |path| retrieve_variable(path, as).unshift(path) }
- end
-
- self
- end
-
- def collection
- if @options.key?(:collection)
- collection = @options[:collection]
- collection.respond_to?(:to_ary) ? collection.to_ary : []
- end
- end
-
- def collection_from_object
- @object.to_ary if @object.respond_to?(:to_ary)
- end
-
- def find_partial
- if path = @path
- find_template(path, @template_keys)
- end
- end
-
- def find_template(path, locals)
- prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
- @lookup_context.find_template(path, prefixes, true, locals, @details)
- end
-
- def collection_with_template
- view, locals, template = @view, @locals, @template
- as, counter = @variable, @variable_counter
-
- if layout = @options[:layout]
- layout = find_template(layout, @template_keys)
- end
-
- index = -1
- @collection.map do |object|
- locals[as] = object
- locals[counter] = (index += 1)
-
- content = template.render(view, locals)
- content = layout.render(view, locals) { content } if layout
- content
- end
- end
-
- def collection_without_template
- view, locals, collection_data = @view, @locals, @collection_data
- cache = {}
- keys = @locals.keys
-
- index = -1
- @collection.map do |object|
- index += 1
- path, as, counter = collection_data[index]
-
- locals[as] = object
- locals[counter] = index
-
- template = (cache[path] ||= find_template(path, keys + [as, counter]))
- template.render(view, locals)
- end
- end
-
- def partial_path(object = @object)
- object = object.to_model if object.respond_to?(:to_model)
-
- path = if object.respond_to?(:to_partial_path)
- object.to_partial_path
- else
- raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
- end
-
- if @view.prefix_partial_path_with_controller_namespace
- prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
- else
- path
- end
- end
-
- def prefixed_partial_names
- @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
- end
-
- def merge_prefix_into_object_path(prefix, object_path)
- if prefix.include?(?/) && object_path.include?(?/)
- prefixes = []
- prefix_array = File.dirname(prefix).split('/')
- object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
-
- prefix_array.each_with_index do |dir, index|
- break if dir == object_path_array[index]
- prefixes << dir
- end
-
- (prefixes << object_path).join("/")
- else
- object_path
- end
- end
-
- def retrieve_template_keys
- keys = @locals.keys
- keys << @variable
- keys << @variable_counter if @collection
- keys
- end
-
- def retrieve_variable(path, as)
- variable = as || begin
- base = path[-1] == "/" ? "" : File.basename(path)
- 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]
- end
-
- IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores."
-
- def raise_invalid_identifier(path)
- raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
- end
- end
-end
diff --git a/actionpack/lib/action_view/renderer/renderer.rb b/actionpack/lib/action_view/renderer/renderer.rb
deleted file mode 100644
index bf1b5a7d22..0000000000
--- a/actionpack/lib/action_view/renderer/renderer.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module ActionView
- # This is the main entry point for rendering. It basically delegates
- # to other objects like TemplateRenderer and PartialRenderer which
- # actually renders the template.
- class Renderer
- attr_accessor :lookup_context
-
- def initialize(lookup_context)
- @lookup_context = lookup_context
- end
-
- # Main render entry point shared by AV and AC.
- def render(context, options)
- if options.key?(:partial)
- render_partial(context, options)
- else
- render_template(context, options)
- end
- end
-
- # Render but returns a valid Rack body. If fibers are defined, we return
- # a streaming body that renders the template piece by piece.
- #
- # Note that partials are not supported to be rendered with streaming,
- # so in such cases, we just wrap them in an array.
- def render_body(context, options)
- if options.key?(:partial)
- [render_partial(context, options)]
- else
- StreamingTemplateRenderer.new(@lookup_context).render(context, options)
- end
- end
-
- # Direct accessor to template rendering.
- def render_template(context, options) #:nodoc:
- _template_renderer.render(context, options)
- end
-
- # Direct access to partial rendering.
- def render_partial(context, options, &block) #:nodoc:
- _partial_renderer.render(context, options, block)
- end
-
- private
-
- def _template_renderer #:nodoc:
- @_template_renderer ||= TemplateRenderer.new(@lookup_context)
- end
-
- def _partial_renderer #:nodoc:
- @_partial_renderer ||= PartialRenderer.new(@lookup_context)
- end
- end
-end
diff --git a/actionpack/lib/action_view/renderer/template_renderer.rb b/actionpack/lib/action_view/renderer/template_renderer.rb
deleted file mode 100644
index 156ad4e547..0000000000
--- a/actionpack/lib/action_view/renderer/template_renderer.rb
+++ /dev/null
@@ -1,93 +0,0 @@
-require 'active_support/core_ext/object/try'
-
-module ActionView
- class TemplateRenderer < AbstractRenderer #:nodoc:
- def render(context, options)
- @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.last
- end
-
- render_template(template, options[:layout], options[:locals])
- end
-
- # Determine the template to be rendered using the given options.
- def determine_template(options) #:nodoc:
- keys = options.fetch(:locals, {}).keys
-
- if options.key?(:text)
- Template::Text.new(options[:text], formats.first)
- elsif options.key?(:file)
- with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
- elsif options.key?(:inline)
- handler = Template.handler_for_extension(options[:type] || "erb")
- Template.new(options[:inline], "inline template", handler, :locals => keys)
- elsif options.key?(:template)
- options[:template].respond_to?(:render) ?
- options[:template] : find_template(options[:template], options[:prefixes], false, keys, @details)
- else
- raise ArgumentError, "You invoked render but did not give any of :partial, :template, :inline, :file or :text option."
- end
- end
-
- # Renders the given template. A string representing the layout can be
- # supplied as well.
- def render_template(template, layout_name = nil, locals = {}) #:nodoc:
- view, locals = @view, locals || {}
-
- render_with_layout(layout_name, locals) do |layout|
- instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
- template.render(view, locals) { |*name| view._layout_for(*name) }
- end
- end
- end
-
- def render_with_layout(path, locals) #:nodoc:
- layout = path && find_layout(path, locals.keys)
- content = yield(layout)
-
- if layout
- view = @view
- view.view_flow.set(:layout, content)
- layout.render(view, locals){ |*name| view._layout_for(*name) }
- else
- content
- end
- end
-
- # This is the method which actually finds the layout using details in the lookup
- # context object. If no layout is found, it checks if at least a layout with
- # the given name exists across all details before raising the error.
- def find_layout(layout, keys)
- with_layout_format { resolve_layout(layout, keys) }
- end
-
- def resolve_layout(layout, keys)
- case layout
- when String
- begin
- if layout =~ /^\//
- with_fallbacks { find_template(layout, nil, false, keys, @details) }
- else
- find_template(layout, nil, false, keys, @details)
- end
- rescue ActionView::MissingTemplate
- all_details = @details.merge(:formats => @lookup_context.default_formats)
- raise unless template_exists?(layout, nil, false, keys, all_details)
- end
- when Proc
- resolve_layout(layout.call, keys)
- when FalseClass
- nil
- else
- layout
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/routing_url_for.rb b/actionpack/lib/action_view/routing_url_for.rb
deleted file mode 100644
index d1488e2332..0000000000
--- a/actionpack/lib/action_view/routing_url_for.rb
+++ /dev/null
@@ -1,107 +0,0 @@
-module ActionView
- module RoutingUrlFor
-
- # Returns the URL for the set of +options+ provided. This takes the
- # same options as +url_for+ in Action Controller (see the
- # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
- # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
- # instead of the fully qualified URL like "http://example.com/controller/action".
- #
- # ==== Options
- # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
- # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
- # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
- # is currently not recommended since it breaks caching.
- # * <tt>:host</tt> - Overrides the default (current) host if provided.
- # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
- # * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
- # * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
- #
- # ==== Relying on named routes
- #
- # Passing a record (like an Active Record) instead of a hash as the options parameter will
- # trigger the named route for that record. The lookup will happen on the name of the class. So passing a
- # Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
- # +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
- #
- # ==== Implicit Controller Namespacing
- #
- # Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
- #
- # ==== Examples
- # <%= url_for(:action => 'index') %>
- # # => /blog/
- #
- # <%= url_for(:action => 'find', :controller => 'books') %>
- # # => /books/find
- #
- # <%= url_for(:action => 'login', :controller => 'members', :only_path => false, :protocol => 'https') %>
- # # => https://www.example.com/members/login/
- #
- # <%= url_for(:action => 'play', :anchor => 'player') %>
- # # => /messages/play/#player
- #
- # <%= url_for(:action => 'jump', :anchor => 'tax&ship') %>
- # # => /testing/jump/#tax&ship
- #
- # <%= url_for(Workshop.new) %>
- # # relies on Workshop answering a persisted? call (and in this case returning false)
- # # => /workshops
- #
- # <%= url_for(@workshop) %>
- # # calls @workshop.to_param which by default returns the id
- # # => /workshops/5
- #
- # # to_param can be re-defined in a model to provide different URL names:
- # # => /workshops/1-workshop-name
- #
- # <%= url_for("http://www.example.com") %>
- # # => http://www.example.com
- #
- # <%= url_for(:back) %>
- # # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
- # # => http://www.example.com
- #
- # <%= url_for(:back) %>
- # # if request.env["HTTP_REFERER"] is not set or is blank
- # # => javascript:history.back()
- #
- # <%= url_for(:action => 'index', :controller => 'users') %>
- # # Assuming an "admin" namespace
- # # => /admin/users
- #
- # <%= url_for(:action => 'index', :controller => '/users') %>
- # # Specify absolute path with beginning slash
- # # => /users
- def url_for(options = nil)
- case options
- when String
- options
- when nil, Hash
- options ||= {}
- options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys)
- super
- when :back
- _back_url
- else
- polymorphic_path(options)
- end
- end
-
- def url_options #:nodoc:
- return super unless controller.respond_to?(:url_options)
- controller.url_options
- end
-
- def _routes_context #:nodoc:
- controller
- end
- protected :_routes_context
-
- def optimize_routes_generation? #:nodoc:
- controller.respond_to?(:optimize_routes_generation?, true) ?
- controller.optimize_routes_generation? : super
- end
- protected :optimize_routes_generation?
- end
-end
diff --git a/actionpack/lib/action_view/template.rb b/actionpack/lib/action_view/template.rb
deleted file mode 100644
index 379cdc8a25..0000000000
--- a/actionpack/lib/action_view/template.rb
+++ /dev/null
@@ -1,339 +0,0 @@
-require 'active_support/core_ext/object/try'
-require 'active_support/core_ext/kernel/singleton_class'
-require 'active_support/deprecation'
-require 'thread'
-
-module ActionView
- # = Action View Template
- class Template
- extend ActiveSupport::Autoload
-
- # === Encodings in ActionView::Template
- #
- # ActionView::Template is one of a few sources of potential
- # encoding issues in Rails. This is because the source for
- # templates are usually read from disk, and Ruby (like most
- # encoding-aware programming languages) assumes that the
- # String retrieved through File IO is encoded in the
- # <tt>default_external</tt> encoding. In Rails, the default
- # <tt>default_external</tt> encoding is UTF-8.
- #
- # As a result, if a user saves their template as ISO-8859-1
- # (for instance, using a non-Unicode-aware text editor),
- # and uses characters outside of the ASCII range, their
- # users will see diamonds with question marks in them in
- # the browser.
- #
- # For the rest of this documentation, when we say "UTF-8",
- # we mean "UTF-8 or whatever the default_internal encoding
- # is set to". By default, it will be UTF-8.
- #
- # To mitigate this problem, we use a few strategies:
- # 1. If the source is not valid UTF-8, we raise an exception
- # when the template is compiled to alert the user
- # to the problem.
- # 2. The user can specify the encoding using Ruby-style
- # encoding comments in any template engine. If such
- # a comment is supplied, Rails will apply that encoding
- # to the resulting compiled source returned by the
- # template handler.
- # 3. In all cases, we transcode the resulting String to
- # the UTF-8.
- #
- # This means that other parts of Rails can always assume
- # that templates are encoded in UTF-8, even if the original
- # source of the template was not UTF-8.
- #
- # From a user's perspective, the easiest thing to do is
- # to save your templates as UTF-8. If you do this, you
- # do not need to do anything else for things to "just work".
- #
- # === Instructions for template handlers
- #
- # The easiest thing for you to do is to simply ignore
- # encodings. Rails will hand you the template source
- # as the default_internal (generally UTF-8), raising
- # an exception for the user before sending the template
- # to you if it could not determine the original encoding.
- #
- # For the greatest simplicity, you can support only
- # UTF-8 as the <tt>default_internal</tt>. This means
- # that from the perspective of your handler, the
- # entire pipeline is just UTF-8.
- #
- # === Advanced: Handlers with alternate metadata sources
- #
- # If you want to provide an alternate mechanism for
- # specifying encodings (like ERB does via <%# encoding: ... %>),
- # you may indicate that you will handle encodings yourself
- # by implementing <tt>self.handles_encoding?</tt>
- # on your handler.
- #
- # If you do, Rails will not try to encode the String
- # into the default_internal, passing you the unaltered
- # bytes tagged with the assumed encoding (from
- # default_external).
- #
- # In this case, make sure you return a String from
- # your handler encoded in the default_internal. Since
- # you are handling out-of-band metadata, you are
- # also responsible for alerting the user to any
- # problems with converting the user's data to
- # the <tt>default_internal</tt>.
- #
- # To do so, simply raise the raise +WrongEncodingError+
- # as follows:
- #
- # raise WrongEncodingError.new(
- # problematic_string,
- # expected_encoding
- # )
-
- eager_autoload do
- autoload :Error
- autoload :Handlers
- autoload :Text
- autoload :Types
- end
-
- extend Template::Handlers
-
- attr_accessor :locals, :formats, :virtual_path
-
- attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
-
- # This finalizer is needed (and exactly with a proc inside another proc)
- # otherwise templates leak in development.
- Finalizer = proc do |method_name, mod|
- proc do
- mod.module_eval do
- remove_possible_method method_name
- end
- end
- end
-
- def initialize(source, identifier, handler, details)
- format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
-
- @source = source
- @identifier = identifier
- @handler = handler
- @compiled = false
- @original_encoding = nil
- @locals = details[:locals] || []
- @virtual_path = details[:virtual_path]
- @updated_at = details[:updated_at] || Time.now
- @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
- @compile_mutex = Mutex.new
- end
-
- # Returns if the underlying handler supports streaming. If so,
- # a streaming buffer *may* be passed when it start rendering.
- def supports_streaming?
- handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
- end
-
- # Render a template. If the template was not compiled yet, it is done
- # exactly before rendering.
- #
- # This method is instrumented as "!render_template.action_view". Notice that
- # we use a bang in this instrumentation because you don't want to
- # consume this in production. This is only slow if it's being listened to.
- def render(view, locals, buffer=nil, &block)
- ActiveSupport::Notifications.instrument("!render_template.action_view", :virtual_path => @virtual_path) do
- compile!(view)
- view.send(method_name, locals, buffer, &block)
- end
- rescue Exception => e
- handle_render_error(view, e)
- end
-
- def mime_type
- ActiveSupport::Deprecation.warn 'Template#mime_type is deprecated and will be removed in Rails 4.1. Please use type method instead.'
- @mime_type ||= Mime::Type.lookup_by_extension(@formats.first.to_s) if @formats.first
- end
-
- def type
- @type ||= Types[@formats.first] if @formats.first
- end
-
- # Receives a view object and return a template similar to self by using @virtual_path.
- #
- # This method is useful if you have a template object but it does not contain its source
- # anymore since it was already compiled. In such cases, all you need to do is to call
- # refresh passing in the view object.
- #
- # Notice this method raises an error if the template to be refreshed does not have a
- # virtual path set (true just for inline templates).
- def refresh(view)
- raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
- lookup = view.lookup_context
- pieces = @virtual_path.split("/")
- name = pieces.pop
- partial = !!name.sub!(/^_/, "")
- lookup.disable_cache do
- lookup.find_template(name, [ pieces.join('/') ], partial, @locals)
- end
- end
-
- def inspect
- @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
- end
-
- # This method is responsible for properly setting the encoding of the
- # source. Until this point, we assume that the source is BINARY data.
- # If no additional information is supplied, we assume the encoding is
- # the same as <tt>Encoding.default_external</tt>.
- #
- # The user can also specify the encoding via a comment on the first
- # line of the template (# encoding: NAME-OF-ENCODING). This will work
- # with any template engine, as we process out the encoding comment
- # before passing the source on to the template engine, leaving a
- # blank line in its stead.
- def encode!
- return unless source.encoding == Encoding::BINARY
-
- # Look for # encoding: *. If we find one, we'll encode the
- # String in that encoding, otherwise, we'll use the
- # default external encoding.
- if source.sub!(/\A#{ENCODING_FLAG}/, '')
- encoding = magic_encoding = $1
- else
- encoding = Encoding.default_external
- end
-
- # Tag the source with the default external encoding
- # or the encoding specified in the file
- source.force_encoding(encoding)
-
- # If the user didn't specify an encoding, and the handler
- # handles encodings, we simply pass the String as is to
- # the handler (with the default_external tag)
- if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
- source
- # Otherwise, if the String is valid in the encoding,
- # encode immediately to default_internal. This means
- # that if a handler doesn't handle encodings, it will
- # always get Strings in the default_internal
- elsif source.valid_encoding?
- source.encode!
- # Otherwise, since the String is invalid in the encoding
- # specified, raise an exception
- else
- raise WrongEncodingError.new(source, encoding)
- end
- end
-
- protected
-
- # Compile a template. This method ensures a template is compiled
- # just once and removes the source after it is compiled.
- def compile!(view) #:nodoc:
- return if @compiled
-
- # Templates can be used concurrently in threaded environments
- # so compilation and any instance variable modification must
- # be synchronized
- @compile_mutex.synchronize do
- # Any thread holding this lock will be compiling the template needed
- # by the threads waiting. So re-check the @compiled flag to avoid
- # re-compilation
- return if @compiled
-
- if view.is_a?(ActionView::CompiledTemplates)
- mod = ActionView::CompiledTemplates
- else
- mod = view.singleton_class
- end
-
- compile(view, mod)
-
- # Just discard the source if we have a virtual path. This
- # means we can get the template back.
- @source = nil if @virtual_path
- @compiled = true
- end
- end
-
- # Among other things, this method is responsible for properly setting
- # the encoding of the compiled template.
- #
- # If the template engine handles encodings, we send the encoded
- # String to the engine without further processing. This allows
- # the template engine to support additional mechanisms for
- # specifying the encoding. For instance, ERB supports <%# encoding: %>
- #
- # Otherwise, after we figure out the correct encoding, we then
- # encode the source into <tt>Encoding.default_internal</tt>.
- # In general, this means that templates will be UTF-8 inside of Rails,
- # regardless of the original source encoding.
- def compile(view, mod) #:nodoc:
- encode!
- method_name = self.method_name
- code = @handler.call(self)
-
- # Make sure that the resulting String to be evalled is in the
- # encoding of the code
- source = <<-end_src
- def #{method_name}(local_assigns, output_buffer)
- _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
- ensure
- @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
- end
- end_src
-
- # Make sure the source is in the encoding of the returned code
- source.force_encoding(code.encoding)
-
- # In case we get back a String from a handler that is not in
- # BINARY or the default_internal, encode it to the default_internal
- source.encode!
-
- # Now, validate that the source we got back from the template
- # handler is valid in the default_internal. This is for handlers
- # that handle encoding but screw up
- unless source.valid_encoding?
- raise WrongEncodingError.new(@source, Encoding.default_internal)
- end
-
- begin
- mod.module_eval(source, identifier, 0)
- ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
- rescue Exception => e # errors from template code
- if logger = (view && view.logger)
- logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
- logger.debug "Function body: #{source}"
- logger.debug "Backtrace: #{e.backtrace.join("\n")}"
- end
-
- raise ActionView::Template::Error.new(self, e)
- end
- end
-
- def handle_render_error(view, e) #:nodoc:
- if e.is_a?(Template::Error)
- e.sub_template_of(self)
- raise e
- else
- template = self
- unless template.source
- template = refresh(view)
- template.encode!
- end
- raise Template::Error.new(template, e)
- end
- end
-
- def locals_code #:nodoc:
- @locals.map { |key| "#{key} = local_assigns[:#{key}];" }.join
- end
-
- def method_name #:nodoc:
- @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
- end
-
- def identifier_method_name #:nodoc:
- inspect.gsub(/[^a-z_]/, '_')
- end
- end
-end
diff --git a/actionpack/lib/action_view/template/error.rb b/actionpack/lib/action_view/template/error.rb
deleted file mode 100644
index e00056781d..0000000000
--- a/actionpack/lib/action_view/template/error.rb
+++ /dev/null
@@ -1,130 +0,0 @@
-require "active_support/core_ext/enumerable"
-
-module ActionView
- # = Action View Errors
- class ActionViewError < StandardError #:nodoc:
- end
-
- class EncodingError < StandardError #:nodoc:
- end
-
- class MissingRequestError < StandardError #:nodoc:
- end
-
- class WrongEncodingError < EncodingError #:nodoc:
- def initialize(string, encoding)
- @string, @encoding = string, encoding
- end
-
- def message
- @string.force_encoding("BINARY")
- "Your template was not saved as valid #{@encoding}. Please " \
- "either specify #{@encoding} as the encoding for your template " \
- "in your text editor, or mark the template with its " \
- "encoding by inserting the following as the first line " \
- "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \
- "The source of your template was:\n\n#{@string}"
- end
- end
-
- class MissingTemplate < ActionViewError #:nodoc:
- attr_reader :path
-
- def initialize(paths, path, prefixes, partial, details, *)
- @path = path
- prefixes = Array(prefixes)
- template_type = if partial
- "partial"
- elsif path =~ /layouts/i
- 'layout'
- else
- 'template'
- end
-
- searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
-
- out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
- out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
- super out
- end
- end
-
- class Template
- # The Template::Error exception is raised when the compilation or rendering of the template
- # fails. This exception then gathers a bunch of intimate details and uses it to report a
- # precise exception message.
- class Error < ActionViewError #:nodoc:
- SOURCE_CODE_RADIUS = 3
-
- attr_reader :original_exception, :backtrace
-
- def initialize(template, original_exception)
- super(original_exception.message)
- @template, @original_exception = template, original_exception
- @sub_templates = nil
- @backtrace = original_exception.backtrace
- end
-
- def file_name
- @template.identifier
- end
-
- def sub_template_message
- if @sub_templates
- "Trace of template inclusion: " +
- @sub_templates.collect { |template| template.inspect }.join(", ")
- else
- ""
- end
- end
-
- def source_extract(indentation = 0)
- return unless num = line_number
- num = num.to_i
-
- source_code = @template.source.split("\n")
-
- start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
- end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
-
- indent = end_on_line.to_s.size + indentation
- line_counter = start_on_line
- return unless source_code = source_code[start_on_line..end_on_line]
-
- source_code.sum do |line|
- line_counter += 1
- "%#{indent}s: %s\n" % [line_counter, line]
- end
- end
-
- def sub_template_of(template_path)
- @sub_templates ||= []
- @sub_templates << template_path
- end
-
- def line_number
- @line_number ||=
- if file_name
- regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
- $1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
- end
- end
-
- def annoted_source_code
- source_extract(4)
- end
-
- private
-
- def source_location
- if line_number
- "on line ##{line_number} of "
- else
- 'in '
- end + file_name
- end
- end
- end
-
- TemplateError = Template::Error
-end
diff --git a/actionpack/lib/action_view/template/handlers/erb.rb b/actionpack/lib/action_view/template/handlers/erb.rb
deleted file mode 100644
index aa8eac7846..0000000000
--- a/actionpack/lib/action_view/template/handlers/erb.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-require 'action_dispatch/http/mime_type'
-require 'erubis'
-
-module ActionView
- class Template
- module Handlers
- class Erubis < ::Erubis::Eruby
- def add_preamble(src)
- src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
- end
-
- def add_text(src, text)
- return if text.empty?
- src << "@output_buffer.safe_concat('" << escape_text(text) << "');"
- end
-
- BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
-
- def add_expr_literal(src, code)
- if code =~ BLOCK_EXPR
- src << '@output_buffer.append= ' << code
- else
- src << '@output_buffer.append= (' << code << ');'
- end
- end
-
- def add_expr_escaped(src, code)
- if code =~ BLOCK_EXPR
- src << "@output_buffer.safe_append= " << code
- else
- src << "@output_buffer.safe_concat((" << code << ").to_s);"
- end
- end
-
- def add_postamble(src)
- src << '@output_buffer.to_s'
- end
- end
-
- class ERB
- # Specify trim mode for the ERB compiler. Defaults to '-'.
- # See ERB documentation for suitable values.
- class_attribute :erb_trim_mode
- self.erb_trim_mode = '-'
-
- # Default implementation used.
- class_attribute :erb_implementation
- self.erb_implementation = Erubis
-
- ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
-
- def self.call(template)
- new.call(template)
- end
-
- def supports_streaming?
- true
- end
-
- def handles_encoding?
- true
- end
-
- def call(template)
- # First, convert to BINARY, so in case the encoding is
- # wrong, we can still find an encoding tag
- # (<%# encoding %>) inside the String using a regular
- # expression
- template_source = template.source.dup.force_encoding("BINARY")
-
- erb = template_source.gsub(ENCODING_TAG, '')
- encoding = $2
-
- erb.force_encoding valid_encoding(template.source.dup, encoding)
-
- # Always make sure we return a String in the default_internal
- erb.encode!
-
- self.class.erb_implementation.new(
- erb,
- :trim => (self.class.erb_trim_mode == "-")
- ).src
- end
-
- private
-
- def valid_encoding(string, encoding)
- # If a magic encoding comment was found, tag the
- # String with this encoding. This is for a case
- # where the original String was assumed to be,
- # for instance, UTF-8, but a magic comment
- # proved otherwise
- string.force_encoding(encoding) if encoding
-
- # If the String is valid, return the encoding we found
- return string.encoding if string.valid_encoding?
-
- # Otherwise, raise an exception
- raise WrongEncodingError.new(string, string.encoding)
- end
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/template/resolver.rb b/actionpack/lib/action_view/template/resolver.rb
deleted file mode 100644
index 25c6fd4aa8..0000000000
--- a/actionpack/lib/action_view/template/resolver.rb
+++ /dev/null
@@ -1,321 +0,0 @@
-require "pathname"
-require "active_support/core_ext/class"
-require "active_support/core_ext/class/attribute_accessors"
-require "action_view/template"
-require "thread"
-require "mutex_m"
-
-module ActionView
- # = Action View Resolver
- class Resolver
- # Keeps all information about view path and builds virtual path.
- class Path
- attr_reader :name, :prefix, :partial, :virtual
- alias_method :partial?, :partial
-
- def self.build(name, prefix, partial)
- virtual = ""
- virtual << "#{prefix}/" unless prefix.empty?
- virtual << (partial ? "_#{name}" : name)
- new name, prefix, partial, virtual
- end
-
- def initialize(name, prefix, partial, virtual)
- @name = name
- @prefix = prefix
- @partial = partial
- @virtual = virtual
- end
-
- def to_str
- @virtual
- end
- alias :to_s :to_str
- end
-
- # Threadsafe template cache
- class Cache #:nodoc:
- class CacheEntry
- include Mutex_m
-
- attr_accessor :templates
- end
-
- def initialize
- @data = Hash.new { |h1,k1| h1[k1] = Hash.new { |h2,k2|
- h2[k2] = Hash.new { |h3,k3| h3[k3] = Hash.new { |h4,k4| h4[k4] = {} } } } }
- @mutex = Mutex.new
- end
-
- # Cache the templates returned by the block
- def cache(key, name, prefix, partial, locals)
- cache_entry = nil
-
- # first obtain a lock on the main data structure to create the cache entry
- @mutex.synchronize do
- cache_entry = @data[key][name][prefix][partial][locals] ||= CacheEntry.new
- end
-
- # then to avoid a long lasting global lock, obtain a more granular lock
- # on the CacheEntry itself
- cache_entry.synchronize do
- if Resolver.caching?
- cache_entry.templates ||= yield
- else
- fresh_templates = yield
-
- if templates_have_changed?(cache_entry.templates, fresh_templates)
- cache_entry.templates = fresh_templates
- else
- cache_entry.templates ||= []
- end
- end
- end
- end
-
- def clear
- @mutex.synchronize do
- @data.clear
- end
- end
-
- private
-
- def templates_have_changed?(cached_templates, fresh_templates)
- # if either the old or new template list is empty, we don't need to (and can't)
- # compare modification times, and instead just check whether the lists are different
- if cached_templates.blank? || fresh_templates.blank?
- return fresh_templates.blank? != cached_templates.blank?
- end
-
- cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
-
- # if a template has changed, it will be now be newer than all the cached templates
- fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
- end
- end
-
- cattr_accessor :caching
- self.caching = true
-
- class << self
- alias :caching? :caching
- end
-
- def initialize
- @cache = Cache.new
- end
-
- def clear_cache
- @cache.clear
- end
-
- # Normalizes the arguments and passes it on to find_template.
- def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
- cached(key, [name, prefix, partial], details, locals) do
- find_templates(name, prefix, partial, details)
- end
- end
-
- private
-
- delegate :caching?, :to => "self.class"
-
- # This is what child classes implement. No defaults are needed
- # because Resolver guarantees that the arguments are present and
- # normalized.
- def find_templates(name, prefix, partial, details)
- raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
- end
-
- # Helpers that builds a path. Useful for building virtual paths.
- def build_path(name, prefix, partial)
- Path.build(name, prefix, partial)
- end
-
- # Handles templates caching. If a key is given and caching is on
- # always check the cache before hitting the resolver. Otherwise,
- # it always hits the resolver but if the key is present, check if the
- # resolver is fresher before returning it.
- def cached(key, path_info, details, locals) #:nodoc:
- name, prefix, partial = path_info
- locals = locals.map { |x| x.to_s }.sort!
-
- if key
- @cache.cache(key, name, prefix, partial, locals) do
- decorate(yield, path_info, details, locals)
- end
- else
- decorate(yield, path_info, details, locals)
- end
- end
-
- # Ensures all the resolver information is set in the template.
- def decorate(templates, path_info, details, locals) #:nodoc:
- cached = nil
- templates.each do |t|
- t.locals = locals
- t.formats = details[:formats] || [:html] if t.formats.empty?
- t.virtual_path ||= (cached ||= build_path(*path_info))
- end
- end
- end
-
- # An abstract class that implements a Resolver with path semantics.
- class PathResolver < Resolver #:nodoc:
- EXTENSIONS = [:locale, :formats, :handlers]
- DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}"
-
- def initialize(pattern=nil)
- @pattern = pattern || DEFAULT_PATTERN
- super()
- end
-
- private
-
- def find_templates(name, prefix, partial, details)
- path = Path.build(name, prefix, partial)
- query(path, details, details[:formats])
- end
-
- 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.map { |template|
- handler, format = extract_handler_and_format(template, formats)
- contents = File.binread template
-
- Template.new(contents, File.expand_path(template), handler,
- :virtual_path => path.virtual,
- :format => format,
- :updated_at => mtime(template))
- }
- end
-
- # Helper for building query glob string based on resolver's pattern.
- def build_query(path, details)
- query = @pattern.dup
-
- prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
- query.gsub!(/\:prefix(\/)?/, prefix)
-
- partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
- query.gsub!(/\:action/, partial)
-
- details.each do |ext, variants|
- query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
- end
-
- File.expand_path(query, @path)
- end
-
- def escape_entry(entry)
- entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
- end
-
- # Returns the file mtime from the filesystem.
- def mtime(p)
- File.mtime(p)
- end
-
- # Extract handler and formats from path. If a format cannot be a 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)
- pieces = File.basename(path).split(".")
- pieces.shift
- extension = pieces.pop
- ActiveSupport::Deprecation.warn "The file #{path} did not specify a template handler. The default is currently ERB, but will change to RAW in the future." unless extension
- handler = Template.handler_for_extension(extension)
- format = pieces.last && Template::Types[pieces.last]
- [handler, format]
- end
- end
-
- # A resolver that loads files from the filesystem. It allows setting your own
- # resolving pattern. Such pattern can be a glob string supported by some variables.
- #
- # ==== Examples
- #
- # Default pattern, loads views the same way as previous versions of rails, eg. when you're
- # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}")
- #
- # This one allows you to keep files with different formats in seperated subdirectories,
- # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
- # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
- #
- # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}")
- #
- # If you don't specify a pattern then the default will be used.
- #
- # In order to use any of the customized resolvers above in a Rails application, you just need
- # to configure ActionController::Base.view_paths in an initializer, for example:
- #
- # ActionController::Base.view_paths = FileSystemResolver.new(
- # Rails.root.join("app/views"),
- # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}"
- # )
- #
- # ==== Pattern format and variables
- #
- # Pattern has to be a valid glob string, and it allows you to use the
- # following variables:
- #
- # * <tt>:prefix</tt> - usually the controller path
- # * <tt>:action</tt> - name of the action
- # * <tt>:locale</tt> - possible locale versions
- # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
- # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
- #
- class FileSystemResolver < PathResolver
- def initialize(path, pattern=nil)
- raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
- super(pattern)
- @path = File.expand_path(path)
- end
-
- def to_s
- @path.to_s
- end
- alias :to_path :to_s
-
- def eql?(resolver)
- self.class.equal?(resolver.class) && to_path == resolver.to_path
- end
- alias :== :eql?
- end
-
- # An Optimized resolver for Rails' most common case.
- class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
- def build_query(path, details)
- exts = EXTENSIONS.map { |ext| details[ext] }
- query = escape_entry(File.join(@path, path))
-
- query + exts.map { |ext|
- "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}"
- }.join
- end
- end
-
- # The same as FileSystemResolver but does not allow templates to store
- # a virtual path since it is invalid for such resolvers.
- class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
- def self.instances
- [new(""), new("/")]
- end
-
- def decorate(*)
- super.each { |t| t.virtual_path = nil }
- end
- end
-end
diff --git a/actionpack/lib/action_view/template/types.rb b/actionpack/lib/action_view/template/types.rb
deleted file mode 100644
index 7611c9e708..0000000000
--- a/actionpack/lib/action_view/template/types.rb
+++ /dev/null
@@ -1,58 +0,0 @@
-require 'set'
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/core_ext/object/blank'
-
-module ActionView
- class Template
- class Types
- class Type
- cattr_accessor :types
- self.types = Set.new
-
- def self.register(*t)
- types.merge(t.map { |type| type.to_s })
- end
-
- register :html, :text, :js, :css, :xml, :json
-
- def self.[](type)
- return type if type.is_a?(self)
-
- if type.is_a?(Symbol) || types.member?(type.to_s)
- new(type)
- end
- end
-
- attr_reader :symbol
-
- def initialize(symbol)
- @symbol = symbol.to_sym
- end
-
- delegate :to_s, :to_sym, :to => :symbol
- alias to_str to_s
-
- def ref
- to_sym || to_s
- end
-
- def ==(type)
- return false if type.blank?
- symbol.to_sym == type.to_sym
- end
- end
-
- cattr_accessor :type_klass
-
- def self.delegate_to(klass)
- self.type_klass = klass
- end
-
- delegate_to Type
-
- def self.[](type)
- type_klass[type]
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/test_case.rb b/actionpack/lib/action_view/test_case.rb
deleted file mode 100644
index a548b44780..0000000000
--- a/actionpack/lib/action_view/test_case.rb
+++ /dev/null
@@ -1,268 +0,0 @@
-require 'active_support/core_ext/module/remove_method'
-require 'action_controller'
-require 'action_controller/test_case'
-require 'action_view'
-
-module ActionView
- # = Action View Test Case
- class TestCase < ActiveSupport::TestCase
- class TestController < ActionController::Base
- include ActionDispatch::TestProcess
-
- attr_accessor :request, :response, :params
-
- class << self
- attr_writer :controller_path
- end
-
- def controller_path=(path)
- self.class.controller_path=(path)
- end
-
- def initialize
- super
- self.class.controller_path = ""
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
-
- @request.env.delete('PATH_INFO')
- @params = {}
- end
- end
-
- # Use AV::TestCase for the base class for helpers and views
- register_spec_type(/(Helper|View)( ?Test)?\z/i, self)
-
- module Behavior
- extend ActiveSupport::Concern
-
- include ActionDispatch::Assertions, ActionDispatch::TestProcess
- include ActionController::TemplateAssertions
- include ActionView::Context
-
- include ActionDispatch::Routing::PolymorphicRoutes
-
- include AbstractController::Helpers
- include ActionView::Helpers
- include ActionView::RecordIdentifier
- include ActionView::RoutingUrlFor
-
- include ActiveSupport::Testing::ConstantLookup
-
- delegate :lookup_context, :to => :controller
- attr_accessor :controller, :output_buffer, :rendered
-
- module ClassMethods
- def tests(helper_class)
- case helper_class
- when String, Symbol
- self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize
- when Module
- self.helper_class = helper_class
- end
- end
-
- def determine_default_helper_class(name)
- determine_constant_from_test_name(name) do |constant|
- Module === constant && !(Class === constant)
- end
- end
-
- def helper_method(*methods)
- # Almost a duplicate from ActionController::Helpers
- methods.flatten.each do |method|
- _helpers.module_eval <<-end_eval
- def #{method}(*args, &block) # def current_user(*args, &block)
- _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block)
- end # end
- end_eval
- end
- end
-
- attr_writer :helper_class
-
- def helper_class
- @helper_class ||= determine_default_helper_class(name)
- end
-
- def new(*)
- include_helper_modules!
- super
- end
-
- private
-
- def include_helper_modules!
- helper(helper_class) if helper_class
- include _helpers
- end
-
- end
-
- def setup_with_controller
- @controller = ActionView::TestCase::TestController.new
- @request = @controller.request
- @output_buffer = ActiveSupport::SafeBuffer.new
- @rendered = ''
-
- make_test_case_available_to_view!
- say_no_to_protect_against_forgery!
- end
-
- def config
- @controller.config if @controller.respond_to?(:config)
- end
-
- def render(options = {}, local_assigns = {}, &block)
- view.assign(view_assigns)
- @rendered << output = view.render(options, local_assigns, &block)
- output
- end
-
- def rendered_views
- @_rendered_views ||= RenderedViewsCollection.new
- end
-
- class RenderedViewsCollection
- def initialize
- @rendered_views ||= {}
- end
-
- def add(view, locals)
- @rendered_views[view] ||= []
- @rendered_views[view] << locals
- end
-
- def locals_for(view)
- @rendered_views[view]
- end
-
- def view_rendered?(view, expected_locals)
- locals_for(view).any? do |actual_locals|
- expected_locals.all? {|key, value| value == actual_locals[key] }
- end
- end
- end
-
- included do
- setup :setup_with_controller
- end
-
- private
-
- # Support the selector assertions
- #
- # Need to experiment if this priority is the best one: rendered => output_buffer
- def response_from_page
- HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root
- end
-
- def say_no_to_protect_against_forgery!
- _helpers.module_eval do
- remove_possible_method :protect_against_forgery?
- def protect_against_forgery?
- false
- end
- end
- end
-
- def make_test_case_available_to_view!
- test_case_instance = self
- _helpers.module_eval do
- unless private_method_defined?(:_test_case)
- define_method(:_test_case) { test_case_instance }
- private :_test_case
- end
- end
- end
-
- module Locals
- attr_accessor :rendered_views
-
- def render(options = {}, local_assigns = {})
- case options
- when Hash
- if block_given?
- rendered_views.add options[:layout], options[:locals]
- elsif options.key?(:partial)
- rendered_views.add options[:partial], options[:locals]
- end
- else
- rendered_views.add options, local_assigns
- end
-
- super
- end
- end
-
- # The instance of ActionView::Base that is used by +render+.
- def view
- @view ||= begin
- view = @controller.view_context
- view.singleton_class.send :include, _helpers
- view.extend(Locals)
- view.rendered_views = self.rendered_views
- view.output_buffer = self.output_buffer
- view
- end
- end
-
- alias_method :_view, :view
-
- INTERNAL_IVARS = [
- :@__name__,
- :@__io__,
- :@_assertion_wrapped,
- :@_assertions,
- :@_result,
- :@_routes,
- :@controller,
- :@_layouts,
- :@_rendered_views,
- :@method_name,
- :@output_buffer,
- :@_partials,
- :@passed,
- :@rendered,
- :@request,
- :@routes,
- :@tagged_logger,
- :@_templates,
- :@options,
- :@test_passed,
- :@view,
- :@view_context_class
- ]
-
- def _user_defined_ivars
- instance_variables - INTERNAL_IVARS
- end
-
- # Returns a Hash of instance variables and their values, as defined by
- # the user in the test case, which are then assigned to the view being
- # rendered. This is generally intended for internal use and extension
- # frameworks.
- def view_assigns
- Hash[_user_defined_ivars.map do |ivar|
- [ivar[1..-1].to_sym, instance_variable_get(ivar)]
- end]
- end
-
- def _routes
- @controller._routes if @controller.respond_to?(:_routes)
- end
-
- def method_missing(selector, *args)
- if @controller.respond_to?(:_routes) &&
- ( @controller._routes.named_routes.helpers.include?(selector) ||
- @controller._routes.mounted_helpers.method_defined?(selector) )
- @controller.__send__(selector, *args)
- else
- super
- end
- end
- end
-
- include Behavior
- end
-end
diff --git a/actionpack/lib/action_view/vendor/html-scanner.rb b/actionpack/lib/action_view/vendor/html-scanner.rb
deleted file mode 100644
index 879b31e60e..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-$LOAD_PATH << "#{File.dirname(__FILE__)}/html-scanner"
-
-module HTML
- extend ActiveSupport::Autoload
-
- eager_autoload do
- autoload :CDATA, 'html/node'
- autoload :Document, 'html/document'
- autoload :FullSanitizer, 'html/sanitizer'
- autoload :LinkSanitizer, 'html/sanitizer'
- autoload :Node, 'html/node'
- autoload :Sanitizer, 'html/sanitizer'
- autoload :Selector, 'html/selector'
- autoload :Tag, 'html/node'
- autoload :Text, 'html/node'
- autoload :Tokenizer, 'html/tokenizer'
- autoload :Version, 'html/version'
- autoload :WhiteListSanitizer, 'html/sanitizer'
- end
-end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/node.rb b/actionpack/lib/action_view/vendor/html-scanner/html/node.rb
deleted file mode 100644
index 4e1f016431..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/node.rb
+++ /dev/null
@@ -1,532 +0,0 @@
-require 'strscan'
-
-module HTML #:nodoc:
-
- class Conditions < Hash #:nodoc:
- def initialize(hash)
- super()
- hash = { :content => hash } unless Hash === hash
- hash = keys_to_symbols(hash)
- hash.each do |k,v|
- case k
- when :tag, :content then
- # keys are valid, and require no further processing
- when :attributes then
- hash[k] = keys_to_strings(v)
- when :parent, :child, :ancestor, :descendant, :sibling, :before,
- :after
- hash[k] = Conditions.new(v)
- when :children
- hash[k] = v = keys_to_symbols(v)
- v.each do |key,value|
- case key
- when :count, :greater_than, :less_than
- # keys are valid, and require no further processing
- when :only
- v[key] = Conditions.new(value)
- else
- raise "illegal key #{key.inspect} => #{value.inspect}"
- end
- end
- else
- raise "illegal key #{k.inspect} => #{v.inspect}"
- end
- end
- update hash
- end
-
- private
-
- def keys_to_strings(hash)
- Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
- end
-
- def keys_to_symbols(hash)
- Hash[hash.keys.map do |k|
- raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
- [k.to_sym, hash[k]]
- end]
- end
- end
-
- # The base class of all nodes, textual and otherwise, in an HTML document.
- class Node #:nodoc:
- # The array of children of this node. Not all nodes have children.
- attr_reader :children
-
- # The parent node of this node. All nodes have a parent, except for the
- # root node.
- attr_reader :parent
-
- # The line number of the input where this node was begun
- attr_reader :line
-
- # The byte position in the input where this node was begun
- attr_reader :position
-
- # Create a new node as a child of the given parent.
- def initialize(parent, line=0, pos=0)
- @parent = parent
- @children = []
- @line, @position = line, pos
- end
-
- # Return a textual representation of the node.
- def to_s
- @children.join()
- end
-
- # Return false (subclasses must override this to provide specific matching
- # behavior.) +conditions+ may be of any type.
- def match(conditions)
- false
- end
-
- # Search the children of this node for the first node for which #find
- # returns non +nil+. Returns the result of the #find call that succeeded.
- def find(conditions)
- conditions = validate_conditions(conditions)
- @children.each do |child|
- node = child.find(conditions)
- return node if node
- end
- nil
- end
-
- # Search for all nodes that match the given conditions, and return them
- # as an array.
- def find_all(conditions)
- conditions = validate_conditions(conditions)
-
- matches = []
- matches << self if match(conditions)
- @children.each do |child|
- matches.concat child.find_all(conditions)
- end
- matches
- end
-
- # Returns +false+. Subclasses may override this if they define a kind of
- # tag.
- def tag?
- false
- end
-
- def validate_conditions(conditions)
- Conditions === conditions ? conditions : Conditions.new(conditions)
- end
-
- def ==(node)
- return false unless self.class == node.class && children.size == node.children.size
-
- equivalent = true
-
- children.size.times do |i|
- equivalent &&= children[i] == node.children[i]
- end
-
- equivalent
- end
-
- class <<self
- def parse(parent, line, pos, content, strict=true)
- if content !~ /^<\S/
- Text.new(parent, line, pos, content)
- else
- scanner = StringScanner.new(content)
-
- unless scanner.skip(/</)
- if strict
- raise "expected <"
- else
- return Text.new(parent, line, pos, content)
- end
- end
-
- if scanner.skip(/!\[CDATA\[/)
- unless scanner.skip_until(/\]\]>/)
- if strict
- raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
- else
- scanner.skip_until(/\Z/)
- end
- end
-
- return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
- end
-
- closing = ( scanner.scan(/\//) ? :close : nil )
- return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
- name.downcase!
-
- unless closing
- scanner.skip(/\s*/)
- attributes = {}
- while attr = scanner.scan(/[-\w:]+/)
- value = true
- if scanner.scan(/\s*=\s*/)
- if delim = scanner.scan(/['"]/)
- value = ""
- while text = scanner.scan(/[^#{delim}\\]+|./)
- case text
- when "\\" then
- value << text
- break if scanner.eos?
- value << scanner.getch
- when delim
- break
- else value << text
- end
- end
- else
- value = scanner.scan(/[^\s>\/]+/)
- end
- end
- attributes[attr.downcase] = value
- scanner.skip(/\s*/)
- end
-
- closing = ( scanner.scan(/\//) ? :self : nil )
- end
-
- unless scanner.scan(/\s*>/)
- if strict
- raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
- else
- # throw away all text until we find what we're looking for
- scanner.skip_until(/>/) or scanner.terminate
- end
- end
-
- Tag.new(parent, line, pos, name, attributes, closing)
- end
- end
- end
- end
-
- # A node that represents text, rather than markup.
- class Text < Node #:nodoc:
-
- attr_reader :content
-
- # Creates a new text node as a child of the given parent, with the given
- # content.
- def initialize(parent, line, pos, content)
- super(parent, line, pos)
- @content = content
- end
-
- # Returns the content of this node.
- def to_s
- @content
- end
-
- # Returns +self+ if this node meets the given conditions. Text nodes support
- # conditions of the following kinds:
- #
- # * if +conditions+ is a string, it must be a substring of the node's
- # content
- # * if +conditions+ is a regular expression, it must match the node's
- # content
- # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
- # is either a string or a regexp, and which is interpreted as described
- # above.
- def find(conditions)
- match(conditions) && self
- end
-
- # Returns non-+nil+ if this node meets the given conditions, or +nil+
- # otherwise. See the discussion of #find for the valid conditions.
- def match(conditions)
- case conditions
- when String
- @content == conditions
- when Regexp
- @content =~ conditions
- when Hash
- conditions = validate_conditions(conditions)
-
- # Text nodes only have :content, :parent, :ancestor
- unless (conditions.keys - [:content, :parent, :ancestor]).empty?
- return false
- end
-
- match(conditions[:content])
- else
- nil
- end
- end
-
- def ==(node)
- return false unless super
- content == node.content
- end
- end
-
- # A CDATA node is simply a text node with a specialized way of displaying
- # itself.
- class CDATA < Text #:nodoc:
- def to_s
- "<![CDATA[#{super}]]>"
- end
- end
-
- # A Tag is any node that represents markup. It may be an opening tag, a
- # closing tag, or a self-closing tag. It has a name, and may have a hash of
- # attributes.
- class Tag < Node #:nodoc:
-
- # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
- attr_reader :closing
-
- # Either +nil+, or a hash of attributes for this node.
- attr_reader :attributes
-
- # The name of this tag.
- attr_reader :name
-
- # Create a new node as a child of the given parent, using the given content
- # to describe the node. It will be parsed and the node name, attributes and
- # closing status extracted.
- def initialize(parent, line, pos, name, attributes, closing)
- super(parent, line, pos)
- @name = name
- @attributes = attributes
- @closing = closing
- end
-
- # A convenience for obtaining an attribute of the node. Returns +nil+ if
- # the node has no attributes.
- def [](attr)
- @attributes ? @attributes[attr] : nil
- end
-
- # Returns non-+nil+ if this tag can contain child nodes.
- def childless?(xml = false)
- return false if xml && @closing.nil?
- !@closing.nil? ||
- @name =~ /^(img|br|hr|link|meta|area|base|basefont|
- col|frame|input|isindex|param)$/ox
- end
-
- # Returns a textual representation of the node
- def to_s
- if @closing == :close
- "</#{@name}>"
- else
- s = "<#{@name}"
- @attributes.each do |k,v|
- s << " #{k}"
- s << "=\"#{v}\"" if String === v
- end
- s << " /" if @closing == :self
- s << ">"
- @children.each { |child| s << child.to_s }
- s << "</#{@name}>" if @closing != :self && !@children.empty?
- s
- end
- end
-
- # If either the node or any of its children meet the given conditions, the
- # matching node is returned. Otherwise, +nil+ is returned. (See the
- # description of the valid conditions in the +match+ method.)
- def find(conditions)
- match(conditions) && self || super
- end
-
- # Returns +true+, indicating that this node represents an HTML tag.
- def tag?
- true
- end
-
- # Returns +true+ if the node meets any of the given conditions. The
- # +conditions+ parameter must be a hash of any of the following keys
- # (all are optional):
- #
- # * <tt>:tag</tt>: the node name must match the corresponding value
- # * <tt>:attributes</tt>: a hash. The node's values must match the
- # corresponding values in the hash.
- # * <tt>:parent</tt>: a hash. The node's parent must match the
- # corresponding hash.
- # * <tt>:child</tt>: a hash. At least one of the node's immediate children
- # must meet the criteria described by the hash.
- # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
- # meet the criteria described by the hash.
- # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
- # must meet the criteria described by the hash.
- # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
- # meet the criteria described by the hash.
- # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
- # the criteria described by the hash, and at least one sibling must match.
- # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
- # keys:
- # ** <tt>:count</tt>: either a number or a range which must equal (or
- # include) the number of children that match.
- # ** <tt>:less_than</tt>: the number of matching children must be less than
- # this number.
- # ** <tt>:greater_than</tt>: the number of matching children must be
- # greater than this number.
- # ** <tt>:only</tt>: another hash consisting of the keys to use
- # to match on the children, and only matching children will be
- # counted.
- #
- # Conditions are matched using the following algorithm:
- #
- # * if the condition is a string, it must be a substring of the value.
- # * if the condition is a regexp, it must match the value.
- # * if the condition is a number, the value must match number.to_s.
- # * if the condition is +true+, the value must not be +nil+.
- # * if the condition is +false+ or +nil+, the value must be +nil+.
- #
- # Usage:
- #
- # # test if the node is a "span" tag
- # node.match :tag => "span"
- #
- # # test if the node's parent is a "div"
- # node.match :parent => { :tag => "div" }
- #
- # # test if any of the node's ancestors are "table" tags
- # node.match :ancestor => { :tag => "table" }
- #
- # # test if any of the node's immediate children are "em" tags
- # node.match :child => { :tag => "em" }
- #
- # # test if any of the node's descendants are "strong" tags
- # node.match :descendant => { :tag => "strong" }
- #
- # # test if the node has between 2 and 4 span tags as immediate children
- # node.match :children => { :count => 2..4, :only => { :tag => "span" } }
- #
- # # get funky: test to see if the node is a "div", has a "ul" ancestor
- # # and an "li" parent (with "class" = "enum"), and whether or not it has
- # # a "span" descendant that contains # text matching /hello world/:
- # node.match :tag => "div",
- # :ancestor => { :tag => "ul" },
- # :parent => { :tag => "li",
- # :attributes => { :class => "enum" } },
- # :descendant => { :tag => "span",
- # :child => /hello world/ }
- def match(conditions)
- conditions = validate_conditions(conditions)
- # check content of child nodes
- if conditions[:content]
- if children.empty?
- return false unless match_condition("", conditions[:content])
- else
- return false unless children.find { |child| child.match(conditions[:content]) }
- end
- end
-
- # test the name
- return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
-
- # test attributes
- (conditions[:attributes] || {}).each do |key, value|
- return false unless match_condition(self[key], value)
- end
-
- # test parent
- return false unless parent.match(conditions[:parent]) if conditions[:parent]
-
- # test children
- return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
-
- # test ancestors
- if conditions[:ancestor]
- return false unless catch :found do
- p = self
- throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
- end
- end
-
- # test descendants
- if conditions[:descendant]
- return false unless children.find do |child|
- # test the child
- child.match(conditions[:descendant]) ||
- # test the child's descendants
- child.match(:descendant => conditions[:descendant])
- end
- end
-
- # count children
- if opts = conditions[:children]
- matches = children.select do |c|
- (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
- end
-
- matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
- opts.each do |key, value|
- next if key == :only
- case key
- when :count
- if Integer === value
- return false if matches.length != value
- else
- return false unless value.include?(matches.length)
- end
- when :less_than
- return false unless matches.length < value
- when :greater_than
- return false unless matches.length > value
- else raise "unknown count condition #{key}"
- end
- end
- end
-
- # test siblings
- if conditions[:sibling] || conditions[:before] || conditions[:after]
- siblings = parent ? parent.children : []
- self_index = siblings.index(self)
-
- if conditions[:sibling]
- return false unless siblings.detect do |s|
- s != self && s.match(conditions[:sibling])
- end
- end
-
- if conditions[:before]
- return false unless siblings[self_index+1..-1].detect do |s|
- s != self && s.match(conditions[:before])
- end
- end
-
- if conditions[:after]
- return false unless siblings[0,self_index].detect do |s|
- s != self && s.match(conditions[:after])
- end
- end
- end
-
- true
- end
-
- def ==(node)
- return false unless super
- return false unless closing == node.closing && self.name == node.name
- attributes == node.attributes
- end
-
- private
- # Match the given value to the given condition.
- def match_condition(value, condition)
- case condition
- when String
- value && value == condition
- when Regexp
- value && value.match(condition)
- when Numeric
- value == condition.to_s
- when true
- !value.nil?
- when false, nil
- value.nil?
- else
- false
- end
- end
- end
-end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb
deleted file mode 100644
index 6b4ececda2..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/sanitizer.rb
+++ /dev/null
@@ -1,188 +0,0 @@
-require 'set'
-require 'cgi'
-require 'active_support/core_ext/class/attribute_accessors'
-
-module HTML
- class Sanitizer
- def sanitize(text, options = {})
- validate_options(options)
- return text unless sanitizeable?(text)
- tokenize(text, options).join
- end
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !text.index("<"))
- end
-
- protected
- def tokenize(text, options)
- tokenizer = HTML::Tokenizer.new(text)
- result = []
- while token = tokenizer.next
- node = Node.parse(nil, 0, 0, token, false)
- process_node node, result, options
- end
- result
- end
-
- def process_node(node, result, options)
- result << node.to_s
- end
-
- def validate_options(options)
- if options[:tags] && !options[:tags].is_a?(Enumerable)
- raise ArgumentError, "You should pass :tags as an Enumerable"
- end
-
- if options[:attributes] && !options[:attributes].is_a?(Enumerable)
- raise ArgumentError, "You should pass :attributes as an Enumerable"
- end
- end
- end
-
- class FullSanitizer < Sanitizer
- def sanitize(text, options = {})
- result = super
- # strip any comments, and if they have a newline at the end (ie. line with
- # only a comment) strip that too
- result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m)
- # Recurse - handle all dirty nested tags
- result == text ? result : sanitize(result, options)
- end
-
- def process_node(node, result, options)
- result << node.to_s if node.class == HTML::Text
- end
- end
-
- class LinkSanitizer < FullSanitizer
- cattr_accessor :included_tags, :instance_writer => false
- self.included_tags = Set.new(%w(a href))
-
- def sanitizeable?(text)
- !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
- end
-
- protected
- def process_node(node, result, options)
- result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
- end
- end
-
- class WhiteListSanitizer < Sanitizer
- [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
- :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
- class_attribute attr, :instance_writer => false
- end
-
- # A regular expression of the valid characters used to separate protocols like
- # the ':' in 'http://foo.com'
- self.protocol_separator = /:|(&#0*58)|(&#x70)|(%|&#37;)3A/
-
- # Specifies a Set of HTML attributes that can have URIs.
- self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
-
- # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
- # to just escaping harmless tags like &lt;font&gt;
- self.bad_tags = Set.new(%w(script))
-
- # Specifies the default Set of tags that the #sanitize helper will allow unscathed.
- self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
- sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
- acronym a img blockquote del ins))
-
- # Specifies the default Set of html attributes that the #sanitize helper will leave
- # in the allowed tag.
- self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
- feed svn urn aim rsync tag ssh sftp rtsp afs))
-
- # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
- self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
- border-color border-left-color border-right-color border-top-color clear color cursor direction display
- elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
- overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
- speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
- width))
-
- # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
- self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
- collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
- nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
-
- # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
- self.shorthand_css_properties = Set.new(%w(background border margin padding))
-
- # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
- def sanitize_css(style)
- # disallow urls
- style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
-
- # gauntlet
- if style !~ /^([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*$/ ||
- style !~ /^(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*$/
- return ''
- end
-
- clean = []
- style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
- if allowed_css_properties.include?(prop.downcase)
- clean << prop + ': ' + val + ';'
- elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
- unless val.split().any? do |keyword|
- !allowed_css_keywords.include?(keyword) &&
- keyword !~ /^(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)$/
- end
- clean << prop + ': ' + val + ';'
- end
- end
- end
- clean.join(' ')
- end
-
- protected
- def tokenize(text, options)
- options[:parent] = []
- options[:attributes] ||= allowed_attributes
- options[:tags] ||= allowed_tags
- super
- end
-
- def process_node(node, result, options)
- result << case node
- when HTML::Tag
- if node.closing == :close
- options[:parent].shift
- else
- options[:parent].unshift node.name
- end
-
- process_attributes_for node, options
-
- options[:tags].include?(node.name) ? node : nil
- else
- bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
- end
- end
-
- def process_attributes_for(node, options)
- return unless node.attributes
- node.attributes.keys.each do |attr_name|
- value = node.attributes[attr_name].to_s
-
- if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
- node.attributes.delete(attr_name)
- else
- node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
- end
- end
- end
-
- def contains_bad_protocols?(attr_name, value)
- uri_attributes.include?(attr_name) &&
- (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(%|&#37;)3A/ && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
- end
- end
-end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb b/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb
deleted file mode 100644
index 60b6783b19..0000000000
--- a/actionpack/lib/action_view/vendor/html-scanner/html/selector.rb
+++ /dev/null
@@ -1,830 +0,0 @@
-#--
-# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
-# Under MIT and/or CC By license.
-#++
-
-module HTML
-
- # Selects HTML elements using CSS 2 selectors.
- #
- # The +Selector+ class uses CSS selector expressions to match and select
- # HTML elements.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # creates a new selector that matches any +form+ element with the class
- # +login+ and an attribute +action+ with the value <tt>/login</tt>.
- #
- # === Matching Elements
- #
- # Use the #match method to determine if an element matches the selector.
- #
- # For simple selectors, the method returns an array with that element,
- # or +nil+ if the element does not match. For complex selectors (see below)
- # the method returns an array with all matched elements, of +nil+ if no
- # match found.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- #
- # === Selecting Elements
- #
- # Use the #select method to select all matching elements starting with
- # one element and going through all children in depth-first order.
- #
- # This method returns an array of all matching elements, an empty array
- # if no match is found
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- #
- # === Expressions
- #
- # Selectors can match elements using any of the following criteria:
- # * <tt>name</tt> -- Match an element based on its name (tag name).
- # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
- # to match any element.
- # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
- # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
- # * <tt>.class</tt> -- Match an element based on its class name, all
- # class names if more than one specified.
- # * <tt>[attr]</tt> -- Match an element that has the specified attribute.
- # * <tt>[attr=value]</tt> -- Match an element that has the specified
- # attribute and value. (More operators are supported see below)
- # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
- # such as <tt>:nth-child</tt> and <tt>:empty</tt>.
- # * <tt>:not(expr)</tt> -- Match an element that does not match the
- # negation expression.
- #
- # When using a combination of the above, the element name comes first
- # followed by identifier, class names, attributes, pseudo classes and
- # negation in any order. Do not separate these parts with spaces!
- # Space separation is used for descendant selectors.
- #
- # For example:
- # selector = HTML::Selector.new "form.login[action=/login]"
- # The matched element must be of type +form+ and have the class +login+.
- # It may have other classes, but the class +login+ is required to match.
- # It must also have an attribute called +action+ with the value
- # <tt>/login</tt>.
- #
- # This selector will match the following element:
- # <form class="login form" method="post" action="/login">
- # but will not match the element:
- # <form method="post" action="/logout">
- #
- # === Attribute Values
- #
- # Several operators are supported for matching attributes:
- # * <tt>name</tt> -- The element must have an attribute with that name.
- # * <tt>name=value</tt> -- The element must have an attribute with that
- # name and value.
- # * <tt>name^=value</tt> -- The attribute value must start with the
- # specified value.
- # * <tt>name$=value</tt> -- The attribute value must end with the
- # specified value.
- # * <tt>name*=value</tt> -- The attribute value must contain the
- # specified value.
- # * <tt>name~=word</tt> -- The attribute value must contain the specified
- # word (space separated).
- # * <tt>name|=word</tt> -- The attribute value must start with specified
- # word.
- #
- # For example, the following two selectors match the same element:
- # #my_id
- # [id=my_id]
- # and so do the following two selectors:
- # .my_class
- # [class~=my_class]
- #
- # === Alternatives, siblings, children
- #
- # Complex selectors use a combination of expressions to match elements:
- # * <tt>expr1 expr2</tt> -- Match any element against the second expression
- # if it has some parent element that matches the first expression.
- # * <tt>expr1 > expr2</tt> -- Match any element against the second expression
- # if it is the child of an element that matches the first expression.
- # * <tt>expr1 + expr2</tt> -- Match any element against the second expression
- # if it immediately follows an element that matches the first expression.
- # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
- # that comes after an element that matches the first expression.
- # * <tt>expr1, expr2</tt> -- Match any element against the first expression,
- # or against the second expression.
- #
- # Since children and sibling selectors may match more than one element given
- # the first element, the #match method may return more than one match.
- #
- # === Pseudo classes
- #
- # Pseudo classes were introduced in CSS 3. They are most often used to select
- # elements in a given position:
- # * <tt>:root</tt> -- Match the element only if it is the root element
- # (no parent element).
- # * <tt>:empty</tt> -- Match the element only if it has no child elements,
- # and no text content.
- # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt>
- # as its text content (ignoring leading and trailing whitespace).
- # * <tt>:only-child</tt> -- Match the element if it is the only child (element)
- # of its parent element.
- # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
- # of its parent element and its type.
- # * <tt>:first-child</tt> -- Match the element if it is the first child (element)
- # of its parent element.
- # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
- # of its parent element of its type.
- # * <tt>:last-child</tt> -- Match the element if it is the last child (element)
- # of its parent element.
- # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
- # of its parent element of its type.
- # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
- # of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
- # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
- # in each group of <tt>a</tt> child elements of its parent element.
- # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
- # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
- # elements of its parent element.
- # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
- # Same as <tt>:nth-child(2n+1)</tt>.
- # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
- # fourth). Same as <tt>:nth-child(2n+2)</tt>.
- # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
- # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
- # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
- # only elements of its type.
- # * <tt>:not(selector)</tt> -- Match the element only if the element does not
- # match the simple selector.
- #
- # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite
- # tricky and the CSS specification doesn't do a much better job explaining it.
- # But after reading the examples and trying a few combinations, it's easy to
- # figure out.
- #
- # For example:
- # table tr:nth-child(odd)
- # Selects every second row in the table starting with the first one.
- #
- # div p:nth-child(4)
- # Selects the fourth paragraph in the +div+, but not if the +div+ contains
- # other elements, since those are also counted.
- #
- # div p:nth-of-type(4)
- # Selects the fourth paragraph in the +div+, counting only paragraphs, and
- # ignoring all other elements.
- #
- # div p:nth-of-type(-n+4)
- # Selects the first four paragraphs, ignoring all others.
- #
- # And you can always select an element that matches one set of rules but
- # not another using <tt>:not</tt>. For example:
- # p:not(.post)
- # Matches all paragraphs that do not have the class <tt>.post</tt>.
- #
- # === Substitution Values
- #
- # You can use substitution with identifiers, class names and element values.
- # A substitution takes the form of a question mark (<tt>?</tt>) and uses the
- # next value in the argument list following the CSS expression.
- #
- # The substitution value may be a string or a regular expression. All other
- # values are converted to strings.
- #
- # For example:
- # selector = HTML::Selector.new "#?", /^\d+$/
- # matches any element whose identifier consists of one or more digits.
- #
- # See http://www.w3.org/TR/css3-selectors/
- class Selector
-
-
- # An invalid selector.
- class InvalidSelectorError < StandardError #:nodoc:
- end
-
-
- class << self
-
- # :call-seq:
- # Selector.for_class(cls) => selector
- #
- # Creates a new selector for the given class name.
- def for_class(cls)
- self.new([".?", cls])
- end
-
-
- # :call-seq:
- # Selector.for_id(id) => selector
- #
- # Creates a new selector for the given id.
- def for_id(id)
- self.new(["#?", id])
- end
-
- end
-
-
- # :call-seq:
- # Selector.new(string, [values ...]) => selector
- #
- # Creates a new selector from a CSS 2 selector expression.
- #
- # The first argument is the selector expression. All other arguments
- # are used for value substitution.
- #
- # Throws InvalidSelectorError is the selector expression is invalid.
- def initialize(selector, *values)
- raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
- @source = ""
- values = values[0] if values.size == 1 && values[0].is_a?(Array)
-
- # We need a copy to determine if we failed to parse, and also
- # preserve the original pass by-ref statement.
- statement = selector.strip.dup
-
- # Create a simple selector, along with negation.
- simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
-
- @alternates = []
- @depends = nil
-
- # Alternative selector.
- if statement.sub!(/^\s*,\s*/, "")
- second = Selector.new(statement, values)
- @alternates << second
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- @source << " , " << second.to_s
- # Sibling selector: create a dependency into second selector that will
- # match element immediately following this one.
- elsif statement.sub!(/^\s*\+\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- if element = next_element(element)
- second.match(element, first)
- end
- end
- @source << " + " << second.to_s
- # Adjacent selector: create a dependency into second selector that will
- # match all elements following this one.
- elsif statement.sub!(/^\s*~\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- while element = next_element(element)
- if subset = second.match(element, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " ~ " << second.to_s
- # Child selector: create a dependency into second selector that will
- # match a child element of this one.
- elsif statement.sub!(/^\s*>\s*/, "")
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- element.children.each do |child|
- if child.tag? && subset = second.match(child, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " > " << second.to_s
- # Descendant selector: create a dependency into second selector that
- # will match all descendant elements of this one. Note,
- elsif statement =~ /^\s+\S+/ && statement != selector
- second = next_selector(statement, values)
- @depends = lambda do |element, first|
- matches = []
- stack = element.children.reverse
- while node = stack.pop
- next unless node.tag?
- if subset = second.match(node, first)
- if first && !subset.empty?
- matches << subset.first
- break
- else
- matches.concat subset
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches.empty? ? nil : matches
- end
- @source << " " << second.to_s
- else
- # The last selector is where we check that we parsed
- # all the parts.
- unless statement.empty? || statement.strip.empty?
- raise ArgumentError, "Invalid selector: #{statement}"
- end
- end
- end
-
-
- # :call-seq:
- # match(element, first?) => array or nil
- #
- # Matches an element against the selector.
- #
- # For a simple selector this method returns an array with the
- # element if the element matches, nil otherwise.
- #
- # For a complex selector (sibling and descendant) this method
- # returns an array with all matching elements, nil if no match is
- # found.
- #
- # Use +first_only=true+ if you are only interested in the first element.
- #
- # For example:
- # if selector.match(element)
- # puts "Element is a login form"
- # end
- def match(element, first_only = false)
- # Match element if no element name or element name same as element name
- if matched = (!@tag_name || @tag_name == element.name)
- # No match if one of the attribute matches failed
- for attr in @attributes
- if element.attributes[attr[0]] !~ attr[1]
- matched = false
- break
- end
- end
- end
-
- # Pseudo class matches (nth-child, empty, etc).
- if matched
- for pseudo in @pseudo
- unless pseudo.call(element)
- matched = false
- break
- end
- end
- end
-
- # Negation. Same rules as above, but we fail if a match is made.
- if matched && @negation
- for negation in @negation
- if negation[:tag_name] == element.name
- matched = false
- else
- for attr in negation[:attributes]
- if element.attributes[attr[0]] =~ attr[1]
- matched = false
- break
- end
- end
- end
- if matched
- for pseudo in negation[:pseudo]
- if pseudo.call(element)
- matched = false
- break
- end
- end
- end
- break unless matched
- end
- end
-
- # If element matched but depends on another element (child,
- # sibling, etc), apply the dependent matches instead.
- if matched && @depends
- matches = @depends.call(element, first_only)
- else
- matches = matched ? [element] : nil
- end
-
- # If this selector is part of the group, try all the alternative
- # selectors (unless first_only).
- if !first_only || !matches
- @alternates.each do |alternate|
- break if matches && first_only
- if subset = alternate.match(element, first_only)
- if matches
- matches.concat subset
- else
- matches = subset
- end
- end
- end
- end
-
- matches
- end
-
-
- # :call-seq:
- # select(root) => array
- #
- # Selects and returns an array with all matching elements, beginning
- # with one node and traversing through all children depth-first.
- # Returns an empty array if no match is found.
- #
- # The root node may be any element in the document, or the document
- # itself.
- #
- # For example:
- # selector = HTML::Selector.new "input[type=text]"
- # matches = selector.select(element)
- # matches.each do |match|
- # puts "Found text field with name #{match.attributes['name']}"
- # end
- def select(root)
- matches = []
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, false)
- subset.each do |match|
- matches << match unless matches.any? { |item| item.equal?(match) }
- end
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- matches
- end
-
-
- # Similar to #select but returns the first matching element. Returns +nil+
- # if no element matches the selector.
- def select_first(root)
- stack = [root]
- while node = stack.pop
- if node.tag? && subset = match(node, true)
- return subset.first if !subset.empty?
- elsif children = node.children
- stack.concat children.reverse
- end
- end
- nil
- end
-
-
- def to_s #:nodoc:
- @source
- end
-
-
- # Return the next element after this one. Skips sibling text nodes.
- #
- # With the +name+ argument, returns the next element with that name,
- # skipping other sibling elements.
- def next_element(element, name = nil)
- if siblings = element.parent.children
- found = false
- siblings.each do |node|
- if node.equal?(element)
- found = true
- elsif found && node.tag?
- return node if (name.nil? || node.name == name)
- end
- end
- end
- nil
- end
-
-
- protected
-
-
- # Creates a simple selector given the statement and array of
- # substitution values.
- #
- # Returns a hash with the values +tag_name+, +attributes+,
- # +pseudo+ (classes) and +negation+.
- #
- # Called the first time with +can_negate+ true to allow
- # negation. Called a second time with false since negation
- # cannot be negated.
- def simple_selector(statement, values, can_negate = true)
- tag_name = nil
- attributes = []
- pseudo = []
- negation = []
-
- # Element name. (Note that in negation, this can come at
- # any order, but for simplicity we allow if only first).
- statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
- match.strip!
- tag_name = match.downcase unless match == "*"
- @source << match
- "" # Remove
- end
-
- # Get identifier, class, attribute name, pseudo or negation.
- while true
- # Element identifier.
- next if statement.sub!(/^#(\?|[\w\-]+)/) do |match|
- id = $1
- if id == "?"
- id = values.shift
- end
- @source << "##{id}"
- id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
- attributes << ["id", id]
- "" # Remove
- end
-
- # Class name.
- next if statement.sub!(/^\.([\w\-]+)/) do |match|
- class_name = $1
- @source << ".#{class_name}"
- class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
- attributes << ["class", class_name]
- "" # Remove
- end
-
- # Attribute value.
- next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do |match|
- name, equality, value = $1, $2, $3
- if value == "?"
- value = values.shift
- else
- # Handle single and double quotes.
- value.strip!
- if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
- value = value[1..-2]
- end
- end
- @source << "[#{name}#{equality}'#{value}']"
- attributes << [name.downcase.strip, attribute_match(equality, value)]
- "" # Remove
- end
-
- # Root element only.
- next if statement.sub!(/^:root/) do |match|
- pseudo << lambda do |element|
- element.parent.nil? || !element.parent.tag?
- end
- @source << ":root"
- "" # Remove
- end
-
- # Nth-child including last and of-type.
- next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
- reverse = $1 == "last-"
- of_type = $2 == "of-type"
- @source << ":nth-#{$1}#{$2}("
- case $3
- when "odd"
- pseudo << nth_child(2, 1, of_type, reverse)
- @source << "odd)"
- when "even"
- pseudo << nth_child(2, 2, of_type, reverse)
- @source << "even)"
- when /^(\d+|\?)$/ # b only
- b = ($1 == "?" ? values.shift : $1).to_i
- pseudo << nth_child(0, b, of_type, reverse)
- @source << "#{b})"
- when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
- a = ($1 == "?" ? values.shift :
- $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
- b = ($2 == "?" ? values.shift : $2).to_i
- pseudo << nth_child(a, b, of_type, reverse)
- @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
- else
- raise ArgumentError, "Invalid nth-child #{match}"
- end
- "" # Remove
- end
- # First/last child (of type).
- next if statement.sub!(/^:(first|last)-(child|of-type)/) do |match|
- reverse = $1 == "last"
- of_type = $2 == "of-type"
- pseudo << nth_child(0, 1, of_type, reverse)
- @source << ":#{$1}-#{$2}"
- "" # Remove
- end
- # Only child (of type).
- next if statement.sub!(/^:only-(child|of-type)/) do |match|
- of_type = $1 == "of-type"
- pseudo << only_child(of_type)
- @source << ":only-#{$1}"
- "" # Remove
- end
-
- # Empty: no child elements or meaningful content (whitespaces
- # are ignored).
- next if statement.sub!(/^:empty/) do |match|
- pseudo << lambda do |element|
- empty = true
- for child in element.children
- if child.tag? || !child.content.strip.empty?
- empty = false
- break
- end
- end
- empty
- end
- @source << ":empty"
- "" # Remove
- end
- # Content: match the text content of the element, stripping
- # leading and trailing spaces.
- next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do |match|
- content = $1
- if content == "?"
- content = values.shift
- elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
- content = content[1..-2]
- end
- @source << ":content('#{content}')"
- content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
- pseudo << lambda do |element|
- text = ""
- for child in element.children
- unless child.tag?
- text << child.content
- end
- end
- text.strip =~ content
- end
- "" # Remove
- end
-
- # Negation. Create another simple selector to handle it.
- if statement.sub!(/^:not\(\s*/, "")
- raise ArgumentError, "Double negatives are not missing feature" unless can_negate
- @source << ":not("
- negation << simple_selector(statement, values, false)
- raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
- @source << ")"
- next
- end
-
- # No match: moving on.
- break
- end
-
- # Return hash. The keys are mapped to instance variables.
- {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
- end
-
-
- # Create a regular expression to match an attribute value based
- # on the equality operator (=, ^=, |=, etc).
- def attribute_match(equality, value)
- regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
- case equality
- when "=" then
- # Match the attribute value in full
- Regexp.new("^#{regexp}$")
- when "~=" then
- # Match a space-separated word within the attribute value
- Regexp.new("(^|\s)#{regexp}($|\s)")
- when "^="
- # Match the beginning of the attribute value
- Regexp.new("^#{regexp}")
- when "$="
- # Match the end of the attribute value
- Regexp.new("#{regexp}$")
- when "*="
- # Match substring of the attribute value
- regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
- when "|=" then
- # Match the first space-separated item of the attribute value
- Regexp.new("^#{regexp}($|\s)")
- else
- raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
- # Match all attributes values (existence check)
- //
- end
- end
-
-
- # Returns a lambda that can match an element against the nth-child
- # pseudo class, given the following arguments:
- # * +a+ -- Value of a part.
- # * +b+ -- Value of b part.
- # * +of_type+ -- True to test only elements of this type (of-type).
- # * +reverse+ -- True to count in reverse order (last-).
- def nth_child(a, b, of_type, reverse)
- # a = 0 means select at index b, if b = 0 nothing selected
- return lambda { |element| false } if a == 0 && b == 0
- # a < 0 and b < 0 will never match against an index
- return lambda { |element| false } if a < 0 && b < 0
- b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
- b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- index = 0
- # Get siblings, reverse if counting from last.
- siblings = element.parent.children
- siblings = siblings.reverse if reverse
- # Match element name if of-type, otherwise ignore name.
- name = of_type ? element.name : nil
- found = false
- for child in siblings
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- if a == 0
- # Shortcut when a == 0 no need to go past count
- if index == b
- found = child.equal?(element)
- break
- end
- elsif a < 0
- # Only look for first b elements
- break if index > b
- if child.equal?(element)
- found = (index % a) == 0
- break
- end
- else
- # Otherwise, break if child found and count == an+b
- if child.equal?(element)
- found = (index % a) == b
- break
- end
- end
- index += 1
- end
- end
- found
- end
- end
-
-
- # Creates a only child lambda. Pass +of-type+ to only look at
- # elements of its type.
- def only_child(of_type)
- lambda do |element|
- # Element must be inside parent element.
- return false unless element.parent && element.parent.tag?
- name = of_type ? element.name : nil
- other = false
- for child in element.parent.children
- # Skip text nodes/comments.
- if child.tag? && (name == nil || child.name == name)
- unless child.equal?(element)
- other = true
- break
- end
- end
- end
- !other
- end
- end
-
-
- # Called to create a dependent selector (sibling, descendant, etc).
- # Passes the remainder of the statement that will be reduced to zero
- # eventually, and array of substitution values.
- #
- # This method is called from four places, so it helps to put it here
- # for reuse. The only logic deals with the need to detect comma
- # separators (alternate) and apply them to the selector group of the
- # top selector.
- def next_selector(statement, values)
- second = Selector.new(statement, values)
- # If there are alternate selectors, we group them in the top selector.
- if alternates = second.instance_variable_get(:@alternates)
- second.instance_variable_set(:@alternates, [])
- @alternates.concat alternates
- end
- second
- end
-
- end
-
-
- # See HTML::Selector.new
- def self.selector(statement, *values)
- Selector.new(statement, *values)
- end
-
-
- class Tag
-
- def select(selector, *values)
- selector = HTML::Selector.new(selector, values)
- selector.select(self)
- end
-
- end
-
-end
diff --git a/actionpack/test/abstract/abstract_controller_test.rb b/actionpack/test/abstract/abstract_controller_test.rb
deleted file mode 100644
index 62f82a4c7a..0000000000
--- a/actionpack/test/abstract/abstract_controller_test.rb
+++ /dev/null
@@ -1,263 +0,0 @@
-require 'abstract_unit'
-require 'set'
-
-module AbstractController
- module Testing
-
- # Test basic dispatching.
- # ====
- # * Call process
- # * Test that the response_body is set correctly
- class SimpleController < AbstractController::Base
- end
-
- class Me < SimpleController
- def index
- self.response_body = "Hello world"
- "Something else"
- end
- end
-
- class TestBasic < ActiveSupport::TestCase
- test "dispatching works" do
- controller = Me.new
- controller.process(:index)
- assert_equal "Hello world", controller.response_body
- end
- end
-
- # Test Render mixin
- # ====
- class RenderingController < AbstractController::Base
- include AbstractController::Rendering
-
- def _prefixes
- []
- end
-
- def render(options = {})
- if options.is_a?(String)
- options = {:_template_name => options}
- end
- super
- end
-
- append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
- end
-
- class Me2 < RenderingController
- def index
- render "index.erb"
- end
-
- def index_to_string
- self.response_body = render_to_string "index"
- end
-
- def action_with_ivars
- @my_ivar = "Hello"
- render "action_with_ivars.erb"
- end
-
- def naked_render
- render
- end
-
- def rendering_to_body
- self.response_body = render_to_body :template => "naked_render"
- end
-
- def rendering_to_string
- self.response_body = render_to_string :template => "naked_render"
- end
- end
-
- class TestRenderingController < ActiveSupport::TestCase
- def setup
- @controller = Me2.new
- end
-
- test "rendering templates works" do
- @controller.process(:index)
- assert_equal "Hello from index.erb", @controller.response_body
- end
-
- test "render_to_string works with a String as an argument" do
- @controller.process(:index_to_string)
- assert_equal "Hello from index.erb", @controller.response_body
- end
-
- test "rendering passes ivars to the view" do
- @controller.process(:action_with_ivars)
- assert_equal "Hello from index_with_ivars.erb", @controller.response_body
- end
-
- test "rendering with no template name" do
- @controller.process(:naked_render)
- assert_equal "Hello from naked_render.erb", @controller.response_body
- end
-
- test "rendering to a rack body" do
- @controller.process(:rendering_to_body)
- assert_equal "Hello from naked_render.erb", @controller.response_body
- end
-
- test "rendering to a string" do
- @controller.process(:rendering_to_string)
- assert_equal "Hello from naked_render.erb", @controller.response_body
- end
- end
-
- # Test rendering with prefixes
- # ====
- # * self._prefix is used when defined
- class PrefixedViews < RenderingController
- private
- def self.prefix
- name.underscore
- end
-
- def _prefixes
- [self.class.prefix]
- end
- end
-
- class Me3 < PrefixedViews
- def index
- render
- end
-
- def formatted
- self.formats = [:html]
- render
- end
- end
-
- class TestPrefixedViews < ActiveSupport::TestCase
- def setup
- @controller = Me3.new
- end
-
- test "templates are located inside their 'prefix' folder" do
- @controller.process(:index)
- assert_equal "Hello from me3/index.erb", @controller.response_body
- end
-
- test "templates included their format" do
- @controller.process(:formatted)
- assert_equal "Hello from me3/formatted.html.erb", @controller.response_body
- end
- end
-
- # Test rendering with layouts
- # ====
- # self._layout is used when defined
- class WithLayouts < PrefixedViews
- include AbstractController::Layouts
-
- private
- def self.layout(formats)
- begin
- find_template(name.underscore, {:formats => formats}, :_prefixes => ["layouts"])
- rescue ActionView::MissingTemplate
- begin
- find_template("application", {:formats => formats}, :_prefixes => ["layouts"])
- rescue ActionView::MissingTemplate
- end
- end
- end
-
- def render_to_body(options = {})
- options[:_layout] = options[:layout] || _default_layout({})
- super
- end
- end
-
- class Me4 < WithLayouts
- def index
- render
- end
- end
-
- class TestLayouts < ActiveSupport::TestCase
- test "layouts are included" do
- controller = Me4.new
- controller.process(:index)
- assert_equal "Me4 Enter : Hello from me4/index.erb : Exit", controller.response_body
- end
- end
-
- # respond_to_action?(action_name)
- # ====
- # * A method can be used as an action only if this method
- # returns true when passed the method name as an argument
- # * Defaults to true in AbstractController
- class DefaultRespondToActionController < AbstractController::Base
- def index() self.response_body = "success" end
- end
-
- class ActionMissingRespondToActionController < AbstractController::Base
- # No actions
- private
- def action_missing(action_name)
- self.response_body = "success"
- end
- end
-
- class RespondToActionController < AbstractController::Base;
- def index() self.response_body = "success" end
-
- def fail() self.response_body = "fail" end
-
- private
-
- def method_for_action(action_name)
- action_name.to_s != "fail" && action_name
- end
- end
-
- class TestRespondToAction < ActiveSupport::TestCase
-
- def assert_dispatch(klass, body = "success", action = :index)
- controller = klass.new
- controller.process(action)
- assert_equal body, controller.response_body
- end
-
- test "an arbitrary method is available as an action by default" do
- assert_dispatch DefaultRespondToActionController, "success", :index
- end
-
- test "raises ActionNotFound when method does not exist and action_missing is not defined" do
- assert_raise(ActionNotFound) { DefaultRespondToActionController.new.process(:fail) }
- end
-
- test "dispatches to action_missing when method does not exist and action_missing is defined" do
- assert_dispatch ActionMissingRespondToActionController, "success", :ohai
- end
-
- test "a method is available as an action if method_for_action returns true" do
- assert_dispatch RespondToActionController, "success", :index
- end
-
- test "raises ActionNotFound if method is defined but method_for_action returns false" do
- assert_raise(ActionNotFound) { RespondToActionController.new.process(:fail) }
- end
- end
-
- class Me6 < AbstractController::Base
- self.action_methods
-
- def index
- end
- end
-
- class TestActionMethodsReloading < ActiveSupport::TestCase
-
- test "action_methods should be reloaded after defining a new method" do
- assert_equal Set.new(["index"]), Me6.action_methods
- end
- end
-
- end
-end
diff --git a/actionpack/test/abstract/callbacks_test.rb b/actionpack/test/abstract/callbacks_test.rb
index 5d1a703c55..8cba049485 100644
--- a/actionpack/test/abstract/callbacks_test.rb
+++ b/actionpack/test/abstract/callbacks_test.rb
@@ -28,9 +28,9 @@ module AbstractController
end
class Callback2 < ControllerWithCallbacks
- before_filter :first
- after_filter :second
- around_filter :aroundz
+ before_action :first
+ after_action :second
+ around_action :aroundz
def first
@text = "Hello world"
@@ -53,7 +53,7 @@ module AbstractController
end
class Callback2Overwrite < Callback2
- before_filter :first, :except => :index
+ before_action :first, except: :index
end
class TestCallbacks2 < ActiveSupport::TestCase
@@ -61,22 +61,22 @@ module AbstractController
@controller = Callback2.new
end
- test "before_filter works" do
+ test "before_action works" do
@controller.process(:index)
assert_equal "Hello world", @controller.response_body
end
- test "after_filter works" do
+ test "after_action works" do
@controller.process(:index)
assert_equal "Goodbye", @controller.instance_variable_get("@second")
end
- test "around_filter works" do
+ test "around_action works" do
@controller.process(:index)
assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz")
end
- test "before_filter with overwritten condition" do
+ test "before_action with overwritten condition" do
@controller = Callback2Overwrite.new
@controller.process(:index)
assert_equal "", @controller.response_body
@@ -84,11 +84,11 @@ module AbstractController
end
class Callback3 < ControllerWithCallbacks
- before_filter do |c|
+ before_action do |c|
c.instance_variable_set("@text", "Hello world")
end
- after_filter do |c|
+ after_action do |c|
c.instance_variable_set("@second", "Goodbye")
end
@@ -102,20 +102,20 @@ module AbstractController
@controller = Callback3.new
end
- test "before_filter works with procs" do
+ test "before_action works with procs" do
@controller.process(:index)
assert_equal "Hello world", @controller.response_body
end
- test "after_filter works with procs" do
+ test "after_action works with procs" do
@controller.process(:index)
assert_equal "Goodbye", @controller.instance_variable_get("@second")
end
end
class CallbacksWithConditions < ControllerWithCallbacks
- before_filter :list, :only => :index
- before_filter :authenticate, :except => :index
+ before_action :list, :only => :index
+ before_action :authenticate, :except => :index
def index
self.response_body = @list.join(", ")
@@ -141,25 +141,25 @@ module AbstractController
@controller = CallbacksWithConditions.new
end
- test "when :only is specified, a before filter is triggered on that action" do
+ test "when :only is specified, a before action is triggered on that action" do
@controller.process(:index)
assert_equal "Hello, World", @controller.response_body
end
- test "when :only is specified, a before filter is not triggered on other actions" do
+ test "when :only is specified, a before action is not triggered on other actions" do
@controller.process(:sekrit_data)
assert_equal "true", @controller.response_body
end
- test "when :except is specified, an after filter is not triggered on that action" do
+ test "when :except is specified, an after action is not triggered on that action" do
@controller.process(:index)
assert !@controller.instance_variable_defined?("@authenticated")
end
end
class CallbacksWithArrayConditions < ControllerWithCallbacks
- before_filter :list, :only => [:index, :listy]
- before_filter :authenticate, :except => [:index, :listy]
+ before_action :list, only: [:index, :listy]
+ before_action :authenticate, except: [:index, :listy]
def index
self.response_body = @list.join(", ")
@@ -185,24 +185,24 @@ module AbstractController
@controller = CallbacksWithArrayConditions.new
end
- test "when :only is specified with an array, a before filter is triggered on that action" do
+ test "when :only is specified with an array, a before action is triggered on that action" do
@controller.process(:index)
assert_equal "Hello, World", @controller.response_body
end
- test "when :only is specified with an array, a before filter is not triggered on other actions" do
+ test "when :only is specified with an array, a before action is not triggered on other actions" do
@controller.process(:sekrit_data)
assert_equal "true", @controller.response_body
end
- test "when :except is specified with an array, an after filter is not triggered on that action" do
+ test "when :except is specified with an array, an after action is not triggered on that action" do
@controller.process(:index)
assert !@controller.instance_variable_defined?("@authenticated")
end
end
class ChangedConditions < Callback2
- before_filter :first, :only => :index
+ before_action :first, :only => :index
def not_index
@text ||= nil
@@ -227,7 +227,7 @@ module AbstractController
end
class SetsResponseBody < ControllerWithCallbacks
- before_filter :set_body
+ before_action :set_body
def index
self.response_body = "Fail"
@@ -259,13 +259,57 @@ module AbstractController
end
class TestCallbacksWithArgs < ActiveSupport::TestCase
- test "callbacks still work when invoking process with multiple args" do
+ test "callbacks still work when invoking process with multiple arguments" do
controller = CallbacksWithArgs.new
controller.process(:index, " Howdy!")
assert_equal "Hello world Howdy!", controller.response_body
end
end
+ class AliasedCallbacks < ControllerWithCallbacks
+ before_filter :first
+ after_filter :second
+ around_filter :aroundz
+
+ def first
+ @text = "Hello world"
+ end
+
+ def second
+ @second = "Goodbye"
+ end
+
+ def aroundz
+ @aroundz = "FIRST"
+ yield
+ @aroundz << "SECOND"
+ end
+ def index
+ @text ||= nil
+ self.response_body = @text.to_s
+ end
+ end
+
+ class TestAliasedCallbacks < ActiveSupport::TestCase
+ def setup
+ @controller = AliasedCallbacks.new
+ end
+
+ test "before_filter works" do
+ @controller.process(:index)
+ assert_equal "Hello world", @controller.response_body
+ end
+
+ test "after_filter works" do
+ @controller.process(:index)
+ assert_equal "Goodbye", @controller.instance_variable_get("@second")
+ end
+
+ test "around_filter works" do
+ @controller.process(:index)
+ assert_equal "FIRSTSECOND", @controller.instance_variable_get("@aroundz")
+ end
+ end
end
end
diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb
index c14d24905b..5709ad0378 100644
--- a/actionpack/test/abstract/collector_test.rb
+++ b/actionpack/test/abstract/collector_test.rb
@@ -42,7 +42,7 @@ module AbstractController
end
end
- test "generated methods call custom with args received" do
+ test "generated methods call custom with arguments received" do
collector = MyCollector.new
collector.html
collector.text(:foo)
diff --git a/actionpack/test/abstract/helper_test.rb b/actionpack/test/abstract/helper_test.rb
deleted file mode 100644
index e79008fa9d..0000000000
--- a/actionpack/test/abstract/helper_test.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-require 'abstract_unit'
-
-ActionController::Base.helpers_path = File.expand_path('../../fixtures/helpers', __FILE__)
-
-module AbstractController
- module Testing
-
- class ControllerWithHelpers < AbstractController::Base
- include AbstractController::Rendering
- include AbstractController::Helpers
-
- def with_module
- render :inline => "Module <%= included_method %>"
- end
- end
-
- module HelperyTest
- def included_method
- "Included"
- end
- end
-
- class AbstractHelpers < ControllerWithHelpers
- helper(HelperyTest) do
- def helpery_test
- "World"
- end
- end
-
- helper :abc
-
- def with_block
- render :inline => "Hello <%= helpery_test %>"
- end
-
- def with_symbol
- render :inline => "I respond to bare_a: <%= respond_to?(:bare_a) %>"
- end
- end
-
- class ::HelperyTestController < AbstractHelpers
- clear_helpers
- end
-
- class AbstractHelpersBlock < ControllerWithHelpers
- helper do
- include AbstractController::Testing::HelperyTest
- end
- end
-
- class TestHelpers < ActiveSupport::TestCase
- def setup
- @controller = AbstractHelpers.new
- end
-
- def test_helpers_with_block
- @controller.process(:with_block)
- assert_equal "Hello World", @controller.response_body
- end
-
- def test_helpers_with_module
- @controller.process(:with_module)
- assert_equal "Module Included", @controller.response_body
- end
-
- def test_helpers_with_symbol
- @controller.process(:with_symbol)
- assert_equal "I respond to bare_a: true", @controller.response_body
- end
-
- def test_declare_missing_helper
- begin
- AbstractHelpers.helper :missing
- flunk "should have raised an exception"
- rescue LoadError => e
- assert_equal "helpers/missing_helper.rb", e.path
- end
- end
-
- def test_helpers_with_module_through_block
- @controller = AbstractHelpersBlock.new
- @controller.process(:with_module)
- assert_equal "Module Included", @controller.response_body
- end
- end
-
- class ClearHelpersTest < ActiveSupport::TestCase
- def setup
- @controller = HelperyTestController.new
- end
-
- def test_clears_up_previous_helpers
- @controller.process(:with_symbol)
- assert_equal "I respond to bare_a: false", @controller.response_body
- end
-
- def test_includes_controller_default_helper
- @controller.process(:with_block)
- assert_equal "Hello Default", @controller.response_body
- end
- end
- end
-end
diff --git a/actionpack/test/abstract/layouts_test.rb b/actionpack/test/abstract/layouts_test.rb
deleted file mode 100644
index 558a45b87f..0000000000
--- a/actionpack/test/abstract/layouts_test.rb
+++ /dev/null
@@ -1,363 +0,0 @@
-require 'abstract_unit'
-
-module AbstractControllerTests
- module Layouts
-
- # Base controller for these tests
- class Base < AbstractController::Base
- include AbstractController::Rendering
- include AbstractController::Layouts
-
- self.view_paths = [ActionView::FixtureResolver.new(
- "layouts/hello.erb" => "With String <%= yield %>",
- "layouts/hello_override.erb" => "With Override <%= yield %>",
- "layouts/overwrite.erb" => "Overwrite <%= yield %>",
- "layouts/with_false_layout.erb" => "False Layout <%= yield %>",
- "abstract_controller_tests/layouts/with_string_implied_child.erb" =>
- "With Implied <%= yield %>",
- "abstract_controller_tests/layouts/with_grand_child_of_implied.erb" =>
- "With Grand Child <%= yield %>"
-
- )]
- end
-
- class Blank < Base
- self.view_paths = []
-
- def index
- render :template => ActionView::Template::Text.new("Hello blank!")
- end
- end
-
- class WithString < Base
- layout "hello"
-
- def index
- render :template => ActionView::Template::Text.new("Hello string!")
- end
-
- def overwrite_default
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => :default
- end
-
- def overwrite_false
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => false
- end
-
- def overwrite_string
- render :template => ActionView::Template::Text.new("Hello string!"), :layout => "overwrite"
- end
-
- def overwrite_skip
- render :text => "Hello text!"
- end
- end
-
- class WithStringChild < WithString
- end
-
- class WithStringOverriddenChild < WithString
- layout "hello_override"
- end
-
- class WithStringImpliedChild < WithString
- layout nil
- end
-
- class WithChildOfImplied < WithStringImpliedChild
- end
-
- class WithGrandChildOfImplied < WithStringImpliedChild
- layout nil
- end
-
- class WithProc < Base
- layout proc { |c| "overwrite" }
-
- def index
- render :template => ActionView::Template::Text.new("Hello proc!")
- end
- end
-
- class WithZeroArityProc < Base
- layout proc { "overwrite" }
-
- def index
- render :template => ActionView::Template::Text.new("Hello zero arity proc!")
- end
- end
-
- class WithProcInContextOfInstance < Base
- def an_instance_method; end
-
- layout proc {
- break unless respond_to? :an_instance_method
- "overwrite"
- }
-
- def index
- render :template => ActionView::Template::Text.new("Hello again zero arity proc!")
- end
- end
-
- class WithSymbol < Base
- layout :hello
-
- def index
- render :template => ActionView::Template::Text.new("Hello symbol!")
- end
- private
- def hello
- "overwrite"
- end
- end
-
- class WithSymbolReturningNil < Base
- layout :nilz
-
- def index
- render :template => ActionView::Template::Text.new("Hello nilz!")
- end
-
- def nilz() end
- end
-
- class WithSymbolReturningObj < Base
- layout :objekt
-
- def index
- render :template => ActionView::Template::Text.new("Hello nilz!")
- end
-
- def objekt
- Object.new
- end
- end
-
- class WithSymbolAndNoMethod < Base
- layout :no_method
-
- def index
- render :template => ActionView::Template::Text.new("Hello boom!")
- end
- end
-
- class WithMissingLayout < Base
- layout "missing"
-
- def index
- render :template => ActionView::Template::Text.new("Hello missing!")
- end
- end
-
- class WithFalseLayout < Base
- layout false
-
- def index
- render :template => ActionView::Template::Text.new("Hello false!")
- end
- end
-
- class WithNilLayout < Base
- layout nil
-
- def index
- render :template => ActionView::Template::Text.new("Hello nil!")
- end
- end
-
- class WithOnlyConditional < WithStringImpliedChild
- layout "overwrite", :only => :show
-
- def index
- render :template => ActionView::Template::Text.new("Hello index!")
- end
-
- def show
- render :template => ActionView::Template::Text.new("Hello show!")
- end
- end
-
- class WithExceptConditional < WithStringImpliedChild
- layout "overwrite", :except => :show
-
- def index
- render :template => ActionView::Template::Text.new("Hello index!")
- end
-
- def show
- render :template => ActionView::Template::Text.new("Hello show!")
- end
- end
-
- class TestBase < ActiveSupport::TestCase
- test "when no layout is specified, and no default is available, render without a layout" do
- controller = Blank.new
- controller.process(:index)
- assert_equal "Hello blank!", controller.response_body
- end
-
- test "when layout is specified as a string, render with that layout" do
- controller = WithString.new
- controller.process(:index)
- assert_equal "With String Hello string!", controller.response_body
- end
-
- test "when layout is overwriten by :default in render, render default layout" do
- controller = WithString.new
- controller.process(:overwrite_default)
- assert_equal "With String Hello string!", controller.response_body
- end
-
- test "when layout is overwriten by string in render, render new layout" do
- controller = WithString.new
- controller.process(:overwrite_string)
- assert_equal "Overwrite Hello string!", controller.response_body
- end
-
- test "when layout is overwriten by false in render, render no layout" do
- controller = WithString.new
- controller.process(:overwrite_false)
- assert_equal "Hello string!", controller.response_body
- end
-
- test "when text is rendered, render no layout" do
- controller = WithString.new
- controller.process(:overwrite_skip)
- assert_equal "Hello text!", controller.response_body
- end
-
- test "when layout is specified as a string, but the layout is missing, raise an exception" do
- assert_raises(ActionView::MissingTemplate) { WithMissingLayout.new.process(:index) }
- end
-
- test "when layout is specified as false, do not use a layout" do
- controller = WithFalseLayout.new
- controller.process(:index)
- assert_equal "Hello false!", controller.response_body
- end
-
- test "when layout is specified as nil, do not use a layout" do
- controller = WithNilLayout.new
- controller.process(:index)
- assert_equal "Hello nil!", controller.response_body
- end
-
- test "when layout is specified as a proc, call it and use the layout returned" do
- controller = WithProc.new
- controller.process(:index)
- assert_equal "Overwrite Hello proc!", controller.response_body
- end
-
- test "when layout is specified as a proc without parameters it works just the same" do
- controller = WithZeroArityProc.new
- controller.process(:index)
- assert_equal "Overwrite Hello zero arity proc!", controller.response_body
- end
-
- test "when layout is specified as a proc without parameters the block is evaluated in the context of an instance" do
- controller = WithProcInContextOfInstance.new
- controller.process(:index)
- assert_equal "Overwrite Hello again zero arity proc!", controller.response_body
- end
-
- test "when layout is specified as a symbol, call the requested method and use the layout returned" do
- controller = WithSymbol.new
- controller.process(:index)
- assert_equal "Overwrite Hello symbol!", controller.response_body
- end
-
- test "when layout is specified as a symbol and the method returns nil, don't use a layout" do
- controller = WithSymbolReturningNil.new
- controller.process(:index)
- assert_equal "Hello nilz!", controller.response_body
- end
-
- test "when the layout is specified as a symbol and the method doesn't exist, raise an exception" do
- assert_raises(NameError) { WithSymbolAndNoMethod.new.process(:index) }
- end
-
- test "when the layout is specified as a symbol and the method returns something besides a string/false/nil, raise an exception" do
- assert_raises(ArgumentError) { WithSymbolReturningObj.new.process(:index) }
- end
-
- test "when a child controller does not have a layout, use the parent controller layout" do
- controller = WithStringChild.new
- controller.process(:index)
- assert_equal "With String Hello string!", controller.response_body
- end
-
- test "when a child controller has specified a layout, use that layout and not the parent controller layout" do
- controller = WithStringOverriddenChild.new
- controller.process(:index)
- assert_equal "With Override Hello string!", controller.response_body
- end
-
- test "when a child controller has an implied layout, use that layout and not the parent controller layout" do
- controller = WithStringImpliedChild.new
- controller.process(:index)
- assert_equal "With Implied Hello string!", controller.response_body
- end
-
- test "when a grandchild has no layout specified, the child has an implied layout, and the " \
- "parent has specified a layout, use the child controller layout" do
- controller = WithChildOfImplied.new
- controller.process(:index)
- assert_equal "With Implied Hello string!", controller.response_body
- end
-
- test "when a grandchild has nil layout specified, the child has an implied layout, and the " \
- "parent has specified a layout, use the child controller layout" do
- controller = WithGrandChildOfImplied.new
- controller.process(:index)
- assert_equal "With Grand Child Hello string!", controller.response_body
- end
-
- test "raises an exception when specifying layout true" do
- assert_raises ArgumentError do
- Object.class_eval do
- class ::BadFailLayout < AbstractControllerTests::Layouts::Base
- layout true
- end
- end
- end
- end
-
- test "when specify an :only option which match current action name" do
- controller = WithOnlyConditional.new
- controller.process(:show)
- assert_equal "Overwrite Hello show!", controller.response_body
- end
-
- test "when specify an :only option which does not match current action name" do
- controller = WithOnlyConditional.new
- controller.process(:index)
- assert_equal "With Implied Hello index!", controller.response_body
- end
-
- test "when specify an :except option which match current action name" do
- controller = WithExceptConditional.new
- controller.process(:show)
- assert_equal "With Implied Hello show!", controller.response_body
- end
-
- test "when specify an :except option which does not match current action name" do
- controller = WithExceptConditional.new
- controller.process(:index)
- assert_equal "Overwrite Hello index!", controller.response_body
- end
-
- test "layout for anonymous controller" do
- klass = Class.new(WithString) do
- def index
- render :text => 'index', :layout => true
- end
- end
-
- controller = klass.new
- controller.process(:index)
- assert_equal "With String index", controller.response_body
- end
- end
- end
-end
diff --git a/actionpack/test/abstract/render_test.rb b/actionpack/test/abstract/render_test.rb
deleted file mode 100644
index b9293d1241..0000000000
--- a/actionpack/test/abstract/render_test.rb
+++ /dev/null
@@ -1,102 +0,0 @@
-require 'abstract_unit'
-
-module AbstractController
- module Testing
-
- class ControllerRenderer < AbstractController::Base
- include AbstractController::Rendering
-
- def _prefixes
- %w[renderer]
- end
-
- self.view_paths = [ActionView::FixtureResolver.new(
- "template.erb" => "With Template",
- "renderer/default.erb" => "With Default",
- "renderer/string.erb" => "With String",
- "renderer/symbol.erb" => "With Symbol",
- "string/with_path.erb" => "With String With Path",
- "some/file.erb" => "With File"
- )]
-
- def template
- render :template => "template"
- end
-
- def file
- render :file => "some/file"
- end
-
- def inline
- render :inline => "With <%= :Inline %>"
- end
-
- def text
- render :text => "With Text"
- end
-
- def default
- render
- end
-
- def string
- render "string"
- end
-
- def string_with_path
- render "string/with_path"
- end
-
- def symbol
- render :symbol
- end
- end
-
- class TestRenderer < ActiveSupport::TestCase
-
- def setup
- @controller = ControllerRenderer.new
- end
-
- def test_render_template
- @controller.process(:template)
- assert_equal "With Template", @controller.response_body
- end
-
- def test_render_file
- @controller.process(:file)
- assert_equal "With File", @controller.response_body
- end
-
- def test_render_inline
- @controller.process(:inline)
- assert_equal "With Inline", @controller.response_body
- end
-
- def test_render_text
- @controller.process(:text)
- assert_equal "With Text", @controller.response_body
- end
-
- def test_render_default
- @controller.process(:default)
- assert_equal "With Default", @controller.response_body
- end
-
- def test_render_string
- @controller.process(:string)
- assert_equal "With String", @controller.response_body
- end
-
- def test_render_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.response_body
- end
- end
- end
-end
diff --git a/actionpack/test/abstract/translation_test.rb b/actionpack/test/abstract/translation_test.rb
index 99064a8b87..4fdc480b43 100644
--- a/actionpack/test/abstract/translation_test.rb
+++ b/actionpack/test/abstract/translation_test.rb
@@ -1,39 +1,50 @@
require 'abstract_unit'
-# class TranslatingController < ActionController::Base
-# end
-
-class TranslationControllerTest < ActiveSupport::TestCase
- def setup
- @controller = ActionController::Base.new
- end
-
- def test_action_controller_base_responds_to_translate
- assert_respond_to @controller, :translate
- end
-
- def test_action_controller_base_responds_to_t
- assert_respond_to @controller, :t
- end
-
- def test_action_controller_base_responds_to_localize
- assert_respond_to @controller, :localize
- end
-
- def test_action_controller_base_responds_to_l
- assert_respond_to @controller, :l
- end
-
- def test_lazy_lookup
- expected = 'bar'
- @controller.stubs(:action_name => :index)
- I18n.stubs(:translate).with('action_controller.base.index.foo').returns(expected)
- assert_equal expected, @controller.t('.foo')
- end
-
- def test_default_translation
- key, expected = 'one.two' 'bar'
- I18n.stubs(:translate).with(key).returns(expected)
- assert_equal expected, @controller.t(key)
+module AbstractController
+ module Testing
+ class TranslationController < AbstractController::Base
+ include AbstractController::Translation
+ end
+
+ class TranslationControllerTest < ActiveSupport::TestCase
+ def setup
+ @controller = TranslationController.new
+ end
+
+ def test_action_controller_base_responds_to_translate
+ assert_respond_to @controller, :translate
+ end
+
+ def test_action_controller_base_responds_to_t
+ assert_respond_to @controller, :t
+ end
+
+ def test_action_controller_base_responds_to_localize
+ assert_respond_to @controller, :localize
+ end
+
+ def test_action_controller_base_responds_to_l
+ assert_respond_to @controller, :l
+ end
+
+ def test_lazy_lookup
+ expected = 'bar'
+ @controller.stubs(action_name: :index)
+ I18n.stubs(:translate).with('abstract_controller.testing.translation.index.foo').returns(expected)
+ assert_equal expected, @controller.t('.foo')
+ end
+
+ def test_default_translation
+ key, expected = 'one.two', 'bar'
+ I18n.stubs(:translate).with(key).returns(expected)
+ assert_equal expected, @controller.t(key)
+ end
+
+ def test_localize
+ time, expected = Time.gm(2000), 'Sat, 01 Jan 2000 00:00:00 +0000'
+ I18n.stubs(:localize).with(time).returns(expected)
+ assert_equal expected, @controller.l(time)
+ end
+ end
end
end
diff --git a/actionpack/test/abstract_unit.rb b/actionpack/test/abstract_unit.rb
index 4f5b2895c9..a0d90f7eee 100644
--- a/actionpack/test/abstract_unit.rb
+++ b/actionpack/test/abstract_unit.rb
@@ -15,7 +15,7 @@ silence_warnings do
Encoding.default_external = "UTF-8"
end
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'abstract_controller'
require 'action_controller'
require 'action_view'
@@ -25,7 +25,6 @@ require 'active_support/dependencies'
require 'active_model'
require 'active_record'
require 'action_controller/caching'
-require 'action_controller/caching/sweeping'
require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
@@ -39,6 +38,8 @@ end
ActiveSupport::Dependencies.hook!
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
@@ -63,28 +64,6 @@ module RackTestUtils
extend self
end
-module RenderERBUtils
- def view
- @view ||= begin
- path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
- view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths)
- end
- end
-
- def render_erb(string)
- @virtual_path = nil
-
- template = ActionView::Template.new(
- string.strip,
- "test template",
- ActionView::Template::Handlers::ERB,
- {})
-
- template.render(self, {}).strip
- end
-end
-
SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
module ActionDispatch
@@ -267,6 +246,8 @@ class Rack::TestCase < ActionDispatch::IntegrationTest
end
end
+ActionController::Base.superclass.send(:include, ActionView::Layouts)
+
module ActionController
class Base
include ActionController::Testing
@@ -289,15 +270,8 @@ module ActionController
end
end
-class ::ApplicationController < ActionController::Base
-end
-module ActionView
- class TestCase
- # Must repeat the setup because AV::TestCase is a duplication
- # of AC::TestCase
- include ActionDispatch::SharedRoutes
- end
+class ::ApplicationController < ActionController::Base
end
class Workshop
@@ -331,7 +305,7 @@ end
module ActionDispatch
module RoutingVerbs
- def get(uri_or_host, path = nil, port = nil)
+ def get(uri_or_host, path = nil)
host = uri_or_host.host unless path
path ||= uri_or_host.path
diff --git a/actionpack/test/active_record_unit.rb b/actionpack/test/active_record_unit.rb
deleted file mode 100644
index 4dd7406798..0000000000
--- a/actionpack/test/active_record_unit.rb
+++ /dev/null
@@ -1,99 +0,0 @@
-require 'abstract_unit'
-
-# Define the essentials
-class ActiveRecordTestConnector
- cattr_accessor :able_to_connect
- cattr_accessor :connected
-
- # Set our defaults
- self.connected = false
- self.able_to_connect = true
-end
-
-# Try to grab AR
-unless defined?(ActiveRecord) && defined?(FixtureSet)
- begin
- PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib"
- raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR)
- $LOAD_PATH.unshift PATH_TO_AR
- require 'active_record'
- rescue LoadError => e
- $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}"
- ActiveRecordTestConnector.able_to_connect = false
- end
-end
-$stderr.flush
-
-
-# Define the rest of the connector
-class ActiveRecordTestConnector
- class << self
- def setup
- unless self.connected || !self.able_to_connect
- setup_connection
- load_schema
- require_fixture_models
- self.connected = true
- end
- rescue Exception => e # errors from ActiveRecord setup
- $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
- #$stderr.puts " #{e.backtrace.join("\n ")}\n"
- self.able_to_connect = false
- end
-
- private
- def setup_connection
- if Object.const_defined?(:ActiveRecord)
- defaults = { :database => ':memory:' }
- begin
- adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'
- options = defaults.merge :adapter => adapter, :timeout => 500
- ActiveRecord::Base.establish_connection(options)
- ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
- ActiveRecord::Base.connection
- rescue Exception # errors from establishing a connection
- $stderr.puts 'SQLite 3 unavailable; trying SQLite 2.'
- options = defaults.merge :adapter => 'sqlite'
- ActiveRecord::Base.establish_connection(options)
- ActiveRecord::Base.configurations = { 'sqlite2_ar_integration' => options }
- ActiveRecord::Base.connection
- end
-
- Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
- else
- raise "Can't setup connection since ActiveRecord isn't loaded."
- end
- end
-
- # Load actionpack sqlite tables
- def load_schema
- File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
- ActiveRecord::Base.connection.execute(sql) unless sql.blank?
- end
- end
-
- def require_fixture_models
- Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
- end
- end
-end
-
-class ActiveRecordTestCase < ActionController::TestCase
- include ActiveRecord::TestFixtures
-
- # Set our fixture path
- if ActiveRecordTestConnector.able_to_connect
- self.fixture_path = [FIXTURE_LOAD_PATH]
- self.use_transactional_fixtures = false
- end
-
- def self.fixtures(*args)
- super if ActiveRecordTestConnector.connected
- end
-
- def run(*args)
- super if ActiveRecordTestConnector.connected
- end
-end
-
-ActiveRecordTestConnector.setup
diff --git a/actionpack/test/activerecord/controller_runtime_test.rb b/actionpack/test/activerecord/controller_runtime_test.rb
deleted file mode 100644
index 2d789395ce..0000000000
--- a/actionpack/test/activerecord/controller_runtime_test.rb
+++ /dev/null
@@ -1,81 +0,0 @@
-require 'active_record_unit'
-require 'active_record/railties/controller_runtime'
-require 'fixtures/project'
-require 'active_support/log_subscriber/test_helper'
-require 'action_controller/log_subscriber'
-
-ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime
-
-class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
- class LogSubscriberController < ActionController::Base
- def show
- render :inline => "<%= Project.all %>"
- end
-
- def zero
- render :inline => "Zero DB runtime"
- end
-
- def redirect
- Project.all
- redirect_to :action => 'show'
- end
-
- def db_after_render
- render :inline => "Hello world"
- Project.all
- ActiveRecord::LogSubscriber.runtime += 100
- end
- end
-
- include ActiveSupport::LogSubscriber::TestHelper
- tests LogSubscriberController
-
- def setup
- super
- @old_logger = ActionController::Base.logger
- ActionController::LogSubscriber.attach_to :action_controller
- end
-
- def teardown
- super
- ActiveSupport::LogSubscriber.log_subscribers.clear
- ActionController::Base.logger = @old_logger
- end
-
- def set_logger(logger)
- ActionController::Base.logger = logger
- end
-
- def test_log_with_active_record
- get :show
- wait
-
- assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1])
- end
-
- def test_runtime_reset_before_requests
- ActiveRecord::LogSubscriber.runtime += 12345
- get :zero
- wait
-
- assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1])
- end
-
- def test_log_with_active_record_when_redirecting
- get :redirect
- wait
- assert_equal 3, @logger.logged(:info).size
- assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2])
- end
-
- def test_include_time_query_time_after_rendering
- get :db_after_render
- wait
-
- assert_equal 2, @logger.logged(:info).size
- assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1])
- end
-end
diff --git a/actionpack/test/controller/action_pack_assertions_test.rb b/actionpack/test/controller/action_pack_assertions_test.rb
index ca542eb7e2..ba4cffcd3e 100644
--- a/actionpack/test/controller/action_pack_assertions_test.rb
+++ b/actionpack/test/controller/action_pack_assertions_test.rb
@@ -39,6 +39,8 @@ class ActionPackAssertionsController < ActionController::Base
def redirect_external() redirect_to "http://www.rubyonrails.org"; end
+ def redirect_external_protocol_relative() redirect_to "//www.rubyonrails.org"; end
+
def response404() head '404 AWOL' end
def response500() head '500 Sorry' end
@@ -96,6 +98,14 @@ class ActionPackAssertionsController < ActionController::Base
raise "post" if request.post?
render :text => "request method: #{request.env['REQUEST_METHOD']}"
end
+
+ def render_file_absolute_path
+ render :file => File.expand_path('../../../README.rdoc', __FILE__)
+ end
+
+ def render_file_relative_path
+ render :file => 'README.rdoc'
+ end
end
# Used to test that assert_response includes the exception message
@@ -142,6 +152,16 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
assert_tag :content => "/action_pack_assertions/flash_me"
end
+ def test_render_file_absolute_path
+ get :render_file_absolute_path
+ assert_match(/\A= Action Pack/, @response.body)
+ end
+
+ def test_render_file_relative_path
+ get :render_file_relative_path
+ assert_match(/\A= Action Pack/, @response.body)
+ end
+
def test_get_request
assert_raise(RuntimeError) { get :raise_exception_on_get }
get :raise_exception_on_post
@@ -240,6 +260,19 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
end
end
+ def test_assert_redirect_failure_message_with_protocol_relative_url
+ begin
+ process :redirect_external_protocol_relative
+ assert_redirected_to "/foo"
+ rescue ActiveSupport::TestCase::Assertion => ex
+ assert_no_match(
+ /#{request.protocol}#{request.host}\/\/www.rubyonrails.org/,
+ ex.message,
+ 'protocol relative url was incorrectly normalized'
+ )
+ end
+ end
+
def test_template_objects_exist
process :assign_this
assert !@controller.instance_variable_defined?(:"@hi")
@@ -259,7 +292,7 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
def test_flash_exist
process :flash_me
assert flash.any?
- assert_present flash['hello']
+ assert flash['hello'].present?
end
def test_flash_does_not_exist
@@ -291,6 +324,9 @@ class ActionPackAssertionsControllerTest < ActionController::TestCase
process :redirect_external
assert_equal 'http://www.rubyonrails.org', @response.redirect_url
+
+ process :redirect_external_protocol_relative
+ assert_equal '//www.rubyonrails.org', @response.redirect_url
end
def test_no_redirect_url
@@ -430,11 +466,34 @@ end
class AssertTemplateTest < ActionController::TestCase
tests ActionPackAssertionsController
+ def test_with_invalid_hash_keys_raises_argument_error
+ assert_raise(ArgumentError) do
+ assert_template foo: "bar"
+ end
+ end
+
def test_with_partial
get :partial
assert_template :partial => '_partial'
end
+ def test_file_with_absolute_path_success
+ get :render_file_absolute_path
+ assert_template :file => File.expand_path('../../../README.rdoc', __FILE__)
+ end
+
+ def test_file_with_relative_path_success
+ get :render_file_relative_path
+ assert_template :file => 'README.rdoc'
+ end
+
+ def test_with_file_failure
+ get :render_file_absolute_path
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template :file => 'test/hello_world'
+ end
+ end
+
def test_with_nil_passes_when_no_template_rendered
get :nothing
assert_template nil
@@ -447,6 +506,20 @@ class AssertTemplateTest < ActionController::TestCase
end
end
+ def test_with_empty_string_fails_when_template_rendered
+ get :hello_world
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template ""
+ end
+ end
+
+ def test_with_empty_string_fails_when_no_template_rendered
+ get :nothing
+ assert_raise(ActiveSupport::TestCase::Assertion) do
+ assert_template ""
+ end
+ end
+
def test_passes_with_correct_string
get :hello_world
assert_template 'hello_world'
diff --git a/actionpack/test/controller/assert_select_test.rb b/actionpack/test/controller/assert_select_test.rb
index 38598a520c..114bbf3c22 100644
--- a/actionpack/test/controller/assert_select_test.rb
+++ b/actionpack/test/controller/assert_select_test.rb
@@ -8,17 +8,10 @@ require 'abstract_unit'
require 'controller/fake_controllers'
require 'action_mailer'
-ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
+require 'action_view'
-class SynchronousQueue < Queue
- def push(job)
- job.run
- end
- alias << push
- alias enq push
-end
-
-ActionMailer::Base.queue = SynchronousQueue.new
+ActionMailer::Base.send(:include, ActionView::Layouts)
+ActionMailer::Base.view_paths = FIXTURE_LOAD_PATH
class AssertSelectTest < ActionController::TestCase
Assertion = ActiveSupport::TestCase::Assertion
diff --git a/actionpack/test/controller/base_test.rb b/actionpack/test/controller/base_test.rb
index 9b42e7631f..b2bfdae174 100644
--- a/actionpack/test/controller/base_test.rb
+++ b/actionpack/test/controller/base_test.rb
@@ -61,11 +61,14 @@ class UrlOptionsController < ActionController::Base
end
end
-class RecordIdentifierController < ActionController::Base
+class RecordIdentifierIncludedController < ActionController::Base
+ include ActionView::RecordIdentifier
end
-class RecordIdentifierWithoutDeprecationController < ActionController::Base
- include ActionView::RecordIdentifier
+class ActionMissingController < ActionController::Base
+ def action_missing(action)
+ render :text => "Response for #{action}"
+ end
end
class ControllerClassTests < ActiveSupport::TestCase
@@ -82,43 +85,20 @@ class ControllerClassTests < ActiveSupport::TestCase
assert_equal 'contained_empty', Submodule::ContainedEmptyController.controller_name
end
- def test_record_identifier
- assert_respond_to RecordIdentifierController.new, :dom_id
- assert_respond_to RecordIdentifierController.new, :dom_class
- end
-
- def test_record_identifier_is_deprecated
- record = Comment.new
- record.save
-
- dom_id = nil
- assert_deprecated 'dom_id method will no longer' do
- dom_id = RecordIdentifierController.new.dom_id(record)
- end
-
- assert_equal 'comment_1', dom_id
-
- dom_class = nil
- assert_deprecated 'dom_class method will no longer' do
- dom_class = RecordIdentifierController.new.dom_class(record)
- end
- assert_equal 'comment', dom_class
- end
-
def test_no_deprecation_when_action_view_record_identifier_is_included
record = Comment.new
record.save
dom_id = nil
assert_not_deprecated do
- dom_id = RecordIdentifierWithoutDeprecationController.new.dom_id(record)
+ dom_id = RecordIdentifierIncludedController.new.dom_id(record)
end
assert_equal 'comment_1', dom_id
dom_class = nil
assert_not_deprecated do
- dom_class = RecordIdentifierWithoutDeprecationController.new.dom_class(record)
+ dom_class = RecordIdentifierIncludedController.new.dom_class(record)
end
assert_equal 'comment', dom_class
end
@@ -186,6 +166,12 @@ class PerformActionTest < ActionController::TestCase
assert_raise(AbstractController::ActionNotFound) { get :hidden_action }
assert_raise(AbstractController::ActionNotFound) { get :another_hidden_action }
end
+
+ def test_action_missing_should_work
+ use_controller ActionMissingController
+ get :arbitrary_action
+ assert_equal "Response for arbitrary_action", @response.body
+ end
end
class UrlOptionsTest < ActionController::TestCase
diff --git a/actionpack/test/controller/caching_test.rb b/actionpack/test/controller/caching_test.rb
index 65c18dfb64..57b45b8f7b 100644
--- a/actionpack/test/controller/caching_test.rb
+++ b/actionpack/test/controller/caching_test.rb
@@ -1,6 +1,5 @@
require 'fileutils'
require 'abstract_unit'
-require 'active_record_unit'
CACHE_DIR = 'test_cache'
# Don't change '/../temp/' cavalierly or you might hose something you don't want hosed
@@ -21,7 +20,7 @@ class FragmentCachingMetalTest < ActionController::TestCase
@controller = FragmentCachingMetalTestController.new
@controller.perform_caching = true
@controller.cache_store = @store
- @params = { controller: 'posts', action: 'index'}
+ @params = { controller: 'posts', action: 'index' }
@request = ActionController::TestRequest.new
@response = ActionController::TestResponse.new
@controller.params = @params
@@ -41,7 +40,7 @@ class CachingController < ActionController::Base
end
class FragmentCachingTestController < CachingController
- def some_action; end;
+ def some_action; end
end
class FragmentCachingTest < ActionController::TestCase
@@ -164,6 +163,9 @@ class FunctionalCachingController < CachingController
format.xml
end
end
+
+ def fragment_cached_without_digest
+ end
end
class FunctionalFragmentCachingTest < ActionController::TestCase
@@ -200,6 +202,15 @@ CACHED
@store.read("views/test.host/functional_caching/html_fragment_cached_with_partial/#{template_digest("functional_caching/_partial", "html")}"))
end
+ def test_skipping_fragment_cache_digesting
+ get :fragment_cached_without_digest, :format => "html"
+ assert_response :success
+ expected_body = "<body>\n<p>ERB</p>\n</body>\n"
+
+ assert_equal expected_body, @response.body
+ assert_equal "<p>ERB</p>", @store.read("views/nodigest")
+ end
+
def test_render_inline_before_fragment_caching
get :inline_fragment_cached
assert_response :success
@@ -284,17 +295,20 @@ class CacheHelperOutputBufferTest < ActionController::TestCase
end
end
-class DeprecatedPageCacheExtensionTest < ActiveSupport::TestCase
- def test_page_cache_extension_binds_default_static_extension
- deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- old_extension = ActionController::Base.default_static_extension
+class ViewCacheDependencyTest < ActionController::TestCase
+ class NoDependenciesController < ActionController::Base
+ end
+
+ class HasDependenciesController < ActionController::Base
+ view_cache_dependency { "trombone" }
+ view_cache_dependency { "flute" }
+ end
- ActionController::Base.page_cache_extension = '.rss'
+ def test_view_cache_dependencies_are_empty_by_default
+ assert NoDependenciesController.new.view_cache_dependencies.empty?
+ end
- assert_equal '.rss', ActionController::Base.default_static_extension
- ensure
- ActiveSupport::Deprecation.behavior = deprecation_behavior
- ActionController::Base.default_static_extension = old_extension
+ def test_view_cache_dependencies_are_listed_in_declaration_order
+ assert_equal %w(trombone flute), HasDependenciesController.new.view_cache_dependencies
end
end
diff --git a/actionpack/test/controller/capture_test.rb b/actionpack/test/controller/capture_test.rb
deleted file mode 100644
index 72263156d9..0000000000
--- a/actionpack/test/controller/capture_test.rb
+++ /dev/null
@@ -1,79 +0,0 @@
-require 'abstract_unit'
-require 'active_support/logger'
-
-class CaptureController < ActionController::Base
- def self.controller_name; "test"; end
- def self.controller_path; "test"; end
-
- def content_for
- @title = nil
- render :layout => "talk_from_action"
- end
-
- def content_for_with_parameter
- @title = nil
- render :layout => "talk_from_action"
- end
-
- def content_for_concatenated
- @title = nil
- render :layout => "talk_from_action"
- end
-
- def non_erb_block_content_for
- @title = nil
- render :layout => "talk_from_action"
- end
-
- def proper_block_detection
- @todo = "some todo"
- end
-end
-
-class CaptureTest < ActionController::TestCase
- tests CaptureController
-
- def setup
- super
- # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
- # a more accurate simulation of what happens in "real life".
- @controller.logger = ActiveSupport::Logger.new(nil)
-
- @request.host = "www.nextangle.com"
- end
-
- def test_simple_capture
- get :capturing
- assert_equal "Dreamy days", @response.body.strip
- end
-
- def test_content_for
- get :content_for
- assert_equal expected_content_for_output, @response.body
- end
-
- def test_should_concatentate_content_for
- get :content_for_concatenated
- assert_equal expected_content_for_output, @response.body
- end
-
- def test_should_set_content_for_with_parameter
- get :content_for_with_parameter
- assert_equal expected_content_for_output, @response.body
- end
-
- def test_non_erb_block_content_for
- get :non_erb_block_content_for
- assert_equal expected_content_for_output, @response.body
- end
-
- def test_proper_block_detection
- get :proper_block_detection
- assert_equal "some todo", @response.body
- end
-
- private
- def expected_content_for_output
- "<title>Putting stuff in the title!</title>\nGreat stuff!"
- end
-end
diff --git a/actionpack/test/controller/default_url_options_with_before_action_test.rb b/actionpack/test/controller/default_url_options_with_before_action_test.rb
new file mode 100644
index 0000000000..656fd0431e
--- /dev/null
+++ b/actionpack/test/controller/default_url_options_with_before_action_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+
+
+class ControllerWithBeforeActionAndDefaultUrlOptions < ActionController::Base
+
+ before_action { I18n.locale = params[:locale] }
+ after_action { I18n.locale = "en" }
+
+ def target
+ render :text => "final response"
+ end
+
+ def redirect
+ redirect_to :action => "target"
+ end
+
+ def default_url_options
+ {:locale => "de"}
+ end
+end
+
+class ControllerWithBeforeActionAndDefaultUrlOptionsTest < ActionController::TestCase
+
+ # This test has its roots in issue #1872
+ test "should redirect with correct locale :de" do
+ get :redirect, :locale => "de"
+ assert_redirected_to "/controller_with_before_action_and_default_url_options/target?locale=de"
+ end
+end
diff --git a/actionpack/test/controller/default_url_options_with_filter_test.rb b/actionpack/test/controller/default_url_options_with_filter_test.rb
deleted file mode 100644
index ef028e8cdb..0000000000
--- a/actionpack/test/controller/default_url_options_with_filter_test.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'abstract_unit'
-
-
-class ControllerWithBeforeFilterAndDefaultUrlOptions < ActionController::Base
-
- before_filter { I18n.locale = params[:locale] }
- after_filter { I18n.locale = "en" }
-
- def target
- render :text => "final response"
- end
-
- def redirect
- redirect_to :action => "target"
- end
-
- def default_url_options
- {:locale => "de"}
- end
-end
-
-class ControllerWithBeforeFilterAndDefaultUrlOptionsTest < ActionController::TestCase
-
- # This test has its roots in issue #1872
- test "should redirect with correct locale :de" do
- get :redirect, :locale => "de"
- assert_redirected_to "/controller_with_before_filter_and_default_url_options/target?locale=de"
- end
-end
diff --git a/actionpack/test/controller/filters_test.rb b/actionpack/test/controller/filters_test.rb
index d203601771..3b5d7ef446 100644
--- a/actionpack/test/controller/filters_test.rb
+++ b/actionpack/test/controller/filters_test.rb
@@ -10,7 +10,7 @@ class ActionController::Base
def before_filters
filters = _process_action_callbacks.select { |c| c.kind == :before }
- filters.map! { |c| c.instance_variable_get(:@raw_filter) }
+ filters.map! { |c| c.raw_filter }
end
end
@@ -208,11 +208,23 @@ class FilterTest < ActionController::TestCase
before_filter(ConditionalClassFilter, :ensure_login, Proc.new {|c| c.instance_variable_set(:"@ran_proc_filter1", true)}, :except => :show_without_filter) { |c| c.instance_variable_set(:"@ran_proc_filter2", true)}
end
+ class OnlyConditionalOptionsFilter < ConditionalFilterController
+ before_filter :ensure_login, :only => :index, :if => Proc.new {|c| c.instance_variable_set(:"@ran_conditional_index_proc", true) }
+ end
+
class ConditionalOptionsFilter < ConditionalFilterController
before_filter :ensure_login, :if => Proc.new { |c| true }
before_filter :clean_up_tmp, :if => Proc.new { |c| false }
end
+ class ConditionalOptionsSkipFilter < ConditionalFilterController
+ before_filter :ensure_login
+ before_filter :clean_up_tmp
+
+ skip_before_filter :ensure_login, if: -> { false }
+ skip_before_filter :clean_up_tmp, if: -> { true }
+ end
+
class PrependingController < TestController
prepend_before_filter :wonderful_life
# skip_before_filter :fire_flash
@@ -450,11 +462,9 @@ class FilterTest < ActionController::TestCase
class RescuingAroundFilterWithBlock
def around(controller)
- begin
- yield
- rescue ErrorToRescue => ex
- controller.__send__ :render, :text => "I rescued this: #{ex.inspect}"
- end
+ yield
+ rescue ErrorToRescue => ex
+ controller.__send__ :render, :text => "I rescued this: #{ex.inspect}"
end
end
@@ -499,18 +509,6 @@ class FilterTest < ActionController::TestCase
end
- class ::AppSweeper < ActionController::Caching::Sweeper; end
- class SweeperTestController < ActionController::Base
- cache_sweeper :app_sweeper
- def show
- render :text => 'hello world'
- end
-
- def error
- raise StandardError.new
- end
- end
-
class ImplicitActionsController < ActionController::Base
before_filter :find_only, :only => :edit
before_filter :find_except, :except => :edit
@@ -526,35 +524,6 @@ class FilterTest < ActionController::TestCase
end
end
- def test_sweeper_should_not_ignore_no_method_error
- sweeper = ActionController::Caching::Sweeper.send(:new)
- assert_raise NoMethodError do
- sweeper.send_not_defined
- end
- end
-
- def test_sweeper_should_not_block_rendering
- response = test_process(SweeperTestController)
- assert_equal 'hello world', response.body
- end
-
- def test_sweeper_should_clean_up_if_exception_is_raised
- assert_raise StandardError do
- test_process(SweeperTestController, 'error')
- end
- assert_nil AppSweeper.instance.controller
- end
-
- def test_before_method_of_sweeper_should_always_return_true
- sweeper = ActionController::Caching::Sweeper.send(:new)
- assert sweeper.before(TestController.new)
- end
-
- def test_after_method_of_sweeper_should_always_return_nil
- sweeper = ActionController::Caching::Sweeper.send(:new)
- assert_nil sweeper.after(TestController.new)
- end
-
def test_non_yielding_around_filters_not_returning_false_do_not_raise
controller = NonYieldingAroundFilterController.new
controller.instance_variable_set "@filter_return_value", true
@@ -636,6 +605,11 @@ class FilterTest < ActionController::TestCase
assert_equal %w( ensure_login ), assigns["ran_filter"]
end
+ def test_running_conditional_skip_options
+ test_process(ConditionalOptionsSkipFilter)
+ assert_equal %w( ensure_login ), assigns["ran_filter"]
+ end
+
def test_running_collection_condition_filters
test_process(ConditionalCollectionFilterController)
assert_equal %w( ensure_login ), assigns["ran_filter"]
@@ -679,6 +653,11 @@ class FilterTest < ActionController::TestCase
assert !assigns["ran_class_filter"]
end
+ def test_running_only_condition_and_conditional_options
+ test_process(OnlyConditionalOptionsFilter, "show")
+ assert_not assigns["ran_conditional_index_proc"]
+ end
+
def test_running_before_and_after_condition_filters
test_process(BeforeAndAfterConditionController)
assert_equal %w( ensure_login clean_up_tmp), assigns["ran_filter"]
diff --git a/actionpack/test/controller/flash_hash_test.rb b/actionpack/test/controller/flash_hash_test.rb
index ccca0dac17..5490d9394b 100644
--- a/actionpack/test/controller/flash_hash_test.rb
+++ b/actionpack/test/controller/flash_hash_test.rb
@@ -46,6 +46,27 @@ module ActionDispatch
assert_equal({'foo' => 'bar'}, @hash.to_hash)
end
+ def test_to_session_value
+ @hash['foo'] = 'bar'
+ assert_equal({'flashes' => {'foo' => 'bar'}, 'discard' => []}, @hash.to_session_value)
+
+ @hash.discard('foo')
+ assert_equal({'flashes' => {'foo' => 'bar'}, 'discard' => %w[foo]}, @hash.to_session_value)
+
+ @hash.now['qux'] = 1
+ assert_equal({'flashes' => {'foo' => 'bar', 'qux' => 1}, 'discard' => %w[foo qux]}, @hash.to_session_value)
+
+ @hash.sweep
+ assert_equal(nil, @hash.to_session_value)
+ end
+
+ def test_from_session_value
+ rails_3_2_cookie = 'BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJWY4ZTFiODE1MmJhNzYwOWMyOGJiYjE3ZWM5MjYzYmE3BjsAVEkiCmZsYXNoBjsARm86JUFjdGlvbkRpc3BhdGNoOjpGbGFzaDo6Rmxhc2hIYXNoCToKQHVzZWRvOghTZXQGOgpAaGFzaHsAOgxAY2xvc2VkRjoNQGZsYXNoZXN7BkkiDG1lc3NhZ2UGOwBGSSIKSGVsbG8GOwBGOglAbm93MA=='
+ session = Marshal.load(Base64.decode64(rails_3_2_cookie))
+ hash = Flash::FlashHash.from_session_value(session['flash'])
+ assert_equal({'flashes' => {'message' => 'Hello'}, 'discard' => %w[message]}, hash.to_session_value)
+ end
+
def test_empty?
assert @hash.empty?
@hash['zomg'] = 'bears'
diff --git a/actionpack/test/controller/flash_test.rb b/actionpack/test/controller/flash_test.rb
index 8340aab4d2..c64ffef654 100644
--- a/actionpack/test/controller/flash_test.rb
+++ b/actionpack/test/controller/flash_test.rb
@@ -1,4 +1,5 @@
require 'abstract_unit'
+require 'active_support/key_generator'
class FlashTest < ActionController::TestCase
class TestController < ActionController::Base
@@ -51,8 +52,8 @@ class FlashTest < ActionController::TestCase
render :inline => "hello"
end
- # methods for test_sweep_after_halted_filter_chain
- before_filter :halt_and_redir, :only => "filter_halting_action"
+ # methods for test_sweep_after_halted_action_chain
+ before_action :halt_and_redir, only: 'filter_halting_action'
def std_action
@flash_copy = {}.update(flash)
@@ -157,7 +158,7 @@ class FlashTest < ActionController::TestCase
assert_nil session["flash"]
end
- def test_sweep_after_halted_filter_chain
+ def test_sweep_after_halted_action_chain
get :std_action
assert_nil assigns["flash_copy"]["foo"]
get :filter_halting_action
@@ -213,11 +214,23 @@ class FlashTest < ActionController::TestCase
get :redirect_with_foo_flash
assert_equal "for great justice", @controller.send(:flash)[:foo]
end
+
+ class SubclassesTestController < TestController; end
+
+ def test_add_flash_type_to_subclasses
+ TestController.add_flash_types :foo
+ assert SubclassesTestController._flash_types.include?(:foo)
+ end
+
+ def test_do_not_add_flash_type_to_parent_class
+ SubclassesTestController.add_flash_types :bar
+ assert_not TestController._flash_types.include?(:bar)
+ end
end
class FlashIntegrationTest < ActionDispatch::IntegrationTest
SessionKey = '_myapp_session'
- SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
+ Generator = ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
class TestController < ActionController::Base
add_flash_types :bar
@@ -291,7 +304,7 @@ class FlashIntegrationTest < ActionDispatch::IntegrationTest
# Overwrite get to send SessionSecret in env hash
def get(path, parameters = nil, env = {})
- env["action_dispatch.secret_token"] ||= SessionSecret
+ env["action_dispatch.key_generator"] ||= Generator
super
end
diff --git a/actionpack/test/controller/force_ssl_test.rb b/actionpack/test/controller/force_ssl_test.rb
index 6758668b7a..3655b90e32 100644
--- a/actionpack/test/controller/force_ssl_test.rb
+++ b/actionpack/test/controller/force_ssl_test.rb
@@ -14,8 +14,42 @@ class ForceSSLControllerLevel < ForceSSLController
force_ssl
end
-class ForceSSLCustomDomain < ForceSSLController
- force_ssl :host => "secure.test.host"
+class ForceSSLCustomOptions < ForceSSLController
+ force_ssl :host => "secure.example.com", :only => :redirect_host
+ force_ssl :port => 8443, :only => :redirect_port
+ force_ssl :subdomain => 'secure', :only => :redirect_subdomain
+ force_ssl :domain => 'secure.com', :only => :redirect_domain
+ force_ssl :path => '/foo', :only => :redirect_path
+ force_ssl :status => :found, :only => :redirect_status
+ force_ssl :flash => { :message => 'Foo, Bar!' }, :only => :redirect_flash
+ force_ssl :alert => 'Foo, Bar!', :only => :redirect_alert
+ force_ssl :notice => 'Foo, Bar!', :only => :redirect_notice
+
+ def force_ssl_action
+ render :text => action_name
+ end
+
+ alias_method :redirect_host, :force_ssl_action
+ alias_method :redirect_port, :force_ssl_action
+ alias_method :redirect_subdomain, :force_ssl_action
+ alias_method :redirect_domain, :force_ssl_action
+ alias_method :redirect_path, :force_ssl_action
+ alias_method :redirect_status, :force_ssl_action
+ alias_method :redirect_flash, :force_ssl_action
+ alias_method :redirect_alert, :force_ssl_action
+ alias_method :redirect_notice, :force_ssl_action
+
+ def use_flash
+ render :text => flash[:message]
+ end
+
+ def use_alert
+ render :text => flash[:alert]
+ end
+
+ def use_notice
+ render :text => flash[:notice]
+ end
end
class ForceSSLOnlyAction < ForceSSLController
@@ -80,19 +114,77 @@ class ForceSSLControllerLevelTest < ActionController::TestCase
end
end
-class ForceSSLCustomDomainTest < ActionController::TestCase
- tests ForceSSLCustomDomain
+class ForceSSLCustomOptionsTest < ActionController::TestCase
+ tests ForceSSLCustomOptions
- def test_banana_redirects_to_https_with_custom_host
- get :banana
+ def setup
+ @request.env['HTTP_HOST'] = 'www.example.com:80'
+ end
+
+ def test_redirect_to_custom_host
+ get :redirect_host
assert_response 301
- assert_equal "https://secure.test.host/force_ssl_custom_domain/banana", redirect_to_url
+ assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_host", redirect_to_url
end
- def test_cheeseburger_redirects_to_https_with_custom_host
- get :cheeseburger
+ def test_redirect_to_custom_port
+ get :redirect_port
+ assert_response 301
+ assert_equal "https://www.example.com:8443/force_ssl_custom_options/redirect_port", redirect_to_url
+ end
+
+ def test_redirect_to_custom_subdomain
+ get :redirect_subdomain
assert_response 301
- assert_equal "https://secure.test.host/force_ssl_custom_domain/cheeseburger", redirect_to_url
+ assert_equal "https://secure.example.com/force_ssl_custom_options/redirect_subdomain", redirect_to_url
+ end
+
+ def test_redirect_to_custom_domain
+ get :redirect_domain
+ assert_response 301
+ assert_equal "https://www.secure.com/force_ssl_custom_options/redirect_domain", redirect_to_url
+ end
+
+ def test_redirect_to_custom_path
+ get :redirect_path
+ assert_response 301
+ assert_equal "https://www.example.com/foo", redirect_to_url
+ end
+
+ def test_redirect_to_custom_status
+ get :redirect_status
+ assert_response 302
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_status", redirect_to_url
+ end
+
+ def test_redirect_to_custom_flash
+ get :redirect_flash
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_flash", redirect_to_url
+
+ get :use_flash
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
+ end
+
+ def test_redirect_to_custom_alert
+ get :redirect_alert
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_alert", redirect_to_url
+
+ get :use_alert
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
+ end
+
+ def test_redirect_to_custom_notice
+ get :redirect_notice
+ assert_response 301
+ assert_equal "https://www.example.com/force_ssl_custom_options/redirect_notice", redirect_to_url
+
+ get :use_notice
+ assert_response 200
+ assert_equal "Foo, Bar!", @response.body
end
end
@@ -149,16 +241,79 @@ class ForceSSLFlashTest < ActionController::TestCase
assert_response 302
assert_equal "http://test.host/force_ssl_flash/cheeseburger", redirect_to_url
+ # FIXME: AC::TestCase#build_request_uri doesn't build a new uri if PATH_INFO exists
+ @request.env.delete('PATH_INFO')
+
get :cheeseburger
assert_response 301
assert_equal "https://test.host/force_ssl_flash/cheeseburger", redirect_to_url
+ # FIXME: AC::TestCase#build_request_uri doesn't build a new uri if PATH_INFO exists
+ @request.env.delete('PATH_INFO')
+
get :use_flash
assert_equal "hello", assigns["flash_copy"]["that"]
assert_equal "hello", assigns["flashy"]
end
end
+class ForceSSLDuplicateRoutesTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_path
+ with_routing do |set|
+ set.draw do
+ get '/foo', :to => 'force_ssl_controller_level#banana'
+ get '/bar', :to => 'force_ssl_controller_level#banana'
+ end
+
+ @request.env['PATH_INFO'] = '/bar'
+
+ get :banana
+ assert_response 301
+ assert_equal 'https://test.host/bar', redirect_to_url
+ end
+ end
+end
+
+class ForceSSLFormatTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_format
+ with_routing do |set|
+ set.draw do
+ get '/foo', :to => 'force_ssl_controller_level#banana'
+ end
+
+ get :banana, :format => :json
+ assert_response 301
+ assert_equal 'https://test.host/foo.json', redirect_to_url
+ end
+ end
+end
+
+class ForceSSLOptionalSegmentsTest < ActionController::TestCase
+ tests ForceSSLControllerLevel
+
+ def test_force_ssl_redirects_to_same_format
+ with_routing do |set|
+ set.draw do
+ scope '(:locale)' do
+ defaults :locale => 'en' do
+ get '/foo', :to => 'force_ssl_controller_level#banana'
+ end
+ end
+ end
+
+ @request.env['PATH_INFO'] = '/en/foo'
+ get :banana, :locale => 'en'
+ assert_equal 'en', @controller.params[:locale]
+ assert_response 301
+ assert_equal 'https://test.host/en/foo', redirect_to_url
+ end
+ end
+end
+
class RedirectToSSLTest < ActionController::TestCase
tests RedirectToSSL
def test_banana_redirects_to_https_if_not_https
diff --git a/actionpack/test/controller/helper_test.rb b/actionpack/test/controller/helper_test.rb
index 248c81193e..20f99f19ee 100644
--- a/actionpack/test/controller/helper_test.rb
+++ b/actionpack/test/controller/helper_test.rb
@@ -201,6 +201,12 @@ class HelperTest < ActiveSupport::TestCase
# fun/pdf_helper.rb
assert methods.include?(:foobar)
end
+
+ def test_helper_proxy_config
+ AllHelpersController.config.my_var = 'smth'
+
+ assert_equal 'smth', AllHelpersController.helpers.config.my_var
+ end
private
def expected_helper_methods
diff --git a/actionpack/test/controller/http_basic_authentication_test.rb b/actionpack/test/controller/http_basic_authentication_test.rb
index 7286b249c7..90548d4294 100644
--- a/actionpack/test/controller/http_basic_authentication_test.rb
+++ b/actionpack/test/controller/http_basic_authentication_test.rb
@@ -2,9 +2,9 @@ require 'abstract_unit'
class HttpBasicAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
- before_filter :authenticate, :only => :index
- before_filter :authenticate_with_request, :only => :display
- before_filter :authenticate_long_credentials, :only => :show
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
+ before_action :authenticate_long_credentials, only: :show
http_basic_authenticate_with :name => "David", :password => "Goliath", :only => :search
@@ -19,7 +19,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
def show
render :text => 'Only for loooooong credentials'
end
-
+
def search
render :text => 'All inline'
end
@@ -118,7 +118,7 @@ class HttpBasicAuthenticationTest < ActionController::TestCase
assert assigns(:logged_in)
assert_equal 'Definitely Maybe', @response.body
end
-
+
test "authenticate with class method" do
@request.env['HTTP_AUTHORIZATION'] = encode_credentials('David', 'Goliath')
get :search
diff --git a/actionpack/test/controller/http_digest_authentication_test.rb b/actionpack/test/controller/http_digest_authentication_test.rb
index b11ad633bd..9f1c168209 100644
--- a/actionpack/test/controller/http_digest_authentication_test.rb
+++ b/actionpack/test/controller/http_digest_authentication_test.rb
@@ -1,9 +1,10 @@
require 'abstract_unit'
+require 'active_support/key_generator'
class HttpDigestAuthenticationTest < ActionController::TestCase
class DummyDigestController < ActionController::Base
- before_filter :authenticate, :only => :index
- before_filter :authenticate_with_request, :only => :display
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
USERS = { 'lifo' => 'world', 'pretty' => 'please',
'dhh' => ::Digest::MD5::hexdigest(["dhh","SuperSecret","secret"].join(":"))}
@@ -40,8 +41,8 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
setup do
# Used as secret in generating nonce to prevent tampering of timestamp
- @secret = "session_options_secret"
- @request.env["action_dispatch.secret_token"] = @secret
+ @secret = "4fb45da9e4ab4ddeb7580d6a35503d99"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(@secret)
end
teardown do
@@ -247,6 +248,14 @@ class HttpDigestAuthenticationTest < ActionController::TestCase
assert_equal 'Definitely Maybe', @response.body
end
+ test "when sent a basic auth header, returns Unauthorized" do
+ @request.env['HTTP_AUTHORIZATION'] = 'Basic Gwf2aXq8ZLF3Hxq='
+
+ get :display
+
+ assert_response :unauthorized
+ end
+
private
def encode_credentials(options)
diff --git a/actionpack/test/controller/http_token_authentication_test.rb b/actionpack/test/controller/http_token_authentication_test.rb
index ad4e743be8..ebf6d224aa 100644
--- a/actionpack/test/controller/http_token_authentication_test.rb
+++ b/actionpack/test/controller/http_token_authentication_test.rb
@@ -2,9 +2,9 @@ require 'abstract_unit'
class HttpTokenAuthenticationTest < ActionController::TestCase
class DummyController < ActionController::Base
- before_filter :authenticate, :only => :index
- before_filter :authenticate_with_request, :only => :display
- before_filter :authenticate_long_credentials, :only => :show
+ before_action :authenticate, only: :index
+ before_action :authenticate_with_request, only: :display
+ before_action :authenticate_long_credentials, only: :show
def index
render :text => "Hello Secret"
@@ -104,17 +104,40 @@ class HttpTokenAuthenticationTest < ActionController::TestCase
assert_equal 'Token realm="SuperSecret"', @response.headers['WWW-Authenticate']
end
- test "authentication request with valid credential" do
- @request.env['HTTP_AUTHORIZATION'] = encode_credentials('"quote" pretty', :algorithm => 'test')
- get :display
+ test "token_and_options returns correct token" do
+ token = "rcHu+HzSFw89Ypyhn/896A=="
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with value after the equal sign" do
+ token = 'rcHu+=HzSFw89Ypyhn/896A==f34'
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
- assert_response :success
- assert assigns(:logged_in)
- assert_equal 'Definitely Maybe', @response.body
+ test "token_and_options returns correct token with slashes" do
+ token = 'rcHu+\\\\"/896A'
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
+ end
+
+ test "token_and_options returns correct token with quotes" do
+ token = '\"quote\" pretty'
+ actual = ActionController::HttpAuthentication::Token.token_and_options(sample_request(token)).first
+ expected = token
+ assert_equal(expected, actual)
end
private
+ def sample_request(token)
+ @sample_request ||= OpenStruct.new authorization: %{Token token="#{token}"}
+ end
+
def encode_credentials(token, options = {})
ActionController::HttpAuthentication::Token.encode_credentials(token, options)
end
diff --git a/actionpack/test/controller/integration_test.rb b/actionpack/test/controller/integration_test.rb
index cf561d913a..e851cc6a63 100644
--- a/actionpack/test/controller/integration_test.rb
+++ b/actionpack/test/controller/integration_test.rb
@@ -117,12 +117,6 @@ class SessionTest < ActiveSupport::TestCase
@session.head(path,params,headers)
end
- def test_options
- path = "/index"; params = "blah"; headers = {:location => 'blah'}
- @session.expects(:process).with(:options,path,params,headers)
- @session.options(path,params,headers)
- end
-
def test_xml_http_request_get
path = "/index"; params = "blah"; headers = {:location => 'blah'}
headers_after_xhr = headers.merge(
@@ -183,16 +177,6 @@ class SessionTest < ActiveSupport::TestCase
@session.xml_http_request(:head,path,params,headers)
end
- def test_xml_http_request_options
- path = "/index"; params = "blah"; headers = {:location => 'blah'}
- headers_after_xhr = headers.merge(
- "HTTP_X_REQUESTED_WITH" => "XMLHttpRequest",
- "HTTP_ACCEPT" => "text/javascript, text/html, application/xml, text/xml, */*"
- )
- @session.expects(:process).with(:options,path,params,headers_after_xhr)
- @session.xml_http_request(:options,path,params,headers)
- end
-
def test_xml_http_request_override_accept
path = "/index"; params = "blah"; headers = {:location => 'blah', "HTTP_ACCEPT" => "application/xml"}
headers_after_xhr = headers.merge(
@@ -250,7 +234,7 @@ class IntegrationTestUsesCorrectClass < ActionDispatch::IntegrationTest
@integration_session.stubs(:generic_url_rewriter)
@integration_session.stubs(:process)
- %w( get post head patch put delete options ).each do |verb|
+ %w( get post head patch put delete ).each do |verb|
assert_nothing_raised("'#{verb}' should use integration test methods") { __send__(verb, '/') }
end
end
@@ -293,7 +277,7 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
def redirect
- redirect_to :action => "get"
+ redirect_to action_url('get')
end
end
@@ -466,6 +450,58 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
assert_equal 'http://www.example.com/foo', url_for(:controller => "foo")
end
+ def test_port_via_host!
+ with_test_route_set do
+ host! 'www.example.com:8080'
+ get '/get'
+ assert_equal 8080, request.port
+ end
+ end
+
+ def test_port_via_process
+ with_test_route_set do
+ get 'http://www.example.com:8080/get'
+ assert_equal 8080, request.port
+ end
+ end
+
+ def test_https_and_port_via_host_and_https!
+ with_test_route_set do
+ host! 'www.example.com'
+ https! true
+
+ get '/get'
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ host! 'www.example.com:443'
+ https! true
+
+ get '/get'
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ host! 'www.example.com:8443'
+ https! true
+
+ get '/get'
+ assert_equal 8443, request.port
+ assert_equal true, request.ssl?
+ end
+ end
+
+ def test_https_and_port_via_process
+ with_test_route_set do
+ get 'https://www.example.com/get'
+ assert_equal 443, request.port
+ assert_equal true, request.ssl?
+
+ get 'https://www.example.com:8443/get'
+ assert_equal 8443, request.port
+ assert_equal true, request.ssl?
+ end
+ end
+
private
def with_test_route_set
with_routing do |set|
@@ -475,8 +511,8 @@ class IntegrationProcessTest < ActionDispatch::IntegrationTest
end
set.draw do
- match ':action', :to => controller, :via => [:get, :post]
- get 'get/:action', :to => controller
+ match ':action', :to => controller, :via => [:get, :post], :as => :action
+ get 'get/:action', :to => controller, :as => :get_action
end
self.singleton_class.send(:include, set.url_helpers)
@@ -521,6 +557,21 @@ class MetalIntegrationTest < ActionDispatch::IntegrationTest
def test_generate_url_without_controller
assert_equal 'http://www.example.com/foo', url_for(:controller => "foo")
end
+
+ def test_pass_headers
+ get "/success", {}, "Referer" => "http://www.example.com/foo", "Host" => "http://nohost.com"
+
+ assert_equal "http://nohost.com", @request.env["HTTP_HOST"]
+ assert_equal "http://www.example.com/foo", @request.env["HTTP_REFERER"]
+ end
+
+ def test_pass_env
+ get "/success", {}, "HTTP_REFERER" => "http://test.com/", "HTTP_HOST" => "http://test.com"
+
+ assert_equal "http://test.com", @request.env["HTTP_HOST"]
+ assert_equal "http://test.com/", @request.env["HTTP_REFERER"]
+ end
+
end
class ApplicationIntegrationTest < ActionDispatch::IntegrationTest
@@ -699,13 +750,17 @@ class UrlOptionsIntegrationTest < ActionDispatch::IntegrationTest
assert_equal "http://bar.com/foo", foos_url
end
- test "test can override default url options" do
+ def test_can_override_default_url_options
+ original_host = default_url_options.dup
+
default_url_options[:host] = "foobar.com"
assert_equal "http://foobar.com/foo", foos_url
get "/bar"
assert_response :success
assert_equal "http://foobar.com/foo", foos_url
+ ensure
+ ActionDispatch::Integration::Session.default_url_options = self.default_url_options = original_host
end
test "current request path parameters are recalled" do
diff --git a/actionpack/test/controller/layout_test.rb b/actionpack/test/controller/layout_test.rb
deleted file mode 100644
index 94a8d2f180..0000000000
--- a/actionpack/test/controller/layout_test.rb
+++ /dev/null
@@ -1,237 +0,0 @@
-require 'abstract_unit'
-require 'rbconfig'
-
-# The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited
-# 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/layout_tests/' ]
-
-class LayoutTest < ActionController::Base
- def self.controller_path; 'views' end
- def self._implied_layout_name; to_s.underscore.gsub(/_controller$/, '') ; end
- self.view_paths = ActionController::Base.view_paths.dup
-end
-
-# Restore view_paths to previous value
-ActionController::Base.view_paths = old_load_paths
-
-class ProductController < LayoutTest
-end
-
-class ItemController < LayoutTest
-end
-
-class ThirdPartyTemplateLibraryController < LayoutTest
-end
-
-module ControllerNameSpace
-end
-
-class ControllerNameSpace::NestedController < LayoutTest
-end
-
-class MultipleExtensions < LayoutTest
-end
-
-class LayoutAutoDiscoveryTest < ActionController::TestCase
- def setup
- super
- @request.host = "www.nextangle.com"
- end
-
- def test_application_layout_is_default_when_no_controller_match
- @controller = ProductController.new
- get :hello
- assert_equal 'layout_test.erb hello.erb', @response.body
- end
-
- def test_controller_name_layout_name_match
- @controller = ItemController.new
- get :hello
- assert_equal 'item.erb hello.erb', @response.body
- 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
- end
-
- def test_namespaced_controllers_auto_detect_layouts1
- @controller = ControllerNameSpace::NestedController.new
- get :hello
- assert_equal 'controller_name_space/nested.erb hello.erb', @response.body
- end
-
- def test_namespaced_controllers_auto_detect_layouts2
- @controller = MultipleExtensions.new
- get :hello
- assert_equal 'multiple_extensions.html.erb hello.erb', @response.body.strip
- end
-end
-
-class DefaultLayoutController < LayoutTest
-end
-
-class StreamingLayoutController < LayoutTest
- def render(*args)
- options = args.extract_options! || {}
- super(*args, options.merge(:stream => true))
- end
-end
-
-class AbsolutePathLayoutController < LayoutTest
- layout File.expand_path(File.expand_path(__FILE__) + '/../../fixtures/layout_tests/layouts/layout_test')
-end
-
-class HasOwnLayoutController < LayoutTest
- layout 'item'
-end
-
-class PrependsViewPathController < LayoutTest
- def hello
- prepend_view_path File.dirname(__FILE__) + '/../fixtures/layout_tests/alt/'
- render :layout => 'alt'
- end
-end
-
-class OnlyLayoutController < LayoutTest
- layout 'item', :only => "hello"
-end
-
-class ExceptLayoutController < LayoutTest
- layout 'item', :except => "goodbye"
-end
-
-class SetsLayoutInRenderController < LayoutTest
- def hello
- render :layout => 'third_party_template_library'
- end
-end
-
-class RendersNoLayoutController < LayoutTest
- def hello
- render :layout => false
- end
-end
-
-class LayoutSetInResponseTest < ActionController::TestCase
- include ActionView::Template::Handlers
-
- def test_layout_set_when_using_default_layout
- @controller = DefaultLayoutController.new
- get :hello
- assert_template :layout => "layouts/layout_test"
- end
-
- def test_layout_set_when_using_streaming_layout
- @controller = StreamingLayoutController.new
- get :hello
- assert_template :hello
- end
-
- def test_layout_set_when_set_in_controller
- @controller = HasOwnLayoutController.new
- get :hello
- assert_template :layout => "layouts/item"
- end
-
- def test_layout_only_exception_when_included
- @controller = OnlyLayoutController.new
- get :hello
- assert_template :layout => "layouts/item"
- end
-
- def test_layout_only_exception_when_excepted
- @controller = OnlyLayoutController.new
- get :goodbye
- assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
- end
-
- def test_layout_except_exception_when_included
- @controller = ExceptLayoutController.new
- get :hello
- assert_template :layout => "layouts/item"
- end
-
- def test_layout_except_exception_when_excepted
- @controller = ExceptLayoutController.new
- get :goodbye
- assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
- end
-
- def test_layout_set_when_using_render
- @controller = SetsLayoutInRenderController.new
- get :hello
- assert_template :layout => "layouts/third_party_template_library"
- end
-
- def test_layout_is_not_set_when_none_rendered
- @controller = RendersNoLayoutController.new
- get :hello
- assert_template :layout => nil
- end
-
- def test_layout_is_picked_from_the_controller_instances_view_path
- @controller = PrependsViewPathController.new
- get :hello
- assert_template :layout => /layouts\/alt/
- end
-
- def test_absolute_pathed_layout
- @controller = AbsolutePathLayoutController.new
- get :hello
- assert_equal "layout_test.erb hello.erb", @response.body.strip
- end
-end
-
-class RenderWithTemplateOptionController < LayoutTest
- def hello
- render :template => 'alt/hello'
- end
-end
-
-class SetsNonExistentLayoutFile < LayoutTest
- layout "nofile"
-end
-
-class LayoutExceptionRaisedTest < ActionController::TestCase
- def test_exception_raised_when_layout_file_not_found
- @controller = SetsNonExistentLayoutFile.new
- assert_raise(ActionView::MissingTemplate) { get :hello }
- end
-end
-
-class LayoutStatusIsRendered < LayoutTest
- def hello
- render :status => 401
- end
-end
-
-class LayoutStatusIsRenderedTest < ActionController::TestCase
- def test_layout_status_is_rendered
- @controller = LayoutStatusIsRendered.new
- get :hello
- assert_response 401
- end
-end
-
-unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
- class LayoutSymlinkedTest < LayoutTest
- layout "symlinked/symlinked_layout"
- end
-
- class LayoutSymlinkedIsRenderedTest < ActionController::TestCase
- def test_symlinked_layout_is_rendered
- @controller = LayoutSymlinkedTest.new
- get :hello
- assert_response 200
- assert_template :layout => "layouts/symlinked/symlinked_layout"
- end
- end
-end
diff --git a/actionpack/test/controller/live_stream_test.rb b/actionpack/test/controller/live_stream_test.rb
index 20e433d1ec..0a431270b5 100644
--- a/actionpack/test/controller/live_stream_test.rb
+++ b/actionpack/test/controller/live_stream_test.rb
@@ -2,6 +2,94 @@ require 'abstract_unit'
require 'active_support/concurrency/latch'
module ActionController
+ class SSETest < ActionController::TestCase
+ class SSETestController < ActionController::Base
+ include ActionController::Live
+
+ def basic_sse
+ response.headers['Content-Type'] = 'text/event-stream'
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_event
+ sse = SSE.new(response.stream, event: "send-name")
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" })
+ ensure
+ sse.close
+ end
+
+ def sse_with_retry
+ sse = SSE.new(response.stream, retry: 1000)
+ sse.write("{\"name\":\"John\"}")
+ sse.write({ name: "Ryan" }, retry: 1500)
+ ensure
+ sse.close
+ end
+
+ def sse_with_id
+ sse = SSE.new(response.stream)
+ sse.write("{\"name\":\"John\"}", id: 1)
+ sse.write({ name: "Ryan" }, id: 2)
+ ensure
+ sse.close
+ end
+ end
+
+ tests SSETestController
+
+ def wait_for_response_stream_close
+ while !response.stream.closed?
+ sleep 0.01
+ end
+ end
+
+ def test_basic_sse
+ get :basic_sse
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ end
+
+ def test_sse_with_event_name
+ get :sse_with_event
+
+ wait_for_response_stream_close
+ assert_match(/data: {\"name\":\"John\"}/, response.body)
+ assert_match(/data: {\"name\":\"Ryan\"}/, response.body)
+ assert_match(/event: send-name/, response.body)
+ end
+
+ def test_sse_with_retry
+ get :sse_with_retry
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/retry: 1000/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/retry: 1500/, second_response)
+ end
+
+ def test_sse_with_id
+ get :sse_with_id
+
+ wait_for_response_stream_close
+ first_response, second_response = response.body.split("\n\n")
+ assert_match(/data: {\"name\":\"John\"}/, first_response)
+ assert_match(/id: 1/, first_response)
+
+ assert_match(/data: {\"name\":\"Ryan\"}/, second_response)
+ assert_match(/id: 2/, second_response)
+ end
+ end
+
class LiveStreamTest < ActionController::TestCase
class TestController < ActionController::Base
include ActionController::Live
@@ -40,7 +128,7 @@ module ActionController
def thread_locals
tc.assert_equal 'aaron', Thread.current[:setting]
- tc.refute_equal Thread.current.object_id, Thread.current[:originating_thread]
+ tc.assert_not_equal Thread.current.object_id, Thread.current[:originating_thread]
response.headers['Content-Type'] = 'text/event-stream'
%w{ hello world }.each do |word|
@@ -48,6 +136,33 @@ module ActionController
end
response.stream.close
end
+
+ def with_stale
+ render :text => 'stale' if stale?(:etag => "123")
+ end
+
+ def exception_in_view
+ render 'doesntexist'
+ end
+
+ def exception_with_callback
+ response.headers['Content-Type'] = 'text/event-stream'
+
+ response.stream.on_error do
+ response.stream.write %(data: "500 Internal Server Error"\n\n)
+ response.stream.close
+ end
+
+ raise 'An exception occurred...'
+ 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 params[:widget][:didnt_check_for_nil]
+ end
end
tests TestController
@@ -62,6 +177,21 @@ module ActionController
TestResponse.new
end
+ def assert_stream_closed
+ assert response.stream.closed?, 'stream should be closed'
+ end
+
+ def capture_log_output
+ output = StringIO.new
+ old_logger, ActionController::Base.logger = ActionController::Base.logger, ActiveSupport::Logger.new(output)
+
+ begin
+ yield output
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+
def test_set_response!
@controller.set_response!(@request)
assert_kind_of(Live::Response, @controller.response)
@@ -93,7 +223,7 @@ module ActionController
@controller.process :blocking_stream
- assert t.join
+ assert t.join(3), 'timeout expired before the thread terminated'
end
def test_thread_locals_get_copied
@@ -115,7 +245,54 @@ module ActionController
def test_render_text
get :render_text
assert_equal 'zomg', response.body
- assert response.stream.closed?, 'stream should be closed'
+ assert_stream_closed
+ end
+
+ def test_exception_handling_html
+ capture_log_output do |output|
+ get :exception_in_view
+ 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
+ end
+
+ def test_exception_handling_plain_text
+ capture_log_output do |output|
+ get :exception_in_view, 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
+ capture_log_output do |output|
+ get :exception_with_callback, format: 'text/event-stream'
+ assert_equal %(data: "500 Internal Server Error"\n\n), response.body
+ assert_match 'An exception occurred...', output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_exceptions_raised_handling_exceptions
+ capture_log_output do |output|
+ get :exception_in_exception_callback, format: 'text/event-stream'
+ assert_equal '', response.body
+ assert_match 'We need to go deeper', output.rewind && output.read
+ assert_stream_closed
+ end
+ end
+
+ def test_stale_without_etag
+ get :with_stale
+ assert_equal 200, @response.status.to_i
+ end
+
+ def test_stale_with_etag
+ @request.if_none_match = Digest::MD5.hexdigest("123")
+ get :with_stale
+ assert_equal 304, @response.status.to_i
end
end
end
diff --git a/actionpack/test/controller/localized_templates_test.rb b/actionpack/test/controller/localized_templates_test.rb
index 41ff2f3809..6b02eedaed 100644
--- a/actionpack/test/controller/localized_templates_test.rb
+++ b/actionpack/test/controller/localized_templates_test.rb
@@ -9,14 +9,29 @@ class LocalizedTemplatesTest < ActionController::TestCase
tests LocalizedController
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
-end \ No newline at end of file
+
+ def test_use_fallback_locales
+ I18n.locale = :"de-AT"
+ I18n.backend.class.send(:include, I18n::Backend::Fallbacks)
+ I18n.fallbacks[:"de-AT"] = [:de]
+
+ get :hello_world
+ assert_equal "Gutten Tag", @response.body
+ end
+end
diff --git a/actionpack/test/controller/log_subscriber_test.rb b/actionpack/test/controller/log_subscriber_test.rb
index 9efb6ab95f..075347be52 100644
--- a/actionpack/test/controller/log_subscriber_test.rb
+++ b/actionpack/test/controller/log_subscriber_test.rb
@@ -13,7 +13,7 @@ module Another
head :status => 406
end
- before_filter :redirector, :only => :never_executed
+ before_action :redirector, only: :never_executed
def never_executed
end
@@ -26,6 +26,10 @@ module Another
redirect_to "http://foo.bar/"
end
+ def filterable_redirector
+ redirect_to "http://secret.foo.bar/"
+ end
+
def data_sender
send_data "cool data", :filename => "file.txt"
end
@@ -42,6 +46,22 @@ module Another
render :inline => "<%= cache('foo%bar'){ 'Contains % sign in key' } %>"
end
+ def with_fragment_cache_if_with_true_condition
+ render :inline => "<%= cache_if(true, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_if_with_false_condition
+ render :inline => "<%= cache_if(false, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_unless_with_false_condition
+ render :inline => "<%= cache_unless(false, 'foo') { 'bar' } %>"
+ end
+
+ def with_fragment_cache_unless_with_true_condition
+ render :inline => "<%= cache_unless(true, 'foo') { 'bar' } %>"
+ end
+
def with_exception
raise Exception
end
@@ -152,6 +172,24 @@ class ACLogSubscriberTest < ActionController::TestCase
assert_equal "Redirected to http://foo.bar/", logs[1]
end
+ def test_filter_redirect_url_by_string
+ @request.env['action_dispatch.redirect_filter'] = ['secret']
+ get :filterable_redirector
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal "Redirected to [FILTERED]", logs[1]
+ end
+
+ def test_filter_redirect_url_by_regexp
+ @request.env['action_dispatch.redirect_filter'] = [/secret\.foo.+/]
+ get :filterable_redirector
+ wait
+
+ assert_equal 3, logs.size
+ assert_equal "Redirected to [FILTERED]", logs[1]
+ end
+
def test_send_data
get :data_sender
wait
@@ -181,6 +219,54 @@ class ACLogSubscriberTest < ActionController::TestCase
@controller.config.perform_caching = true
end
+ def test_with_fragment_cache_if_with_true
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_if_with_true_condition
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_if_with_false
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_if_with_false_condition
+ wait
+
+ assert_equal 2, logs.size
+ assert_no_match(/Read fragment views\/foo/, logs[1])
+ assert_no_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_unless_with_true
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_unless_with_true_condition
+ wait
+
+ assert_equal 2, logs.size
+ assert_no_match(/Read fragment views\/foo/, logs[1])
+ assert_no_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
+ def test_with_fragment_cache_unless_with_false
+ @controller.config.perform_caching = true
+ get :with_fragment_cache_unless_with_false_condition
+ wait
+
+ assert_equal 4, logs.size
+ assert_match(/Read fragment views\/foo/, logs[1])
+ assert_match(/Write fragment views\/foo/, logs[2])
+ ensure
+ @controller.config.perform_caching = true
+ end
+
def test_with_fragment_cache_and_percent_in_key
@controller.config.perform_caching = true
get :with_fragment_cache_and_percent_in_key
diff --git a/actionpack/test/controller/mime/accept_format_test.rb b/actionpack/test/controller/mime/accept_format_test.rb
new file mode 100644
index 0000000000..c03c7edeb8
--- /dev/null
+++ b/actionpack/test/controller/mime/accept_format_test.rb
@@ -0,0 +1,94 @@
+require 'abstract_unit'
+
+class StarStarMimeController < ActionController::Base
+ layout nil
+
+ def index
+ render
+ end
+end
+
+class StarStarMimeControllerTest < ActionController::TestCase
+ tests StarStarMimeController
+
+ def test_javascript_with_format
+ @request.accept = "text/javascript"
+ get :index, :format => 'js'
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format
+ @request.accept = "text/javascript"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+
+ def test_javascript_with_no_format_only_star_star
+ @request.accept = "*/*"
+ get :index
+ assert_match "function addition(a,b){ return a+b; }", @response.body
+ end
+end
+
+class AbstractPostController < ActionController::Base
+ self.view_paths = File.dirname(__FILE__) + "/../../fixtures/post_test/"
+end
+
+# For testing layouts which are set automatically
+class PostController < AbstractPostController
+ around_action :with_iphone
+
+ def index
+ respond_to(:html, :iphone, :js)
+ end
+
+protected
+
+ def with_iphone
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+ yield
+ end
+end
+
+class SuperPostController < PostController
+end
+
+class MimeControllerLayoutsTest < ActionController::TestCase
+ tests PostController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ end
+
+ def test_missing_layout_renders_properly
+ get :index
+ assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal 'Hello iPhone', @response.body
+ end
+
+ def test_format_with_inherited_layouts
+ @controller = SuperPostController.new
+
+ get :index
+ assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
+
+ @request.accept = "text/iphone"
+ get :index
+ assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
+ end
+
+ def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
+ get :index, :format => :js
+ assert_equal "Hello Firefox", @response.body
+ end
+end
diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb
new file mode 100644
index 0000000000..774dabe105
--- /dev/null
+++ b/actionpack/test/controller/mime/respond_to_test.rb
@@ -0,0 +1,493 @@
+require 'abstract_unit'
+
+class RespondToController < ActionController::Base
+ layout :set_layout
+
+ def html_xml_or_rss
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ type.rss { render :text => "RSS" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def js_or_html
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.js { render :text => "JS" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def json_or_yaml
+ respond_to do |type|
+ type.json { render :text => "JSON" }
+ type.yaml { render :text => "YAML" }
+ end
+ end
+
+ def html_or_xml
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def json_xml_or_html
+ respond_to do |type|
+ type.json { render :text => 'JSON' }
+ type.xml { render :xml => 'XML' }
+ type.html { render :text => 'HTML' }
+ end
+ end
+
+
+ def forced_xml
+ request.format = :xml
+
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.xml { render :text => "XML" }
+ end
+ end
+
+ def just_xml
+ respond_to do |type|
+ type.xml { render :text => "XML" }
+ end
+ end
+
+ def using_defaults
+ respond_to do |type|
+ type.html
+ type.xml
+ end
+ end
+
+ def using_defaults_with_type_list
+ respond_to(:html, :xml)
+ end
+
+ def using_defaults_with_all
+ respond_to do |type|
+ type.html
+ type.all{ render text: "ALL" }
+ end
+ end
+
+ def made_for_content_type
+ respond_to do |type|
+ type.rss { render :text => "RSS" }
+ type.atom { render :text => "ATOM" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+ def custom_type_handling
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.custom("application/crazy-xml") { render :text => "Crazy XML" }
+ type.all { render :text => "Nothing" }
+ end
+ end
+
+
+ def custom_constant_handling
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.mobile { render :text => "Mobile" }
+ end
+ end
+
+ def custom_constant_handling_without_block
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.mobile
+ end
+ end
+
+ def handle_any
+ respond_to do |type|
+ type.html { render :text => "HTML" }
+ type.any(:js, :xml) { render :text => "Either JS or XML" }
+ end
+ end
+
+ def handle_any_any
+ respond_to do |type|
+ type.html { render :text => 'HTML' }
+ type.any { render :text => 'Whatever you ask for, I got it' }
+ end
+ end
+
+ def all_types_with_layout
+ respond_to do |type|
+ type.html
+ end
+ end
+
+ def iphone_with_html_response_type
+ request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox" }
+ type.iphone { @type = "iPhone" }
+ end
+ end
+
+ def iphone_with_html_response_type_without_layout
+ request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
+
+ respond_to do |type|
+ type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
+ type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
+ end
+ end
+
+ protected
+ def set_layout
+ case action_name
+ when "all_types_with_layout", "iphone_with_html_response_type"
+ "respond_to/layouts/standard"
+ when "iphone_with_html_response_type_without_layout"
+ "respond_to/layouts/missing"
+ end
+ end
+end
+
+class RespondToControllerTest < ActionController::TestCase
+ tests RespondToController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias("text/html", :iphone)
+ Mime::Type.register("text/x-mobile", :mobile)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ Mime::Type.unregister(:mobile)
+ end
+
+ def test_html
+ @request.accept = "text/html"
+ get :js_or_html
+ assert_equal 'HTML', @response.body
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ assert_raises(ActionController::UnknownFormat) do
+ get :just_xml
+ end
+ end
+
+ def test_all
+ @request.accept = "*/*"
+ get :js_or_html
+ assert_equal 'HTML', @response.body # js is not part of all
+
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ get :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_xml
+ @request.accept = "application/xml"
+ get :html_xml_or_rss
+ assert_equal 'XML', @response.body
+ end
+
+ def test_js_or_html
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "text/javascript, text/html"
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript, text/html"
+
+ assert_raises(ActionController::UnknownFormat) do
+ xhr :get, :just_xml
+ end
+ end
+
+ def test_json_or_yaml_with_leading_star_star
+ @request.accept = "*/*, application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "*/* , application/json"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_json_or_yaml
+ xhr :get, :json_or_yaml
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'json'
+ assert_equal 'JSON', @response.body
+
+ get :json_or_yaml, :format => 'yaml'
+ assert_equal 'YAML', @response.body
+
+ { 'YAML' => %w(text/yaml),
+ 'JSON' => %w(application/json text/x-json)
+ }.each do |body, content_types|
+ content_types.each do |content_type|
+ @request.accept = content_type
+ get :json_or_yaml
+ assert_equal body, @response.body
+ end
+ end
+ end
+
+ def test_js_or_anything
+ @request.accept = "text/javascript, */*"
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+
+ xhr :get, :html_or_xml
+ assert_equal 'HTML', @response.body
+
+ xhr :get, :just_xml
+ assert_equal 'XML', @response.body
+ end
+
+ def test_using_defaults
+ @request.accept = "*/*"
+ get :using_defaults
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_using_defaults_with_all
+ @request.accept = "*/*"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "text/html"
+ get :using_defaults_with_all
+ assert_equal "HTML!", @response.body.strip
+
+ @request.accept = "application/json"
+ get :using_defaults_with_all
+ assert_equal "ALL", @response.body
+ end
+
+ def test_using_defaults_with_type_list
+ @request.accept = "*/*"
+ get :using_defaults_with_type_list
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "application/xml"
+ get :using_defaults_with_type_list
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<p>Hello world!</p>\n", @response.body
+ end
+
+ def test_with_atom_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/atom+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "ATOM", @response.body
+ end
+
+ def test_with_rss_content_type
+ @request.accept = ""
+ @request.env["CONTENT_TYPE"] = "application/rss+xml"
+ xhr :get, :made_for_content_type
+ assert_equal "RSS", @response.body
+ end
+
+ def test_synonyms
+ @request.accept = "application/javascript"
+ get :js_or_html
+ assert_equal 'JS', @response.body
+
+ @request.accept = "application/x-xml"
+ get :html_xml_or_rss
+ assert_equal "XML", @response.body
+ end
+
+ def test_custom_types
+ @request.accept = "application/crazy-xml"
+ get :custom_type_handling
+ assert_equal "application/crazy-xml", @response.content_type
+ assert_equal 'Crazy XML', @response.body
+
+ @request.accept = "text/html"
+ get :custom_type_handling
+ assert_equal "text/html", @response.content_type
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_xhtml_alias
+ @request.accept = "application/xhtml+xml,application/xml"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_firefox_simulation
+ @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
+ get :html_or_xml
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any
+ @request.accept = "*/*"
+ get :handle_any
+ assert_equal 'HTML', @response.body
+
+ @request.accept = "text/javascript"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+
+ @request.accept = "text/xml"
+ get :handle_any
+ assert_equal 'Either JS or XML', @response.body
+ end
+
+ def test_handle_any_any
+ @request.accept = "*/*"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_parameter_format
+ get :handle_any_any, {:format=>'html'}
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_explicit_html
+ @request.accept = "text/html"
+ get :handle_any_any
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_handle_any_any_javascript
+ @request.accept = "text/javascript"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_handle_any_any_xml
+ @request.accept = "text/xml"
+ get :handle_any_any
+ assert_equal 'Whatever you ask for, I got it', @response.body
+ end
+
+ def test_browser_check_with_any_any
+ @request.accept = "application/json, application/xml"
+ get :json_xml_or_html
+ assert_equal 'JSON', @response.body
+
+ @request.accept = "application/json, application/xml, */*"
+ get :json_xml_or_html
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_html_type_with_layout
+ @request.accept = "text/html"
+ get :all_types_with_layout
+ assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
+ end
+
+ def test_xhr
+ xhr :get, :js_or_html
+ assert_equal 'JS', @response.body
+ end
+
+ def test_custom_constant
+ get :custom_constant_handling, :format => "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_custom_constant_handling_without_block
+ get :custom_constant_handling_without_block, :format => "mobile"
+ assert_equal "text/x-mobile", @response.content_type
+ assert_equal "Mobile", @response.body
+ end
+
+ def test_forced_format
+ get :html_xml_or_rss
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, :format => "html"
+ assert_equal "HTML", @response.body
+
+ get :html_xml_or_rss, :format => "xml"
+ assert_equal "XML", @response.body
+
+ get :html_xml_or_rss, :format => "rss"
+ assert_equal "RSS", @response.body
+ end
+
+ def test_internally_forced_format
+ get :forced_xml
+ assert_equal "XML", @response.body
+
+ get :forced_xml, :format => "html"
+ assert_equal "XML", @response.body
+ end
+
+ def test_extension_synonyms
+ get :html_xml_or_rss, :format => "xhtml"
+ assert_equal "HTML", @response.body
+ end
+
+ def test_render_action_for_html
+ @controller.instance_eval do
+ def render(*args)
+ @action = args.first[:action] unless args.empty?
+ @action ||= action_name
+
+ response.body = "#{@action} - #{formats}"
+ end
+ end
+
+ get :using_defaults
+ assert_equal "using_defaults - #{[:html].to_s}", @response.body
+
+ get :using_defaults, :format => "xml"
+ assert_equal "using_defaults - #{[:xml].to_s}", @response.body
+ end
+
+ def test_format_with_custom_response_type
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
+
+ get :iphone_with_html_response_type, :format => "iphone"
+ assert_equal "text/html", @response.content_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ end
+
+ def test_format_with_custom_response_type_and_request_headers
+ @request.accept = "text/iphone"
+ get :iphone_with_html_response_type
+ assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_invalid_format
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_defaults, :format => "invalidformat"
+ end
+ end
+end
diff --git a/actionpack/test/controller/mime/respond_with_test.rb b/actionpack/test/controller/mime/respond_with_test.rb
new file mode 100644
index 0000000000..a70592fa1b
--- /dev/null
+++ b/actionpack/test/controller/mime/respond_with_test.rb
@@ -0,0 +1,714 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class RespondWithController < ActionController::Base
+ 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' ]
+
+ def using_resource
+ respond_with(resource)
+ end
+
+ def using_hash_resource
+ respond_with({:result => resource})
+ end
+
+ def using_resource_with_block
+ respond_with(resource) do |format|
+ format.csv { render :text => "CSV" }
+ end
+ end
+
+ def using_resource_with_overwrite_block
+ respond_with(resource) do |format|
+ format.html { render :text => "HTML" }
+ end
+ end
+
+ def using_resource_with_collection
+ respond_with([resource, Customer.new("jamis", 9)])
+ end
+
+ def using_resource_with_parent
+ respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13))
+ end
+
+ def using_resource_with_status_and_location
+ respond_with(resource, :location => "http://test.host/", :status => :created)
+ end
+
+ def using_invalid_resource_with_template
+ respond_with(resource)
+ end
+
+ def using_options_with_template
+ @customer = resource
+ respond_with(@customer, :status => 123, :location => "http://test.host/")
+ end
+
+ def using_resource_with_responder
+ responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
+ respond_with(resource, :responder => responder)
+ end
+
+ def using_resource_with_action
+ respond_with(resource, :action => :foo) do |format|
+ format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
+ end
+ end
+
+ def using_responder_with_respond
+ responder = Class.new(ActionController::Responder) do
+ def respond; @controller.render :text => "respond #{format}"; end
+ end
+ respond_with(resource, :responder => responder)
+ end
+
+ def respond_with_additional_params
+ @params = RespondWithController.params
+ respond_with({:result => resource}, @params)
+ end
+
+protected
+ def self.params
+ {
+ :foo => 'bar'
+ }
+ end
+
+ def resource
+ Customer.new("david", request.delete? ? nil : 13)
+ end
+end
+
+class InheritedRespondWithController < RespondWithController
+ clear_respond_to
+ respond_to :xml, :json
+
+ def index
+ respond_with(resource) do |format|
+ format.json { render :text => "JSON" }
+ end
+ end
+end
+
+class RenderJsonRespondWithController < RespondWithController
+ clear_respond_to
+ respond_to :json
+
+ def index
+ respond_with(resource) do |format|
+ format.json { render :json => RenderJsonTestException.new('boom') }
+ end
+ end
+
+ def create
+ resource = ValidatedCustomer.new(params[:name], 1)
+ respond_with(resource) do |format|
+ format.json do
+ if resource.errors.empty?
+ render :json => { :valid => true }
+ else
+ render :json => { :valid => false }
+ end
+ end
+ end
+ end
+end
+
+class CsvRespondWithController < ActionController::Base
+ respond_to :csv
+
+ class RespondWithCsv
+ def to_csv
+ "c,s,v"
+ end
+ end
+
+ def index
+ respond_with(RespondWithCsv.new)
+ end
+end
+
+class EmptyRespondWithController < ActionController::Base
+ def index
+ respond_with(Customer.new("david", 13))
+ end
+end
+
+class RespondWithControllerTest < ActionController::TestCase
+ tests RespondWithController
+
+ def setup
+ super
+ @request.host = "www.example.com"
+ Mime::Type.register_alias('text/html', :iphone)
+ Mime::Type.register_alias('text/html', :touch)
+ Mime::Type.register('text/x-mobile', :mobile)
+ end
+
+ def teardown
+ super
+ Mime::Type.unregister(:iphone)
+ Mime::Type.unregister(:touch)
+ Mime::Type.unregister(:mobile)
+ end
+
+ def test_respond_with_shouldnt_modify_original_hash
+ get :respond_with_additional_params
+ assert_equal RespondWithController.params, assigns(:params)
+ end
+
+ def test_using_resource
+ @request.accept = "application/xml"
+ get :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<name>david</name>", @response.body
+
+ @request.accept = "application/json"
+ assert_raise ActionView::MissingTemplate do
+ get :using_resource
+ end
+ end
+
+ def test_using_resource_with_js_simply_tries_to_render_the_template
+ @request.accept = "text/javascript"
+ get :using_resource
+ assert_equal "text/javascript", @response.content_type
+ assert_equal "alert(\"Hi\");", @response.body
+ end
+
+ def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found
+ @request.accept = "text/javascript"
+ assert_raise ActionView::MissingTemplate do
+ get :using_hash_resource
+ end
+ end
+
+ def test_using_hash_resource
+ @request.accept = "application/xml"
+ get :using_hash_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body
+
+ @request.accept = "application/json"
+ get :using_hash_resource
+ assert_equal "application/json", @response.content_type
+ assert @response.body.include?("result")
+ assert @response.body.include?('"name":"david"')
+ assert @response.body.include?('"id":13')
+ end
+
+ def test_using_hash_resource_with_post
+ @request.accept = "application/json"
+ assert_raise ArgumentError, "Nil location provided. Can't build URI." do
+ post :using_hash_resource
+ end
+ end
+
+ def test_using_resource_with_block
+ @request.accept = "*/*"
+ get :using_resource_with_block
+ assert_equal "text/html", @response.content_type
+ assert_equal 'Hello world!', @response.body
+
+ @request.accept = "text/csv"
+ get :using_resource_with_block
+ assert_equal "text/csv", @response.content_type
+ assert_equal "CSV", @response.body
+
+ @request.accept = "application/xml"
+ get :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<name>david</name>", @response.body
+ end
+
+ def test_using_resource_with_overwrite_block
+ get :using_resource_with_overwrite_block
+ assert_equal "text/html", @response.content_type
+ assert_equal "HTML", @response.body
+ end
+
+ def test_not_acceptable
+ @request.accept = "application/xml"
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_resource_with_block
+ end
+
+ @request.accept = "text/javascript"
+ assert_raises(ActionController::UnknownFormat) do
+ get :using_resource_with_overwrite_block
+ end
+ end
+
+ def test_using_resource_for_post_with_html_redirects_on_success
+ with_test_route_set do
+ post :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 302, @response.status
+ assert_equal "http://www.example.com/customers/13", @response.location
+ assert @response.redirect?
+ end
+ end
+
+ def test_using_resource_for_post_with_html_rerender_on_failure
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ post :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "New world!\n", @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_post_with_xml_yields_created_on_success
+ with_test_route_set do
+ @request.accept = "application/xml"
+ post :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 201, @response.status
+ assert_equal "<name>david</name>", @response.body
+ assert_equal "http://www.example.com/customers/13", @response.location
+ end
+ end
+
+ def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
+ with_test_route_set do
+ @request.accept = "application/xml"
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ post :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 422, @response.status
+ assert_equal errors.to_xml, @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure
+ with_test_route_set do
+ @request.accept = "application/json"
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ post :using_resource
+ assert_equal "application/json", @response.content_type
+ assert_equal 422, @response.status
+ errors = {:errors => errors}
+ assert_equal errors.to_json, @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_patch_with_html_redirects_on_success
+ with_test_route_set do
+ patch :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 302, @response.status
+ assert_equal "http://www.example.com/customers/13", @response.location
+ assert @response.redirect?
+ end
+ end
+
+ def test_using_resource_for_patch_with_html_rerender_on_failure
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ patch :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "Edit world!\n", @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ @request.env["rack.methodoverride.original_method"] = "POST"
+ patch :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "Edit world!\n", @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_put_with_html_redirects_on_success
+ with_test_route_set do
+ put :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 302, @response.status
+ assert_equal "http://www.example.com/customers/13", @response.location
+ assert @response.redirect?
+ end
+ end
+
+ def test_using_resource_for_put_with_html_rerender_on_failure
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ put :using_resource
+
+ assert_equal "text/html", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "Edit world!\n", @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ @request.env["rack.methodoverride.original_method"] = "POST"
+ put :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "Edit world!\n", @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_for_put_with_xml_yields_no_content_on_success
+ @request.accept = "application/xml"
+ put :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 204, @response.status
+ assert_equal "", @response.body
+ 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
+ assert_equal "application/json", @response.content_type
+ assert_equal 204, @response.status
+ assert_equal "", @response.body
+ end
+
+ def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure
+ @request.accept = "application/xml"
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ put :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 422, @response.status
+ assert_equal errors.to_xml, @response.body
+ assert_nil @response.location
+ end
+
+ def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure
+ @request.accept = "application/json"
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ put :using_resource
+ assert_equal "application/json", @response.content_type
+ assert_equal 422, @response.status
+ errors = {:errors => errors}
+ assert_equal errors.to_json, @response.body
+ assert_nil @response.location
+ end
+
+ def test_using_resource_for_delete_with_html_redirects_on_success
+ with_test_route_set do
+ Customer.any_instance.stubs(:destroyed?).returns(true)
+ delete :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 302, @response.status
+ assert_equal "http://www.example.com/customers", @response.location
+ end
+ end
+
+ def test_using_resource_for_delete_with_xml_yields_no_content_on_success
+ Customer.any_instance.stubs(:destroyed?).returns(true)
+ @request.accept = "application/xml"
+ delete :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 204, @response.status
+ assert_equal "", @response.body
+ 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
+ assert_equal "application/json", @response.content_type
+ assert_equal 204, @response.status
+ assert_equal "", @response.body
+ end
+
+ def test_using_resource_for_delete_with_html_redirects_on_failure
+ with_test_route_set do
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ Customer.any_instance.stubs(:destroyed?).returns(false)
+ delete :using_resource
+ assert_equal "text/html", @response.content_type
+ assert_equal 302, @response.status
+ assert_equal "http://www.example.com/customers", @response.location
+ end
+ end
+
+ def test_using_resource_with_parent_for_get
+ @request.accept = "application/xml"
+ get :using_resource_with_parent
+ assert_equal "application/xml", @response.content_type
+ assert_equal 200, @response.status
+ assert_equal "<name>david</name>", @response.body
+ end
+
+ def test_using_resource_with_parent_for_post
+ with_test_route_set do
+ @request.accept = "application/xml"
+
+ post :using_resource_with_parent
+ assert_equal "application/xml", @response.content_type
+ assert_equal 201, @response.status
+ assert_equal "<name>david</name>", @response.body
+ assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location
+
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+ post :using_resource
+ assert_equal "application/xml", @response.content_type
+ assert_equal 422, @response.status
+ assert_equal errors.to_xml, @response.body
+ assert_nil @response.location
+ end
+ end
+
+ def test_using_resource_with_collection
+ @request.accept = "application/xml"
+ get :using_resource_with_collection
+ assert_equal "application/xml", @response.content_type
+ assert_equal 200, @response.status
+ assert_match(/<name>david<\/name>/, @response.body)
+ assert_match(/<name>jamis<\/name>/, @response.body)
+ end
+
+ def test_using_resource_with_action
+ @controller.instance_eval do
+ def render(params={})
+ self.response_body = "#{params[:action]} - #{formats}"
+ end
+ end
+
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+
+ post :using_resource_with_action
+ assert_equal "foo - #{[:html].to_s}", @controller.response.body
+ end
+
+ def test_respond_as_responder_entry_point
+ @request.accept = "text/html"
+ get :using_responder_with_respond
+ assert_equal "respond html", @response.body
+
+ @request.accept = "application/xml"
+ get :using_responder_with_respond
+ assert_equal "respond xml", @response.body
+ end
+
+ def test_clear_respond_to
+ @controller = InheritedRespondWithController.new
+ @request.accept = "text/html"
+ assert_raises(ActionController::UnknownFormat) do
+ get :index
+ end
+ end
+
+ def test_first_in_respond_to_has_higher_priority
+ @controller = InheritedRespondWithController.new
+ @request.accept = "*/*"
+ get :index
+ assert_equal "application/xml", @response.content_type
+ assert_equal "<name>david</name>", @response.body
+ end
+
+ def test_block_inside_respond_with_is_rendered
+ @controller = InheritedRespondWithController.new
+ @request.accept = "application/json"
+ get :index
+ assert_equal "JSON", @response.body
+ end
+
+ def test_render_json_object_responds_to_str_still_produce_json
+ @controller = RenderJsonRespondWithController.new
+ @request.accept = "application/json"
+ get :index, :format => :json
+ assert_match(/"message":"boom"/, @response.body)
+ assert_match(/"error":"RenderJsonTestException"/, @response.body)
+ end
+
+ def test_api_response_with_valid_resource_respect_override_block
+ @controller = RenderJsonRespondWithController.new
+ post :create, :name => "sikachu", :format => :json
+ assert_equal '{"valid":true}', @response.body
+ end
+
+ def test_api_response_with_invalid_resource_respect_override_block
+ @controller = RenderJsonRespondWithController.new
+ post :create, :name => "david", :format => :json
+ assert_equal '{"valid":false}', @response.body
+ end
+
+ def test_no_double_render_is_raised
+ @request.accept = "text/html"
+ assert_raise ActionView::MissingTemplate do
+ get :using_resource
+ end
+ end
+
+ def test_using_resource_with_status_and_location
+ @request.accept = "text/html"
+ post :using_resource_with_status_and_location
+ assert @response.redirect?
+ assert_equal "http://test.host/", @response.location
+
+ @request.accept = "application/xml"
+ get :using_resource_with_status_and_location
+ assert_equal 201, @response.status
+ end
+
+ def test_using_resource_with_status_and_location_with_invalid_resource
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+
+ @request.accept = "text/xml"
+
+ post :using_resource_with_status_and_location
+ assert_equal errors.to_xml, @response.body
+ assert_equal 422, @response.status
+ assert_equal nil, @response.location
+
+ put :using_resource_with_status_and_location
+ assert_equal errors.to_xml, @response.body
+ assert_equal 422, @response.status
+ assert_equal nil, @response.location
+ end
+
+ def test_using_invalid_resource_with_template
+ errors = { :name => :invalid }
+ Customer.any_instance.stubs(:errors).returns(errors)
+
+ @request.accept = "text/xml"
+
+ post :using_invalid_resource_with_template
+ assert_equal errors.to_xml, @response.body
+ assert_equal 422, @response.status
+ assert_equal nil, @response.location
+
+ put :using_invalid_resource_with_template
+ assert_equal errors.to_xml, @response.body
+ assert_equal 422, @response.status
+ assert_equal nil, @response.location
+ end
+
+ def test_using_options_with_template
+ @request.accept = "text/xml"
+
+ post :using_options_with_template
+ assert_equal "<customer-name>david</customer-name>", @response.body
+ assert_equal 123, @response.status
+ assert_equal "http://test.host/", @response.location
+
+ put :using_options_with_template
+ assert_equal "<customer-name>david</customer-name>", @response.body
+ assert_equal 123, @response.status
+ assert_equal "http://test.host/", @response.location
+ end
+
+ def test_using_resource_with_responder
+ get :using_resource_with_responder
+ assert_equal "Resource name is david", @response.body
+ end
+
+ def test_using_resource_with_set_responder
+ RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
+ get :using_resource
+ assert_equal "Resource name is david", @response.body
+ ensure
+ RespondWithController.responder = ActionController::Responder
+ end
+
+ def test_uses_renderer_if_an_api_behavior
+ ActionController::Renderers.add :csv do |obj, options|
+ send_data obj.to_csv, type: Mime::CSV
+ end
+ @controller = CsvRespondWithController.new
+ get :index, format: 'csv'
+ assert_equal Mime::CSV, @response.content_type
+ assert_equal "c,s,v", @response.body
+ end
+
+ def test_raises_missing_renderer_if_an_api_behavior_with_no_renderer
+ @controller = CsvRespondWithController.new
+ assert_raise ActionController::MissingRenderer do
+ get :index, format: 'csv'
+ end
+ end
+
+ def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
+ @controller = EmptyRespondWithController.new
+ @request.accept = "*/*"
+ assert_raise RuntimeError do
+ get :index
+ end
+ end
+
+ private
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ resources :customers
+ resources :quiz_stores do
+ resources :customers
+ end
+ get ":controller/:action"
+ end
+ yield
+ end
+ end
+end
+
+class FlashResponder < ActionController::Responder
+ def initialize(controller, resources, options={})
+ super
+ end
+
+ def to_html
+ controller.flash[:notice] = 'Success'
+ super
+ end
+end
+
+class FlashResponderController < ActionController::Base
+ self.responder = FlashResponder
+ respond_to :html
+
+ def index
+ respond_with Object.new do |format|
+ format.html { render :text => 'HTML' }
+ end
+ end
+end
+
+class FlashResponderControllerTest < ActionController::TestCase
+ tests FlashResponderController
+
+ def test_respond_with_block_executed
+ get :index
+ assert_equal 'HTML', @response.body
+ end
+
+ def test_flash_responder_executed
+ get :index
+ assert_equal 'Success', flash[:notice]
+ end
+end
diff --git a/actionpack/test/controller/mime_responds_test.rb b/actionpack/test/controller/mime_responds_test.rb
deleted file mode 100644
index b21d35c846..0000000000
--- a/actionpack/test/controller/mime_responds_test.rb
+++ /dev/null
@@ -1,1233 +0,0 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-require 'active_support/core_ext/hash/conversions'
-
-class StarStarMimeController < ActionController::Base
- layout nil
-
- def index
- render
- end
-end
-
-class RespondToController < ActionController::Base
- layout :set_layout
-
- def html_xml_or_rss
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.rss { render :text => "RSS" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def js_or_html
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.js { render :text => "JS" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def json_or_yaml
- respond_to do |type|
- type.json { render :text => "JSON" }
- type.yaml { render :text => "YAML" }
- end
- end
-
- def html_or_xml
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def json_xml_or_html
- respond_to do |type|
- type.json { render :text => 'JSON' }
- type.xml { render :xml => 'XML' }
- type.html { render :text => 'HTML' }
- end
- end
-
-
- def forced_xml
- request.format = :xml
-
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.xml { render :text => "XML" }
- end
- end
-
- def just_xml
- respond_to do |type|
- type.xml { render :text => "XML" }
- end
- end
-
- def using_defaults
- respond_to do |type|
- type.html
- type.xml
- end
- end
-
- def using_defaults_with_type_list
- respond_to(:html, :xml)
- end
-
- def made_for_content_type
- respond_to do |type|
- type.rss { render :text => "RSS" }
- type.atom { render :text => "ATOM" }
- type.all { render :text => "Nothing" }
- end
- end
-
- def custom_type_handling
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.custom("application/crazy-xml") { render :text => "Crazy XML" }
- type.all { render :text => "Nothing" }
- end
- end
-
-
- def custom_constant_handling
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.mobile { render :text => "Mobile" }
- end
- end
-
- def custom_constant_handling_without_block
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.mobile
- end
- end
-
- def handle_any
- respond_to do |type|
- type.html { render :text => "HTML" }
- type.any(:js, :xml) { render :text => "Either JS or XML" }
- end
- end
-
- def handle_any_any
- respond_to do |type|
- type.html { render :text => 'HTML' }
- type.any { render :text => 'Whatever you ask for, I got it' }
- end
- end
-
- def all_types_with_layout
- respond_to do |type|
- type.html
- end
- end
-
- def iphone_with_html_response_type
- request.format = :iphone if request.env["HTTP_ACCEPT"] == "text/iphone"
-
- respond_to do |type|
- type.html { @type = "Firefox" }
- type.iphone { @type = "iPhone" }
- end
- end
-
- def iphone_with_html_response_type_without_layout
- request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
-
- respond_to do |type|
- type.html { @type = "Firefox"; render :action => "iphone_with_html_response_type" }
- type.iphone { @type = "iPhone" ; render :action => "iphone_with_html_response_type" }
- end
- end
-
- protected
- def set_layout
- case action_name
- when "all_types_with_layout", "iphone_with_html_response_type"
- "respond_to/layouts/standard"
- when "iphone_with_html_response_type_without_layout"
- "respond_to/layouts/missing"
- end
- end
-end
-
-class StarStarMimeControllerTest < ActionController::TestCase
- tests StarStarMimeController
-
- def test_javascript_with_format
- @request.accept = "text/javascript"
- get :index, :format => 'js'
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
- def test_javascript_with_no_format
- @request.accept = "text/javascript"
- get :index
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
- def test_javascript_with_no_format_only_star_star
- @request.accept = "*/*"
- get :index
- assert_match "function addition(a,b){ return a+b; }", @response.body
- end
-
-end
-
-class RespondToControllerTest < ActionController::TestCase
- tests RespondToController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias("text/html", :iphone)
- Mime::Type.register("text/x-mobile", :mobile)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- Mime::Type.unregister(:mobile)
- end
-
- def test_html
- @request.accept = "text/html"
- get :js_or_html
- assert_equal 'HTML', @response.body
-
- get :html_or_xml
- assert_equal 'HTML', @response.body
-
- assert_raises(ActionController::UnknownFormat) do
- get :just_xml
- end
- end
-
- def test_all
- @request.accept = "*/*"
- get :js_or_html
- assert_equal 'HTML', @response.body # js is not part of all
-
- get :html_or_xml
- assert_equal 'HTML', @response.body
-
- get :just_xml
- assert_equal 'XML', @response.body
- end
-
- def test_xml
- @request.accept = "application/xml"
- get :html_xml_or_rss
- assert_equal 'XML', @response.body
- end
-
- def test_js_or_html
- @request.accept = "text/javascript, text/html"
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
-
- @request.accept = "text/javascript, text/html"
- xhr :get, :html_or_xml
- assert_equal 'HTML', @response.body
-
- @request.accept = "text/javascript, text/html"
-
- assert_raises(ActionController::UnknownFormat) do
- xhr :get, :just_xml
- end
- end
-
- def test_json_or_yaml_with_leading_star_star
- @request.accept = "*/*, application/json"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
-
- @request.accept = "*/* , application/json"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
- end
-
- def test_json_or_yaml
- xhr :get, :json_or_yaml
- assert_equal 'JSON', @response.body
-
- get :json_or_yaml, :format => 'json'
- assert_equal 'JSON', @response.body
-
- get :json_or_yaml, :format => 'yaml'
- assert_equal 'YAML', @response.body
-
- { 'YAML' => %w(text/yaml),
- 'JSON' => %w(application/json text/x-json)
- }.each do |body, content_types|
- content_types.each do |content_type|
- @request.accept = content_type
- get :json_or_yaml
- assert_equal body, @response.body
- end
- end
- end
-
- def test_js_or_anything
- @request.accept = "text/javascript, */*"
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
-
- xhr :get, :html_or_xml
- assert_equal 'HTML', @response.body
-
- xhr :get, :just_xml
- assert_equal 'XML', @response.body
- end
-
- def test_using_defaults
- @request.accept = "*/*"
- get :using_defaults
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "application/xml"
- get :using_defaults
- assert_equal "application/xml", @response.content_type
- assert_equal "<p>Hello world!</p>\n", @response.body
- end
-
- def test_using_defaults_with_type_list
- @request.accept = "*/*"
- get :using_defaults_with_type_list
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "application/xml"
- get :using_defaults_with_type_list
- assert_equal "application/xml", @response.content_type
- assert_equal "<p>Hello world!</p>\n", @response.body
- end
-
- def test_with_atom_content_type
- @request.accept = ""
- @request.env["CONTENT_TYPE"] = "application/atom+xml"
- xhr :get, :made_for_content_type
- assert_equal "ATOM", @response.body
- end
-
- def test_with_rss_content_type
- @request.accept = ""
- @request.env["CONTENT_TYPE"] = "application/rss+xml"
- xhr :get, :made_for_content_type
- assert_equal "RSS", @response.body
- end
-
- def test_synonyms
- @request.accept = "application/javascript"
- get :js_or_html
- assert_equal 'JS', @response.body
-
- @request.accept = "application/x-xml"
- get :html_xml_or_rss
- assert_equal "XML", @response.body
- end
-
- def test_custom_types
- @request.accept = "application/crazy-xml"
- get :custom_type_handling
- assert_equal "application/crazy-xml", @response.content_type
- assert_equal 'Crazy XML', @response.body
-
- @request.accept = "text/html"
- get :custom_type_handling
- assert_equal "text/html", @response.content_type
- assert_equal 'HTML', @response.body
- end
-
- def test_xhtml_alias
- @request.accept = "application/xhtml+xml,application/xml"
- get :html_or_xml
- assert_equal 'HTML', @response.body
- end
-
- def test_firefox_simulation
- @request.accept = "text/xml,application/xml,application/xhtml+xml,text/html;q=0.9,text/plain;q=0.8,image/png,*/*;q=0.5"
- get :html_or_xml
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any
- @request.accept = "*/*"
- get :handle_any
- assert_equal 'HTML', @response.body
-
- @request.accept = "text/javascript"
- get :handle_any
- assert_equal 'Either JS or XML', @response.body
-
- @request.accept = "text/xml"
- get :handle_any
- assert_equal 'Either JS or XML', @response.body
- end
-
- def test_handle_any_any
- @request.accept = "*/*"
- get :handle_any_any
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_parameter_format
- get :handle_any_any, {:format=>'html'}
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_explicit_html
- @request.accept = "text/html"
- get :handle_any_any
- assert_equal 'HTML', @response.body
- end
-
- def test_handle_any_any_javascript
- @request.accept = "text/javascript"
- get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
- end
-
- def test_handle_any_any_xml
- @request.accept = "text/xml"
- get :handle_any_any
- assert_equal 'Whatever you ask for, I got it', @response.body
- end
-
- def test_browser_check_with_any_any
- @request.accept = "application/json, application/xml"
- get :json_xml_or_html
- assert_equal 'JSON', @response.body
-
- @request.accept = "application/json, application/xml, */*"
- get :json_xml_or_html
- assert_equal 'HTML', @response.body
- end
-
- def test_html_type_with_layout
- @request.accept = "text/html"
- get :all_types_with_layout
- assert_equal '<html><div id="html">HTML for all_types_with_layout</div></html>', @response.body
- end
-
- def test_xhr
- xhr :get, :js_or_html
- assert_equal 'JS', @response.body
- end
-
- def test_custom_constant
- get :custom_constant_handling, :format => "mobile"
- assert_equal "text/x-mobile", @response.content_type
- assert_equal "Mobile", @response.body
- end
-
- def test_custom_constant_handling_without_block
- get :custom_constant_handling_without_block, :format => "mobile"
- assert_equal "text/x-mobile", @response.content_type
- assert_equal "Mobile", @response.body
- end
-
- def test_forced_format
- get :html_xml_or_rss
- assert_equal "HTML", @response.body
-
- get :html_xml_or_rss, :format => "html"
- assert_equal "HTML", @response.body
-
- get :html_xml_or_rss, :format => "xml"
- assert_equal "XML", @response.body
-
- get :html_xml_or_rss, :format => "rss"
- assert_equal "RSS", @response.body
- end
-
- def test_internally_forced_format
- get :forced_xml
- assert_equal "XML", @response.body
-
- get :forced_xml, :format => "html"
- assert_equal "XML", @response.body
- end
-
- def test_extension_synonyms
- get :html_xml_or_rss, :format => "xhtml"
- assert_equal "HTML", @response.body
- end
-
- def test_render_action_for_html
- @controller.instance_eval do
- def render(*args)
- @action = args.first[:action] unless args.empty?
- @action ||= action_name
-
- response.body = "#{@action} - #{formats}"
- end
- end
-
- get :using_defaults
- assert_equal "using_defaults - #{[:html].to_s}", @response.body
-
- get :using_defaults, :format => "xml"
- assert_equal "using_defaults - #{[:xml].to_s}", @response.body
- end
-
- def test_format_with_custom_response_type
- get :iphone_with_html_response_type
- assert_equal '<html><div id="html">Hello future from Firefox!</div></html>', @response.body
-
- get :iphone_with_html_response_type, :format => "iphone"
- assert_equal "text/html", @response.content_type
- assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
- end
-
- def test_format_with_custom_response_type_and_request_headers
- @request.accept = "text/iphone"
- get :iphone_with_html_response_type
- assert_equal '<html><div id="iphone">Hello iPhone future from iPhone!</div></html>', @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_invalid_format
- assert_raises(ActionController::UnknownFormat) do
- get :using_defaults, :format => "invalidformat"
- end
- end
-end
-
-class RespondWithController < ActionController::Base
- respond_to :html, :json, :touch
- respond_to :xml, :except => :using_resource_with_block
- respond_to :js, :only => [ :using_resource_with_block, :using_resource, 'using_hash_resource' ]
-
- def using_resource
- respond_with(resource)
- end
-
- def using_hash_resource
- respond_with({:result => resource})
- end
-
- def using_resource_with_block
- respond_with(resource) do |format|
- format.csv { render :text => "CSV" }
- end
- end
-
- def using_resource_with_overwrite_block
- respond_with(resource) do |format|
- format.html { render :text => "HTML" }
- end
- end
-
- def using_resource_with_collection
- respond_with([resource, Customer.new("jamis", 9)])
- end
-
- def using_resource_with_parent
- respond_with(Quiz::Store.new("developer?", 11), Customer.new("david", 13))
- end
-
- def using_resource_with_status_and_location
- respond_with(resource, :location => "http://test.host/", :status => :created)
- end
-
- def using_invalid_resource_with_template
- respond_with(resource)
- end
-
- def using_options_with_template
- @customer = resource
- respond_with(@customer, :status => 123, :location => "http://test.host/")
- end
-
- def using_resource_with_responder
- responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
- respond_with(resource, :responder => responder)
- end
-
- def using_resource_with_action
- respond_with(resource, :action => :foo) do |format|
- format.html { raise ActionView::MissingTemplate.new([], "bar", ["foo"], {}, false) }
- end
- end
-
- def using_responder_with_respond
- responder = Class.new(ActionController::Responder) do
- def respond; @controller.render :text => "respond #{format}"; end
- end
- respond_with(resource, :responder => responder)
- end
-
-protected
-
- def resource
- Customer.new("david", request.delete? ? nil : 13)
- end
-end
-
-class InheritedRespondWithController < RespondWithController
- clear_respond_to
- respond_to :xml, :json
-
- def index
- respond_with(resource) do |format|
- format.json { render :text => "JSON" }
- end
- end
-end
-
-class RenderJsonRespondWithController < RespondWithController
- clear_respond_to
- respond_to :json
-
- def index
- respond_with(resource) do |format|
- format.json { render :json => RenderJsonTestException.new('boom') }
- end
- end
-
- def create
- resource = ValidatedCustomer.new(params[:name], 1)
- respond_with(resource) do |format|
- format.json do
- if resource.errors.empty?
- render :json => { :valid => true }
- else
- render :json => { :valid => false }
- end
- end
- end
- end
-end
-
-class EmptyRespondWithController < ActionController::Base
- def index
- respond_with(Customer.new("david", 13))
- end
-end
-
-class RespondWithControllerTest < ActionController::TestCase
- tests RespondWithController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias('text/html', :iphone)
- Mime::Type.register_alias('text/html', :touch)
- Mime::Type.register('text/x-mobile', :mobile)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- Mime::Type.unregister(:touch)
- Mime::Type.unregister(:mobile)
- end
-
- def test_using_resource
- @request.accept = "application/xml"
- get :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
-
- @request.accept = "application/json"
- assert_raise ActionView::MissingTemplate do
- get :using_resource
- end
- end
-
- def test_using_resource_with_js_simply_tries_to_render_the_template
- @request.accept = "text/javascript"
- get :using_resource
- assert_equal "text/javascript", @response.content_type
- assert_equal "alert(\"Hi\");", @response.body
- end
-
- def test_using_hash_resource_with_js_raises_an_error_if_template_cant_be_found
- @request.accept = "text/javascript"
- assert_raise ActionView::MissingTemplate do
- get :using_hash_resource
- end
- end
-
- def test_using_hash_resource
- @request.accept = "application/xml"
- get :using_hash_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<hash>\n <name>david</name>\n</hash>\n", @response.body
-
- @request.accept = "application/json"
- get :using_hash_resource
- assert_equal "application/json", @response.content_type
- assert @response.body.include?("result")
- assert @response.body.include?('"name":"david"')
- assert @response.body.include?('"id":13')
- end
-
- def test_using_hash_resource_with_post
- @request.accept = "application/json"
- assert_raise ArgumentError, "Nil location provided. Can't build URI." do
- post :using_hash_resource
- end
- end
-
- def test_using_resource_with_block
- @request.accept = "*/*"
- get :using_resource_with_block
- assert_equal "text/html", @response.content_type
- assert_equal 'Hello world!', @response.body
-
- @request.accept = "text/csv"
- get :using_resource_with_block
- assert_equal "text/csv", @response.content_type
- assert_equal "CSV", @response.body
-
- @request.accept = "application/xml"
- get :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_using_resource_with_overwrite_block
- get :using_resource_with_overwrite_block
- assert_equal "text/html", @response.content_type
- assert_equal "HTML", @response.body
- end
-
- def test_not_acceptable
- @request.accept = "application/xml"
- assert_raises(ActionController::UnknownFormat) do
- get :using_resource_with_block
- end
-
- @request.accept = "text/javascript"
- assert_raises(ActionController::UnknownFormat) do
- get :using_resource_with_overwrite_block
- end
- end
-
- def test_using_resource_for_post_with_html_redirects_on_success
- with_test_route_set do
- post :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_post_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "New world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_post_with_xml_yields_created_on_success
- with_test_route_set do
- @request.accept = "application/xml"
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 201, @response.status
- assert_equal "<name>david</name>", @response.body
- assert_equal "http://www.example.com/customers/13", @response.location
- end
- end
-
- def test_using_resource_for_post_with_xml_yields_unprocessable_entity_on_failure
- with_test_route_set do
- @request.accept = "application/xml"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_post_with_json_yields_unprocessable_entity_on_failure
- with_test_route_set do
- @request.accept = "application/json"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/json", @response.content_type
- assert_equal 422, @response.status
- errors = {:errors => errors}
- assert_equal errors.to_json, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_patch_with_html_redirects_on_success
- with_test_route_set do
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_patch_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_patch_with_html_rerender_on_failure_even_on_method_override
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- @request.env["rack.methodoverride.original_method"] = "POST"
- patch :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_html_redirects_on_success
- with_test_route_set do
- put :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers/13", @response.location
- assert @response.redirect?
- end
- end
-
- def test_using_resource_for_put_with_html_rerender_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_html_rerender_on_failure_even_on_method_override
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- @request.env["rack.methodoverride.original_method"] = "POST"
- put :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 200, @response.status
- assert_equal "Edit world!\n", @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_for_put_with_xml_yields_no_content_on_success
- @request.accept = "application/xml"
- put :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- 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
- assert_equal "application/json", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_put_with_xml_yields_unprocessable_entity_on_failure
- @request.accept = "application/xml"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
-
- def test_using_resource_for_put_with_json_yields_unprocessable_entity_on_failure
- @request.accept = "application/json"
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- put :using_resource
- assert_equal "application/json", @response.content_type
- assert_equal 422, @response.status
- errors = {:errors => errors}
- assert_equal errors.to_json, @response.body
- assert_nil @response.location
- end
-
- def test_using_resource_for_delete_with_html_redirects_on_success
- with_test_route_set do
- Customer.any_instance.stubs(:destroyed?).returns(true)
- delete :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers", @response.location
- end
- end
-
- def test_using_resource_for_delete_with_xml_yields_no_content_on_success
- Customer.any_instance.stubs(:destroyed?).returns(true)
- @request.accept = "application/xml"
- delete :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- 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
- assert_equal "application/json", @response.content_type
- assert_equal 204, @response.status
- assert_equal "", @response.body
- end
-
- def test_using_resource_for_delete_with_html_redirects_on_failure
- with_test_route_set do
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- Customer.any_instance.stubs(:destroyed?).returns(false)
- delete :using_resource
- assert_equal "text/html", @response.content_type
- assert_equal 302, @response.status
- assert_equal "http://www.example.com/customers", @response.location
- end
- end
-
- def test_using_resource_with_parent_for_get
- @request.accept = "application/xml"
- get :using_resource_with_parent
- assert_equal "application/xml", @response.content_type
- assert_equal 200, @response.status
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_using_resource_with_parent_for_post
- with_test_route_set do
- @request.accept = "application/xml"
-
- post :using_resource_with_parent
- assert_equal "application/xml", @response.content_type
- assert_equal 201, @response.status
- assert_equal "<name>david</name>", @response.body
- assert_equal "http://www.example.com/quiz_stores/11/customers/13", @response.location
-
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
- post :using_resource
- assert_equal "application/xml", @response.content_type
- assert_equal 422, @response.status
- assert_equal errors.to_xml, @response.body
- assert_nil @response.location
- end
- end
-
- def test_using_resource_with_collection
- @request.accept = "application/xml"
- get :using_resource_with_collection
- assert_equal "application/xml", @response.content_type
- assert_equal 200, @response.status
- assert_match(/<name>david<\/name>/, @response.body)
- assert_match(/<name>jamis<\/name>/, @response.body)
- end
-
- def test_using_resource_with_action
- @controller.instance_eval do
- def render(params={})
- self.response_body = "#{params[:action]} - #{formats}"
- end
- end
-
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- post :using_resource_with_action
- assert_equal "foo - #{[:html].to_s}", @controller.response.body
- end
-
- def test_respond_as_responder_entry_point
- @request.accept = "text/html"
- get :using_responder_with_respond
- assert_equal "respond html", @response.body
-
- @request.accept = "application/xml"
- get :using_responder_with_respond
- assert_equal "respond xml", @response.body
- end
-
- def test_clear_respond_to
- @controller = InheritedRespondWithController.new
- @request.accept = "text/html"
- assert_raises(ActionController::UnknownFormat) do
- get :index
- end
- end
-
- def test_first_in_respond_to_has_higher_priority
- @controller = InheritedRespondWithController.new
- @request.accept = "*/*"
- get :index
- assert_equal "application/xml", @response.content_type
- assert_equal "<name>david</name>", @response.body
- end
-
- def test_block_inside_respond_with_is_rendered
- @controller = InheritedRespondWithController.new
- @request.accept = "application/json"
- get :index
- assert_equal "JSON", @response.body
- end
-
- def test_render_json_object_responds_to_str_still_produce_json
- @controller = RenderJsonRespondWithController.new
- @request.accept = "application/json"
- get :index, :format => :json
- assert_match(/"message":"boom"/, @response.body)
- assert_match(/"error":"RenderJsonTestException"/, @response.body)
- end
-
- def test_api_response_with_valid_resource_respect_override_block
- @controller = RenderJsonRespondWithController.new
- post :create, :name => "sikachu", :format => :json
- assert_equal '{"valid":true}', @response.body
- end
-
- def test_api_response_with_invalid_resource_respect_override_block
- @controller = RenderJsonRespondWithController.new
- post :create, :name => "david", :format => :json
- assert_equal '{"valid":false}', @response.body
- end
-
- def test_no_double_render_is_raised
- @request.accept = "text/html"
- assert_raise ActionView::MissingTemplate do
- get :using_resource
- end
- end
-
- def test_using_resource_with_status_and_location
- @request.accept = "text/html"
- post :using_resource_with_status_and_location
- assert @response.redirect?
- assert_equal "http://test.host/", @response.location
-
- @request.accept = "application/xml"
- get :using_resource_with_status_and_location
- assert_equal 201, @response.status
- end
-
- def test_using_resource_with_status_and_location_with_invalid_resource
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- @request.accept = "text/xml"
-
- post :using_resource_with_status_and_location
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
-
- put :using_resource_with_status_and_location
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
- end
-
- def test_using_invalid_resource_with_template
- errors = { :name => :invalid }
- Customer.any_instance.stubs(:errors).returns(errors)
-
- @request.accept = "text/xml"
-
- post :using_invalid_resource_with_template
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
-
- put :using_invalid_resource_with_template
- assert_equal errors.to_xml, @response.body
- assert_equal 422, @response.status
- assert_equal nil, @response.location
- end
-
- def test_using_options_with_template
- @request.accept = "text/xml"
-
- post :using_options_with_template
- assert_equal "<customer-name>david</customer-name>", @response.body
- assert_equal 123, @response.status
- assert_equal "http://test.host/", @response.location
-
- put :using_options_with_template
- assert_equal "<customer-name>david</customer-name>", @response.body
- assert_equal 123, @response.status
- assert_equal "http://test.host/", @response.location
- end
-
- def test_using_resource_with_responder
- get :using_resource_with_responder
- assert_equal "Resource name is david", @response.body
- end
-
- def test_using_resource_with_set_responder
- RespondWithController.responder = proc { |c, r, o| c.render :text => "Resource name is #{r.first.name}" }
- get :using_resource
- assert_equal "Resource name is david", @response.body
- ensure
- RespondWithController.responder = ActionController::Responder
- end
-
- def test_error_is_raised_if_no_respond_to_is_declared_and_respond_with_is_called
- @controller = EmptyRespondWithController.new
- @request.accept = "*/*"
- assert_raise RuntimeError do
- get :index
- end
- end
-
- private
- def with_test_route_set
- with_routing do |set|
- set.draw do
- resources :customers
- resources :quiz_stores do
- resources :customers
- end
- get ":controller/:action"
- end
- yield
- end
- end
-end
-
-class AbstractPostController < ActionController::Base
- self.view_paths = File.dirname(__FILE__) + "/../fixtures/post_test/"
-end
-
-# For testing layouts which are set automatically
-class PostController < AbstractPostController
- around_filter :with_iphone
-
- def index
- respond_to(:html, :iphone, :js)
- end
-
-protected
-
- def with_iphone
- request.format = "iphone" if request.env["HTTP_ACCEPT"] == "text/iphone"
- yield
- end
-end
-
-class SuperPostController < PostController
-end
-
-class MimeControllerLayoutsTest < ActionController::TestCase
- tests PostController
-
- def setup
- super
- @request.host = "www.example.com"
- Mime::Type.register_alias("text/html", :iphone)
- end
-
- def teardown
- super
- Mime::Type.unregister(:iphone)
- end
-
- def test_missing_layout_renders_properly
- get :index
- assert_equal '<html><div id="html">Hello Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal 'Hello iPhone', @response.body
- end
-
- def test_format_with_inherited_layouts
- @controller = SuperPostController.new
-
- get :index
- assert_equal '<html><div id="html">Super Firefox</div></html>', @response.body
-
- @request.accept = "text/iphone"
- get :index
- assert_equal '<html><div id="super_iphone">Super iPhone</div></html>', @response.body
- end
-
- def test_non_navigational_format_with_no_template_fallbacks_to_html_template_with_no_layout
- get :index, :format => :js
- assert_equal "Hello Firefox", @response.body
- end
-end
-
-class FlashResponder < ActionController::Responder
- def initialize(controller, resources, options={})
- super
- end
-
- def to_html
- controller.flash[:notice] = 'Success'
- super
- end
-end
-
-class FlashResponderController < ActionController::Base
- self.responder = FlashResponder
- respond_to :html
-
- def index
- respond_with Object.new do |format|
- format.html { render :text => 'HTML' }
- end
- end
-end
-
-class FlashResponderControllerTest < ActionController::TestCase
- tests FlashResponderController
-
- def test_respond_with_block_executed
- get :index
- assert_equal 'HTML', @response.body
- end
-
- def test_flash_responder_executed
- get :index
- assert_equal 'Success', flash[:notice]
- end
-end
diff --git a/actionpack/test/controller/new_base/base_test.rb b/actionpack/test/controller/new_base/base_test.rb
index ed244513a5..964f22eb03 100644
--- a/actionpack/test/controller/new_base/base_test.rb
+++ b/actionpack/test/controller/new_base/base_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
# Tests the controller dispatching happy path
module Dispatching
class SimpleController < ActionController::Base
- before_filter :authenticate
+ before_action :authenticate
def index
render :text => "success"
diff --git a/actionpack/test/controller/new_base/middleware_test.rb b/actionpack/test/controller/new_base/middleware_test.rb
index ccef060863..6b7b5e10e3 100644
--- a/actionpack/test/controller/new_base/middleware_test.rb
+++ b/actionpack/test/controller/new_base/middleware_test.rb
@@ -25,14 +25,14 @@ module MiddlewareTest
result
end
end
-
+
class BlockMiddleware
attr_accessor :configurable_message
def initialize(app, &block)
@app = app
yield(self) if block_given?
end
-
+
def call(env)
result = @app.call(env)
result[1]["Configurable-Message"] = configurable_message
diff --git a/actionpack/test/controller/new_base/render_context_test.rb b/actionpack/test/controller/new_base/render_context_test.rb
index f41b14d5d6..177a1c088d 100644
--- a/actionpack/test/controller/new_base/render_context_test.rb
+++ b/actionpack/test/controller/new_base/render_context_test.rb
@@ -14,7 +14,7 @@ module RenderContext
include ActionView::Context
# 2) Call _prepare_context that will do the required initialization
- before_filter :_prepare_context
+ before_action :_prepare_context
def hello_world
@value = "Hello"
diff --git a/actionpack/test/controller/new_base/render_partial_test.rb b/actionpack/test/controller/new_base/render_partial_test.rb
index b4a25c49c9..9e5022c9f4 100644
--- a/actionpack/test/controller/new_base/render_partial_test.rb
+++ b/actionpack/test/controller/new_base/render_partial_test.rb
@@ -5,14 +5,14 @@ module RenderPartial
class BasicController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new(
- "render_partial/basic/_basic.html.erb" => "BasicPartial!",
- "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>",
- "render_partial/basic/with_json.html.erb" => "<%= render :partial => 'with_json', :formats => [:json] %>",
- "render_partial/basic/_with_json.json.erb" => "<%= render :partial => 'final', :formats => [:json] %>",
- "render_partial/basic/_final.json.erb" => "{ final: json }",
- "render_partial/basic/overriden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overriden' %><%= @test_unchanged %>",
- "render_partial/basic/_overriden.html.erb" => "ParentPartial!",
- "render_partial/child/_overriden.html.erb" => "OverridenPartial!"
+ "render_partial/basic/_basic.html.erb" => "BasicPartial!",
+ "render_partial/basic/basic.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'basic' %><%= @test_unchanged %>",
+ "render_partial/basic/with_json.html.erb" => "<%= render :partial => 'with_json', :formats => [:json] %>",
+ "render_partial/basic/_with_json.json.erb" => "<%= render :partial => 'final', :formats => [:json] %>",
+ "render_partial/basic/_final.json.erb" => "{ final: json }",
+ "render_partial/basic/overridden.html.erb" => "<%= @test_unchanged = 'goodbye' %><%= render :partial => 'overridden' %><%= @test_unchanged %>",
+ "render_partial/basic/_overridden.html.erb" => "ParentPartial!",
+ "render_partial/child/_overridden.html.erb" => "OverriddenPartial!"
)]
def html_with_json_inside_json
@@ -24,11 +24,11 @@ module RenderPartial
render :action => "basic"
end
- def overriden
+ def overridden
@test_unchanged = 'hello'
end
end
-
+
class ChildController < BasicController; end
class TestPartial < Rack::TestCase
@@ -55,8 +55,8 @@ module RenderPartial
end
test "partial from child controller gets picked" do
- get :overriden
- assert_response("goodbyeOverridenPartial!goodbye")
+ get :overridden
+ assert_response("goodbyeOverriddenPartial!goodbye")
end
end
diff --git a/actionpack/test/controller/new_base/render_streaming_test.rb b/actionpack/test/controller/new_base/render_streaming_test.rb
index f4bdd3e1d4..2b36a399bb 100644
--- a/actionpack/test/controller/new_base/render_streaming_test.rb
+++ b/actionpack/test/controller/new_base/render_streaming_test.rb
@@ -92,7 +92,7 @@ module RenderStreaming
io.rewind
assert_match "(undefined method `invalid!' for nil:NilClass)", io.read
ensure
- ActionController::Base.logger = _old
+ ActionView::Base.logger = _old
end
end
diff --git a/actionpack/test/controller/new_base/render_template_test.rb b/actionpack/test/controller/new_base/render_template_test.rb
index d0be4f66d1..6b2ae2b2a9 100644
--- a/actionpack/test/controller/new_base/render_template_test.rb
+++ b/actionpack/test/controller/new_base/render_template_test.rb
@@ -9,7 +9,8 @@ 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>' %>",
+ "with_implicit_raw.html.erb" => "Hello <%== '<strong>this is also raw</strong>' %> in a 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] %>",
"test/final.json.erb" => "{ final: json }",
@@ -113,7 +114,12 @@ module RenderTemplate
get :with_implicit_raw
- assert_body "Hello <strong>this is also raw</strong>"
+ assert_body "Hello <strong>this is also raw</strong> in a html template"
+ assert_status 200
+
+ get :with_implicit_raw, format: 'text'
+
+ assert_body "Hello <strong>this is also raw</strong> in a text template"
assert_status 200
end
diff --git a/actionpack/test/controller/new_base/render_test.rb b/actionpack/test/controller/new_base/render_test.rb
index cc7f12ac6d..5635e16234 100644
--- a/actionpack/test/controller/new_base/render_test.rb
+++ b/actionpack/test/controller/new_base/render_test.rb
@@ -7,10 +7,10 @@ module Render
"render/blank_render/access_request.html.erb" => "The request: <%= request.method.to_s.upcase %>",
"render/blank_render/access_action_name.html.erb" => "Action Name: <%= action_name %>",
"render/blank_render/access_controller_name.html.erb" => "Controller Name: <%= controller_name %>",
- "render/blank_render/overriden_with_own_view_paths_appended.html.erb" => "parent content",
- "render/blank_render/overriden_with_own_view_paths_prepended.html.erb" => "parent content",
- "render/blank_render/overriden.html.erb" => "parent content",
- "render/child_render/overriden.html.erb" => "child content"
+ "render/blank_render/overridden_with_own_view_paths_appended.html.erb" => "parent content",
+ "render/blank_render/overridden_with_own_view_paths_prepended.html.erb" => "parent content",
+ "render/blank_render/overridden.html.erb" => "parent content",
+ "render/child_render/overridden.html.erb" => "child content"
)]
def index
@@ -25,13 +25,13 @@ module Render
render :action => "access_action_name"
end
- def overriden_with_own_view_paths_appended
+ def overridden_with_own_view_paths_appended
end
- def overriden_with_own_view_paths_prepended
+ def overridden_with_own_view_paths_prepended
end
- def overriden
+ def overridden
end
private
@@ -49,8 +49,8 @@ module Render
end
class ChildRenderController < BlankRenderController
- append_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_appended.html.erb" => "child content")
- prepend_view_path ActionView::FixtureResolver.new("render/child_render/overriden_with_own_view_paths_prepended.html.erb" => "child content")
+ append_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_appended.html.erb" => "child content")
+ prepend_view_path ActionView::FixtureResolver.new("render/child_render/overridden_with_own_view_paths_prepended.html.erb" => "child content")
end
class RenderTest < Rack::TestCase
@@ -114,17 +114,17 @@ module Render
class TestViewInheritance < Rack::TestCase
test "Template from child controller gets picked over parent one" do
- get "/render/child_render/overriden"
+ get "/render/child_render/overridden"
assert_body "child content"
end
test "Template from child controller with custom view_paths prepended gets picked over parent one" do
- get "/render/child_render/overriden_with_own_view_paths_prepended"
+ get "/render/child_render/overridden_with_own_view_paths_prepended"
assert_body "child content"
end
test "Template from child controller with custom view_paths appended gets picked over parent one" do
- get "/render/child_render/overriden_with_own_view_paths_appended"
+ get "/render/child_render/overridden_with_own_view_paths_appended"
assert_body "child content"
end
diff --git a/actionpack/test/controller/new_base/render_text_test.rb b/actionpack/test/controller/new_base/render_text_test.rb
index d6c3926a4d..2a253799f3 100644
--- a/actionpack/test/controller/new_base/render_text_test.rb
+++ b/actionpack/test/controller/new_base/render_text_test.rb
@@ -1,6 +1,15 @@
require 'abstract_unit'
module RenderText
+ class MinimalController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionController::Rendering
+
+ def index
+ render text: "Hello World!"
+ end
+ end
+
class SimpleController < ActionController::Base
self.view_paths = [ActionView::FixtureResolver.new]
@@ -63,6 +72,12 @@ module RenderText
end
class RenderTextTest < Rack::TestCase
+ test "rendering text from a minimal controller" do
+ get "/render_text/minimal/index"
+ assert_body "Hello World!"
+ assert_status 200
+ end
+
test "rendering text from an action with default options renders the text with the layout" do
with_routing do |set|
set.draw { get ':controller', :action => 'index' }
diff --git a/actionpack/test/controller/output_escaping_test.rb b/actionpack/test/controller/output_escaping_test.rb
index f6913a2138..c3c549fbfc 100644
--- a/actionpack/test/controller/output_escaping_test.rb
+++ b/actionpack/test/controller/output_escaping_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
class OutputEscapingTest < ActiveSupport::TestCase
test "escape_html shouldn't die when passed nil" do
- assert_blank ERB::Util.h(nil)
+ assert ERB::Util.h(nil).blank?
end
test "escapeHTML should escape strings" do
@@ -11,8 +11,6 @@ class OutputEscapingTest < ActiveSupport::TestCase
end
test "escapeHTML shouldn't touch explicitly safe strings" do
- # TODO this seems easier to compose and reason about, but
- # this should be verified
assert_equal "<", ERB::Util.h("<".html_safe)
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
new file mode 100644
index 0000000000..22e603b881
--- /dev/null
+++ b/actionpack/test/controller/parameters/log_on_unpermitted_params_test.rb
@@ -0,0 +1,50 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+
+class LogOnUnpermittedParamsTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :log
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ end
+
+ test "logs on unexpected params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ fishing: "Turnips"
+ })
+
+ assert_logged("Unpermitted parameters: fishing") 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." }
+ })
+
+ assert_logged("Unpermitted parameters: title") do
+ params.permit(book: [:pages])
+ end
+ end
+
+ private
+
+ def assert_logged(message)
+ old_logger = ActionController::Base.logger
+ log = StringIO.new
+ ActionController::Base.logger = Logger.new(log)
+
+ begin
+ yield
+
+ log.rewind
+ assert_match message, log.read
+ ensure
+ ActionController::Base.logger = old_logger
+ end
+ end
+end
diff --git a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
index 2214ec769c..15338059bc 100644
--- a/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
+++ b/actionpack/test/controller/parameters/multi_parameter_attributes_test.rb
@@ -5,18 +5,20 @@ class MultiParameterAttributesTest < ActiveSupport::TestCase
test "permitted multi-parameter attribute keys" do
params = ActionController::Parameters.new({
book: {
- "shipped_at(1i)" => "2012",
- "shipped_at(2i)" => "3",
- "shipped_at(3i)" => "25",
- "shipped_at(4i)" => "10",
- "shipped_at(5i)" => "15",
- "published_at(1i)" => "1999",
- "published_at(2i)" => "2",
- "published_at(3i)" => "5"
+ "shipped_at(1i)" => "2012",
+ "shipped_at(2i)" => "3",
+ "shipped_at(3i)" => "25",
+ "shipped_at(4i)" => "10",
+ "shipped_at(5i)" => "15",
+ "published_at(1i)" => "1999",
+ "published_at(2i)" => "2",
+ "published_at(3i)" => "5",
+ "price(1)" => "R$",
+ "price(2f)" => "2.02"
}
})
- permitted = params.permit book: [ :shipped_at ]
+ permitted = params.permit book: [ :shipped_at, :price ]
assert permitted.permitted?
@@ -26,6 +28,9 @@ class MultiParameterAttributesTest < ActiveSupport::TestCase
assert_equal "10", permitted[:book]["shipped_at(4i)"]
assert_equal "15", permitted[:book]["shipped_at(5i)"]
+ assert_equal "R$", permitted[:book]["price(1)"]
+ assert_equal "2.02", permitted[:book]["price(2f)"]
+
assert_nil permitted[:book]["published_at(1i)"]
assert_nil permitted[:book]["published_at(2i)"]
assert_nil permitted[:book]["published_at(3i)"]
diff --git a/actionpack/test/controller/parameters/nested_parameters_test.rb b/actionpack/test/controller/parameters/nested_parameters_test.rb
index d287e79cba..3b1257e8d5 100644
--- a/actionpack/test/controller/parameters/nested_parameters_test.rb
+++ b/actionpack/test/controller/parameters/nested_parameters_test.rb
@@ -2,6 +2,10 @@ require 'abstract_unit'
require 'action_controller/metal/strong_parameters'
class NestedParametersTest < ActiveSupport::TestCase
+ def assert_filtered_out(params, key)
+ assert !params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ end
+
test "permitted nested parameters" do
params = ActionController::Parameters.new({
book: {
@@ -11,6 +15,8 @@ class NestedParametersTest < ActiveSupport::TestCase
born: "1564-04-26"
}, {
name: "Christopher Marlowe"
+ }, {
+ name: %w(malicious injected names)
}],
details: {
pages: 200,
@@ -30,33 +36,60 @@ class NestedParametersTest < ActiveSupport::TestCase
assert_equal "William Shakespeare", permitted[:book][:authors][0][:name]
assert_equal "Christopher Marlowe", permitted[:book][:authors][1][:name]
assert_equal 200, permitted[:book][:details][:pages]
- assert_nil permitted[:book][:id]
- assert_nil permitted[:book][:details][:genre]
- assert_nil permitted[:book][:authors][0][:born]
- assert_nil permitted[:magazine]
+
+ assert_filtered_out permitted, :magazine
+ assert_filtered_out permitted[:book], :id
+ assert_filtered_out permitted[:book][:details], :genre
+ assert_filtered_out permitted[:book][:authors][0], :born
+ assert_filtered_out permitted[:book][:authors][2], :name
+ end
+
+ test "permitted nested parameters with a string or a symbol as a key" do
+ params = ActionController::Parameters.new({
+ book: {
+ 'authors' => [
+ { name: 'William Shakespeare', born: '1564-04-26' },
+ { name: 'Christopher Marlowe' }
+ ]
+ }
+ })
+
+ permitted = params.permit book: [ { 'authors' => [ :name ] } ]
+
+ assert_equal 'William Shakespeare', permitted[:book]['authors'][0][:name]
+ assert_equal 'William Shakespeare', permitted[:book][:authors][0][:name]
+ assert_equal 'Christopher Marlowe', permitted[:book]['authors'][1][:name]
+ assert_equal 'Christopher Marlowe', permitted[:book][:authors][1][:name]
+
+ permitted = params.permit book: [ { authors: [ :name ] } ]
+
+ assert_equal 'William Shakespeare', permitted[:book]['authors'][0][:name]
+ assert_equal 'William Shakespeare', permitted[:book][:authors][0][:name]
+ assert_equal 'Christopher Marlowe', permitted[:book]['authors'][1][:name]
+ assert_equal 'Christopher Marlowe', permitted[:book][:authors][1][:name]
end
test "nested arrays with strings" do
params = ActionController::Parameters.new({
- :book => {
- :genres => ["Tragedy"]
+ book: {
+ genres: ["Tragedy"]
}
})
- permitted = params.permit :book => :genres
+ permitted = params.permit book: {genres: []}
assert_equal ["Tragedy"], permitted[:book][:genres]
end
test "permit may specify symbols or strings" do
params = ActionController::Parameters.new({
- :book => {
- :title => "Romeo and Juliet",
- :author => "William Shakespeare"
+ book: {
+ title: "Romeo and Juliet",
+ author: "William Shakespeare"
},
- :magazine => "Shakespeare Today"
+ magazine: "Shakespeare Today"
})
- permitted = params.permit({:book => ["title", :author]}, "magazine")
+ permitted = params.permit({book: ["title", :author]}, "magazine")
assert_equal "Romeo and Juliet", permitted[:book][:title]
assert_equal "William Shakespeare", permitted[:book][:author]
assert_equal "Shakespeare Today", permitted[:magazine]
@@ -102,16 +135,53 @@ class NestedParametersTest < ActiveSupport::TestCase
book: {
authors_attributes: {
:'0' => { name: 'William Shakespeare', age_of_death: '52' },
- :'-1' => { name: 'Unattributed Assistant' }
+ :'1' => { name: 'Unattributed Assistant' },
+ :'2' => { name: %w(injected names)}
}
}
})
permitted = params.permit book: { authors_attributes: [ :name ] }
assert_not_nil permitted[:book][:authors_attributes]['0']
- assert_not_nil permitted[:book][:authors_attributes]['-1']
- assert_nil permitted[:book][:authors_attributes]['0'][:age_of_death]
+ assert_not_nil permitted[:book][:authors_attributes]['1']
+ assert_empty permitted[:book][:authors_attributes]['2']
assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['0'][:name]
- assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-1'][:name]
+ assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['1'][:name]
+
+ assert_filtered_out permitted[:book][:authors_attributes]['0'], :age_of_death
+ end
+
+ test "fields_for-style nested params with negative numbers" do
+ params = ActionController::Parameters.new({
+ book: {
+ authors_attributes: {
+ :'-1' => { name: 'William Shakespeare', age_of_death: '52' },
+ :'-2' => { name: 'Unattributed Assistant' }
+ }
+ }
+ })
+ permitted = params.permit book: { authors_attributes: [:name] }
+
+ assert_not_nil permitted[:book][:authors_attributes]['-1']
+ assert_not_nil permitted[:book][:authors_attributes]['-2']
+ assert_equal 'William Shakespeare', permitted[:book][:authors_attributes]['-1'][:name]
+ assert_equal 'Unattributed Assistant', permitted[:book][:authors_attributes]['-2'][:name]
+
+ assert_filtered_out permitted[:book][:authors_attributes]['-1'], :age_of_death
+ end
+
+ test "nested number as key" do
+ params = ActionController::Parameters.new({
+ product: {
+ properties: {
+ '0' => "prop0",
+ '1' => "prop1"
+ }
+ }
+ })
+ params = params.require(:product).permit(:properties => ["0"])
+ assert_not_nil params[:properties]["0"]
+ assert_nil params[:properties]["1"]
+ assert_equal "prop0", params[:properties]["0"]
end
end
diff --git a/actionpack/test/controller/parameters/parameters_permit_test.rb b/actionpack/test/controller/parameters/parameters_permit_test.rb
index ad970f0a9a..b60c5f058d 100644
--- a/actionpack/test/controller/parameters/parameters_permit_test.rb
+++ b/actionpack/test/controller/parameters/parameters_permit_test.rb
@@ -1,11 +1,143 @@
require 'abstract_unit'
+require 'action_dispatch/http/upload'
require 'action_controller/metal/strong_parameters'
class ParametersPermitTest < ActiveSupport::TestCase
+ def assert_filtered_out(params, key)
+ assert !params.has_key?(key), "key #{key.inspect} has not been filtered out"
+ end
+
setup do
@params = ActionController::Parameters.new({ person: {
age: "32", name: { first: "David", last: "Heinemeier Hansson" }
}})
+
+ @struct_fields = []
+ %w(0 1 12).each do |number|
+ ['', 'i', 'f'].each do |suffix|
+ @struct_fields << "sf(#{number}#{suffix})"
+ end
+ end
+ end
+
+ test 'if nothing is permitted, the hash becomes empty' do
+ params = ActionController::Parameters.new(id: '1234')
+ permitted = params.permit
+ assert permitted.permitted?
+ assert permitted.empty?
+ end
+
+ test 'key: permitted scalar values' do
+ values = ['a', :a, nil]
+ values += [0, 1.0, 2**128, BigDecimal.new(1)]
+ values += [true, false]
+ values += [Date.today, Time.now, DateTime.now]
+ values += [STDOUT, StringIO.new, ActionDispatch::Http::UploadedFile.new(tempfile: __FILE__),
+ Rack::Test::UploadedFile.new(__FILE__)]
+
+ values.each do |value|
+ params = ActionController::Parameters.new(id: value)
+ permitted = params.permit(:id)
+ assert_equal value, permitted[:id]
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => value)
+ permitted = params.permit(:sf)
+ assert_equal value, permitted[sf]
+ end
+ end
+ end
+
+ test 'key: unknown keys are filtered out' do
+ params = ActionController::Parameters.new(id: '1234', injected: 'injected')
+ permitted = params.permit(:id)
+ assert_equal '1234', permitted[:id]
+ assert_filtered_out permitted, :injected
+ end
+
+ test 'key: arrays are filtered out' do
+ [[], [1], ['1']].each do |array|
+ params = ActionController::Parameters.new(id: array)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => array)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+ end
+
+ test 'key: hashes are filtered out' do
+ [{}, {foo: 1}, {foo: 'bar'}].each do |hash|
+ params = ActionController::Parameters.new(id: hash)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => hash)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+ end
+
+ test 'key: non-permitted scalar values are filtered out' do
+ params = ActionController::Parameters.new(id: Object.new)
+ permitted = params.permit(:id)
+ assert_filtered_out permitted, :id
+
+ @struct_fields.each do |sf|
+ params = ActionController::Parameters.new(sf => Object.new)
+ permitted = params.permit(:sf)
+ assert_filtered_out permitted, sf
+ end
+ end
+
+ test 'key: it is not assigned if not present in params' do
+ params = ActionController::Parameters.new(name: 'Joe')
+ permitted = params.permit(:id)
+ assert !permitted.has_key?(:id)
+ end
+
+ test 'key to empty array: empty arrays pass' do
+ params = ActionController::Parameters.new(id: [])
+ permitted = params.permit(id: [])
+ assert_equal [], permitted[:id]
+ end
+
+ test 'do not break params filtering on nil values' do
+ params = ActionController::Parameters.new(a: 1, b: [1, 2, 3], c: nil)
+
+ permitted = params.permit(:a, c: [], b: [])
+ assert_equal 1, permitted[:a]
+ assert_equal [1, 2, 3], permitted[:b]
+ assert_equal nil, permitted[:c]
+ end
+
+ test 'key to empty array: arrays of permitted scalars pass' do
+ [['foo'], [1], ['foo', 'bar'], [1, 2, 3]].each do |array|
+ params = ActionController::Parameters.new(id: array)
+ permitted = params.permit(id: [])
+ assert_equal array, permitted[:id]
+ end
+ end
+
+ test 'key to empty array: permitted scalar values do not pass' do
+ ['foo', 1].each do |permitted_scalar|
+ params = ActionController::Parameters.new(id: permitted_scalar)
+ permitted = params.permit(id: [])
+ assert_filtered_out permitted, :id
+ end
+ end
+
+ test 'key to empty array: arrays of non-permitted scalar do not pass' do
+ [[Object.new], [[]], [[1]], [{}], [{id: '1'}]].each do |non_permitted_scalar|
+ params = ActionController::Parameters.new(id: non_permitted_scalar)
+ permitted = params.permit(id: [])
+ assert_filtered_out permitted, :id
+ end
end
test "fetch raises ParameterMissing exception" do
@@ -15,31 +147,62 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal :foo, e.param
end
+ test "fetch with a default value of a hash does not mutate the object" do
+ params = ActionController::Parameters.new({})
+ params.fetch :foo, {}
+ assert_equal nil, params[:foo]
+ end
+
test "fetch doesnt raise ParameterMissing exception if there is a default" do
assert_equal "monkey", @params.fetch(:foo, "monkey")
assert_equal "monkey", @params.fetch(:foo) { "monkey" }
end
- test "permitted is sticky on accessors" do
+ test "not permitted is sticky on accessors" do
assert !@params.slice(:person).permitted?
assert !@params[:person][:name].permitted?
+ assert !@params[:person].except(:name).permitted?
- @params.each { |key, value| assert(value.permitted?) if key == :person }
+ @params.each { |key, value| assert(!value.permitted?) if key == "person" }
assert !@params.fetch(:person).permitted?
assert !@params.values_at(:person).first.permitted?
end
+ test "permitted is sticky on accessors" do
+ @params.permit!
+ assert @params.slice(:person).permitted?
+ assert @params[:person][:name].permitted?
+ assert @params[:person].except(:name).permitted?
+
+ @params.each { |key, value| assert(value.permitted?) if key == "person" }
+
+ assert @params.fetch(:person).permitted?
+
+ assert @params.values_at(:person).first.permitted?
+ end
+
+ test "not permitted is sticky on mutators" do
+ assert !@params.delete_if { |k| k == "person" }.permitted?
+ assert !@params.keep_if { |k,v| k == "person" }.permitted?
+ end
+
test "permitted is sticky on mutators" do
- assert !@params.delete_if { |k| k == :person }.permitted?
- assert !@params.keep_if { |k,v| k == :person }.permitted?
+ @params.permit!
+ assert @params.delete_if { |k| k == "person" }.permitted?
+ assert @params.keep_if { |k,v| k == "person" }.permitted?
end
- test "permitted is sticky beyond merges" do
+ test "not permitted is sticky beyond merges" do
assert !@params.merge(a: "b").permitted?
end
+ test "permitted is sticky beyond merges" do
+ @params.permit!
+ assert @params.merge(a: "b").permitted?
+ end
+
test "modifying the parameters" do
@params[:person][:hometown] = "Chicago"
@params[:person][:family] = { brother: "Jonas" }
@@ -48,10 +211,6 @@ class ParametersPermitTest < ActiveSupport::TestCase
assert_equal "Jonas", @params[:person][:family][:brother]
end
- test "permitting parameters that are not there should not include the keys" do
- assert !@params.permit(:person, :funky).has_key?(:funky)
- end
-
test "permit state is kept on a dup" do
@params.permit!
assert_equal @params.permitted?, @params.dup.permitted?
@@ -77,4 +236,8 @@ class ParametersPermitTest < ActiveSupport::TestCase
ActionController::Parameters.permit_all_parameters = false
end
end
+
+ test "permitting parameters as an array" do
+ assert_equal "32", @params[:person].permit([ :age ])[:age]
+ end
end
diff --git a/actionpack/test/controller/parameters/parameters_require_test.rb b/actionpack/test/controller/parameters/parameters_require_test.rb
index bdaba8d2d8..21b3eaa6b5 100644
--- a/actionpack/test/controller/parameters/parameters_require_test.rb
+++ b/actionpack/test/controller/parameters/parameters_require_test.rb
@@ -2,8 +2,14 @@ require 'abstract_unit'
require 'action_controller/metal/strong_parameters'
class ParametersRequireTest < ActiveSupport::TestCase
- test "required parameters must be present not merely not nil" do
+ test "required parameters must be present" do
assert_raises(ActionController::ParameterMissing) do
+ ActionController::Parameters.new(name: {}).require(:person)
+ end
+ end
+
+ test "required parameters can't be blank" do
+ assert_raises(ActionController::EmptyParameter) do
ActionController::Parameters.new(person: {}).require(:person)
end
end
diff --git a/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
new file mode 100644
index 0000000000..f9cc9f96f1
--- /dev/null
+++ b/actionpack/test/controller/parameters/raise_on_unpermitted_params_test.rb
@@ -0,0 +1,33 @@
+require 'abstract_unit'
+require 'action_controller/metal/strong_parameters'
+
+class RaiseOnUnpermittedParamsTest < ActiveSupport::TestCase
+ def setup
+ ActionController::Parameters.action_on_unpermitted_parameters = :raise
+ end
+
+ def teardown
+ ActionController::Parameters.action_on_unpermitted_parameters = false
+ end
+
+ test "raises on unexpected params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65 },
+ fishing: "Turnips"
+ })
+
+ assert_raises(ActionController::UnpermittedParameters) do
+ params.permit(book: [:pages])
+ end
+ end
+
+ test "raises on unexpected nested params" do
+ params = ActionController::Parameters.new({
+ book: { pages: 65, title: "Green Cats and where to find then." }
+ })
+
+ assert_raises(ActionController::UnpermittedParameters) do
+ params.permit(book: [:pages])
+ end
+ end
+end
diff --git a/actionpack/test/controller/params_wrapper_test.rb b/actionpack/test/controller/params_wrapper_test.rb
index 209f021cf7..d87e2b85b0 100644
--- a/actionpack/test/controller/params_wrapper_test.rb
+++ b/actionpack/test/controller/params_wrapper_test.rb
@@ -4,7 +4,7 @@ module Admin; class User; end; end
module ParamsWrapperTestHelp
def with_default_wrapper_options(&block)
- @controller.class._wrapper_options = {:format => [:json]}
+ @controller.class._set_wrapper_options({:format => [:json]})
@controller.class.inherited(@controller.class)
yield
end
diff --git a/actionpack/test/controller/render_test.rb b/actionpack/test/controller/render_test.rb
index aa33f01d02..f41287381a 100644
--- a/actionpack/test/controller/render_test.rb
+++ b/actionpack/test/controller/render_test.rb
@@ -2,26 +2,6 @@ require 'abstract_unit'
require 'controller/fake_models'
require 'pathname'
-module Fun
- class GamesController < ActionController::Base
- # :ported:
- def hello_world
- end
-
- def nested_partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
- end
- end
-end
-
-module Quiz
- class QuestionsController < ActionController::Base
- def new
- render :partial => Quiz::Question.new("Namespaced Partial")
- end
- end
-end
-
class TestControllerWithExtraEtags < ActionController::Base
etag { nil }
etag { 'ab' }
@@ -32,12 +12,16 @@ class TestControllerWithExtraEtags < ActionController::Base
def fresh
render text: "stale" if stale?(etag: '123')
end
+
+ def array
+ render text: "stale" if stale?(etag: %w(1 2 3))
+ end
end
class TestController < ActionController::Base
protect_from_forgery
- before_filter :set_variable_for_layout
+ before_action :set_variable_for_layout
class LabellingFormBuilder < ActionView::Helpers::FormBuilder
end
@@ -54,10 +38,6 @@ class TestController < ActionController::Base
def hello_world
end
- def hello_world_file
- render :file => File.expand_path("../../fixtures/hello", __FILE__), :formats => [:html]
- end
-
def conditional_hello
if stale?(:last_modified => Time.now.utc.beginning_of_day, :etag => [:foo, 123])
render :action => 'hello_world'
@@ -137,333 +117,16 @@ class TestController < ActionController::Base
def conditional_hello_with_bangs
render :action => 'hello_world'
end
- before_filter :handle_last_modified_and_etags, :only=>:conditional_hello_with_bangs
+ before_action :handle_last_modified_and_etags, :only=>:conditional_hello_with_bangs
def handle_last_modified_and_etags
fresh_when(:last_modified => Time.now.utc.beginning_of_day, :etag => [ :foo, 123 ])
end
- # :ported:
- def render_hello_world
- render :template => "test/hello_world"
- end
-
- def render_hello_world_with_last_modified_set
- response.last_modified = Date.new(2008, 10, 10).to_time
- render :template => "test/hello_world"
- end
-
- # :ported: compatibility
- def render_hello_world_with_forward_slash
- render :template => "/test/hello_world"
- end
-
- # :ported:
- def render_template_in_top_directory
- render :template => 'shared'
- end
-
- # :deprecated:
- def render_template_in_top_directory_with_slash
- render :template => '/shared'
- end
-
- # :ported:
- def render_hello_world_from_variable
- @person = "david"
- render :text => "hello #{@person}"
- end
-
- # :ported:
- def render_action_hello_world
- render :action => "hello_world"
- end
-
- def render_action_upcased_hello_world
- render :action => "Hello_world"
- end
-
- def render_action_hello_world_as_string
- render "hello_world"
- end
-
- def render_action_hello_world_with_symbol
- render :action => :hello_world
- end
-
- # :ported:
- def render_text_hello_world
- render :text => "hello world"
- end
-
- # :ported:
- def render_text_hello_world_with_layout
- @variable_for_layout = ", I am here!"
- render :text => "hello world", :layout => true
- end
-
- def hello_world_with_layout_false
- render :layout => false
- end
-
- # :ported:
- def render_file_with_instance_variables
- @secret = 'in the sauce'
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar')
- render :file => path
- end
-
- # :ported:
- def render_file_as_string_with_instance_variables
- @secret = 'in the sauce'
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar'))
- render path
- end
-
- # :ported:
- def render_file_not_using_full_path
- @secret = 'in the sauce'
- render :file => 'test/render_file_with_ivar'
- end
-
- def render_file_not_using_full_path_with_dot_in_path
- @secret = 'in the sauce'
- render :file => 'test/dot.directory/render_file_with_ivar'
- end
-
- def render_file_using_pathname
- @secret = 'in the sauce'
- render :file => Pathname.new(File.dirname(__FILE__)).join('..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar')
- end
-
- def render_file_from_template
- @secret = 'in the sauce'
- @path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_ivar'))
- end
-
- def render_file_with_locals
- path = File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals')
- render :file => path, :locals => {:secret => 'in the sauce'}
- end
-
- def render_file_as_string_with_locals
- path = File.expand_path(File.join(File.dirname(__FILE__), '../fixtures/test/render_file_with_locals'))
- render path, :locals => {:secret => 'in the sauce'}
- end
-
- def accessing_request_in_template
- render :inline => "Hello: <%= request.host %>"
- end
-
- def accessing_logger_in_template
- render :inline => "<%= logger.class %>"
- end
-
- def accessing_action_name_in_template
- render :inline => "<%= action_name %>"
- end
-
- def accessing_controller_name_in_template
- render :inline => "<%= controller_name %>"
- end
-
- # :ported:
- def render_custom_code
- render :text => "hello world", :status => 404
- end
-
- # :ported:
- def render_text_with_nil
- render :text => nil
- end
-
- # :ported:
- def render_text_with_false
- render :text => false
- end
-
- def render_text_with_resource
- render :text => Customer.new("David")
- end
-
- # :ported:
- def render_nothing_with_appendix
- render :text => "appended"
- end
-
- # This test is testing 3 things:
- # render :file in AV :ported:
- # render :template in AC :ported:
- # setting content type
- def render_xml_hello
- @name = "David"
- render :template => "test/hello"
- end
-
- def render_xml_hello_as_string_template
- @name = "David"
- render "test/hello"
- end
-
- def render_line_offset
- render :inline => '<% raise %>', :locals => {:foo => 'bar'}
- end
-
def heading
head :ok
end
- def greeting
- # let's just rely on the template
- end
-
- # :ported:
- def blank_response
- render :text => ' '
- end
-
- # :ported:
- def layout_test
- render :action => "hello_world"
- end
-
- # :ported:
- def builder_layout_test
- @name = nil
- render :action => "hello", :layout => "layouts/builder"
- end
-
- # :move: test this in Action View
- def builder_partial_test
- render :action => "hello_world_container"
- end
-
- # :ported:
- def partials_list
- @test_unchanged = 'hello'
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :action => "list"
- end
-
- def partial_only
- render :partial => true
- end
-
- def hello_in_a_string
- @customers = [ Customer.new("david"), Customer.new("mary") ]
- render :text => "How's there? " + render_to_string(:template => "test/list")
- end
-
- def accessing_params_in_template
- render :inline => "Hello: <%= params[:name] %>"
- end
-
- def accessing_local_assigns_in_inline_template
- name = params[:local_name]
- render :inline => "<%= 'Goodbye, ' + local_name %>",
- :locals => { :local_name => name }
- end
-
- def render_implicit_html_template_from_xhr_request
- end
-
- def render_implicit_js_template_without_layout
- end
-
- def formatted_html_erb
- end
-
- def formatted_xml_erb
- end
-
- def render_to_string_test
- @foo = render_to_string :inline => "this is a test"
- end
-
- def default_render
- @alternate_default_render ||= nil
- if @alternate_default_render
- @alternate_default_render.call
- else
- super
- end
- end
-
- def render_action_hello_world_as_symbol
- render :action => :hello_world
- end
-
- def layout_test_with_different_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_string_action
- render "hello_world", :layout => "standard"
- end
-
- def layout_test_with_different_layout_and_symbol_action
- render :hello_world, :layout => "standard"
- end
-
- def rendering_without_layout
- render :action => "hello_world", :layout => false
- end
-
- def layout_overriding_layout
- render :action => "hello_world", :layout => "standard"
- end
-
- def rendering_nothing_on_layout
- render :nothing => true
- end
-
- def render_to_string_with_assigns
- @before = "i'm before the render"
- render_to_string :text => "foo"
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_exception
- render_to_string :file => "exception that will not be caught - this will certainly not work"
- end
-
- def render_to_string_with_caught_exception
- @before = "i'm before the render"
- begin
- render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
- rescue
- end
- @after = "i'm after the render"
- render :template => "test/hello_world"
- end
-
- def accessing_params_in_template_with_layout
- render :layout => true, :inline => "Hello: <%= params[:name] %>"
- end
-
- # :ported:
- def render_with_explicit_template
- render :template => "test/hello_world"
- end
-
- def render_with_explicit_unescaped_template
- render :template => "test/h*llo_world"
- end
-
- def render_with_explicit_escaped_template
- render :template => "test/hello,world"
- end
-
- def render_with_explicit_string_template
- render "test/hello_world"
- end
-
- # :ported:
- def render_with_explicit_template_with_locals
- render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
- end
-
# :ported:
def double_render
render :text => "hello"
@@ -504,25 +167,6 @@ class TestController < ActionController::Base
render :template => "test/hello_world_from_rxml", :handlers => [:builder]
end
- def action_talk_to_layout
- # Action template sets variable that's picked up by layout
- end
-
- # :addressed:
- def render_text_with_assigns
- @hello = "world"
- render :text => "foo"
- end
-
- def yield_content_for
- render :action => "content_for", :layout => "yield"
- end
-
- def render_content_type_from_body
- response.content_type = Mime::RSS
- render :text => "hello world!"
- end
-
def head_created
head :created
end
@@ -531,6 +175,10 @@ class TestController < ActionController::Base
head :created, :content_type => "application/json"
end
+ def head_ok_with_image_png_content_type
+ head :ok, :content_type => "image/png"
+ end
+
def head_with_location_header
head :location => "/foo"
end
@@ -563,158 +211,6 @@ class TestController < ActionController::Base
head :forbidden, :x_custom_header => "something"
end
- def render_using_layout_around_block
- render :action => "using_layout_around_block"
- end
-
- def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
- end
-
- def partial_formats_html
- render :partial => 'partial', :formats => [:html]
- end
-
- def partial
- render :partial => 'partial'
- end
-
- def partial_html_erb
- render :partial => 'partial_html_erb'
- end
-
- def render_to_string_with_partial
- @partial_only = render_to_string :partial => "partial_only"
- @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
- render :template => "test/hello_world"
- end
-
- def render_to_string_with_template_and_html_partial
- @text = render_to_string :template => "test/with_partial", :formats => [:text]
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_html_partial"
- end
-
- def render_to_string_and_render_with_different_formats
- @html = render_to_string :template => "test/with_partial", :formats => [:html]
- render :template => "test/with_partial", :formats => [:text]
- end
-
- def render_template_within_a_template_with_other_format
- render :template => "test/with_xml_template",
- :formats => [:html],
- :layout => "with_html_partial"
- end
-
- def partial_with_counter
- render :partial => "counter", :locals => { :counter_counter => 5 }
- end
-
- def partial_with_locals
- render :partial => "customer", :locals => { :customer => Customer.new("david") }
- end
-
- def partial_with_form_builder
- render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_with_form_builder_subclass
- render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
- end
-
- def partial_collection
- render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as
- render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
- end
-
- def partial_collection_with_counter
- render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_with_as_and_counter
- render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
- end
-
- def partial_collection_with_locals
- render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_with_spacer
- render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
- end
-
- def partial_collection_shorthand_with_locals
- render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
- end
-
- def partial_collection_shorthand_with_different_types_of_records
- render :partial => [
- BadCustomer.new("mark"),
- GoodCustomer.new("craig"),
- BadCustomer.new("john"),
- GoodCustomer.new("zach"),
- GoodCustomer.new("brandon"),
- BadCustomer.new("dan") ],
- :locals => { :greeting => "Bonjour" }
- end
-
- def empty_partial_collection
- render :partial => "customer", :collection => []
- end
-
- def partial_collection_shorthand_with_different_types_of_records_with_counter
- partial_collection_shorthand_with_different_types_of_records
- end
-
- def missing_partial
- render :partial => 'thisFileIsntHere'
- end
-
- def partial_with_hash_object
- render :partial => "hash_object", :object => {:first_name => "Sam"}
- end
-
- def partial_with_nested_object
- render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
- end
-
- def partial_with_nested_object_shorthand
- render Quiz::Question.new("first")
- end
-
- def partial_hash_collection
- render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
- end
-
- def partial_hash_collection_with_locals
- render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
- end
-
- def partial_with_implicit_local_assignment
- @customer = Customer.new("Marcel")
- render :partial => "customer"
- end
-
- def render_call_to_partial_with_layout
- render :action => "calling_partial_with_layout"
- end
-
- def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
- end
-
- before_filter :only => :render_with_filters do
- request.format = :xml
- end
-
- # Ensure that the before filter is executed *before* self.formats is set.
- def render_with_filters
- render :action => :formatted_xml_erb
- end
-
private
def set_variable_for_layout
@@ -743,6 +239,8 @@ class TestController < ActionController::Base
end
class MetalTestController < ActionController::Metal
+ include AbstractController::Rendering
+ include ActionView::Rendering
include ActionController::Rendering
def accessing_logger_in_template
@@ -750,770 +248,9 @@ class MetalTestController < ActionController::Metal
end
end
-class RenderTest < ActionController::TestCase
- tests TestController
-
- def setup
- # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
- # a more accurate simulation of what happens in "real life".
- super
- @controller.logger = ActiveSupport::Logger.new(nil)
- ActionView::Base.logger = ActiveSupport::Logger.new(nil)
-
- @request.host = "www.nextangle.com"
- end
-
- # :ported:
- def test_simple_show
- get :hello_world
- assert_response 200
- assert_response :success
- assert_template "test/hello_world"
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- # :ported:
- def test_renders_default_template_for_missing_action
- get :'hyphen-ated'
- assert_template 'test/hyphen-ated'
- end
-
- # :ported:
- def test_render
- get :render_hello_world
- assert_template "test/hello_world"
- end
-
- def test_line_offset
- begin
- get :render_line_offset
- flunk "the action should have raised an exception"
- rescue StandardError => exc
- line = exc.backtrace.first
- assert(line =~ %r{:(\d+):})
- assert_equal "1", $1,
- "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
- end
- end
-
- # :ported: compatibility
- def test_render_with_forward_slash
- get :render_hello_world_with_forward_slash
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_in_top_directory
- get :render_template_in_top_directory
- assert_template "shared"
- assert_equal "Elastica", @response.body
- end
-
- # :ported:
- def test_render_in_top_directory_with_slash
- get :render_template_in_top_directory_with_slash
- assert_template "shared"
- assert_equal "Elastica", @response.body
- end
-
- # :ported:
- def test_render_from_variable
- get :render_hello_world_from_variable
- assert_equal "hello david", @response.body
- end
-
- # :ported:
- def test_render_action
- get :render_action_hello_world
- assert_template "test/hello_world"
- end
-
- def test_render_action_upcased
- assert_raise ActionView::MissingTemplate do
- get :render_action_upcased_hello_world
- end
- end
-
- # :ported:
- def test_render_action_hello_world_as_string
- get :render_action_hello_world_as_string
- assert_equal "Hello world!", @response.body
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_action_with_symbol
- get :render_action_hello_world_with_symbol
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_render_text
- get :render_text_hello_world
- assert_equal "hello world", @response.body
- end
-
- # :ported:
- def test_do_with_render_text_and_layout
- get :render_text_hello_world_with_layout
- assert_equal "<html>hello world, I am here!</html>", @response.body
- end
-
- # :ported:
- def test_do_with_render_action_and_layout_false
- get :hello_world_with_layout_false
- assert_equal 'Hello world!', @response.body
- end
-
- # :ported:
- def test_render_file_with_instance_variables
- get :render_file_with_instance_variables
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- def test_render_file
- get :hello_world_file
- assert_equal "Hello world!", @response.body
- end
-
- # :ported:
- def test_render_file_as_string_with_instance_variables
- get :render_file_as_string_with_instance_variables
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_not_using_full_path
- get :render_file_not_using_full_path
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_not_using_full_path_with_dot_in_path
- get :render_file_not_using_full_path_with_dot_in_path
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_using_pathname
- get :render_file_using_pathname
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_with_locals
- get :render_file_with_locals
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_file_as_string_with_locals
- get :render_file_as_string_with_locals
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :assessed:
- def test_render_file_from_template
- get :render_file_from_template
- assert_equal "The secret is in the sauce\n", @response.body
- end
-
- # :ported:
- def test_render_custom_code
- get :render_custom_code
- assert_response 404
- assert_response :missing
- assert_equal 'hello world', @response.body
- end
-
- # :ported:
- def test_render_text_with_nil
- get :render_text_with_nil
- assert_response 200
- assert_equal ' ', @response.body
- end
-
- # :ported:
- def test_render_text_with_false
- get :render_text_with_false
- assert_equal 'false', @response.body
- end
-
- # :ported:
- def test_render_nothing_with_appendix
- get :render_nothing_with_appendix
- assert_response 200
- assert_equal 'appended', @response.body
- end
-
- def test_render_text_with_resource
- get :render_text_with_resource
- assert_equal 'name: "David"', @response.body
- end
-
- # :ported:
- def test_attempt_to_access_object_method
- assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
- end
-
- # :ported:
- def test_private_methods
- assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
- end
-
- # :ported:
- def test_access_to_request_in_view
- get :accessing_request_in_template
- assert_equal "Hello: www.nextangle.com", @response.body
- end
-
- def test_access_to_logger_in_view
- get :accessing_logger_in_template
- assert_equal "ActiveSupport::Logger", @response.body
- end
-
- # :ported:
- def test_access_to_action_name_in_view
- get :accessing_action_name_in_template
- assert_equal "accessing_action_name_in_template", @response.body
- end
-
- # :ported:
- def test_access_to_controller_name_in_view
- get :accessing_controller_name_in_template
- assert_equal "test", @response.body # name is explicitly set in the controller.
- end
-
- # :ported:
- def test_render_xml
- get :render_xml_hello
- assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
- assert_equal "application/xml", @response.content_type
- end
-
- # :ported:
- def test_render_xml_as_string_template
- get :render_xml_hello_as_string_template
- assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
- assert_equal "application/xml", @response.content_type
- end
-
- # :ported:
- def test_render_xml_with_default
- get :greeting
- assert_equal "<p>This is grand!</p>\n", @response.body
- end
-
- # :move: test in AV
- def test_render_xml_with_partial
- get :builder_partial_test
- assert_equal "<test>\n <hello/>\n</test>\n", @response.body
- end
-
- # :ported:
- def test_layout_rendering
- get :layout_test
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_render_xml_with_layouts
- get :builder_layout_test
- assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
- end
-
- def test_partials_list
- get :partials_list
- assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
- end
-
- def test_render_to_string
- get :hello_in_a_string
- assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body
- end
-
- def test_render_to_string_resets_assigns
- get :render_to_string_test
- assert_equal "The value of foo is: ::this is a test::\n", @response.body
- end
-
- def test_render_to_string_inline
- get :render_to_string_with_inline_and_render
- assert_template "test/hello_world"
- end
-
- # :ported:
- def test_nested_rendering
- @controller = Fun::GamesController.new
- get :hello_world
- assert_equal "Living in a nested world", @response.body
- end
-
- def test_accessing_params_in_template
- get :accessing_params_in_template, :name => "David"
- assert_equal "Hello: David", @response.body
- end
-
- def test_accessing_local_assigns_in_inline_template
- get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
- assert_equal "Goodbye, Local David", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_implicitly_render_html_template_from_xhr_request
- xhr :get, :render_implicit_html_template_from_xhr_request
- assert_equal "XHR!\nHello HTML!", @response.body
- end
-
- def test_should_implicitly_render_js_template_without_layout
- get :render_implicit_js_template_without_layout, :format => :js
- assert_no_match %r{<html>}, @response.body
- end
-
- def test_should_render_formatted_template
- get :formatted_html_erb
- assert_equal 'formatted html erb', @response.body
- end
-
- def test_should_render_formatted_html_erb_template
- get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
- end
-
- def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
- @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
- get :formatted_xml_erb
- assert_equal '<test>passed formatted html erb</test>', @response.body
- end
-
- def test_layout_test_with_different_layout
- get :layout_test_with_different_layout
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_layout_test_with_different_layout_and_string_action
- get :layout_test_with_different_layout_and_string_action
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_layout_test_with_different_layout_and_symbol_action
- get :layout_test_with_different_layout_and_symbol_action
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_rendering_without_layout
- get :rendering_without_layout
- assert_equal "Hello world!", @response.body
- end
-
- def test_layout_overriding_layout
- get :layout_overriding_layout
- assert_no_match %r{<title>}, @response.body
- end
-
- def test_rendering_nothing_on_layout
- get :rendering_nothing_on_layout
- assert_equal " ", @response.body
- end
-
- def test_render_to_string_doesnt_break_assigns
- get :render_to_string_with_assigns
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
- end
-
- def test_bad_render_to_string_still_throws_exception
- assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
- end
-
- def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
- assert_nothing_raised { get :render_to_string_with_caught_exception }
- assert_equal "i'm before the render", assigns(:before)
- assert_equal "i'm after the render", assigns(:after)
- end
-
- def test_accessing_params_in_template_with_layout
- get :accessing_params_in_template_with_layout, :name => "David"
- assert_equal "<html>Hello: David</html>", @response.body
- end
-
- def test_render_with_explicit_template
- get :render_with_explicit_template
- assert_response :success
- end
-
- def test_render_with_explicit_unescaped_template
- assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
- get :render_with_explicit_escaped_template
- assert_equal "Hello w*rld!", @response.body
- end
-
- def test_render_with_explicit_string_template
- get :render_with_explicit_string_template
- assert_equal "<html>Hello world!</html>", @response.body
- end
-
- def test_render_with_filters
- get :render_with_filters
- assert_equal "<test>passed formatted xml erb</test>", @response.body
- end
-
- # :ported:
- def test_double_render
- assert_raise(AbstractController::DoubleRenderError) { get :double_render }
- end
-
- def test_double_redirect
- assert_raise(AbstractController::DoubleRenderError) { get :double_redirect }
- end
-
- def test_render_and_redirect
- assert_raise(AbstractController::DoubleRenderError) { get :render_and_redirect }
- end
-
- # specify the one exception to double render rule - render_to_string followed by render
- def test_render_to_string_and_render
- get :render_to_string_and_render
- assert_equal("Hi web users! here is some cached stuff", @response.body)
- end
-
- def test_rendering_with_conflicting_local_vars
- get :rendering_with_conflicting_local_vars
- assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body)
- end
-
- def test_action_talk_to_layout
- get :action_talk_to_layout
- assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
- end
-
- # :addressed:
- def test_render_text_with_assigns
- get :render_text_with_assigns
- assert_equal "world", assigns["hello"]
- end
-
- # :ported:
- def test_template_with_locals
- get :render_with_explicit_template_with_locals
- assert_equal "The secret is area51\n", @response.body
- end
-
- def test_yield_content_for
- get :yield_content_for
- assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
- end
-
- def test_overwritting_rendering_relative_file_with_extension
- get :hello_world_from_rxml_using_template
- assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
-
- get :hello_world_from_rxml_using_action
- assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
- end
-
- def test_head_created
- post :head_created
- assert_blank @response.body
- assert_response :created
- end
-
- def test_head_created_with_application_json_content_type
- post :head_created_with_application_json_content_type
- assert_blank @response.body
- assert_equal "application/json", @response.content_type
- assert_response :created
- end
-
- def test_head_with_location_header
- get :head_with_location_header
- assert_blank @response.body
- assert_equal "/foo", @response.headers["Location"]
- assert_response :ok
- end
-
- def test_head_with_location_object
- with_routing do |set|
- set.draw do
- resources :customers
- get ':controller/:action'
- end
-
- get :head_with_location_object
- assert_blank @response.body
- assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
- assert_response :ok
- end
- end
-
- def test_head_with_custom_header
- get :head_with_custom_header
- assert_blank @response.body
- assert_equal "something", @response.headers["X-Custom-Header"]
- assert_response :ok
- end
-
- def test_head_with_www_authenticate_header
- get :head_with_www_authenticate_header
- assert_blank @response.body
- assert_equal "something", @response.headers["WWW-Authenticate"]
- assert_response :ok
- end
-
- def test_head_with_symbolic_status
- get :head_with_symbolic_status, :status => "ok"
- assert_equal 200, @response.status
- assert_response :ok
-
- get :head_with_symbolic_status, :status => "not_found"
- assert_equal 404, @response.status
- assert_response :not_found
-
- get :head_with_symbolic_status, :status => "no_content"
- assert_equal 204, @response.status
- assert !@response.headers.include?('Content-Length')
- assert_response :no_content
-
- Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
- get :head_with_symbolic_status, :status => status.to_s
- assert_equal code, @response.response_code
- assert_response status
- end
- end
-
- def test_head_with_integer_status
- Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
- get :head_with_integer_status, :status => code.to_s
- assert_equal message, @response.message
- end
- end
-
- def test_head_with_string_status
- get :head_with_string_status, :status => "404 Eat Dirt"
- assert_equal 404, @response.response_code
- assert_equal "Not Found", @response.message
- assert_response :not_found
- end
-
- def test_head_with_status_code_first
- get :head_with_status_code_first
- assert_equal 403, @response.response_code
- assert_equal "Forbidden", @response.message
- assert_equal "something", @response.headers["X-Custom-Header"]
- assert_response :forbidden
- end
-
- def test_using_layout_around_block
- get :render_using_layout_around_block
- assert_equal "Before (David)\nInside from block\nAfter", @response.body
- end
-
- def test_using_layout_around_block_in_main_layout_and_within_content_for_layout
- get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
- assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
- end
-
- def test_partial_only
- get :partial_only
- assert_equal "only partial", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_render_html_formatted_partial
- get :partial
- assert_equal "partial html", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_html_formatted_partial_even_with_other_mime_time_in_accept
- @request.accept = "text/javascript, text/html"
-
- get :partial_html_erb
-
- assert_equal "partial.html.erb", @response.body.strip
- assert_equal "text/html", @response.content_type
- end
-
- def test_should_render_html_partial_with_formats
- get :partial_formats_html
- assert_equal "partial html", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_partial
- get :render_to_string_with_partial
- assert_equal "only partial", assigns(:partial_only)
- assert_equal "Hello: david", assigns(:partial_with_locals)
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_with_template_and_html_partial
- get :render_to_string_with_template_and_html_partial
- assert_equal "**only partial**\n", assigns(:text)
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
- assert_equal "<strong>only html partial</strong>\n", @response.body
- assert_equal "text/html", @response.content_type
- end
-
- def test_render_to_string_and_render_with_different_formats
- get :render_to_string_and_render_with_different_formats
- assert_equal "<strong>only partial</strong>\n", assigns(:html)
- assert_equal "**only partial**\n", @response.body
- assert_equal "text/plain", @response.content_type
- end
-
- def test_render_template_within_a_template_with_other_format
- get :render_template_within_a_template_with_other_format
- expected = "only html partial<p>This is grand!</p>"
- assert_equal expected, @response.body.strip
- assert_equal "text/html", @response.content_type
- end
-
- def test_partial_with_counter
- get :partial_with_counter
- assert_equal "5", @response.body
- end
-
- def test_partial_with_locals
- get :partial_with_locals
- assert_equal "Hello: david", @response.body
- end
-
- def test_partial_with_form_builder
- get :partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('test/_form')
- end
-
- def test_partial_with_form_builder_subclass
- get :partial_with_form_builder_subclass
- assert_match(/<label/, @response.body)
- assert_template('test/_labelling_form')
- end
-
- def test_nested_partial_with_form_builder
- @controller = Fun::GamesController.new
- get :nested_partial_with_form_builder
- assert_match(/<label/, @response.body)
- assert_template('fun/games/_form')
- end
-
- def test_namespaced_object_partial
- @controller = Quiz::QuestionsController.new
- get :new
- assert_equal "Namespaced Partial", @response.body
- end
-
- def test_partial_collection
- get :partial_collection
- assert_equal "Hello: davidHello: mary", @response.body
- end
-
- def test_partial_collection_with_as
- get :partial_collection_with_as
- assert_equal "david david davidmary mary mary", @response.body
- end
-
- def test_partial_collection_with_counter
- get :partial_collection_with_counter
- assert_equal "david0mary1", @response.body
- end
-
- def test_partial_collection_with_as_and_counter
- get :partial_collection_with_as_and_counter
- assert_equal "david0mary1", @response.body
- end
-
- def test_partial_collection_with_locals
- get :partial_collection_with_locals
- assert_equal "Bonjour: davidBonjour: mary", @response.body
- end
-
- def test_locals_option_to_assert_template_is_not_supported
- warning_buffer = StringIO.new
- $stderr = warning_buffer
-
- get :partial_collection_with_locals
- assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' }
- assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string
- ensure
- $stderr = STDERR
- end
-
- def test_partial_collection_with_spacer
- get :partial_collection_with_spacer
- assert_equal "Hello: davidonly partialHello: mary", @response.body
- assert_template :partial => '_customer'
- end
-
- def test_partial_collection_shorthand_with_locals
- get :partial_collection_shorthand_with_locals
- assert_equal "Bonjour: davidBonjour: mary", @response.body
- assert_template :partial => 'customers/_customer', :count => 2
- assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
- end
-
- def test_partial_collection_shorthand_with_different_types_of_records
- get :partial_collection_shorthand_with_different_types_of_records
- assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
- assert_template :partial => 'good_customers/_good_customer', :count => 3
- assert_template :partial => 'bad_customers/_bad_customer', :count => 3
- end
-
- def test_empty_partial_collection
- get :empty_partial_collection
- assert_equal " ", @response.body
- assert_template :partial => false
- end
-
- def test_partial_with_hash_object
- get :partial_with_hash_object
- assert_equal "Sam\nmaS\n", @response.body
- end
-
- def test_partial_with_nested_object
- get :partial_with_nested_object
- assert_equal "first", @response.body
- end
-
- def test_partial_with_nested_object_shorthand
- get :partial_with_nested_object_shorthand
- assert_equal "first", @response.body
- end
-
- def test_hash_partial_collection
- get :partial_hash_collection
- assert_equal "Pratik\nkitarP\nAmy\nymA\n", @response.body
- end
-
- def test_partial_hash_collection_with_locals
- get :partial_hash_collection_with_locals
- assert_equal "Hola: PratikHola: Amy", @response.body
- end
-
- def test_render_missing_partial_template
- assert_raise(ActionView::MissingTemplate) do
- get :missing_partial
- end
- end
-
- def test_render_call_to_partial_with_layout
- get :render_call_to_partial_with_layout
- assert_equal "Before (David)\nInside from partial (David)\nAfter", @response.body
- end
-
- def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
- assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
- end
-end
-
class ExpiresInRenderTest < ActionController::TestCase
tests TestController
- def setup
- @request.host = "www.nextangle.com"
- end
-
def test_expires_in_header
get :conditional_hello_with_expires_in
assert_equal "max-age=60, private", @response.headers["Cache-Control"]
@@ -1568,7 +305,6 @@ class LastModifiedRenderTest < ActionController::TestCase
def setup
super
- @request.host = "www.nextangle.com"
@last_modified = Time.now.utc.beginning_of_day.httpdate
end
@@ -1581,7 +317,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :conditional_hello
assert_equal 304, @response.status.to_i
- assert_blank @response.body
+ assert @response.body.blank?
assert_equal @last_modified, @response.headers['Last-Modified']
end
@@ -1596,7 +332,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
get :conditional_hello
assert_equal 200, @response.status.to_i
- assert_present @response.body
+ assert @response.body.present?
assert_equal @last_modified, @response.headers['Last-Modified']
end
@@ -1610,7 +346,7 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = @last_modified
get :conditional_hello_with_record
assert_equal 304, @response.status.to_i
- assert_blank @response.body
+ assert @response.body.blank?
assert_equal @last_modified, @response.headers['Last-Modified']
end
@@ -1625,11 +361,10 @@ class LastModifiedRenderTest < ActionController::TestCase
@request.if_modified_since = 'Thu, 16 Jul 2008 00:00:00 GMT'
get :conditional_hello_with_record
assert_equal 200, @response.status.to_i
- assert_present @response.body
+ assert @response.body.present?
assert_equal @last_modified, @response.headers['Last-Modified']
end
-
def test_request_with_bang_gets_last_modified
get :conditional_hello_with_bangs
assert_equal @last_modified, @response.headers['Last-Modified']
@@ -1654,11 +389,10 @@ class EtagRenderTest < ActionController::TestCase
def setup
super
- @request.host = "www.nextangle.com"
end
def test_multiple_etags
- @request.if_none_match = %("#{Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key([ "123", 'ab', :cde, [:f] ]))}")
+ @request.if_none_match = etag(["123", 'ab', :cde, [:f]])
get :fresh
assert_response :not_modified
@@ -1666,8 +400,21 @@ class EtagRenderTest < ActionController::TestCase
get :fresh
assert_response :success
end
-end
+ def test_array
+ @request.if_none_match = etag([%w(1 2 3), 'ab', :cde, [:f]])
+ get :array
+ assert_response :not_modified
+
+ @request.if_none_match = %("nomatch")
+ get :array
+ assert_response :success
+ end
+
+ def etag(record)
+ Digest::MD5.hexdigest(ActiveSupport::Cache.expand_cache_key(record)).inspect
+ end
+end
class MetalRenderTest < ActionController::TestCase
tests MetalTestController
@@ -1677,3 +424,109 @@ class MetalRenderTest < ActionController::TestCase
assert_equal "NilClass", @response.body
end
end
+
+class HeadRenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_head_created
+ post :head_created
+ assert @response.body.blank?
+ assert_response :created
+ end
+
+ def test_head_created_with_application_json_content_type
+ post :head_created_with_application_json_content_type
+ assert @response.body.blank?
+ assert_equal "application/json", @response.header["Content-Type"]
+ assert_response :created
+ end
+
+ def test_head_ok_with_image_png_content_type
+ post :head_ok_with_image_png_content_type
+ assert @response.body.blank?
+ assert_equal "image/png", @response.header["Content-Type"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_header
+ get :head_with_location_header
+ assert @response.body.blank?
+ assert_equal "/foo", @response.headers["Location"]
+ assert_response :ok
+ end
+
+ def test_head_with_location_object
+ with_routing do |set|
+ set.draw do
+ resources :customers
+ get ':controller/:action'
+ end
+
+ get :head_with_location_object
+ assert @response.body.blank?
+ assert_equal "http://www.nextangle.com/customers/1", @response.headers["Location"]
+ assert_response :ok
+ end
+ end
+
+ def test_head_with_custom_header
+ get :head_with_custom_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :ok
+ end
+
+ def test_head_with_www_authenticate_header
+ get :head_with_www_authenticate_header
+ assert @response.body.blank?
+ assert_equal "something", @response.headers["WWW-Authenticate"]
+ assert_response :ok
+ end
+
+ def test_head_with_symbolic_status
+ get :head_with_symbolic_status, :status => "ok"
+ assert_equal 200, @response.status
+ assert_response :ok
+
+ get :head_with_symbolic_status, :status => "not_found"
+ assert_equal 404, @response.status
+ assert_response :not_found
+
+ get :head_with_symbolic_status, :status => "no_content"
+ assert_equal 204, @response.status
+ assert !@response.headers.include?('Content-Length')
+ assert_response :no_content
+
+ Rack::Utils::SYMBOL_TO_STATUS_CODE.each do |status, code|
+ get :head_with_symbolic_status, :status => status.to_s
+ assert_equal code, @response.response_code
+ assert_response status
+ end
+ end
+
+ def test_head_with_integer_status
+ Rack::Utils::HTTP_STATUS_CODES.each do |code, message|
+ get :head_with_integer_status, :status => code.to_s
+ assert_equal message, @response.message
+ end
+ end
+
+ def test_head_with_string_status
+ get :head_with_string_status, :status => "404 Eat Dirt"
+ assert_equal 404, @response.response_code
+ assert_equal "Not Found", @response.message
+ assert_response :not_found
+ end
+
+ def test_head_with_status_code_first
+ get :head_with_status_code_first
+ assert_equal 403, @response.response_code
+ assert_equal "Forbidden", @response.message
+ assert_equal "something", @response.headers["X-Custom-Header"]
+ assert_response :forbidden
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/controller/request_forgery_protection_test.rb b/actionpack/test/controller/request_forgery_protection_test.rb
index 1f637eb791..727db79241 100644
--- a/actionpack/test/controller/request_forgery_protection_test.rb
+++ b/actionpack/test/controller/request_forgery_protection_test.rb
@@ -66,6 +66,24 @@ class RequestForgeryProtectionControllerUsingException < ActionController::Base
protect_from_forgery :only => %w(index meta), :with => :exception
end
+class RequestForgeryProtectionControllerUsingNullSession < ActionController::Base
+ protect_from_forgery :with => :null_session
+
+ def signed
+ cookies.signed[:foo] = 'bar'
+ render :nothing => true
+ end
+
+ def encrypted
+ cookies.encrypted[:foo] = 'bar'
+ render :nothing => true
+ end
+
+ def try_to_reset_session
+ reset_session
+ render :nothing => true
+ end
+end
class FreeCookieController < RequestForgeryProtectionControllerUsingResetSession
self.allow_forgery_protection = false
@@ -170,6 +188,10 @@ module RequestForgeryProtectionTests
assert_not_blocked { get :index }
end
+ def test_should_allow_head
+ assert_not_blocked { head :index }
+ end
+
def test_should_allow_post_without_token_on_unsafe_action
assert_not_blocked { post :unsafe }
end
@@ -283,6 +305,33 @@ class RequestForgeryProtectionControllerUsingResetSessionTest < ActionController
end
end
+class NullSessionDummyKeyGenerator
+ def generate_key(secret)
+ '03312270731a2ed0d11ed091c2338a06'
+ end
+end
+
+class RequestForgeryProtectionControllerUsingNullSessionTest < ActionController::TestCase
+ def setup
+ @request.env[ActionDispatch::Cookies::GENERATOR_KEY] = NullSessionDummyKeyGenerator.new
+ end
+
+ test 'should allow to set signed cookies' do
+ post :signed
+ assert_response :ok
+ end
+
+ test 'should allow to set encrypted cookies' do
+ post :encrypted
+ assert_response :ok
+ end
+
+ test 'should allow reset_session' do
+ post :try_to_reset_session
+ assert_response :ok
+ end
+end
+
class RequestForgeryProtectionControllerUsingExceptionTest < ActionController::TestCase
include RequestForgeryProtectionTests
def assert_blocked
@@ -320,7 +369,7 @@ class FreeCookieControllerTest < ActionController::TestCase
test 'should not emit a csrf-token meta tag' do
get :meta
- assert_blank @response.body
+ assert @response.body.blank?
end
end
diff --git a/actionpack/test/controller/required_params_test.rb b/actionpack/test/controller/required_params_test.rb
index 661bcb3945..b27069140b 100644
--- a/actionpack/test/controller/required_params_test.rb
+++ b/actionpack/test/controller/required_params_test.rb
@@ -5,26 +5,42 @@ class BooksController < ActionController::Base
params.require(:book).require(:name)
head :ok
end
+
+ def update
+ params.require(:book)
+ head :ok
+ end
end
class ActionControllerRequiredParamsTest < ActionController::TestCase
tests BooksController
test "missing required parameters will raise exception" do
- post :create, { magazine: { name: "Mjallo!" } }
- assert_response :bad_request
+ assert_raise ActionController::ParameterMissing do
+ post :create, { magazine: { name: "Mjallo!" } }
+ end
- post :create, { book: { title: "Mjallo!" } }
- assert_response :bad_request
+ assert_raise ActionController::ParameterMissing do
+ post :create, { book: { title: "Mjallo!" } }
+ end
+ end
+
+ test "empty required parameters will raise an exception" do
+ assert_raise ActionController::EmptyParameter do
+ put :update, {book: {}}
+ end
+
+ assert_raise ActionController::EmptyParameter do
+ put :update, {book: ''}
+ end
+
+ assert_raise ActionController::EmptyParameter do
+ put :update, {book: nil}
+ end
end
test "required parameters that are present will not raise" do
post :create, { book: { name: "Mjallo!" } }
assert_response :ok
end
-
- test "missing parameters will be mentioned in the return" do
- post :create, { magazine: { name: "Mjallo!" } }
- assert_equal "Required parameter missing: book", response.body
- end
end
diff --git a/actionpack/test/controller/rescue_test.rb b/actionpack/test/controller/rescue_test.rb
index 48e2d6491e..4898b0c57f 100644
--- a/actionpack/test/controller/rescue_test.rb
+++ b/actionpack/test/controller/rescue_test.rb
@@ -68,9 +68,9 @@ class RescueController < ActionController::Base
render :text => 'io error'
end
- before_filter(:only => :before_filter_raises) { raise 'umm nice' }
+ before_action(only: :before_action_raises) { raise 'umm nice' }
- def before_filter_raises
+ def before_action_raises
end
def raises
diff --git a/actionpack/test/controller/resources_test.rb b/actionpack/test/controller/resources_test.rb
index 305659b219..9aea7e860a 100644
--- a/actionpack/test/controller/resources_test.rb
+++ b/actionpack/test/controller/resources_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/with_options'
+require 'active_support/core_ext/array/extract_options'
class ResourcesTest < ActionController::TestCase
def test_default_restful_routes
diff --git a/actionpack/test/controller/routing_test.rb b/actionpack/test/controller/routing_test.rb
index f0430e516f..46df1a7bd5 100644
--- a/actionpack/test/controller/routing_test.rb
+++ b/actionpack/test/controller/routing_test.rb
@@ -57,13 +57,13 @@ class UriReservedCharactersRoutingTest < ActiveSupport::TestCase
end
class MockController
- def self.build(helpers)
+ def self.build(helpers, additional_options = {})
Class.new do
- def url_options
- options = super
+ define_method :url_options do
+ options = super()
options[:protocol] ||= "http"
options[:host] ||= "test.host"
- options
+ options.merge(additional_options)
end
include helpers
@@ -428,8 +428,8 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
routes.send(:pages_url)
end
- def setup_for_named_route
- MockController.build(rs.url_helpers).new
+ def setup_for_named_route(options = {})
+ MockController.build(rs.url_helpers, options).new
end
def test_named_route_without_hash
@@ -456,6 +456,32 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
assert_equal("/", routes.send(:root_path))
end
+ def test_named_route_root_with_hash
+ rs.draw do
+ root "hello#index", as: :index
+ end
+
+ routes = setup_for_named_route
+ assert_equal("http://test.host/", routes.send(:index_url))
+ assert_equal("/", routes.send(:index_path))
+ end
+
+ def test_root_without_path_raises_argument_error
+ assert_raises ArgumentError do
+ rs.draw { root nil }
+ end
+ end
+
+ def test_named_route_root_with_trailing_slash
+ rs.draw do
+ root "hello#index"
+ end
+
+ routes = setup_for_named_route(trailing_slash: true)
+ assert_equal("http://test.host/", routes.send(:root_url))
+ assert_equal("http://test.host/?foo=bar", routes.send(:root_url, foo: :bar))
+ end
+
def test_named_route_with_regexps
rs.draw do
get 'page/:year/:month/:day/:title' => 'page#show', :as => 'article',
@@ -689,17 +715,13 @@ class LegacyRouteSetTests < ActiveSupport::TestCase
def setup_request_method_routes_for(method)
rs.draw do
- match '/match' => 'books#get', :via => :get
- match '/match' => 'books#post', :via => :post
- match '/match' => 'books#put', :via => :put
- match '/match' => 'books#patch', :via => :patch
- match '/match' => 'books#delete', :via => :delete
+ match '/match' => "books##{method}", :via => method.to_sym
end
end
%w(GET PATCH POST PUT DELETE).each do |request_method|
define_method("test_request_method_recognized_with_#{request_method}") do
- setup_request_method_routes_for(request_method)
+ setup_request_method_routes_for(request_method.downcase)
params = rs.recognize_path("/match", :method => request_method)
assert_equal request_method.downcase, params[:action]
end
@@ -882,12 +904,13 @@ class RouteSetTest < ActiveSupport::TestCase
assert_equal set.routes.first, set.named_routes[:hello]
end
- def test_earlier_named_routes_take_precedence
- set.draw do
- get '/hello/world' => 'a#b', :as => 'hello'
- get '/hello' => 'a#b', :as => 'hello'
+ def test_duplicate_named_route_raises_rather_than_pick_precedence
+ assert_raise ArgumentError do
+ set.draw do
+ get '/hello/world' => 'a#b', :as => 'hello'
+ get '/hello' => 'a#b', :as => 'hello'
+ end
end
- assert_equal set.routes.first, set.named_routes[:hello]
end
def setup_named_route_test
@@ -1881,6 +1904,10 @@ class RackMountIntegrationTests < ActiveSupport::TestCase
assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('ã“ã‚“ã«ã¡ã¯/世界'), :method => :get))
end
+ def test_downcased_unicode_path
+ assert_equal({:controller => 'news', :action => 'index'}, @routes.recognize_path(URI.parser.escape('ã“ã‚“ã«ã¡ã¯/世界').downcase, :method => :get))
+ end
+
private
def sort_extras!(extras)
if extras.length == 2
diff --git a/actionpack/test/controller/send_file_test.rb b/actionpack/test/controller/send_file_test.rb
index 97ede35317..8ecc1c7d73 100644
--- a/actionpack/test/controller/send_file_test.rb
+++ b/actionpack/test/controller/send_file_test.rb
@@ -114,6 +114,18 @@ class SendFileTest < ActionController::TestCase
assert_equal 'private', h['Cache-Control']
end
+ def test_send_file_headers_with_disposition_as_a_symbol
+ options = {
+ :type => Mime::PNG,
+ :disposition => :disposition,
+ :filename => 'filename'
+ }
+
+ @controller.headers = {}
+ @controller.send(:send_file_headers!, options)
+ assert_equal 'disposition; filename="filename"', @controller.headers['Content-Disposition']
+ end
+
def test_send_file_headers_with_mime_lookup_with_symbol
options = {
:type => :png
@@ -132,9 +144,9 @@ class SendFileTest < ActionController::TestCase
}
@controller.headers = {}
- assert_raise(ArgumentError){ @controller.send(:send_file_headers!, options) }
+ assert !@controller.send(:send_file_headers!, options)
end
-
+
def test_send_file_headers_guess_type_from_extension
{
'image.png' => 'image/png',
diff --git a/actionpack/test/controller/show_exceptions_test.rb b/actionpack/test/controller/show_exceptions_test.rb
index ab1bd0e3b6..ff23b22040 100644
--- a/actionpack/test/controller/show_exceptions_test.rb
+++ b/actionpack/test/controller/show_exceptions_test.rb
@@ -5,7 +5,7 @@ module ShowExceptions
use ActionDispatch::ShowExceptions, ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
use ActionDispatch::DebugExceptions
- before_filter :only => :another_boom do
+ before_action only: :another_boom do
request.env["action_dispatch.show_detailed_exceptions"] = true
end
@@ -47,7 +47,7 @@ module ShowExceptions
end
end
- class ShowExceptionsOverridenController < ShowExceptionsController
+ class ShowExceptionsOverriddenController < ShowExceptionsController
private
def show_detailed_exceptions?
@@ -55,15 +55,15 @@ module ShowExceptions
end
end
- class ShowExceptionsOverridenTest < ActionDispatch::IntegrationTest
+ class ShowExceptionsOverriddenTest < ActionDispatch::IntegrationTest
test 'show error page' do
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
get '/', {'detailed' => '0'}
assert_equal "500 error fixture\n", body
end
test 'show diagnostics message' do
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
get '/', {'detailed' => '1'}
assert_match(/boom/, body)
end
@@ -71,23 +71,23 @@ module ShowExceptions
class ShowExceptionsFormatsTest < ActionDispatch::IntegrationTest
def test_render_json_exception
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
get "/", {}, 'HTTP_ACCEPT' => 'application/json'
assert_response :internal_server_error
assert_equal 'application/json', response.content_type.to_s
- assert_equal({ :status => '500', :error => 'boom!' }.to_json, response.body)
+ assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_json, response.body)
end
def test_render_xml_exception
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
get "/", {}, 'HTTP_ACCEPT' => 'application/xml'
assert_response :internal_server_error
assert_equal 'application/xml', response.content_type.to_s
- assert_equal({ :status => '500', :error => 'boom!' }.to_xml, response.body)
+ assert_equal({ :status => '500', :error => 'Internal Server Error' }.to_xml, response.body)
end
def test_render_fallback_exception
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
get "/", {}, 'HTTP_ACCEPT' => 'text/csv'
assert_response :internal_server_error
assert_equal 'text/html', response.content_type.to_s
@@ -96,7 +96,7 @@ module ShowExceptions
class ShowFailsafeExceptionsTest < ActionDispatch::IntegrationTest
def test_render_failsafe_exception
- @app = ShowExceptionsOverridenController.action(:boom)
+ @app = ShowExceptionsOverriddenController.action(:boom)
@exceptions_app = @app.instance_variable_get(:@exceptions_app)
@app.instance_variable_set(:@exceptions_app, nil)
$stderr = StringIO.new
@@ -104,7 +104,7 @@ module ShowExceptions
get '/', {}, 'HTTP_ACCEPT' => 'text/json'
assert_response :internal_server_error
assert_equal 'text/plain', response.content_type.to_s
-
+ ensure
@app.instance_variable_set(:@exceptions_app, @exceptions_app)
$stderr = STDERR
end
diff --git a/actionpack/test/controller/spec_style_test.rb b/actionpack/test/controller/spec_style_test.rb
deleted file mode 100644
index e118c584ca..0000000000
--- a/actionpack/test/controller/spec_style_test.rb
+++ /dev/null
@@ -1,208 +0,0 @@
-require "abstract_unit"
-
-class ApplicationController < ActionController::Base; end
-class ModelsController < ApplicationController; end
-module Admin
- class WidgetsController < ApplicationController; end
-end
-
-# ApplicationController
-describe ApplicationController do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-describe ApplicationController, :index do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-describe ApplicationController, "unauthenticated user" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-describe "ApplicationControllerTest" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-describe "ApplicationControllerTest", :index do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-describe "ApplicationControllerTest", "unauthenticated user" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ApplicationController, @controller
- end
- end
- end
-end
-
-# ModelsController
-describe ModelsController do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-describe ModelsController, :index do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-describe ModelsController, "unauthenticated user" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-describe "ModelsControllerTest" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-describe "ModelsControllerTest", :index do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-describe "ModelsControllerTest", "unauthenticated user" do
- describe "nested" do
- describe "even deeper" do
- it "exists" do
- assert_kind_of ModelsController, @controller
- end
- end
- end
-end
-
-# Nested Admin::WidgetsControllerTest
-module Admin
- class WidgetsControllerTest < ActionController::TestCase
- test "exists" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-
- describe WidgetsController do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
- end
-
- describe WidgetsController, "unauthenticated users" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
- end
-end
-
-class Admin::WidgetsControllerTest < ActionController::TestCase
- test "exists here too" do
- assert_kind_of Admin::WidgetsController, @controller
- end
-end
-
-describe Admin::WidgetsController do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
-
-describe Admin::WidgetsController, "unauthenticated users" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
-
-describe "Admin::WidgetsController" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
-
-describe "Admin::WidgetsControllerTest" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
-
-describe "Admin::WidgetsController", "unauthenticated users" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
-
-describe "Admin::WidgetsControllerTest", "unauthenticated users" do
- describe "index" do
- it "respond successful" do
- assert_kind_of Admin::WidgetsController, @controller
- end
- end
-end
diff --git a/actionpack/test/controller/spec_type_test.rb b/actionpack/test/controller/spec_type_test.rb
deleted file mode 100644
index 13be8a3405..0000000000
--- a/actionpack/test/controller/spec_type_test.rb
+++ /dev/null
@@ -1,37 +0,0 @@
-require "abstract_unit"
-
-class ApplicationController < ActionController::Base; end
-class ModelsController < ApplicationController; end
-
-class ActionControllerSpecTypeTest < ActiveSupport::TestCase
- def assert_controller actual
- assert_equal ActionController::TestCase, actual
- end
-
- def refute_controller actual
- refute_equal ActionController::TestCase, actual
- end
-
- def test_spec_type_resolves_for_class_constants
- assert_controller MiniTest::Spec.spec_type(ApplicationController)
- assert_controller MiniTest::Spec.spec_type(ModelsController)
- end
-
- def test_spec_type_resolves_for_matching_strings
- assert_controller MiniTest::Spec.spec_type("WidgetController")
- assert_controller MiniTest::Spec.spec_type("WidgetControllerTest")
- assert_controller MiniTest::Spec.spec_type("Widget Controller Test")
- # And is not case sensitive
- assert_controller MiniTest::Spec.spec_type("widgetcontroller")
- assert_controller MiniTest::Spec.spec_type("widgetcontrollertest")
- assert_controller MiniTest::Spec.spec_type("widget controller test")
- end
-
- def test_spec_type_wont_match_non_space_characters
- refute_controller MiniTest::Spec.spec_type("Widget Controller\tTest")
- refute_controller MiniTest::Spec.spec_type("Widget Controller\rTest")
- refute_controller MiniTest::Spec.spec_type("Widget Controller\nTest")
- refute_controller MiniTest::Spec.spec_type("Widget Controller\fTest")
- refute_controller MiniTest::Spec.spec_type("Widget ControllerXTest")
- end
-end
diff --git a/actionpack/test/controller/sweeper_test.rb b/actionpack/test/controller/sweeper_test.rb
deleted file mode 100644
index 0561efc62f..0000000000
--- a/actionpack/test/controller/sweeper_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'abstract_unit'
-
-
-class SweeperTest < ActionController::TestCase
-
- class ::AppSweeper < ActionController::Caching::Sweeper; end
-
- def test_sweeper_should_not_ignore_unknown_method_calls
- sweeper = ActionController::Caching::Sweeper.send(:new)
- assert_raise NameError do
- sweeper.instance_eval do
- some_method_that_doesnt_exist
- end
- end
- end
-end
diff --git a/actionpack/test/controller/test_case_test.rb b/actionpack/test/controller/test_case_test.rb
index 8990fc34d6..f75c604277 100644
--- a/actionpack/test/controller/test_case_test.rb
+++ b/actionpack/test/controller/test_case_test.rb
@@ -57,6 +57,10 @@ class TestCaseTest < ActionController::TestCase
render :text => request.protocol
end
+ def test_headers
+ render text: request.headers.env.to_json
+ end
+
def test_html_output
render :text => <<HTML
<html>
@@ -197,11 +201,6 @@ XML
assert_raise(NoMethodError) { head :test_params, "document body", :id => 10 }
end
- def test_options
- options :test_params
- assert_equal 200, @response.status
- end
-
def test_process_without_flash
process :set_flash
assert_equal '><', flash['test']
@@ -281,13 +280,6 @@ XML
assert_equal "/test_case_test/test/test_uri/7", @response.body
end
- def test_process_with_old_api
- assert_deprecated do
- process :test_uri, :id => 7
- assert_equal "/test_case_test/test/test_uri/7", @response.body
- end
- end
-
def test_process_with_request_uri_with_params_with_explicit_uri
@request.env['PATH_INFO'] = "/explicit/uri"
process :test_uri, "GET", :id => 7
@@ -626,6 +618,24 @@ XML
assert_equal 2004, page[:year]
end
+ test "set additional HTTP headers" do
+ @request.headers['Referer'] = "http://nohost.com/home"
+ @request.headers['Content-Type'] = "application/rss+xml"
+ get :test_headers
+ parsed_env = JSON.parse(@response.body)
+ assert_equal "http://nohost.com/home", parsed_env["HTTP_REFERER"]
+ assert_equal "application/rss+xml", parsed_env["CONTENT_TYPE"]
+ end
+
+ test "set additional env variables" do
+ @request.headers['HTTP_REFERER'] = "http://example.com/about"
+ @request.headers['CONTENT_TYPE'] = "application/json"
+ get :test_headers
+ parsed_env = JSON.parse(@response.body)
+ assert_equal "http://example.com/about", parsed_env["HTTP_REFERER"]
+ assert_equal "application/json", parsed_env["CONTENT_TYPE"]
+ end
+
def test_id_converted_to_string
get :test_params, :id => 20, :foo => Object.new
assert_kind_of String, @request.path_parameters['id']
@@ -692,7 +702,7 @@ XML
assert_equal "bar", @request.params[:foo]
@request.recycle!
post :no_op
- assert_blank @request.params[:foo]
+ assert @request.params[:foo].blank?
end
def test_symbolized_path_params_reset_after_request
@@ -818,6 +828,18 @@ XML
assert_equal '159528', @response.body
end
+ def test_fixture_file_upload_relative_to_fixture_path
+ TestCaseTest.stubs(:fixture_path).returns(FILES_DIR)
+ uploaded_file = fixture_file_upload("mona_lisa.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+
+ def test_fixture_file_upload_ignores_nil_fixture_path
+ TestCaseTest.stubs(:fixture_path).returns(nil)
+ uploaded_file = fixture_file_upload("#{FILES_DIR}/mona_lisa.jpg", "image/jpg")
+ assert_equal File.open("#{FILES_DIR}/mona_lisa.jpg", READ_PLAIN).read, uploaded_file.read
+ end
+
def test_action_dispatch_uploaded_file_upload
filename = 'mona_lisa.jpg'
path = "#{FILES_DIR}/#{filename}"
@@ -919,3 +941,34 @@ class AnonymousControllerTest < ActionController::TestCase
assert_equal 'anonymous', @response.body
end
end
+
+class RoutingDefaultsTest < ActionController::TestCase
+ def setup
+ @controller = Class.new(ActionController::Base) do
+ def post
+ render :text => request.fullpath
+ end
+
+ def project
+ render :text => request.fullpath
+ end
+ end.new
+
+ @routes = ActionDispatch::Routing::RouteSet.new.tap do |r|
+ r.draw do
+ get '/posts/:id', :to => 'anonymous#post', :bucket_type => 'post'
+ get '/projects/:id', :to => 'anonymous#project', :defaults => { :bucket_type => 'project' }
+ end
+ end
+ end
+
+ def test_route_option_can_be_passed_via_process
+ get :post, :id => 1, :bucket_type => 'post'
+ assert_equal '/posts/1', @response.body
+ end
+
+ def test_route_default_is_not_required_for_building_request_uri
+ get :project, :id => 2
+ assert_equal '/projects/2', @response.body
+ end
+end
diff --git a/actionpack/test/controller/url_for_test.rb b/actionpack/test/controller/url_for_test.rb
index d3fc7128e9..088ad73f2f 100644
--- a/actionpack/test/controller/url_for_test.rb
+++ b/actionpack/test/controller/url_for_test.rb
@@ -2,7 +2,6 @@ require 'abstract_unit'
module AbstractController
module Testing
-
class UrlForTest < ActionController::TestCase
class W
include ActionDispatch::Routing::RouteSet.new.tap { |r| r.draw { get ':controller(/:action(/:id(.:format)))' } }.url_helpers
@@ -90,6 +89,13 @@ module AbstractController
)
end
+ def test_subdomain_may_be_removed_with_blank_string
+ W.default_url_options[:host] = 'api.basecamphq.com'
+ assert_equal('http://basecamphq.com/c/a/i',
+ W.new.url_for(:subdomain => '', :controller => 'c', :action => 'a', :id => 'i')
+ )
+ end
+
def test_multiple_subdomains_may_be_removed
W.default_url_options[:host] = 'mobile.www.api.basecamphq.com'
assert_equal('http://basecamphq.com/c/a/i',
@@ -350,10 +356,10 @@ module AbstractController
def test_with_hash_with_indifferent_access
W.default_url_options[:controller] = 'd'
W.default_url_options[:only_path] = false
- assert_equal("/c", W.new.url_for(HashWithIndifferentAccess.new('controller' => 'c', 'only_path' => true)))
+ assert_equal("/c", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new('controller' => 'c', 'only_path' => true)))
W.default_url_options[:action] = 'b'
- assert_equal("/c/a", W.new.url_for(HashWithIndifferentAccess.new('controller' => 'c', 'action' => 'a', 'only_path' => true)))
+ assert_equal("/c/a", W.new.url_for(ActiveSupport::HashWithIndifferentAccess.new('controller' => 'c', 'action' => 'a', 'only_path' => true)))
end
def test_url_params_with_nil_to_param_are_not_in_url
diff --git a/actionpack/test/controller/view_paths_test.rb b/actionpack/test/controller/view_paths_test.rb
deleted file mode 100644
index 40f6dc6f0f..0000000000
--- a/actionpack/test/controller/view_paths_test.rb
+++ /dev/null
@@ -1,174 +0,0 @@
-require 'abstract_unit'
-
-class ViewLoadPathsTest < ActionController::TestCase
- class TestController < ActionController::Base
- def self.controller_path() "test" end
-
- before_filter :add_view_path, :only => :hello_world_at_request_time
-
- def hello_world() end
- def hello_world_at_request_time() render(:action => 'hello_world') end
-
- private
- def add_view_path
- prepend_view_path "#{FIXTURE_LOAD_PATH}/override"
- end
- end
-
- module Test
- class SubController < ActionController::Base
- layout 'test/sub'
- def hello_world; render(:template => 'test/hello_world'); end
- end
- end
-
- def setup
- @request = ActionController::TestRequest.new
- @response = ActionController::TestResponse.new
- @controller = TestController.new
- @paths = TestController.view_paths
- end
-
- def teardown
- TestController.view_paths = @paths
- end
-
- def expand(array)
- array.map {|x| File.expand_path(x.to_s)}
- end
-
- def assert_paths(*paths)
- controller = paths.first.is_a?(Class) ? paths.shift : @controller
- assert_equal expand(paths), controller.view_paths.map { |p| p.to_s }
- end
-
- def test_template_load_path_was_set_correctly
- assert_paths FIXTURE_LOAD_PATH
- end
-
- def test_controller_appends_view_path_correctly
- @controller.append_view_path 'foo'
- assert_paths(FIXTURE_LOAD_PATH, "foo")
-
- @controller.append_view_path(%w(bar baz))
- assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz")
-
- @controller.append_view_path(FIXTURE_LOAD_PATH)
- assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz", FIXTURE_LOAD_PATH)
- end
-
- def test_controller_prepends_view_path_correctly
- @controller.prepend_view_path 'baz'
- assert_paths("baz", FIXTURE_LOAD_PATH)
-
- @controller.prepend_view_path(%w(foo bar))
- assert_paths "foo", "bar", "baz", FIXTURE_LOAD_PATH
-
- @controller.prepend_view_path(FIXTURE_LOAD_PATH)
- assert_paths FIXTURE_LOAD_PATH, "foo", "bar", "baz", FIXTURE_LOAD_PATH
- end
-
- def test_template_appends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
- class_view_paths = TestController.view_paths
-
- @controller.append_view_path 'foo'
- assert_paths FIXTURE_LOAD_PATH, "foo"
-
- @controller.append_view_path(%w(bar baz))
- assert_paths FIXTURE_LOAD_PATH, "foo", "bar", "baz"
- assert_paths TestController, *class_view_paths
- end
-
- def test_template_prepends_view_path_correctly
- @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
- class_view_paths = TestController.view_paths
-
- @controller.prepend_view_path 'baz'
- assert_paths "baz", FIXTURE_LOAD_PATH
-
- @controller.prepend_view_path(%w(foo bar))
- assert_paths "foo", "bar", "baz", FIXTURE_LOAD_PATH
- assert_paths TestController, *class_view_paths
- end
-
- def test_view_paths
- get :hello_world
- assert_response :success
- assert_equal "Hello world!", @response.body
- end
-
- def test_view_paths_override
- TestController.prepend_view_path "#{FIXTURE_LOAD_PATH}/override"
- get :hello_world
- assert_response :success
- assert_equal "Hello overridden world!", @response.body
- end
-
- def test_view_paths_override_for_layouts_in_controllers_with_a_module
- @controller = Test::SubController.new
- Test::SubController.view_paths = [ "#{FIXTURE_LOAD_PATH}/override", FIXTURE_LOAD_PATH, "#{FIXTURE_LOAD_PATH}/override2" ]
- get :hello_world
- assert_response :success
- assert_equal "layout: Hello overridden world!", @response.body
- end
-
- def test_view_paths_override_at_request_time
- get :hello_world_at_request_time
- assert_response :success
- assert_equal "Hello overridden world!", @response.body
- end
-
- def test_decorate_view_paths_with_custom_resolver
- decorator_class = Class.new(ActionView::PathResolver) do
- def initialize(path_set)
- @path_set = path_set
- end
-
- def find_all(*args)
- @path_set.find_all(*args).collect do |template|
- ::ActionView::Template.new(
- "Decorated body",
- template.identifier,
- template.handler,
- {
- :virtual_path => template.virtual_path,
- :format => template.formats
- }
- )
- end
- end
- end
-
- decorator = decorator_class.new(TestController.view_paths)
- TestController.view_paths = ActionView::PathSet.new.push(decorator)
-
- get :hello_world
- assert_response :success
- assert_equal "Decorated body", @response.body
- end
-
- def test_inheritance
- original_load_paths = ActionController::Base.view_paths
-
- self.class.class_eval %{
- class A < ActionController::Base; end
- class B < A; end
- class C < ActionController::Base; end
- }
-
- A.view_paths = ['a/path']
-
- assert_paths A, "a/path"
- assert_paths A, *B.view_paths
- assert_paths C, *original_load_paths
-
- C.view_paths = []
- assert_nothing_raised { C.append_view_path 'c/path' }
- assert_paths C, "c/path"
- end
-
- def test_lookup_context_accessor
- assert_equal ["test"], TestController.new.lookup_context.prefixes
- end
-end
diff --git a/actionpack/test/controller/webservice_test.rb b/actionpack/test/controller/webservice_test.rb
index c0b9833603..b2dfd96606 100644
--- a/actionpack/test/controller/webservice_test.rb
+++ b/actionpack/test/controller/webservice_test.rb
@@ -32,212 +32,53 @@ class WebServiceTest < ActionDispatch::IntegrationTest
end
end
- def test_post_xml
+ def test_post_json
with_test_route_set do
- post "/", '<entry attributed="true"><summary>content...</summary></entry>',
- {'CONTENT_TYPE' => 'application/xml'}
+ post "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json'
assert_equal 'entry', @controller.response.body
assert @controller.params.has_key?(:entry)
assert_equal 'content...', @controller.params["entry"]['summary']
- assert_equal 'true', @controller.params["entry"]['attributed']
end
end
- def test_put_xml
+ def test_put_json
with_test_route_set do
- put "/", '<entry attributed="true"><summary>content...</summary></entry>',
- {'CONTENT_TYPE' => 'application/xml'}
+ put "/", '{"entry":{"summary":"content..."}}', 'CONTENT_TYPE' => 'application/json'
assert_equal 'entry', @controller.response.body
assert @controller.params.has_key?(:entry)
assert_equal 'content...', @controller.params["entry"]['summary']
- assert_equal 'true', @controller.params["entry"]['attributed']
end
end
- def test_put_xml_using_a_type_node
+ def test_register_and_use_json_simple
with_test_route_set do
- put "/", '<type attributed="true"><summary>content...</summary></type>',
- {'CONTENT_TYPE' => 'application/xml'}
-
- assert_equal 'type', @controller.response.body
- assert @controller.params.has_key?(:type)
- assert_equal 'content...', @controller.params["type"]['summary']
- assert_equal 'true', @controller.params["type"]['attributed']
- end
- end
-
- def test_put_xml_using_a_type_node_and_attribute
- with_test_route_set do
- put "/", '<type attributed="true"><summary type="boolean">false</summary></type>',
- {'CONTENT_TYPE' => 'application/xml'}
-
- assert_equal 'type', @controller.response.body
- assert @controller.params.has_key?(:type)
- assert_equal false, @controller.params["type"]['summary']
- assert_equal 'true', @controller.params["type"]['attributed']
- end
- end
-
- def test_post_xml_using_a_type_node
- with_test_route_set do
- post "/", '<font attributed="true"><type>arial</type></font>',
- {'CONTENT_TYPE' => 'application/xml'}
-
- assert_equal 'font', @controller.response.body
- assert @controller.params.has_key?(:font)
- assert_equal 'arial', @controller.params['font']['type']
- assert_equal 'true', @controller.params["font"]['attributed']
- end
- end
-
- def test_post_xml_using_a_root_node_named_type
- with_test_route_set do
- post "/", '<type type="integer">33</type>',
- {'CONTENT_TYPE' => 'application/xml'}
-
- assert @controller.params.has_key?(:type)
- assert_equal 33, @controller.params['type']
- end
- end
-
- def test_post_xml_using_an_attributted_node_named_type
- with_test_route_set do
- with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do
- post "/", '<request><type type="string">Arial,12</type><z>3</z></request>',
- {'CONTENT_TYPE' => 'application/xml'}
-
- assert_equal 'type, z', @controller.response.body
- assert @controller.params.has_key?(:type)
- assert_equal 'Arial,12', @controller.params['type'], @controller.params.inspect
- assert_equal '3', @controller.params['z'], @controller.params.inspect
- end
- end
- end
-
- def test_register_and_use_yaml
- with_test_route_set do
- with_params_parsers Mime::YAML => Proc.new { |d| YAML.load(d) } do
- post "/", {"entry" => "loaded from yaml"}.to_yaml,
- {'CONTENT_TYPE' => 'application/x-yaml'}
-
- assert_equal 'entry', @controller.response.body
- assert @controller.params.has_key?(:entry)
- assert_equal 'loaded from yaml', @controller.params["entry"]
- end
- end
- end
-
- def test_register_and_use_yaml_as_symbol
- with_test_route_set do
- with_params_parsers Mime::YAML => :yaml do
- post "/", {"entry" => "loaded from yaml"}.to_yaml,
- {'CONTENT_TYPE' => 'application/x-yaml'}
-
- assert_equal 'entry', @controller.response.body
- assert @controller.params.has_key?(:entry)
- assert_equal 'loaded from yaml', @controller.params["entry"]
- end
- end
- end
-
- def test_register_and_use_xml_simple
- with_test_route_set do
- with_params_parsers Mime::XML => Proc.new { |data| Hash.from_xml(data)['request'].with_indifferent_access } do
- post "/", '<request><summary>content...</summary><title>SimpleXml</title></request>',
- {'CONTENT_TYPE' => 'application/xml'}
+ with_params_parsers Mime::JSON => Proc.new { |data| JSON.parse(data)['request'].with_indifferent_access } do
+ post "/", '{"request":{"summary":"content...","title":"JSON"}}',
+ 'CONTENT_TYPE' => 'application/json'
assert_equal 'summary, title', @controller.response.body
assert @controller.params.has_key?(:summary)
assert @controller.params.has_key?(:title)
assert_equal 'content...', @controller.params["summary"]
- assert_equal 'SimpleXml', @controller.params["title"]
+ assert_equal 'JSON', @controller.params["title"]
end
end
end
- def test_use_xml_ximple_with_empty_request
+ def test_use_json_with_empty_request
with_test_route_set do
- assert_nothing_raised { post "/", "", {'CONTENT_TYPE' => 'application/xml'} }
+ assert_nothing_raised { post "/", "", 'CONTENT_TYPE' => 'application/json' }
assert_equal '', @controller.response.body
end
end
- def test_dasherized_keys_as_xml
- with_test_route_set do
- post "/?full=1", "<first-key>\n<sub-key>...</sub-key>\n</first-key>",
- {'CONTENT_TYPE' => 'application/xml'}
- assert_equal 'action, controller, first_key(sub_key), full', @controller.response.body
- assert_equal "...", @controller.params[:first_key][:sub_key]
- end
- end
-
- def test_typecast_as_xml
+ def test_dasherized_keys_as_json
with_test_route_set do
- xml = <<-XML
- <data>
- <a type="integer">15</a>
- <b type="boolean">false</b>
- <c type="boolean">true</c>
- <d type="date">2005-03-17</d>
- <e type="datetime">2005-03-17T21:41:07Z</e>
- <f>unparsed</f>
- <g type="integer">1</g>
- <g>hello</g>
- <g type="date">1974-07-25</g>
- </data>
- XML
- post "/", xml, {'CONTENT_TYPE' => 'application/xml'}
-
- params = @controller.params
- assert_equal 15, params[:data][:a]
- assert_equal false, params[:data][:b]
- assert_equal true, params[:data][:c]
- assert_equal Date.new(2005,3,17), params[:data][:d]
- assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e]
- assert_equal "unparsed", params[:data][:f]
- assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g]
- end
- end
-
- def test_entities_unescaped_as_xml_simple
- with_test_route_set do
- xml = <<-XML
- <data>&lt;foo &quot;bar&apos;s&quot; &amp; friends&gt;</data>
- XML
- post "/", xml, {'CONTENT_TYPE' => 'application/xml'}
- assert_equal %(<foo "bar's" & friends>), @controller.params[:data]
- end
- end
-
- def test_typecast_as_yaml
- with_test_route_set do
- with_params_parsers Mime::YAML => :yaml do
- yaml = (<<-YAML).strip
- ---
- data:
- a: 15
- b: false
- c: true
- d: 2005-03-17
- e: 2005-03-17T21:41:07Z
- f: unparsed
- g:
- - 1
- - hello
- - 1974-07-25
- YAML
- post "/", yaml, {'CONTENT_TYPE' => 'application/x-yaml'}
- params = @controller.params
- assert_equal 15, params[:data][:a]
- assert_equal false, params[:data][:b]
- assert_equal true, params[:data][:c]
- assert_equal Date.new(2005,3,17), params[:data][:d]
- assert_equal Time.utc(2005,3,17,21,41,7), params[:data][:e]
- assert_equal "unparsed", params[:data][:f]
- assert_equal [1, "hello", Date.new(1974,7,25)], params[:data][:g]
- end
+ post "/?full=1", '{"first-key":{"sub-key":"..."}}', 'CONTENT_TYPE' => 'application/json'
+ assert_equal 'action, controller, first-key(sub-key), full', @controller.response.body
+ assert_equal "...", @controller.params['first-key']['sub-key']
end
end
diff --git a/actionpack/test/dispatch/cookies_test.rb b/actionpack/test/dispatch/cookies_test.rb
index 347b3b3b5a..91ac13e7c6 100644
--- a/actionpack/test/dispatch/cookies_test.rb
+++ b/actionpack/test/dispatch/cookies_test.rb
@@ -1,5 +1,15 @@
require 'abstract_unit'
+begin
+ require 'openssl'
+ OpenSSL::PKCS5
+rescue LoadError, NameError
+ $stderr.puts "Skipping KeyGenerator test: broken OpenSSL install"
+else
+
+require 'active_support/key_generator'
+require 'active_support/message_verifier'
+
class CookiesTest < ActionController::TestCase
class TestController < ActionController::Base
def authenticate
@@ -65,6 +75,26 @@ class CookiesTest < ActionController::TestCase
head :ok
end
+ def get_signed_cookie
+ cookies.signed[:user_id]
+ head :ok
+ end
+
+ def set_encrypted_cookie
+ cookies.encrypted[:foo] = 'bar'
+ head :ok
+ end
+
+ def get_encrypted_cookie
+ cookies.encrypted[:foo]
+ head :ok
+ end
+
+ def set_invalid_encrypted_cookie
+ cookies[:invalid_cookie] = 'invalid--9170e00a57cfc27083363b5c75b835e477bd90cf'
+ head :ok
+ end
+
def raise_data_overflow
cookies.signed[:foo] = 'bye!' * 1024
head :ok
@@ -146,10 +176,43 @@ class CookiesTest < ActionController::TestCase
def setup
super
- @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::KeyGenerator.new("b3c631c314c0bbca50c1b2843150fe33")
+ @request.env["action_dispatch.signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.encrypted_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.encrypted_signed_cookie_salt"] = "b3c631c314c0bbca50c1b2843150fe33"
@request.host = "www.nextangle.com"
end
+ def test_fetch
+ x = Object.new
+ assert_not request.cookie_jar.key?('zzzzzz')
+ assert_equal x, request.cookie_jar.fetch('zzzzzz', x)
+ assert_not request.cookie_jar.key?('zzzzzz')
+ end
+
+ def test_fetch_exists
+ x = Object.new
+ request.cookie_jar['foo'] = 'bar'
+ assert_equal 'bar', request.cookie_jar.fetch('foo', x)
+ end
+
+ def test_fetch_block
+ x = Object.new
+ assert_not request.cookie_jar.key?('zzzzzz')
+ assert_equal x, request.cookie_jar.fetch('zzzzzz') { x }
+ end
+
+ def test_key_is_to_s
+ request.cookie_jar['foo'] = 'bar'
+ assert_equal 'bar', request.cookie_jar.fetch(:foo)
+ end
+
+ def test_fetch_type_error
+ assert_raises(KeyError) do
+ request.cookie_jar.fetch(:omglolwut)
+ end
+ end
+
def test_each
request.cookie_jar['foo'] = :bar
list = []
@@ -201,13 +264,13 @@ class CookiesTest < ActionController::TestCase
def test_setting_cookie_for_fourteen_days
get :authenticate_for_fourteen_days
- assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"
+ assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
assert_equal({"user_name" => "david"}, @response.cookies)
end
def test_setting_cookie_for_fourteen_days_with_symbols
get :authenticate_for_fourteen_days_with_symbols
- assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"
+ assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
assert_equal({"user_name" => "david"}, @response.cookies)
end
@@ -240,7 +303,7 @@ class CookiesTest < ActionController::TestCase
def test_multiple_cookies
get :set_multiple_cookies
assert_equal 2, @response.cookies.size
- assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT\nlogin=XJ-122; path=/"
+ assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000\nlogin=XJ-122; path=/"
assert_equal({"login" => "XJ-122", "user_name" => "david"}, @response.cookies)
end
@@ -251,14 +314,14 @@ class CookiesTest < ActionController::TestCase
def test_expiring_cookie
request.cookies[:user_name] = 'Joe'
get :logout
- assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
assert_equal({"user_name" => nil}, @response.cookies)
end
def test_delete_cookie_with_path
request.cookies[:user_name] = 'Joe'
get :delete_cookie_with_path
- assert_cookie_header "user_name=; path=/beaten; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; path=/beaten; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
def test_delete_unexisting_cookie
@@ -285,12 +348,17 @@ class CookiesTest < ActionController::TestCase
assert response.headers["Set-Cookie"] =~ /user_name=david/
end
- def test_permanent_cookie
+ def test_set_permanent_cookie
get :set_permanent_cookie
assert_match(/Jamie/, @response.headers["Set-Cookie"])
assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"])
end
+ def test_read_permanent_cookie
+ get :set_permanent_cookie
+ assert_equal 'Jamie', @controller.send(:cookies).permanent[:user_name]
+ end
+
def test_signed_cookie
get :set_signed_cookie
assert_equal 45, @controller.send(:cookies).signed[:user_id]
@@ -301,6 +369,26 @@ class CookiesTest < ActionController::TestCase
assert_nil @controller.send(:cookies).signed[:non_existant_attribute]
end
+ def test_encrypted_cookie
+ get :set_encrypted_cookie
+ cookies = @controller.send :cookies
+ assert_not_equal 'bar', cookies[:foo]
+ assert_raises TypeError do
+ cookies.signed[:foo]
+ end
+ assert_equal 'bar', cookies.encrypted[:foo]
+ end
+
+ def test_accessing_nonexistant_encrypted_cookie_should_not_raise_invalid_message
+ get :set_encrypted_cookie
+ assert_nil @controller.send(:cookies).encrypted[:non_existant_attribute]
+ end
+
+ def test_setting_invalid_encrypted_cookie_should_return_nil_when_accessing_it
+ get :set_invalid_encrypted_cookie
+ assert_nil @controller.send(:cookies).encrypted[:invalid_cookie]
+ end
+
def test_permanent_signed_cookie
get :set_permanent_signed_cookie
assert_match(%r(#{20.years.from_now.utc.year}), @response.headers["Set-Cookie"])
@@ -310,7 +398,7 @@ class CookiesTest < ActionController::TestCase
def test_delete_and_set_cookie
request.cookies[:user_name] = 'Joe'
get :delete_and_set_cookie
- assert_cookie_header "user_name=david; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT"
+ assert_cookie_header "user_name=david; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000"
assert_equal({"user_name" => "david"}, @response.cookies)
end
@@ -329,33 +417,148 @@ class CookiesTest < ActionController::TestCase
def test_raises_argument_error_if_missing_secret
assert_raise(ArgumentError, nil.inspect) {
- @request.env["action_dispatch.secret_token"] = nil
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new(nil)
get :set_signed_cookie
}
assert_raise(ArgumentError, ''.inspect) {
- @request.env["action_dispatch.secret_token"] = ""
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("")
get :set_signed_cookie
}
end
def test_raises_argument_error_if_secret_is_probably_insecure
assert_raise(ArgumentError, "password".inspect) {
- @request.env["action_dispatch.secret_token"] = "password"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("password")
get :set_signed_cookie
}
assert_raise(ArgumentError, "secret".inspect) {
- @request.env["action_dispatch.secret_token"] = "secret"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("secret")
get :set_signed_cookie
}
assert_raise(ArgumentError, "12345678901234567890123456789".inspect) {
- @request.env["action_dispatch.secret_token"] = "12345678901234567890123456789"
+ @request.env["action_dispatch.key_generator"] = ActiveSupport::LegacyKeyGenerator.new("12345678901234567890123456789")
get :set_signed_cookie
}
end
+ def test_signed_uses_signed_cookie_jar_if_only_secret_token_is_set
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = nil
+ get :set_signed_cookie
+ assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
+ end
+
+ def test_signed_uses_signed_cookie_jar_if_only_secret_key_base_is_set
+ @request.env["action_dispatch.secret_token"] = nil
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :set_signed_cookie
+ assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed
+ end
+
+ def test_signed_uses_upgrade_legacy_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :set_signed_cookie
+ assert_kind_of ActionDispatch::Cookies::UpgradeLegacySignedCookieJar, cookies.signed
+ end
+
+ def test_signed_or_encrypted_uses_signed_cookie_jar_if_only_secret_token_is_set
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = nil
+ get :get_encrypted_cookie
+ assert_kind_of ActionDispatch::Cookies::SignedCookieJar, cookies.signed_or_encrypted
+ end
+
+ def test_signed_or_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
+ @request.env["action_dispatch.secret_token"] = nil
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :get_encrypted_cookie
+ assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.signed_or_encrypted
+ end
+
+ def test_signed_or_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :get_encrypted_cookie
+ assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.signed_or_encrypted
+ end
+
+ def test_encrypted_uses_encrypted_cookie_jar_if_only_secret_key_base_is_set
+ @request.env["action_dispatch.secret_token"] = nil
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :get_encrypted_cookie
+ assert_kind_of ActionDispatch::Cookies::EncryptedCookieJar, cookies.encrypted
+ end
+
+ def test_encrypted_uses_upgrade_legacy_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+ get :get_encrypted_cookie
+ assert_kind_of ActionDispatch::Cookies::UpgradeLegacyEncryptedCookieJar, cookies.encrypted
+ end
+
+ def test_legacy_signed_cookie_is_read_and_transparently_upgraded_by_signed_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @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)
+ assert_equal 45, verifier.verify(@response.cookies["user_id"])
+ end
+
+ def test_legacy_signed_cookie_is_read_and_transparently_encrypted_by_encrypted_cookie_jar_if_both_secret_token_and_secret_key_base_are_set
+ @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)
+ 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"
+
+ @request.headers["Cookie"] = "user_id=45"
+ get :get_signed_cookie
+
+ assert_equal nil, @controller.send(:cookies).signed[:user_id]
+ assert_equal nil, @response.cookies["user_id"]
+ end
+
+ def test_legacy_signed_cookie_is_treated_as_nil_by_encrypted_cookie_jar_if_tampered
+ @request.env["action_dispatch.secret_token"] = "b3c631c314c0bbca50c1b2843150fe33"
+ @request.env["action_dispatch.secret_key_base"] = "c3b95688f35581fad38df788add315ff"
+
+ @request.headers["Cookie"] = "foo=baz"
+ get :get_encrypted_cookie
+
+ assert_equal nil, @controller.send(:cookies).encrypted[:foo]
+ assert_equal nil, @response.cookies["foo"]
+ end
+
def test_cookie_with_all_domain_option
get :set_cookie_with_domain
assert_response :success
@@ -415,7 +618,7 @@ class CookiesTest < ActionController::TestCase
request.cookies[:user_name] = 'Joe'
get :delete_cookie_with_domain
assert_response :success
- assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
def test_cookie_with_all_domain_option_and_tld_length
@@ -442,7 +645,7 @@ class CookiesTest < ActionController::TestCase
request.cookies[:user_name] = 'Joe'
get :delete_cookie_with_domain_and_tld
assert_response :success
- assert_cookie_header "user_name=; domain=.nextangle.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; domain=.nextangle.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
def test_cookie_with_several_preset_domains_using_one_of_these_domains
@@ -471,7 +674,7 @@ class CookiesTest < ActionController::TestCase
request.cookies[:user_name] = 'Joe'
get :delete_cookie_with_domains
assert_response :success
- assert_cookie_header "user_name=; domain=example2.com; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; domain=example2.com; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
def test_deletings_cookie_with_several_preset_domains_using_other_domain
@@ -479,7 +682,7 @@ class CookiesTest < ActionController::TestCase
request.cookies[:user_name] = 'Joe'
get :delete_cookie_with_domains
assert_response :success
- assert_cookie_header "user_name=; path=/; expires=Thu, 01-Jan-1970 00:00:00 GMT"
+ assert_cookie_header "user_name=; path=/; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000"
end
def test_cookies_hash_is_indifferent_access
@@ -604,3 +807,5 @@ class CookiesTest < ActionController::TestCase
end
end
end
+
+end
diff --git a/actionpack/test/dispatch/debug_exceptions_test.rb b/actionpack/test/dispatch/debug_exceptions_test.rb
index d236b14e02..6e28e4a982 100644
--- a/actionpack/test/dispatch/debug_exceptions_test.rb
+++ b/actionpack/test/dispatch/debug_exceptions_test.rb
@@ -29,6 +29,8 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise RuntimeError
when "/method_not_allowed"
raise ActionController::MethodNotAllowed
+ when "/unknown_http_method"
+ raise ActionController::UnknownHttpMethod
when "/not_implemented"
raise ActionController::NotImplemented
when "/unprocessable_entity"
@@ -39,14 +41,27 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
raise ActionController::BadRequest
when "/missing_keys"
raise ActionController::UrlGenerationError, "No route matches"
+ when "/parameter_missing"
+ raise ActionController::ParameterMissing, :missing_param_key
+ when "/required_key_empty_value"
+ raise ActionController::EmptyParameter, :empty_param_key
else
raise "puke!"
end
end
end
- ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false))
- DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true))
+ def setup
+ app = ActiveSupport::OrderedOptions.new
+ app.config = ActiveSupport::OrderedOptions.new
+ app.config.assets = ActiveSupport::OrderedOptions.new
+ app.config.assets.prefix = '/sprockets'
+ Rails.stubs(:application).returns(app)
+ end
+
+ RoutesApp = Struct.new(:routes).new(SharedTestRoutes)
+ ProductionApp = ActionDispatch::DebugExceptions.new(Boomer.new(false), RoutesApp)
+ DevelopmentApp = ActionDispatch::DebugExceptions.new(Boomer.new(true), RoutesApp)
test 'skip diagnosis if not showing detailed exceptions' do
@app = ProductionApp
@@ -78,6 +93,15 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert boomer.closed, "Expected to close the response body"
end
+ test 'displays routes in a table when a RoutingError occurs' do
+ @app = DevelopmentApp
+ get "/pass", {}, {'action_dispatch.show_exceptions' => true}
+ routing_table = body[/route_table.*<.table>/m]
+ assert_match '/:controller(/:action)(.:format)', routing_table
+ assert_match ':controller#:action', routing_table
+ assert_no_match '&lt;|&gt;', routing_table, "there should not be escaped html in the output"
+ end
+
test "rescue with diagnostics message" do
@app = DevelopmentApp
@@ -93,9 +117,62 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
assert_response 405
assert_match(/ActionController::MethodNotAllowed/, body)
+ get "/unknown_http_method", {}, {'action_dispatch.show_exceptions' => true}
+ assert_response 405
+ assert_match(/ActionController::UnknownHttpMethod/, body)
+
get "/bad_request", {}, {'action_dispatch.show_exceptions' => true}
assert_response 400
assert_match(/ActionController::BadRequest/, body)
+
+ get "/parameter_missing", {}, {'action_dispatch.show_exceptions' => true}
+ assert_response 400
+ assert_match(/ActionController::ParameterMissing/, body)
+
+ get "/required_key_empty_value", {}, {'action_dispatch.show_exceptions' => true}
+ assert_response 400
+ assert_match(/ActionController::EmptyParameter/, body)
+ end
+
+ test "rescue with text error for xhr request" do
+ @app = DevelopmentApp
+ xhr_request_env = {'action_dispatch.show_exceptions' => true, 'HTTP_X_REQUESTED_WITH' => 'XMLHttpRequest'}
+
+ get "/", {}, xhr_request_env
+ assert_response 500
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/puke/, body)
+
+ get "/not_found", {}, xhr_request_env
+ assert_response 404
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/#{AbstractController::ActionNotFound.name}/, body)
+
+ get "/method_not_allowed", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::MethodNotAllowed/, body)
+
+ get "/unknown_http_method", {}, xhr_request_env
+ assert_response 405
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::UnknownHttpMethod/, body)
+
+ get "/bad_request", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::BadRequest/, body)
+
+ get "/parameter_missing", {}, xhr_request_env
+ assert_response 400
+ assert_no_match(/<body>/, body)
+ assert_equal response.content_type, "text/plain"
+ assert_match(/ActionController::ParameterMissing/, body)
end
test "does not show filtered parameters" do
@@ -135,7 +212,7 @@ class DebugExceptionsTest < ActionDispatch::IntegrationTest
}
})
assert_response 500
- assert_match(/RuntimeError\n in FeaturedTileController/, body)
+ assert_match(/RuntimeError\n\s+in FeaturedTileController/, body)
end
test "sets the HTTP charset parameter" do
diff --git a/actionpack/test/dispatch/header_test.rb b/actionpack/test/dispatch/header_test.rb
index bc7cad8db5..9e37b96951 100644
--- a/actionpack/test/dispatch/header_test.rb
+++ b/actionpack/test/dispatch/header_test.rb
@@ -1,21 +1,137 @@
-require 'abstract_unit'
+require "abstract_unit"
class HeaderTest < ActiveSupport::TestCase
- def setup
+ setup do
@headers = ActionDispatch::Http::Headers.new(
- "HTTP_CONTENT_TYPE" => "text/plain"
+ "CONTENT_TYPE" => "text/plain",
+ "HTTP_REFERER" => "/some/page"
)
end
- test "content type" do
+ test "#new does not normalize the data" do
+ headers = ActionDispatch::Http::Headers.new(
+ "Content-Type" => "application/json",
+ "HTTP_REFERER" => "/some/page",
+ "Host" => "http://test.com")
+
+ assert_equal({"Content-Type" => "application/json",
+ "HTTP_REFERER" => "/some/page",
+ "Host" => "http://test.com"}, headers.env)
+ end
+
+ test "#env returns the headers as env variables" do
+ assert_equal({"CONTENT_TYPE" => "text/plain",
+ "HTTP_REFERER" => "/some/page"}, @headers.env)
+ end
+
+ test "#each iterates through the env variables" do
+ headers = []
+ @headers.each { |pair| headers << pair }
+ assert_equal [["CONTENT_TYPE", "text/plain"],
+ ["HTTP_REFERER", "/some/page"]], headers
+ end
+
+ test "set new headers" do
+ @headers["Host"] = "127.0.0.1"
+
+ assert_equal "127.0.0.1", @headers["Host"]
+ assert_equal "127.0.0.1", @headers["HTTP_HOST"]
+ end
+
+ test "headers can contain numbers" do
+ @headers["Content-MD5"] = "Q2hlY2sgSW50ZWdyaXR5IQ=="
+
+ assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["Content-MD5"]
+ assert_equal "Q2hlY2sgSW50ZWdyaXR5IQ==", @headers["HTTP_CONTENT_MD5"]
+ end
+
+ test "set new env variables" do
+ @headers["HTTP_HOST"] = "127.0.0.1"
+
+ assert_equal "127.0.0.1", @headers["Host"]
+ assert_equal "127.0.0.1", @headers["HTTP_HOST"]
+ end
+
+ test "key?" do
+ assert @headers.key?("CONTENT_TYPE")
+ assert @headers.include?("CONTENT_TYPE")
+ end
+
+ test "fetch with block" do
+ assert_equal "omg", @headers.fetch("notthere") { "omg" }
+ end
+
+ test "accessing http header" do
+ assert_equal "/some/page", @headers["Referer"]
+ assert_equal "/some/page", @headers["referer"]
+ assert_equal "/some/page", @headers["HTTP_REFERER"]
+ end
+
+ test "accessing special header" do
assert_equal "text/plain", @headers["Content-Type"]
assert_equal "text/plain", @headers["content-type"]
assert_equal "text/plain", @headers["CONTENT_TYPE"]
- assert_equal "text/plain", @headers["HTTP_CONTENT_TYPE"]
end
test "fetch" do
assert_equal "text/plain", @headers.fetch("content-type", nil)
- assert_equal "not found", @headers.fetch('not-found', 'not found')
+ assert_equal "not found", @headers.fetch("not-found", "not found")
+ end
+
+ test "#merge! headers with mutation" do
+ @headers.merge!("Host" => "http://example.test",
+ "Content-Type" => "text/html")
+ assert_equal({"HTTP_HOST" => "http://example.test",
+ "CONTENT_TYPE" => "text/html",
+ "HTTP_REFERER" => "/some/page"}, @headers.env)
+ end
+
+ test "#merge! env with mutation" do
+ @headers.merge!("HTTP_HOST" => "http://first.com",
+ "CONTENT_TYPE" => "text/html")
+ assert_equal({"HTTP_HOST" => "http://first.com",
+ "CONTENT_TYPE" => "text/html",
+ "HTTP_REFERER" => "/some/page"}, @headers.env)
+ end
+
+ test "merge without mutation" do
+ combined = @headers.merge("HTTP_HOST" => "http://example.com",
+ "CONTENT_TYPE" => "text/html")
+ assert_equal({"HTTP_HOST" => "http://example.com",
+ "CONTENT_TYPE" => "text/html",
+ "HTTP_REFERER" => "/some/page"}, combined.env)
+
+ assert_equal({"CONTENT_TYPE" => "text/plain",
+ "HTTP_REFERER" => "/some/page"}, @headers.env)
+ end
+
+ test "env variables with . are not modified" do
+ headers = ActionDispatch::Http::Headers.new
+ headers.merge! "rack.input" => "",
+ "rack.request.cookie_hash" => "",
+ "action_dispatch.logger" => ""
+
+ assert_equal(["action_dispatch.logger",
+ "rack.input",
+ "rack.request.cookie_hash"], headers.env.keys.sort)
+ end
+
+ test "symbols are treated as strings" do
+ headers = ActionDispatch::Http::Headers.new
+ headers.merge!(:SERVER_NAME => "example.com",
+ "HTTP_REFERER" => "/",
+ :Host => "test.com")
+ assert_equal "example.com", headers["SERVER_NAME"]
+ assert_equal "/", headers[:HTTP_REFERER]
+ assert_equal "test.com", headers["HTTP_HOST"]
+ end
+
+ test "headers directly modifies the passed environment" do
+ env = {"HTTP_REFERER" => "/"}
+ headers = ActionDispatch::Http::Headers.new(env)
+ headers['Referer'] = "http://example.com/"
+ headers.merge! "CONTENT_TYPE" => "text/plain"
+ assert_equal({"HTTP_REFERER"=>"http://example.com/",
+ "CONTENT_TYPE"=>"text/plain"}, env)
end
end
diff --git a/actionpack/test/dispatch/live_response_test.rb b/actionpack/test/dispatch/live_response_test.rb
index e16f23914b..e0cfb73acf 100644
--- a/actionpack/test/dispatch/live_response_test.rb
+++ b/actionpack/test/dispatch/live_response_test.rb
@@ -11,7 +11,7 @@ module ActionController
def test_header_merge
header = @response.header.merge('Foo' => 'Bar')
assert_kind_of(ActionController::Live::Response::Header, header)
- refute_equal header, @response.header
+ assert_not_equal header, @response.header
end
def test_initialize_with_default_headers
diff --git a/actionpack/test/dispatch/mime_type_test.rb b/actionpack/test/dispatch/mime_type_test.rb
index e2a9ba782d..8a19129695 100644
--- a/actionpack/test/dispatch/mime_type_test.rb
+++ b/actionpack/test/dispatch/mime_type_test.rb
@@ -75,7 +75,7 @@ class MimeTypeTest < ActiveSupport::TestCase
assert_equal expect, Mime::Type.parse(accept)
end
- test "parse arbitarry media type parameters" do
+ test "parse arbitrary media type parameters" do
accept = 'multipart/form-data; boundary="simple boundary"'
expect = [Mime::MULTIPART_FORM]
assert_equal expect, Mime::Type.parse(accept)
@@ -185,11 +185,6 @@ class MimeTypeTest < ActiveSupport::TestCase
all_types.uniq!
# Remove custom Mime::Type instances set in other tests, like Mime::GIF and Mime::IPHONE
all_types.delete_if { |type| !Mime.const_defined?(type.upcase) }
- assert_deprecated do
- verified, unverified = all_types.partition { |type| Mime::Type.browser_generated_types.include? type }
- assert verified.each { |type| assert Mime.const_get(type.upcase).verify_request?, "Verifiable Mime Type is not verified: #{type.inspect}" }
- assert unverified.each { |type| assert !Mime.const_get(type.upcase).verify_request?, "Nonverifiable Mime Type is verified: #{type.inspect}" }
- end
end
test "references gives preference to symbols before strings" do
diff --git a/actionpack/test/dispatch/mount_test.rb b/actionpack/test/dispatch/mount_test.rb
index 3b008fdff0..30e95a0b75 100644
--- a/actionpack/test/dispatch/mount_test.rb
+++ b/actionpack/test/dispatch/mount_test.rb
@@ -21,7 +21,7 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
mount SprocketsApp, :at => "/sprockets"
mount SprocketsApp => "/shorthand"
- mount FakeEngine, :at => "/fakeengine"
+ mount FakeEngine, :at => "/fakeengine", :as => :fake
mount FakeEngine, :at => "/getfake", :via => :get
scope "/its_a" do
@@ -33,6 +33,11 @@ class TestRoutingMount < ActionDispatch::IntegrationTest
Router
end
+ def test_trailing_slash_is_not_removed_from_path_info
+ get "/sprockets/omg/"
+ assert_equal "/sprockets -- /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 cfbf970a37..e519fff51e 100644
--- a/actionpack/test/dispatch/prefix_generation_test.rb
+++ b/actionpack/test/dispatch/prefix_generation_test.rb
@@ -31,6 +31,14 @@ module TestGenerationPrefix
get "/polymorphic_path_for_engine", :to => "inside_engine_generating#polymorphic_path_for_engine"
get "/conflicting_url", :to => "inside_engine_generating#conflicting"
get "/foo", :to => "never#invoked", :as => :named_helper_that_should_be_invoked_only_in_respond_to_test
+
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
routes
@@ -182,6 +190,48 @@ module TestGenerationPrefix
assert_equal "engine", last_response.body
end
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_path_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_option_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/awesome/blog/relative_custom_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/awesome/blog/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/awesome/blog/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_path_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_option_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/awesome/blog/absolute_custom_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
# Inside Application
test "[APP] generating engine's route includes prefix" do
get "/generate"
@@ -241,6 +291,11 @@ module TestGenerationPrefix
assert_equal "/something/", app_object.root_path
end
+ test "[OBJECT] generating application's route includes default_url_options[:trailing_slash]" do
+ RailsApplication.routes.default_url_options[:trailing_slash] = true
+ assert_equal "/awesome/blog/posts", engine_object.posts_path
+ end
+
test "[OBJECT] generating engine's route with url_for" do
path = engine_object.url_for(:controller => "inside_engine_generating",
:action => "show",
@@ -276,6 +331,14 @@ module TestGenerationPrefix
routes = ActionDispatch::Routing::RouteSet.new
routes.draw do
get "/posts/:id", :to => "posts#show", :as => :post
+
+ get "/relative_path_redirect", :to => redirect("foo")
+ get "/relative_option_redirect", :to => redirect(:path => "foo")
+ get "/relative_custom_redirect", :to => redirect { |params, request| "foo" }
+
+ get "/absolute_path_redirect", :to => redirect("/foo")
+ get "/absolute_option_redirect", :to => redirect(:path => "/foo")
+ get "/absolute_custom_redirect", :to => redirect { |params, request| "/foo" }
end
routes
@@ -326,5 +389,47 @@ module TestGenerationPrefix
get "/posts/1"
assert_equal "/posts/1", last_response.body
end
+
+ test "[ENGINE] relative path redirect uses SCRIPT_NAME from request" do
+ get "/relative_path_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] relative option redirect uses SCRIPT_NAME from request" do
+ get "/relative_option_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] relative custom redirect uses SCRIPT_NAME from request" do
+ get "/relative_custom_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute path redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_path_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute option redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_option_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
+
+ test "[ENGINE] absolute custom redirect doesn't use SCRIPT_NAME from request" do
+ get "/absolute_custom_redirect"
+ assert_equal 301, last_response.status
+ assert_equal "http://example.org/foo", last_response.headers["Location"]
+ assert_equal %(<html><body>You are being <a href="http://example.org/foo">redirected</a>.</body></html>), last_response.body
+ end
end
end
diff --git a/actionpack/test/dispatch/rack_test.rb b/actionpack/test/dispatch/rack_test.rb
index 6d239d0a0c..42067854ee 100644
--- a/actionpack/test/dispatch/rack_test.rb
+++ b/actionpack/test/dispatch/rack_test.rb
@@ -174,22 +174,6 @@ class RackRequestParamsParsingTest < BaseRackTest
end
end
-class RackRequestContentTypeTest < BaseRackTest
- test "html content type verification" do
- assert_deprecated do
- @request.env['CONTENT_TYPE'] = Mime::HTML.to_s
- assert @request.content_mime_type.verify_request?
- end
- end
-
- test "xml content type verification" do
- assert_deprecated do
- @request.env['CONTENT_TYPE'] = Mime::XML.to_s
- assert !@request.content_mime_type.verify_request?
- end
- end
-end
-
class RackRequestNeedsRewoundTest < BaseRackTest
test "body should be rewound" do
data = 'foo'
diff --git a/actionpack/test/dispatch/request/json_params_parsing_test.rb b/actionpack/test/dispatch/request/json_params_parsing_test.rb
index c0c3147e37..dba9ab688f 100644
--- a/actionpack/test/dispatch/request/json_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/json_params_parsing_test.rb
@@ -30,12 +30,27 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
)
end
+ test "nils are stripped from collections" do
+ assert_parses(
+ {"person" => nil},
+ "{\"person\":[null]}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ assert_parses(
+ {"person" => ['foo']},
+ "{\"person\":[\"foo\",null]}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ assert_parses(
+ {"person" => nil},
+ "{\"person\":[null, null]}", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ end
+
test "logs error if parsing unsuccessful" do
with_test_routing do
output = StringIO.new
json = "[\"person]\": {\"name\": \"David\"}}"
post "/parse", json, {'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output)}
- assert_response :error
+ assert_response :bad_request
output.rewind && err = output.read
assert err =~ /Error occurred while parsing request parameters/
end
@@ -47,7 +62,7 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
$stderr = StringIO.new # suppress the log
json = "[\"person]\": {\"name\": \"David\"}}"
exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", json, {'CONTENT_TYPE' => 'application/json', 'action_dispatch.show_exceptions' => false} }
- assert_equal MultiJson::DecodeError, exception.original_exception.class
+ assert_equal JSON::ParserError, exception.original_exception.class
assert_equal exception.original_exception.message, exception.message
ensure
$stderr = STDERR
@@ -55,6 +70,13 @@ class JsonParamsParsingTest < ActionDispatch::IntegrationTest
end
end
+ test 'raw_post is not empty for JSON request' do
+ with_test_routing do
+ post '/parse', '{"posts": [{"title": "Post Title"}]}', 'CONTENT_TYPE' => 'application/json'
+ assert_equal '{"posts": [{"title": "Post Title"}]}', request.raw_post
+ end
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing do
@@ -107,6 +129,13 @@ class RootLessJSONParamsParsingTest < ActionDispatch::IntegrationTest
)
end
+ test "parses json with non-object JSON content" do
+ assert_parses(
+ {"user" => {"_json" => "string content" }, "_json" => "string content" },
+ "\"string content\"", { 'CONTENT_TYPE' => 'application/json' }
+ )
+ end
+
private
def assert_parses(expected, actual, headers = {})
with_test_routing(UsersController) do
diff --git a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
index 63c5ea26a6..2a2f92b5b3 100644
--- a/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/multipart_params_parsing_test.rb
@@ -1,13 +1,15 @@
+# encoding: utf-8
require 'abstract_unit'
class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
class TestController < ActionController::Base
class << self
- attr_accessor :last_request_parameters
+ attr_accessor :last_request_parameters, :last_parameters
end
def parse
self.class.last_request_parameters = request.request_parameters
+ self.class.last_parameters = request.parameters
head :ok
end
@@ -30,6 +32,23 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
assert_equal({ 'foo' => { 'baz' => 'bar'}}, parse_multipart('bracketed_param'))
end
+ test "parse single utf8 parameter" do
+ assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => 'Iñtërnâtiônàlizætiøn_value'},
+ parse_multipart('single_utf8_param'), "request.request_parameters")
+ assert_equal(
+ 'Iñtërnâtiônàlizætiøn_value',
+ TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters")
+ end
+
+ test "parse bracketed utf8 parameter" do
+ assert_equal({ 'Iñtërnâtiônàlizætiøn_name' => {
+ 'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'} },
+ parse_multipart('bracketed_utf8_param'), "request.request_parameters")
+ assert_equal(
+ {'Iñtërnâtiônàlizætiøn_nested_name' => 'Iñtërnâtiônàlizætiøn_value'},
+ TestController.last_parameters['Iñtërnâtiônàlizætiøn_name'], "request.parameters")
+ end
+
test "parses text file" do
params = parse_multipart('text_file')
assert_equal %w(file foo), params.keys.sort
@@ -89,8 +108,7 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
# Rack doesn't handle multipart/mixed for us.
files = params['files']
- files.force_encoding('ASCII-8BIT')
- assert_equal 19756, files.size
+ assert_equal 19756, files.bytesize
end
test "does not create tempfile if no file has been selected" do
@@ -123,6 +141,18 @@ class MultipartParamsParsingTest < ActionDispatch::IntegrationTest
end
end
+ # This can happen in Internet Explorer when redirecting after multipart form submit.
+ 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'
+ end
+ headers = { "CONTENT_TYPE" => "multipart/form-data; boundary=AaB03x" }
+ get "/parse", {}, headers
+ assert_response :ok
+ end
+ end
+
private
def fixture(name)
File.open(File.join(FIXTURE_PATH, name), 'rb') do |file|
diff --git a/actionpack/test/dispatch/request/query_string_parsing_test.rb b/actionpack/test/dispatch/request/query_string_parsing_test.rb
index 3cb430d83d..f072a9f717 100644
--- a/actionpack/test/dispatch/request/query_string_parsing_test.rb
+++ b/actionpack/test/dispatch/request/query_string_parsing_test.rb
@@ -84,8 +84,8 @@ class QueryStringParsingTest < ActionDispatch::IntegrationTest
assert_parses({"action" => nil}, "action")
assert_parses({"action" => {"foo" => nil}}, "action[foo]")
assert_parses({"action" => {"foo" => { "bar" => nil }}}, "action[foo][bar]")
- assert_parses({"action" => {"foo" => { "bar" => [] }}}, "action[foo][bar][]")
- assert_parses({"action" => {"foo" => []}}, "action[foo][]")
+ assert_parses({"action" => {"foo" => { "bar" => nil }}}, "action[foo][bar][]")
+ assert_parses({"action" => {"foo" => nil }}, "action[foo][]")
assert_parses({"action"=>{"foo"=>[{"bar"=>nil}]}}, "action[foo][][bar]")
end
diff --git a/actionpack/test/dispatch/request/session_test.rb b/actionpack/test/dispatch/request/session_test.rb
index 3f36d4f1a9..a244d1364c 100644
--- a/actionpack/test/dispatch/request/session_test.rb
+++ b/actionpack/test/dispatch/request/session_test.rb
@@ -24,7 +24,7 @@ module ActionDispatch
s['foo'] = 'bar'
s1 = Session.create(store, env, {})
- refute_equal s, s1
+ assert_not_equal s, s1
assert_equal 'bar', s1['foo']
end
@@ -61,6 +61,23 @@ module ActionDispatch
assert_equal([], s.values)
end
+ def test_fetch
+ session = Session.create(store, {}, {})
+
+ session['one'] = '1'
+ assert_equal '1', session.fetch(:one)
+
+ assert_equal '2', session.fetch(:two, '2')
+ assert_equal '2', session.fetch(:two)
+
+ assert_equal 'three', session.fetch(:three) {|el| el.to_s }
+ assert_equal 'three', session.fetch(:three)
+
+ assert_raise KeyError do
+ session.fetch(:four)
+ end
+ end
+
private
def store
Class.new {
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 e9b59f55a7..9a77454f30 100644
--- a/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
+++ b/actionpack/test/dispatch/request/url_encoded_params_parsing_test.rb
@@ -17,10 +17,9 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
end
test "parses unbalanced query string with array" do
- assert_parses(
- {'location' => ["1", "2"], 'age_group' => ["2"]},
- "location[]=1&location[]=2&age_group[]=2"
- )
+ query = "location[]=1&location[]=2&age_group[]=2"
+ expected = { 'location' => ["1", "2"], 'age_group' => ["2"] }
+ assert_parses expected, query
end
test "parses nested hash" do
@@ -30,9 +29,17 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"note[viewers][viewer][][type]=Group",
"note[viewers][viewer][][id]=2"
].join("&")
-
- expected = { "note" => { "viewers"=>{"viewer"=>[{ "id"=>"1", "type"=>"User"}, {"type"=>"Group", "id"=>"2"} ]} } }
- assert_parses(expected, query)
+ expected = {
+ "note" => {
+ "viewers" => {
+ "viewer" => [
+ { "id" => "1", "type" => "User" },
+ { "type" => "Group", "id" => "2" }
+ ]
+ }
+ }
+ }
+ assert_parses expected, query
end
test "parses more complex nesting" do
@@ -48,7 +55,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"products[second]=Pc",
"=Save"
].join("&")
-
expected = {
"customers" => {
"boston" => {
@@ -70,13 +76,12 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"second" => "Pc"
}
}
-
assert_parses expected, query
end
test "parses params with array" do
- query = "selected[]=1&selected[]=2&selected[]=3"
- expected = { "selected" => [ "1", "2", "3" ] }
+ query = "selected[]=1&selected[]=2&selected[]=3"
+ expected = { "selected" => ["1", "2", "3"] }
assert_parses expected, query
end
@@ -88,13 +93,13 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
test "parses params with array prefix and hashes" do
query = "a[][b][c]=d"
- expected = {"a" => [{"b" => {"c" => "d"}}]}
+ expected = { "a" => [{ "b" => { "c" => "d" } }] }
assert_parses expected, query
end
test "parses params with complex nesting" do
query = "a[][b][c][][d][]=e"
- expected = {"a" => [{"b" => {"c" => [{"d" => ["e"]}]}}]}
+ expected = { "a" => [{ "b" => { "c" => [{ "d" => ["e"] }] } }] }
assert_parses expected, query
end
@@ -104,7 +109,6 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"something_else=blah",
"logo=#{File.expand_path(__FILE__)}"
].join("&")
-
expected = {
"customers" => {
"boston" => {
@@ -116,22 +120,20 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
"something_else" => "blah",
"logo" => File.expand_path(__FILE__),
}
-
assert_parses expected, query
end
test "parses params with Safari 2 trailing null character" do
- query = "selected[]=1&selected[]=2&selected[]=3\0"
- expected = { "selected" => [ "1", "2", "3" ] }
+ query = "selected[]=1&selected[]=2&selected[]=3\0"
+ expected = { "selected" => ["1", "2", "3"] }
assert_parses expected, query
end
test "ambiguous params returns a bad request" do
with_routing do |set|
set.draw do
- post ':action', :to => ::UrlEncodedParamsParsingTest::TestController
+ post ':action', to: ::UrlEncodedParamsParsingTest::TestController
end
-
post "/parse", "foo[]=bar&foo[4]=bar"
assert_response :bad_request
end
@@ -141,7 +143,7 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
def with_test_routing
with_routing do |set|
set.draw do
- post ':action', :to => ::UrlEncodedParamsParsingTest::TestController
+ post ':action', to: ::UrlEncodedParamsParsingTest::TestController
end
yield
end
@@ -151,8 +153,8 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
with_test_routing do
post "/parse", actual
assert_response :ok
- assert_equal(expected, TestController.last_request_parameters)
- assert_utf8(TestController.last_request_parameters)
+ assert_equal expected, TestController.last_request_parameters
+ assert_utf8 TestController.last_request_parameters
end
end
@@ -164,14 +166,14 @@ class UrlEncodedParamsParsingTest < ActionDispatch::IntegrationTest
return
end
- object.each do |k,v|
+ object.each_value do |v|
case v
when Hash
- assert_utf8(v)
+ assert_utf8 v
when Array
- v.each {|el| assert_utf8(el) }
+ v.each { |el| assert_utf8 el }
else
- assert_utf8(v)
+ assert_utf8 v
end
end
end
diff --git a/actionpack/test/dispatch/request/xml_params_parsing_test.rb b/actionpack/test/dispatch/request/xml_params_parsing_test.rb
deleted file mode 100644
index cb68667002..0000000000
--- a/actionpack/test/dispatch/request/xml_params_parsing_test.rb
+++ /dev/null
@@ -1,165 +0,0 @@
-require 'abstract_unit'
-
-class XmlParamsParsingTest < ActionDispatch::IntegrationTest
- class TestController < ActionController::Base
- class << self
- attr_accessor :last_request_parameters
- end
-
- def parse
- self.class.last_request_parameters = request.request_parameters
- head :ok
- end
- end
-
- def teardown
- TestController.last_request_parameters = nil
- end
-
- test "parses a strict rack.input" do
- class Linted
- undef call if method_defined?(:call)
- def call(env)
- bar = env['action_dispatch.request.request_parameters']['foo']
- result = "<ok>#{bar}</ok>"
- [200, {"Content-Type" => "application/xml", "Content-Length" => result.length.to_s}, [result]]
- end
- end
- req = Rack::MockRequest.new(ActionDispatch::ParamsParser.new(Linted.new))
- resp = req.post('/', "CONTENT_TYPE" => "application/xml", :input => "<foo>bar</foo>", :lint => true)
- assert_equal "<ok>bar</ok>", resp.body
- end
-
- test "parses hash params" do
- with_test_routing do
- xml = "<person><name>David</name></person>"
- post "/parse", xml, default_headers
- assert_response :ok
- assert_equal({"person" => {"name" => "David"}}, TestController.last_request_parameters)
- end
- end
-
- test "parses single file" do
- with_test_routing do
- xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></person>"
- post "/parse", xml, default_headers
- assert_response :ok
-
- person = TestController.last_request_parameters
- assert_equal "image/jpg", person['person']['avatar'].content_type
- assert_equal "me.jpg", person['person']['avatar'].original_filename
- assert_equal "ABC", person['person']['avatar'].read
- end
- end
-
- test "logs error if parsing unsuccessful" do
- with_test_routing do
- output = StringIO.new
- xml = "<person><name>David</name><avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar></pineapple>"
- post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => true, 'action_dispatch.logger' => ActiveSupport::Logger.new(output))
- assert_response :error
- output.rewind && err = output.read
- assert err =~ /Error occurred while parsing request parameters/
- end
- end
-
- test "occurring a parse error if parsing unsuccessful" do
- with_test_routing do
- begin
- $stderr = StringIO.new # suppress the log
- xml = "<person><name>David</name></pineapple>"
- exception = assert_raise(ActionDispatch::ParamsParser::ParseError) { post "/parse", xml, default_headers.merge('action_dispatch.show_exceptions' => false) }
- assert_equal REXML::ParseException, exception.original_exception.class
- assert_equal exception.original_exception.message, exception.message
- ensure
- $stderr = STDERR
- end
- end
- end
-
- test "parses multiple files" do
- xml = <<-end_body
- <person>
- <name>David</name>
- <avatars>
- <avatar type='file' name='me.jpg' content_type='image/jpg'>#{::Base64.encode64('ABC')}</avatar>
- <avatar type='file' name='you.gif' content_type='image/gif'>#{::Base64.encode64('DEF')}</avatar>
- </avatars>
- </person>
- end_body
-
- with_test_routing do
- post "/parse", xml, default_headers
- assert_response :ok
- end
-
- person = TestController.last_request_parameters
-
- assert_equal "image/jpg", person['person']['avatars']['avatar'].first.content_type
- assert_equal "me.jpg", person['person']['avatars']['avatar'].first.original_filename
- assert_equal "ABC", person['person']['avatars']['avatar'].first.read
-
- assert_equal "image/gif", person['person']['avatars']['avatar'].last.content_type
- assert_equal "you.gif", person['person']['avatars']['avatar'].last.original_filename
- assert_equal "DEF", person['person']['avatars']['avatar'].last.read
- end
-
- private
- def with_test_routing
- with_routing do |set|
- set.draw do
- post ':action', :to => ::XmlParamsParsingTest::TestController
- end
- yield
- end
- end
-
- def default_headers
- {'CONTENT_TYPE' => 'application/xml'}
- end
-end
-
-class LegacyXmlParamsParsingTest < XmlParamsParsingTest
- private
- def default_headers
- {'HTTP_X_POST_DATA_FORMAT' => 'xml'}
- end
-end
-
-class RootLessXmlParamsParsingTest < ActionDispatch::IntegrationTest
- class TestController < ActionController::Base
- wrap_parameters :person, :format => :xml
-
- class << self
- attr_accessor :last_request_parameters
- end
-
- def parse
- self.class.last_request_parameters = request.request_parameters
- head :ok
- end
- end
-
- def teardown
- TestController.last_request_parameters = nil
- end
-
- test "parses hash params" do
- with_test_routing do
- xml = "<name>David</name>"
- post "/parse", xml, {'CONTENT_TYPE' => 'application/xml'}
- assert_response :ok
- assert_equal({"name" => "David", "person" => {"name" => "David"}}, TestController.last_request_parameters)
- end
- end
-
- private
- def with_test_routing
- with_routing do |set|
- set.draw do
- post ':action', :to => ::RootLessXmlParamsParsingTest::TestController
- end
- yield
- end
- end
-end
diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb
index a2b9571660..f6de9748ca 100644
--- a/actionpack/test/dispatch/request_test.rb
+++ b/actionpack/test/dispatch/request_test.rb
@@ -3,7 +3,7 @@ require 'abstract_unit'
class RequestTest < ActiveSupport::TestCase
def url_for(options = {})
- options.reverse_merge!(:host => 'www.example.com')
+ options = { host: 'www.example.com' }.merge!(options)
ActionDispatch::Http::URL.url_for(options)
end
@@ -13,6 +13,9 @@ class RequestTest < ActiveSupport::TestCase
assert_equal '/books', url_for(:only_path => true, :path => '/books')
+ assert_equal 'http://www.example.com/books/?q=code', url_for(trailing_slash: true, path: '/books?q=code')
+ assert_equal 'http://www.example.com/books/?spareslashes=////', url_for(trailing_slash: true, path: '/books?spareslashes=////')
+
assert_equal 'http://www.example.com', url_for
assert_equal 'http://api.example.com', url_for(:subdomain => 'api')
assert_equal 'http://example.com', url_for(:subdomain => false)
@@ -25,6 +28,8 @@ class RequestTest < ActiveSupport::TestCase
assert_equal 'http://www.example.com/', url_for(:trailing_slash => true)
assert_equal 'http://dhh:supersecret@www.example.com', url_for(:user => 'dhh', :password => 'supersecret')
assert_equal 'http://www.example.com?search=books', url_for(:params => { :search => 'books' })
+ assert_equal 'http://www.example.com?params=', url_for(:params => '')
+ assert_equal 'http://www.example.com?params=1', url_for(:params => 1)
end
test "remote ip" do
@@ -32,7 +37,7 @@ class RequestTest < ActiveSupport::TestCase
assert_equal '1.2.3.4', request.remote_ip
request = stub_request 'REMOTE_ADDR' => '1.2.3.4,3.4.5.6'
- assert_equal '1.2.3.4', request.remote_ip
+ assert_equal '3.4.5.6', request.remote_ip
request = stub_request 'REMOTE_ADDR' => '1.2.3.4',
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
@@ -45,30 +50,32 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,unknown'
assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '172.16.0.1,3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,172.16.0.1'
+ assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '192.168.0.1,3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,192.168.0.1'
+ assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1,3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,10.0.0.1'
+ assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1, 10.0.0.1, 3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 10.0.0.1, 10.0.0.1'
+ assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '127.0.0.1,3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6,127.0.0.1'
+ assert_equal '3.4.5.6', request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,192.168.0.1'
assert_equal nil, request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 9.9.9.9, 10.0.0.1, 172.31.4.4'
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '9.9.9.9, 3.4.5.6, 172.31.4.4, 10.0.0.1'
assert_equal '3.4.5.6', request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
assert_equal nil, request.remote_ip
+ end
+ test "remote ip spoof detection" do
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
'HTTP_CLIENT_IP' => '2.2.2.2'
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
@@ -77,26 +84,20 @@ class RequestTest < ActiveSupport::TestCase
assert_match(/IP spoofing attack/, e.message)
assert_match(/HTTP_X_FORWARDED_FOR="1.1.1.1"/, e.message)
assert_match(/HTTP_CLIENT_IP="2.2.2.2"/, e.message)
+ end
- # turn IP Spoofing detection off.
- # This is useful for sites that are aimed at non-IP clients. The typical
- # example is WAP. Since the cellular network is not IP based, it's a
- # leap of faith to assume that their proxies are ever going to set the
- # HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly.
+ test "remote ip with spoof detection disabled" do
request = stub_request 'HTTP_X_FORWARDED_FOR' => '1.1.1.1',
'HTTP_CLIENT_IP' => '2.2.2.2',
:ip_spoofing_check => false
- assert_equal '2.2.2.2', request.remote_ip
-
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '9.9.9.9, 8.8.8.8'
- assert_equal '9.9.9.9', request.remote_ip
+ assert_equal '1.1.1.1', request.remote_ip
end
test "remote ip v6" do
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
- request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
+ request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334'
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
request = stub_request 'REMOTE_ADDR' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
@@ -107,30 +108,26 @@ class RequestTest < ActiveSupport::TestCase
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal nil, request.remote_ip
-
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal nil, request.remote_ip
-
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,unknown'
+ assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1'
+ assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '::1, ::1, fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, ::1, ::1'
+ assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
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::'
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'not_ip_address'
assert_equal nil, request.remote_ip
+ end
+ test "remote ip v6 spoof detection" do
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334'
e = assert_raise(ActionDispatch::RemoteIp::IpSpoofAttackError) {
@@ -139,26 +136,15 @@ class RequestTest < ActiveSupport::TestCase
assert_match(/IP spoofing attack/, e.message)
assert_match(/HTTP_X_FORWARDED_FOR="fe80:0000:0000:0000:0202:b3ff:fe1e:8329"/, e.message)
assert_match(/HTTP_CLIENT_IP="2001:0db8:85a3:0000:0000:8a2e:0370:7334"/, e.message)
+ end
- # Turn IP Spoofing detection off.
- # This is useful for sites that are aimed at non-IP clients. The typical
- # example is WAP. Since the cellular network is not IP based, it's a
- # leap of faith to assume that their proxies are ever going to set the
- # HTTP_CLIENT_IP/HTTP_X_FORWARDED_FOR headers properly.
+ test "remote ip v6 spoof detection disabled" do
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329',
'HTTP_CLIENT_IP' => '2001:0db8:85a3:0000:0000:8a2e:0370:7334',
:ip_spoofing_check => false
- assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
-
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
end
- test "remote ip when the remote ip middleware returns nil" do
- request = stub_request 'REMOTE_ADDR' => '127.0.0.1'
- assert_equal '127.0.0.1', request.remote_ip
- end
-
test "remote ip with user specified trusted proxies String" do
@trusted_proxies = "67.205.106.73"
@@ -168,16 +154,16 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'REMOTE_ADDR' => '172.16.0.1,67.205.106.73',
'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
- assert_equal '172.16.0.1', request.remote_ip
+ assert_equal '67.205.106.73', request.remote_ip
request = stub_request 'REMOTE_ADDR' => '67.205.106.73,3.4.5.6',
'HTTP_X_FORWARDED_FOR' => '67.205.106.73'
assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,67.205.106.73'
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '67.205.106.73,unknown'
assert_equal nil, request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '3.4.5.6, 9.9.9.9, 10.0.0.1, 67.205.106.73'
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '9.9.9.9, 3.4.5.6, 10.0.0.1, 67.205.106.73'
assert_equal '3.4.5.6', request.remote_ip
end
@@ -194,13 +180,13 @@ class RequestTest < ActiveSupport::TestCase
request = stub_request 'REMOTE_ADDR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,::1',
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
- assert_equal 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329', request.remote_ip
+ assert_equal '::1', request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'unknown,fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
assert_equal nil, request.remote_ip
request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329,2001:0db8:85a3:0000:0000:8a2e:0370:7334'
- assert_equal nil, request.remote_ip
+ assert_equal "2001:0db8:85a3:0000:0000:8a2e:0370:7334", request.remote_ip
end
test "remote ip with user specified trusted proxies Regexp" do
@@ -210,8 +196,8 @@ class RequestTest < ActiveSupport::TestCase
'HTTP_X_FORWARDED_FOR' => '3.4.5.6'
assert_equal '3.4.5.6', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => '67.205.106.73, 10.0.0.1, 9.9.9.9, 3.4.5.6'
- assert_equal nil, request.remote_ip
+ request = stub_request 'HTTP_X_FORWARDED_FOR' => '10.0.0.1, 9.9.9.9, 3.4.5.6, 67.205.106.73'
+ assert_equal '3.4.5.6', request.remote_ip
end
test "remote ip v6 with user specified trusted proxies Regexp" do
@@ -221,8 +207,13 @@ class RequestTest < ActiveSupport::TestCase
'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329'
assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
- request = stub_request 'HTTP_X_FORWARDED_FOR' => 'fe80:0000:0000:0000:0202:b3ff:fe1e:8329, 2001:0db8:85a3:0000:0000:8a2e:0370:7334'
- 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'
+ assert_equal '2001:0db8:85a3:0000:0000:8a2e:0370:7334', request.remote_ip
+ end
+
+ test "remote ip middleware not present still returns an IP" do
+ request = ActionDispatch::Request.new({'REMOTE_ADDR' => '127.0.0.1'})
+ assert_equal '127.0.0.1', request.remote_ip
end
test "domains" do
@@ -355,7 +346,6 @@ class RequestTest < ActiveSupport::TestCase
assert_equal "/of/some/uri", request.path_info
end
-
test "host with default port" do
request = stub_request 'HTTP_HOST' => 'rubyonrails.org:80'
assert_equal "rubyonrails.org", request.host_with_port
@@ -475,6 +465,27 @@ class RequestTest < ActiveSupport::TestCase
assert request.put?
end
+ test "post uneffected by local inflections" do
+ existing_acrnoyms = ActiveSupport::Inflector.inflections.acronyms.dup
+ existing_acrnoym_regex = ActiveSupport::Inflector.inflections.acronym_regex.dup
+ begin
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.acronym "POS"
+ end
+ assert_equal "pos_t", "POST".underscore
+ request = stub_request "REQUEST_METHOD" => "POST"
+ assert_equal :post, ActionDispatch::Request::HTTP_METHOD_LOOKUP["POST"]
+ assert_equal :post, request.method_symbol
+ assert request.post?
+ ensure
+ # Reset original acronym set
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.send(:instance_variable_set,"@acronyms",existing_acrnoyms)
+ inflect.send(:instance_variable_set,"@acronym_regex",existing_acrnoym_regex)
+ end
+ end
+ end
+
test "xml format" do
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => 'xml' })
@@ -550,32 +561,65 @@ class RequestTest < ActiveSupport::TestCase
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
- begin
- request = stub_request(mock_rack_env)
- request.parameters
- rescue ActionController::BadRequest
+ 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
request = stub_request 'HTTP_ACCEPT' => 'text/html'
request.expects(:parameters).at_least_once.returns({})
- assert_equal [ Mime::HTML ], request.formats
+ assert_equal [Mime::HTML], request.formats
+ request = stub_request 'HTTP_ACCEPT' => ''
+ request.expects(:parameters).at_least_once.returns({})
+ assert_equal [Mime::HTML], request.formats
+
+ request = stub_request 'HTTP_ACCEPT' => '',
+ 'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
+ request.expects(:parameters).at_least_once.returns({})
+ assert_equal [Mime::JS], request.formats
+
request = stub_request 'CONTENT_TYPE' => 'application/xml; charset=UTF-8',
'HTTP_X_REQUESTED_WITH' => "XMLHttpRequest"
request.expects(:parameters).at_least_once.returns({})
- assert_equal with_set(Mime::XML), request.formats
+ assert_equal [Mime::XML], request.formats
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :txt })
- assert_equal with_set(Mime::TEXT), request.formats
+ assert_equal [Mime::TEXT], request.formats
request = stub_request
request.expects(:parameters).at_least_once.returns({ :format => :unknown })
- assert request.formats.empty?
+ assert_instance_of Mime::NullType, request.format
+ end
+
+ test "format is not nil with unknown format" do
+ request = stub_request
+ request.expects(:parameters).at_least_once.returns({ format: :hello })
+ assert_equal request.format.nil?, true
+ assert_equal request.format.html?, false
+ assert_equal request.format.xml?, false
+ assert_equal request.format.json?, false
end
test "formats with xhr request" do
@@ -634,6 +678,13 @@ class RequestTest < ActiveSupport::TestCase
assert_equal Mime::XML, request.negotiate_mime([Mime::XML, Mime::CSV])
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
+ end
+
test "process parameter filter" do
test_hashes = [
[{'foo'=>'bar'},{'foo'=>'bar'},%w'food'],
@@ -796,8 +847,4 @@ protected
ActionDispatch::Http::URL.tld_length = tld_length
ActionDispatch::Request.new(env)
end
-
- def with_set(*args)
- args
- end
end
diff --git a/actionpack/test/dispatch/response_test.rb b/actionpack/test/dispatch/response_test.rb
index 185a9e9b18..4501ea095c 100644
--- a/actionpack/test/dispatch/response_test.rb
+++ b/actionpack/test/dispatch/response_test.rb
@@ -35,7 +35,7 @@ class ResponseTest < ActiveSupport::TestCase
end
def test_response_body_encoding
- body = ["hello".encode('utf-8')]
+ body = ["hello".encode(Encoding::UTF_8)]
response = ActionDispatch::Response.new 200, {}, body
assert_equal Encoding::UTF_8, response.body.encoding
end
@@ -129,7 +129,7 @@ class ResponseTest < ActiveSupport::TestCase
@response.set_cookie("login", :value => "foo&bar", :path => "/", :expires => Time.utc(2005, 10, 10,5))
status, headers, body = @response.to_a
- assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10-Oct-2005 05:00:00 GMT", headers["Set-Cookie"]
+ assert_equal "user_name=david; path=/\nlogin=foo%26bar; path=/; expires=Mon, 10 Oct 2005 05:00:00 -0000", headers["Set-Cookie"]
assert_equal({"login" => "foo&bar", "user_name" => "david"}, @response.cookies)
@response.delete_cookie("login")
@@ -212,6 +212,11 @@ class ResponseTest < ActiveSupport::TestCase
ActionDispatch::Response.default_headers = nil
end
end
+
+ test "respond_to? accepts include_private" do
+ assert_not @response.respond_to?(:method_missing)
+ assert @response.respond_to?(:method_missing, true)
+ end
end
class ResponseIntegrationTest < ActionDispatch::IntegrationTest
diff --git a/actionpack/test/dispatch/routing/inspector_test.rb b/actionpack/test/dispatch/routing/inspector_test.rb
index a3034ce001..4f97d28d2b 100644
--- a/actionpack/test/dispatch/routing/inspector_test.rb
+++ b/actionpack/test/dispatch/routing/inspector_test.rb
@@ -1,5 +1,4 @@
-require 'minitest/autorun'
-require 'action_controller'
+require 'abstract_unit'
require 'rails/engine'
require 'action_dispatch/routing/inspector'
@@ -8,7 +7,6 @@ module ActionDispatch
class RoutesInspectorTest < ActiveSupport::TestCase
def setup
@set = ActionDispatch::Routing::RouteSet.new
- @inspector = ActionDispatch::Routing::RoutesInspector.new
app = ActiveSupport::OrderedOptions.new
app.config = ActiveSupport::OrderedOptions.new
app.config.assets = ActiveSupport::OrderedOptions.new
@@ -17,9 +15,18 @@ module ActionDispatch
Rails.stubs(:env).returns("development")
end
- def draw(&block)
+ def draw(options = {}, &block)
@set.draw(&block)
- @inspector.format(@set.routes)
+ inspector = ActionDispatch::Routing::RoutesInspector.new(@set.routes)
+ inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, options[:filter]).split("\n")
+ end
+
+ def test_json_regexp_converter
+ @set.draw do
+ get '/cart', :to => 'cart#show'
+ end
+ route = ActionDispatch::Routing::RouteWrapper.new(@set.routes.first)
+ assert_equal "^\\/cart(?:\\.([^\\/.?]+))?$", route.json_regexp
end
def test_displaying_routes_for_engines
@@ -37,34 +44,45 @@ module ActionDispatch
mount engine => "/blog", :as => "blog"
end
- expected = [
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
"custom_assets GET /custom/assets(.:format) custom_assets#show",
" blog /blog Blog::Engine",
- "\nRoutes for Blog::Engine:",
+ "",
+ "Routes for Blog::Engine:",
"cart GET /cart(.:format) cart#show"
- ]
- assert_equal expected, output
+ ], output
end
def test_cart_inspect
output = draw do
get '/cart', :to => 'cart#show'
end
- assert_equal ["cart GET /cart(.:format) cart#show"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ "cart GET /cart(.:format) cart#show"
+ ], output
end
def test_inspect_shows_custom_assets
output = draw do
get '/custom/assets', :to => 'custom_assets#show'
end
- assert_equal ["custom_assets GET /custom/assets(.:format) custom_assets#show"], output
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
+ "custom_assets GET /custom/assets(.:format) custom_assets#show"
+ ], output
end
def test_inspect_routes_shows_resources_route
output = draw do
resources :articles
end
- expected = [
+
+ assert_equal [
+ " Prefix Verb URI Pattern Controller#Action",
" articles GET /articles(.:format) articles#index",
" POST /articles(.:format) articles#create",
" new_article GET /articles/new(.:format) articles#new",
@@ -72,50 +90,74 @@ module ActionDispatch
" article GET /articles/:id(.:format) articles#show",
" PATCH /articles/:id(.:format) articles#update",
" PUT /articles/:id(.:format) articles#update",
- " DELETE /articles/:id(.:format) articles#destroy" ]
- assert_equal expected, output
+ " DELETE /articles/:id(.:format) articles#destroy"
+ ], output
end
def test_inspect_routes_shows_root_route
output = draw do
root :to => 'pages#main'
end
- assert_equal ["root GET / pages#main"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ "root GET / pages#main"
+ ], output
end
def test_inspect_routes_shows_dynamic_action_route
output = draw do
get 'api/:action' => 'api'
end
- assert_equal [" GET /api/:action(.:format) api#:action"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " GET /api/:action(.:format) api#:action"
+ ], output
end
def test_inspect_routes_shows_controller_and_action_only_route
output = draw do
get ':controller/:action'
end
- assert_equal [" GET /:controller/:action(.:format) :controller#:action"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " GET /:controller/:action(.:format) :controller#:action"
+ ], output
end
def test_inspect_routes_shows_controller_and_action_route_with_constraints
output = draw do
get ':controller(/:action(/:id))', :id => /\d+/
end
- assert_equal [" GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " GET /:controller(/:action(/:id))(.:format) :controller#:action {:id=>/\\d+/}"
+ ], output
end
def test_rake_routes_shows_route_with_defaults
output = draw do
get 'photos/:id' => 'photos#show', :defaults => {:format => 'jpg'}
end
- assert_equal [%Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ %Q[ GET /photos/:id(.:format) photos#show {:format=>"jpg"}]
+ ], output
end
def test_rake_routes_shows_route_with_constraints
output = draw do
get 'photos/:id' => 'photos#show', :id => /[A-Z]\d{5}/
end
- assert_equal [" GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " GET /photos/:id(.:format) photos#show {:id=>/[A-Z]\\d{5}/}"
+ ], output
end
class RackApp
@@ -127,7 +169,11 @@ module ActionDispatch
output = draw do
get 'foo/:id' => RackApp, :id => /[A-Z]\d{5}/
end
- assert_equal [" GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"], output
+
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " GET /foo/:id(.:format) #{RackApp.name} {:id=>/[A-Z]\\d{5}/}"
+ ], output
end
def test_rake_routes_shows_route_with_rack_app_nested_with_dynamic_constraints
@@ -143,7 +189,10 @@ module ActionDispatch
end
end
- assert_equal [" /foo #{RackApp.name} {:constraint=>( my custom constraint )}"], output
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " /foo #{RackApp.name} {:constraint=>( my custom constraint )}"
+ ], output
end
def test_rake_routes_dont_show_app_mounted_in_assets_prefix
@@ -161,9 +210,38 @@ module ActionDispatch
get "/foobar" => redirect{ "/foo/bar" }
end
- assert_equal " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}", output[0]
- assert_equal " bar GET /bar(.:format) redirect(307, path: /foo/bar)", output[1]
- assert_equal "foobar GET /foobar(.:format) redirect(301)", output[2]
+ assert_equal [
+ "Prefix Verb URI Pattern Controller#Action",
+ " foo GET /foo(.:format) redirect(301, /foo/bar) {:subdomain=>\"admin\"}",
+ " bar GET /bar(.:format) redirect(307, path: /foo/bar)",
+ "foobar GET /foobar(.:format) redirect(301)"
+ ], output
+ end
+
+ def test_routes_can_be_filtered
+ output = draw(filter: 'posts') do
+ resources :articles
+ resources :posts
+ end
+
+ assert_equal [" Prefix Verb URI Pattern Controller#Action",
+ " posts GET /posts(.:format) posts#index",
+ " POST /posts(.:format) posts#create",
+ " new_post GET /posts/new(.:format) posts#new",
+ "edit_post GET /posts/:id/edit(.:format) posts#edit",
+ " post GET /posts/:id(.:format) posts#show",
+ " PATCH /posts/:id(.:format) posts#update",
+ " PUT /posts/:id(.:format) posts#update",
+ " DELETE /posts/:id(.:format) posts#destroy"], output
+ end
+
+ def test_regression_route_with_controller_regexp
+ output = draw do
+ get ':controller(/:action)', controller: /api\/[^\/]+/, format: false
+ end
+
+ assert_equal ["Prefix Verb URI Pattern Controller#Action",
+ " GET /:controller(/:action) (?-mix:api\\/[^\\/]+)#:action"], output
end
end
end
diff --git a/actionpack/test/dispatch/routing/route_set_test.rb b/actionpack/test/dispatch/routing/route_set_test.rb
new file mode 100644
index 0000000000..0e488d2b88
--- /dev/null
+++ b/actionpack/test/dispatch/routing/route_set_test.rb
@@ -0,0 +1,97 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Routing
+ class RouteSetTest < ActiveSupport::TestCase
+ class SimpleApp
+ def initialize(response)
+ @response = response
+ end
+
+ def call(env)
+ [ 200, { 'Content-Type' => 'text/plain' }, [response] ]
+ end
+ end
+
+ setup do
+ @set = RouteSet.new
+ end
+
+ test "url helpers are added when route is added" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_equal '/foo', url_helpers.foo_path
+ assert_raises NoMethodError do
+ assert_equal '/bar', url_helpers.bar_path
+ end
+
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ get 'bar', to: SimpleApp.new('bar#index')
+ end
+
+ assert_equal '/foo', url_helpers.foo_path
+ assert_equal '/bar', url_helpers.bar_path
+ end
+
+ test "url helpers are updated when route is updated" do
+ draw do
+ get 'bar', to: SimpleApp.new('bar#index'), as: :bar
+ end
+
+ assert_equal '/bar', url_helpers.bar_path
+
+ draw do
+ get 'baz', to: SimpleApp.new('baz#index'), as: :bar
+ end
+
+ assert_equal '/baz', url_helpers.bar_path
+ end
+
+ test "url helpers are removed when route is removed" do
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ get 'bar', to: SimpleApp.new('bar#index')
+ end
+
+ assert_equal '/foo', url_helpers.foo_path
+ assert_equal '/bar', url_helpers.bar_path
+
+ draw do
+ get 'foo', to: SimpleApp.new('foo#index')
+ end
+
+ assert_equal '/foo', url_helpers.foo_path
+ assert_raises NoMethodError do
+ assert_equal '/bar', url_helpers.bar_path
+ end
+ end
+
+ test "explicit keys win over implicit keys" do
+ draw do
+ resources :foo do
+ resources :bar, to: SimpleApp.new('foo#show')
+ end
+ end
+
+ assert_equal '/foo/1/bar/2', url_helpers.foo_bar_path(1, 2)
+ assert_equal '/foo/1/bar/2', url_helpers.foo_bar_path(2, foo_id: 1)
+ end
+
+ private
+ def clear!
+ @set.clear!
+ end
+
+ def draw(&block)
+ @set.draw(&block)
+ end
+
+ def url_helpers
+ @set.url_helpers
+ end
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/routing_test.rb b/actionpack/test/dispatch/routing_test.rb
index 93d89f7568..3e9e90a950 100644
--- a/actionpack/test/dispatch/routing_test.rb
+++ b/actionpack/test/dispatch/routing_test.rb
@@ -20,583 +20,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
end
- stub_controllers do |routes|
- Routes = routes
- Routes.draw do
- default_url_options :host => "rubyonrails.org"
- resources_path_names :correlation_indexes => "info_about_correlation_indexes"
-
+ def test_logout
+ draw do
controller :sessions do
- get 'login' => :new
- post 'login' => :create
delete 'logout' => :destroy
end
-
- resource :session do
- get :create
- post :reset
-
- resource :info
-
- member do
- get :crush
- end
- end
-
- scope "bookmark", :controller => "bookmarks", :as => :bookmark do
- get :new, :path => "build"
- post :create, :path => "create", :as => ""
- put :update
- get :remove, :action => :destroy, :as => :remove
- end
-
- scope "pagemark", :controller => "pagemarks", :as => :pagemark do
- get "new", :path => "build"
- post "create", :as => ""
- put "update"
- get "remove", :action => :destroy, :as => :remove
- end
-
- get 'account/logout' => redirect("/logout"), :as => :logout_redirect
- get 'account/login', :to => redirect("/login")
- get 'secure', :to => redirect("/secure/login")
-
- get 'mobile', :to => redirect(:subdomain => 'mobile')
- get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '')
- get 'new_documentation', :to => redirect(:path => '/documentation/new')
- get 'super_new_documentation', :to => redirect(:host => 'super-docs.com')
-
- get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
- get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}')
-
- get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector)
-
- constraints(lambda { |req| true }) do
- get 'account/overview'
- end
-
- get '/account/nested/overview'
- get 'sign_in' => "sessions#new"
-
- get 'account/modulo/:name', :to => redirect("/%{name}s")
- get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" }
- get 'account/proc_req' => redirect {|params, req| "/#{req.method}" }
-
- get 'account/google' => redirect('http://www.google.com/', :status => 302)
-
- match 'openid/login', :via => [:get, :post], :to => "openid#login"
-
- 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/:action'
- end
-
- get "/local/:action", :controller => "local"
-
- get "/projects/status(.:format)"
- get "/404", :to => lambda { |env| [404, {"Content-Type" => "text/plain"}, ["NOT FOUND"]] }
-
- constraints(:ip => /192\.168\.1\.\d\d\d/) do
- get 'admin' => "queenbee#index"
- end
-
- constraints ::TestRoutingMapper::IpRestrictor do
- get 'admin/accounts' => "queenbee#accounts"
- end
-
- get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor
-
- scope 'pt', :as => 'pt' do
- resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos' do
- post :preview, :on => :new
- put :close, :on => :member, :path => 'fechar'
- get :open, :on => :new, :path => 'abrir'
- end
- resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do
- post :preview, :on => :new
- put :activate, :on => :member
- end
- resources :products, :path_names => { :new => 'novo' } do
- new do
- post :preview
- end
- end
- end
-
- resources :projects, :controller => :project do
- resources :involvements, :attachments
- get :correlation_indexes, :on => :collection
-
- resources :participants do
- put :update_all, :on => :collection
- end
-
- resources :companies do
- resources :people
- resource :avatar, :controller => :avatar
- end
-
- resources :images, :as => :funny_images do
- post :revise, :on => :member
- end
-
- resource :manager, :as => :super_manager do
- post :fire
- end
-
- resources :people do
- nested do
- scope "/:access_token" do
- resource :avatar
- end
- end
-
- member do
- get 'some_path_with_name'
- put :accessible_projects
- post :resend, :generate_new_password
- end
- end
-
- resources :posts do
- get :archive, :toggle_view, :on => :collection
- post :preview, :on => :member
-
- resource :subscription
-
- resources :comments do
- post :preview, :on => :collection
- end
- end
-
- post 'new', :action => 'new', :on => :collection, :as => :new
- end
-
- resources :replies do
- collection do
- get 'page/:page' => 'replies#index', :page => %r{\d+}
- get ':page' => 'replies#index', :page => %r{\d+}
- end
-
- new do
- post :preview
- end
-
- member do
- put :answer, :to => :mark_as_answer
- delete :answer, :to => :unmark_as_answer
- end
- end
-
- resources :posts, :only => [:index, :show] do
- namespace :admin do
- root :to => "index#index"
- end
- resources :comments, :except => :destroy do
- get "views" => "comments#views", :as => :views
- end
- end
-
- resource :past, :only => :destroy
- resource :present, :only => :update
- resource :future, :only => :create
- resources :relationships, :only => [:create, :destroy]
- resources :friendships, :only => [:update]
-
- shallow do
- namespace :api do
- resources :teams do
- resources :players
- resource :captain
- end
- end
- end
-
- scope '/hello' do
- shallow do
- resources :notes do
- resources :trackbacks
- end
- end
- end
-
- resources :threads, :shallow => true do
- resource :owner
- resources :messages do
- resources :comments do
- member do
- post :preview
- end
- end
- end
- end
-
- resources :sheep do
- get "_it", :on => :member
- end
-
- resources :clients do
- namespace :google do
- resource :account do
- namespace :secret do
- resource :info
- end
- end
- end
- end
-
- resources :customers do
- get :recent, :on => :collection
- get "profile", :on => :member
- get "secret/profile" => "customers#secret", :on => :member
- post "preview" => "customers#preview", :as => :another_preview, :on => :new
- resource :avatar do
- get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member
- end
- resources :invoices do
- get "outstanding" => "invoices#outstanding", :on => :collection
- get "overdue", :to => :overdue, :on => :collection
- get "print" => "invoices#print", :as => :print, :on => :member
- post "preview" => "invoices#preview", :as => :preview, :on => :new
- get "aged/:months", :on => :collection, :action => :aged, :as => :aged
- end
- resources :notes, :shallow => true do
- get "preview" => "notes#preview", :as => :preview, :on => :new
- get "print" => "notes#print", :as => :print, :on => :member
- end
- get "inactive", :on => :collection
- post "deactivate", :on => :member
- get "old", :on => :collection, :as => :stale
- get "export"
- end
-
- namespace :api do
- resources :customers do
- get "recent" => "customers#recent", :as => :recent, :on => :collection
- get "profile" => "customers#profile", :as => :profile, :on => :member
- post "preview" => "customers#preview", :as => :preview, :on => :new
- end
- scope(':version', :version => /.+/) do
- resources :users, :id => /.+?/, :format => /json|xml/
- end
- end
-
- get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp
-
- get 'people/:id/update', :to => 'people#update', :as => :update_person
- get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person
-
- # misc
- get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article
-
- # default params
- get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home'
- get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' }
- defaults :id => 'home' do
- get 'scoped_pages/(:id)', :to => 'pages#show'
- end
-
- namespace :account do
- get 'shorthand'
- get 'description', :to => :description, :as => "description"
- get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback
- resource :subscription, :credit, :credit_card
-
- root :to => "account#index"
-
- namespace :admin do
- resource :subscription
- end
- end
-
- namespace :forum do
- resources :products, :path => '' do
- resources :questions
- end
- end
-
- namespace :users, :path => 'usuarios' do
- root :to => 'home#index'
- end
-
- controller :articles do
- scope '/articles', :as => 'article' do
- scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do
- get '/:id', :to => :with_id, :as => ""
- end
- end
- end
-
- scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do
- resources :rooms
- end
-
- get '/info' => 'projects#info', :as => 'info'
-
- namespace :admin do
- scope '(:locale)', :locale => /en|pl/ do
- resources :descriptions
- end
- end
-
- scope '(:locale)', :locale => /en|pl/ do
- get "registrations/new"
- resources :descriptions
- root :to => 'projects#index'
- end
-
- scope :only => [:index, :show] do
- resources :products, :constraints => { :id => /\d{4}/ } do
- root :to => "products#root"
- get :favorite, :on => :collection
- resources :images
- end
- resource :account
- end
-
- resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ }
-
- resource :token, :module => :api
- scope :module => :api do
- resources :errors, :shallow => true do
- resources :notices
- end
- get 'api/version'
- end
-
- scope :path => 'api' do
- resource :me
- get '/' => 'mes#index'
- end
-
- get "(/:username)/followers" => "followers#index"
- get "/groups(/user/:username)" => "groups#index"
- get "(/user/:username)/photos" => "photos#index"
-
- scope '(groups)' do
- scope '(discussions)' do
- resources :messages
- end
- end
-
- get "whatever/:controller(/:action(/:id))", :id => /\d+/
-
- resource :profile do
- get :settings
-
- new do
- post :preview
- end
- end
-
- resources :content
-
- namespace :transport do
- resources :taxis
- end
-
- namespace :medical do
- resource :taxis
- end
-
- scope :constraints => { :id => /\d+/ } do
- get '/tickets', :to => 'tickets#index', :as => :tickets
- end
-
- scope :constraints => { :id => /\d{4}/ } do
- resources :movies do
- resources :reviews
- resource :trailer
- end
- end
-
- namespace :private do
- root :to => redirect('/private/index')
- get "index", :to => 'private#index'
- end
-
- scope :only => [:index, :show] do
- namespace :only do
- resources :clubs do
- resources :players
- resource :chairman
- end
- end
- end
-
- scope :except => [:new, :create, :edit, :update, :destroy] do
- namespace :except do
- resources :clubs do
- resources :players
- resource :chairman
- end
- end
- end
-
- namespace :wiki do
- resources :articles, :id => /[^\/]+/ do
- resources :comments, :only => [:create, :new]
- end
- end
-
- resources :wiki_pages, :path => :pages
- resource :wiki_account, :path => :my_account
-
- scope :only => :show do
- namespace :only do
- resources :sectors, :only => :index do
- resources :companies do
- scope :only => :index do
- resources :divisions
- end
- scope :except => [:show, :update, :destroy] do
- resources :departments
- end
- end
- resource :leader
- resources :managers, :except => [:show, :update, :destroy]
- end
- end
- end
-
- scope :except => :index do
- namespace :except do
- resources :sectors, :except => [:show, :update, :destroy] do
- resources :companies do
- scope :except => [:show, :update, :destroy] do
- resources :divisions
- end
- scope :only => :index do
- resources :departments
- end
- end
- resource :leader
- resources :managers, :only => :index
- end
- end
- end
-
- resources :sections, :id => /.+/ do
- get :preview, :on => :member
- end
-
- resources :profiles, :param => :username, :username => /[a-z]+/ do
- get :details, :on => :member
- resources :messages
- end
-
- resources :orders do
- constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do
- resources :downloads, :param => :download, :shallow => true
- end
- end
-
- scope :as => "routes" do
- get "/c/:id", :as => :collision, :to => "collision#show"
- get "/collision", :to => "collision#show"
- get "/no_collision", :to => "collision#show", :as => nil
-
- get "/fc/:id", :as => :forced_collision, :to => "forced_collision#show"
- get "/forced_collision", :as => :forced_collision, :to => "forced_collision#show"
- end
-
- get '/purchases/:token/:filename',
- :to => 'purchases#fetch',
- :token => /[[:alnum:]]{10}/,
- :filename => /(.+)/,
- :as => :purchase
-
- resources :lists, :id => /([A-Za-z0-9]{25})|default/ do
- resources :todos, :id => /\d+/
- end
-
- scope '/countries/:country', :constraints => lambda { |params, req| %w(all France).include?(params[:country]) } do
- get '/', :to => 'countries#index'
- get '/cities', :to => 'countries#cities'
- end
-
- get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' }
-
- get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/
-
- scope '/italians' do
- get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor
- get '/sculptors', :to => 'italians#sculptors'
- get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/}
- end
- end
- end
-
- class TestAltApp < ActionDispatch::IntegrationTest
- class AltRequest
- def initialize(env)
- @env = env
- end
-
- def path_info
- "/"
- end
-
- def request_method
- "GET"
- end
-
- def ip
- "127.0.0.1"
- end
-
- def x_header
- @env["HTTP_X_HEADER"] || ""
- end
- end
-
- class XHeader
- def call(env)
- [200, {"Content-Type" => "text/html"}, ["XHeader"]]
- end
- end
-
- class AltApp
- def call(env)
- [200, {"Content-Type" => "text/html"}, ["Alternative App"]]
- end
- end
-
- AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest)
- AltRoutes.draw do
- get "/" => TestRoutingMapper::TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/}
- get "/" => TestRoutingMapper::TestAltApp::AltApp.new
- end
-
- def app
- AltRoutes
- end
-
- def test_alt_request_without_header
- get "/"
- assert_equal "Alternative App", @response.body
- end
-
- def test_alt_request_with_matched_header
- get "/", {}, "HTTP_X_HEADER" => "HEADER"
- assert_equal "XHeader", @response.body
- end
-
- def test_alt_request_with_unmatched_header
- get "/", {}, "HTTP_X_HEADER" => "NON_MATCH"
- assert_equal "Alternative App", @response.body
end
- end
-
- def app
- Routes
- end
-
- include Routes.url_helpers
- def test_logout
delete '/logout'
assert_equal 'sessions#destroy', @response.body
@@ -605,6 +35,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_login
+ draw do
+ default_url_options :host => "rubyonrails.org"
+
+ controller :sessions do
+ get 'login' => :new
+ post 'login' => :create
+ end
+ end
+
get '/login'
assert_equal 'sessions#new', @response.body
assert_equal '/login', login_path
@@ -615,39 +54,59 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal '/login', url_for(:controller => 'sessions', :action => 'create', :only_path => true)
assert_equal '/login', url_for(:controller => 'sessions', :action => 'new', :only_path => true)
- assert_equal 'http://rubyonrails.org/login', Routes.url_for(:controller => 'sessions', :action => 'create')
- assert_equal 'http://rubyonrails.org/login', Routes.url_helpers.login_url
+ assert_equal 'http://rubyonrails.org/login', url_for(:controller => 'sessions', :action => 'create')
+ assert_equal 'http://rubyonrails.org/login', login_url
end
def test_login_redirect
+ draw do
+ get 'account/login', :to => redirect("/login")
+ end
+
get '/account/login'
verify_redirect 'http://www.example.com/login'
end
def test_logout_redirect_without_to
+ draw do
+ get 'account/logout' => redirect("/logout"), :as => :logout_redirect
+ end
+
assert_equal '/account/logout', logout_redirect_path
get '/account/logout'
verify_redirect 'http://www.example.com/logout'
end
def test_namespace_redirect
+ draw do
+ namespace :private do
+ root :to => redirect('/private/index')
+ get "index", :to => 'private#index'
+ end
+ end
+
get '/private'
verify_redirect 'http://www.example.com/private/index'
end
def test_namespace_with_controller_segment
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw do
- namespace :admin do
- get '/:controller(/:action(/:id(.:format)))'
- end
+ draw do
+ namespace :admin do
+ get '/:controller(/:action(/:id(.:format)))'
end
end
end
end
def test_session_singleton_resource
+ draw do
+ resource :session do
+ get :create
+ post :reset
+ end
+ end
+
get '/session'
assert_equal 'sessions#create', @response.body
assert_equal '/session', session_path
@@ -675,68 +134,126 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_session_info_nested_singleton_resource
+ draw do
+ resource :session do
+ resource :info
+ end
+ end
+
get '/session/info'
assert_equal 'infos#show', @response.body
assert_equal '/session/info', session_info_path
end
def test_member_on_resource
+ draw do
+ resource :session do
+ member do
+ get :crush
+ end
+ end
+ end
+
get '/session/crush'
assert_equal 'sessions#crush', @response.body
assert_equal '/session/crush', crush_session_path
end
def test_redirect_modulo
+ draw do
+ get 'account/modulo/:name', :to => redirect("/%{name}s")
+ end
+
get '/account/modulo/name'
verify_redirect 'http://www.example.com/names'
end
def test_redirect_proc
+ draw do
+ get 'account/proc/:name', :to => redirect {|params, req| "/#{params[:name].pluralize}" }
+ end
+
get '/account/proc/person'
verify_redirect 'http://www.example.com/people'
end
def test_redirect_proc_with_request
+ draw do
+ get 'account/proc_req' => redirect {|params, req| "/#{req.method}" }
+ end
+
get '/account/proc_req'
verify_redirect 'http://www.example.com/GET'
end
def test_redirect_hash_with_subdomain
+ draw do
+ get 'mobile', :to => redirect(:subdomain => 'mobile')
+ end
+
get '/mobile'
verify_redirect 'http://mobile.example.com/mobile'
end
def test_redirect_hash_with_domain_and_path
+ draw do
+ get 'documentation', :to => redirect(:domain => 'example-documentation.com', :path => '')
+ end
+
get '/documentation'
verify_redirect 'http://www.example-documentation.com'
end
def test_redirect_hash_with_path
+ draw do
+ get 'new_documentation', :to => redirect(:path => '/documentation/new')
+ end
+
get '/new_documentation'
verify_redirect 'http://www.example.com/documentation/new'
end
def test_redirect_hash_with_host
+ draw do
+ get 'super_new_documentation', :to => redirect(:host => 'super-docs.com')
+ end
+
get '/super_new_documentation?section=top'
verify_redirect 'http://super-docs.com/super_new_documentation?section=top'
end
def test_redirect_hash_path_substitution
+ draw do
+ get 'stores/:name', :to => redirect(:subdomain => 'stores', :path => '/%{name}')
+ end
+
get '/stores/iernest'
verify_redirect 'http://stores.example.com/iernest'
end
def test_redirect_hash_path_substitution_with_catch_all
+ draw do
+ get 'stores/:name(*rest)', :to => redirect(:subdomain => 'stores', :path => '/%{name}%{rest}')
+ end
+
get '/stores/iernest/products'
verify_redirect 'http://stores.example.com/iernest/products'
end
def test_redirect_class
+ draw do
+ get 'youtube_favorites/:youtube_id/:name', :to => redirect(YoutubeFavoritesRedirector)
+ end
+
get '/youtube_favorites/oHg5SJYRHA0/rick-rolld'
verify_redirect 'http://www.youtube.com/watch?v=oHg5SJYRHA0'
end
def test_openid
+ draw do
+ match 'openid/login', :via => [:get, :post], :to => "openid#login"
+ end
+
get '/openid/login'
assert_equal 'openid#login', @response.body
@@ -745,6 +262,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_bookmarks
+ draw do
+ scope "bookmark", :controller => "bookmarks", :as => :bookmark do
+ get :new, :path => "build"
+ post :create, :path => "create", :as => ""
+ put :update
+ get :remove, :action => :destroy, :as => :remove
+ end
+ end
+
get '/bookmark/build'
assert_equal 'bookmarks#new', @response.body
assert_equal '/bookmark/build', bookmark_new_path
@@ -763,6 +289,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_pagemarks
+ draw do
+ scope "pagemark", :controller => "pagemarks", :as => :pagemark do
+ get "new", :path => "build"
+ post "create", :as => ""
+ put "update"
+ get "remove", :action => :destroy, :as => :remove
+ end
+ end
+
get '/pagemark/build'
assert_equal 'pagemarks#new', @response.body
assert_equal '/pagemark/build', pagemark_new_path
@@ -781,6 +316,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_admin
+ draw do
+ constraints(:ip => /192\.168\.1\.\d\d\d/) do
+ get 'admin' => "queenbee#index"
+ end
+
+ constraints ::TestRoutingMapper::IpRestrictor do
+ get 'admin/accounts' => "queenbee#accounts"
+ end
+
+ get 'admin/passwords' => "queenbee#passwords", :constraints => ::TestRoutingMapper::IpRestrictor
+ end
+
get '/admin', {}, {'REMOTE_ADDR' => '192.168.1.100'}
assert_equal 'queenbee#index', @response.body
@@ -801,6 +348,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_global
+ 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/:action'
+ end
+ end
+
get '/global/dashboard'
assert_equal 'global#dashboard', @response.body
@@ -819,12 +375,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_local
+ draw do
+ get "/local/:action", :controller => "local"
+ end
+
get '/local/dashboard'
assert_equal 'local#dashboard', @response.body
end
# tests the use of dup in url_for
def test_url_for_with_no_side_effects
+ draw do
+ get "/projects/status(.:format)"
+ end
+
# without dup, additional (and possibly unwanted) values will be present in the options (eg. :host)
original_options = {:controller => 'projects', :action => 'status'}
options = original_options.dup
@@ -836,6 +400,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_url_for_does_not_modify_controller
+ draw do
+ get "/projects/status(.:format)"
+ end
+
controller = '/projects'
options = {:controller => controller, :action => 'status', :only_path => true}
url = url_for(options)
@@ -846,6 +414,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
# tests the arguments modification free version of define_hash_access
def test_named_route_with_no_side_effects
+ draw do
+ resources :customers do
+ get "profile", :on => :member
+ end
+ end
+
original_options = { :host => 'test.host' }
options = original_options.dup
@@ -856,11 +430,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_status
+ draw do
+ get "/projects/status(.:format)"
+ end
+
assert_equal '/projects/status', url_for(:controller => 'projects', :action => 'status', :only_path => true)
assert_equal '/projects/status.json', url_for(:controller => 'projects', :action => 'status', :format => 'json', :only_path => true)
end
def test_projects
+ draw do
+ resources :projects, :controller => :project
+ end
+
get '/projects'
assert_equal 'project#index', @response.body
assert_equal '/projects', projects_path
@@ -894,12 +476,24 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_with_post_action_and_new_path_on_collection
+ draw do
+ resources :projects, :controller => :project do
+ post 'new', :action => 'new', :on => :collection, :as => :new
+ end
+ end
+
post '/projects/new'
assert_equal "project#new", @response.body
assert_equal "/projects/new", new_projects_path
end
def test_projects_involvements
+ draw do
+ resources :projects, :controller => :project do
+ resources :involvements, :attachments
+ end
+ end
+
get '/projects/1/involvements'
assert_equal 'involvements#index', @response.body
assert_equal '/projects/1/involvements', project_involvements_path(:project_id => '1')
@@ -924,12 +518,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_attachments
+ draw do
+ resources :projects, :controller => :project do
+ resources :involvements, :attachments
+ end
+ end
+
get '/projects/1/attachments'
assert_equal 'attachments#index', @response.body
assert_equal '/projects/1/attachments', project_attachments_path(:project_id => '1')
end
def test_projects_participants
+ draw do
+ resources :projects, :controller => :project do
+ resources :participants do
+ put :update_all, :on => :collection
+ end
+ end
+ end
+
get '/projects/1/participants'
assert_equal 'participants#index', @response.body
assert_equal '/projects/1/participants', project_participants_path(:project_id => '1')
@@ -940,6 +548,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_companies
+ draw do
+ resources :projects, :controller => :project do
+ resources :companies do
+ resources :people
+ resource :avatar, :controller => :avatar
+ end
+ end
+ end
+
get '/projects/1/companies'
assert_equal 'companies#index', @response.body
assert_equal '/projects/1/companies', project_companies_path(:project_id => '1')
@@ -954,6 +571,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_project_manager
+ draw do
+ resources :projects do
+ resource :manager, :as => :super_manager do
+ post :fire
+ end
+ end
+ end
+
get '/projects/1/manager'
assert_equal 'managers#show', @response.body
assert_equal '/projects/1/manager', project_super_manager_path(:project_id => '1')
@@ -968,6 +593,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_project_images
+ draw do
+ resources :projects do
+ resources :images, :as => :funny_images do
+ post :revise, :on => :member
+ end
+ end
+ end
+
get '/projects/1/images'
assert_equal 'images#index', @response.body
assert_equal '/projects/1/images', project_funny_images_path(:project_id => '1')
@@ -982,6 +615,23 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_people
+ draw do
+ resources :projects do
+ resources :people do
+ nested do
+ scope "/:access_token" do
+ resource :avatar
+ end
+ end
+
+ member do
+ put :accessible_projects
+ post :resend, :generate_new_password
+ end
+ end
+ end
+ end
+
get '/projects/1/people'
assert_equal 'people#index', @response.body
assert_equal '/projects/1/people', project_people_path(:project_id => '1')
@@ -1008,12 +658,35 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_projects_with_resources_path_names
+ draw do
+ resources_path_names :correlation_indexes => "info_about_correlation_indexes"
+
+ resources :projects do
+ get :correlation_indexes, :on => :collection
+ end
+ end
+
get '/projects/info_about_correlation_indexes'
- assert_equal 'project#correlation_indexes', @response.body
+ assert_equal 'projects#correlation_indexes', @response.body
assert_equal '/projects/info_about_correlation_indexes', correlation_indexes_projects_path
end
def test_projects_posts
+ draw do
+ resources :projects do
+ resources :posts do
+ get :archive, :toggle_view, :on => :collection
+ post :preview, :on => :member
+
+ resource :subscription
+
+ resources :comments do
+ post :preview, :on => :collection
+ end
+ end
+ end
+ end
+
get '/projects/1/posts'
assert_equal 'posts#index', @response.body
assert_equal '/projects/1/posts', project_posts_path(:project_id => '1')
@@ -1044,6 +717,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_replies
+ draw do
+ resources :replies do
+ member do
+ put :answer, :to => :mark_as_answer
+ delete :answer, :to => :unmark_as_answer
+ end
+ end
+ end
+
put '/replies/1/answer'
assert_equal 'replies#mark_as_answer', @response.body
@@ -1052,6 +734,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resource_routes_with_only_and_except
+ draw do
+ resources :posts, :only => [:index, :show] do
+ resources :comments, :except => :destroy
+ end
+ end
+
get '/posts'
assert_equal 'posts#index', @response.body
assert_equal '/posts', posts_path
@@ -1075,6 +763,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resource_routes_only_create_update_destroy
+ draw do
+ resource :past, :only => :destroy
+ resource :present, :only => :update
+ resource :future, :only => :create
+ end
+
delete '/past'
assert_equal 'pasts#destroy', @response.body
assert_equal '/past', past_path
@@ -1093,6 +787,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resources_routes_only_create_update_destroy
+ draw do
+ resources :relationships, :only => [:create, :destroy]
+ resources :friendships, :only => [:update]
+ end
+
post '/relationships'
assert_equal 'relationships#create', @response.body
assert_equal '/relationships', relationships_path
@@ -1111,12 +810,22 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resource_with_slugs_in_ids
+ draw do
+ resources :posts
+ end
+
get '/posts/rails-rocks'
assert_equal 'posts#show', @response.body
assert_equal '/posts/rails-rocks', post_path(:id => 'rails-rocks')
end
def test_resources_for_uncountable_names
+ draw do
+ resources :sheep do
+ get "_it", :on => :member
+ end
+ end
+
assert_equal '/sheep', sheep_index_path
assert_equal '/sheep/1', sheep_path(1)
assert_equal '/sheep/new', new_sheep_path
@@ -1126,25 +835,26 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
def test_resource_does_not_modify_passed_options
options = {:id => /.+?/, :format => /json|xml/}
- self.class.stub_controllers do |routes|
- routes.draw do
- resource :user, options
- end
- end
+ draw { resource :user, options }
assert_equal({:id => /.+?/, :format => /json|xml/}, options)
end
def test_resources_does_not_modify_passed_options
options = {:id => /.+?/, :format => /json|xml/}
- self.class.stub_controllers do |routes|
- routes.draw do
- resources :users, options
- end
- end
+ draw { resources :users, options }
assert_equal({:id => /.+?/, :format => /json|xml/}, options)
end
def test_path_names
+ draw do
+ scope 'pt', :as => 'pt' do
+ resources :projects, :path_names => { :edit => 'editar', :new => 'novo' }, :path => 'projetos'
+ resource :admin, :path_names => { :new => 'novo', :activate => 'ativar' }, :path => 'administrador' do
+ put :activate, :on => :member
+ end
+ end
+ end
+
get '/pt/projetos'
assert_equal 'projects#index', @response.body
assert_equal '/pt/projetos', pt_projects_path
@@ -1167,6 +877,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_path_option_override
+ draw do
+ scope 'pt', :as => 'pt' do
+ resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do
+ put :close, :on => :member, :path => 'fechar'
+ get :open, :on => :new, :path => 'abrir'
+ end
+ end
+ end
+
get '/pt/projetos/novo/abrir'
assert_equal 'projects#open', @response.body
assert_equal '/pt/projetos/novo/abrir', open_new_pt_project_path
@@ -1177,11 +896,19 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_sprockets
+ draw do
+ get 'sprockets.js' => ::TestRoutingMapper::SprocketsApp
+ end
+
get '/sprockets.js'
assert_equal 'javascripts', @response.body
end
def test_update_person_route
+ draw do
+ get 'people/:id/update', :to => 'people#update', :as => :update_person
+ end
+
get '/people/1/update'
assert_equal 'people#update', @response.body
@@ -1189,6 +916,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_update_project_person
+ draw do
+ get '/projects/:project_id/people/:id/update', :to => 'people#update', :as => :update_project_person
+ end
+
get '/projects/1/people/2/update'
assert_equal 'people#update', @response.body
@@ -1196,6 +927,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_forum_products
+ draw do
+ namespace :forum do
+ resources :products, :path => '' do
+ resources :questions
+ end
+ end
+ end
+
get '/forum'
assert_equal 'forum/products#index', @response.body
assert_equal '/forum', forum_products_path
@@ -1214,6 +953,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_articles_perma
+ draw do
+ get 'articles/:year/:month/:day/:title', :to => "articles#show", :as => :article
+ end
+
get '/articles/2009/08/18/rails-3'
assert_equal 'articles#show', @response.body
@@ -1221,6 +964,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_account_namespace
+ draw do
+ namespace :account do
+ resource :subscription, :credit, :credit_card
+ end
+ end
+
get '/account/subscription'
assert_equal 'account/subscriptions#show', @response.body
assert_equal '/account/subscription', account_subscription_path
@@ -1235,12 +984,32 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_nested_namespace
+ draw do
+ namespace :account do
+ namespace :admin do
+ resource :subscription
+ end
+ end
+ end
+
get '/account/admin/subscription'
assert_equal 'account/admin/subscriptions#show', @response.body
assert_equal '/account/admin/subscription', account_admin_subscription_path
end
def test_namespace_nested_in_resources
+ draw do
+ resources :clients do
+ namespace :google do
+ resource :account do
+ namespace :secret do
+ resource :info
+ end
+ end
+ end
+ end
+ end
+
get '/clients/1/google/account'
assert_equal '/clients/1/google/account', client_google_account_path(1)
assert_equal 'google/accounts#show', @response.body
@@ -1251,12 +1020,40 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_namespace_with_options
+ draw do
+ namespace :users, :path => 'usuarios' do
+ root :to => 'home#index'
+ end
+ end
+
get '/usuarios'
assert_equal '/usuarios', users_root_path
assert_equal 'users/home#index', @response.body
end
+ def test_namespace_containing_numbers
+ draw do
+ namespace :v2 do
+ resources :subscriptions
+ end
+ end
+
+ get '/v2/subscriptions'
+ assert_equal 'v2/subscriptions#index', @response.body
+ assert_equal '/v2/subscriptions', v2_subscriptions_path
+ end
+
def test_articles_with_id
+ draw do
+ controller :articles do
+ scope '/articles', :as => 'article' do
+ scope :path => '/:title', :title => /[a-z]+/, :as => :with_title do
+ get '/:id', :to => :with_id, :as => ""
+ end
+ end
+ end
+ end
+
get '/articles/rails/1'
assert_equal 'articles#with_id', @response.body
@@ -1267,6 +1064,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_access_token_rooms
+ draw do
+ scope ':access_token', :constraints => { :access_token => /\w{5,5}/ } do
+ resources :rooms
+ end
+ end
+
get '/12345/rooms'
assert_equal 'rooms#index', @response.body
@@ -1278,40 +1081,224 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_root
+ draw do
+ root :to => 'projects#index'
+ end
+
assert_equal '/', root_path
get '/'
assert_equal 'projects#index', @response.body
end
+ def test_scoped_root
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ root :to => 'projects#index'
+ end
+ end
+
+ assert_equal '/en', root_path(:locale => 'en')
+ get '/en'
+ assert_equal 'projects#index', @response.body
+ end
+
+ def test_scoped_root_as_name
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ root :to => 'projects#index', :as => 'projects'
+ end
+ end
+
+ assert_equal '/en', projects_path(:locale => 'en')
+ assert_equal '/', projects_path
+ get '/en'
+ assert_equal 'projects#index', @response.body
+ end
+
+ def test_scope_with_format_option
+ draw do
+ get "direct/index", as: :no_format_direct, format: false
+
+ scope format: false do
+ get "scoped/index", as: :no_format_scoped
+ end
+ end
+
+ assert_equal "/direct/index", no_format_direct_path
+ assert_equal "/direct/index?format=html", no_format_direct_path(format: "html")
+
+ assert_equal "/scoped/index", no_format_scoped_path
+ assert_equal "/scoped/index?format=html", no_format_scoped_path(format: "html")
+
+ get '/scoped/index'
+ assert_equal "scoped#index", @response.body
+
+ get '/scoped/index.html'
+ assert_equal "Not Found", @response.body
+ end
+
+ def test_resources_with_format_false_from_scope
+ draw do
+ scope format: false do
+ resources :posts
+ resource :user
+ end
+ end
+
+ get "/posts"
+ assert_response :success
+ assert_equal "posts#index", @response.body
+ assert_equal "/posts", posts_path
+
+ get "/posts.html"
+ assert_response :not_found
+ assert_equal "Not Found", @response.body
+ assert_equal "/posts?format=html", posts_path(format: "html")
+
+ get "/user"
+ assert_response :success
+ assert_equal "users#show", @response.body
+ assert_equal "/user", user_path
+
+ get "/user.html"
+ assert_response :not_found
+ assert_equal "Not Found", @response.body
+ assert_equal "/user?format=html", user_path(format: "html")
+ end
+
def test_index
+ draw do
+ get '/info' => 'projects#info', :as => 'info'
+ end
+
assert_equal '/info', info_path
get '/info'
assert_equal 'projects#info', @response.body
end
+ def test_match_with_many_paths_containing_a_slash
+ draw do
+ get 'get/first', 'get/second', 'get/third', :to => 'get#show'
+ end
+
+ get '/get/first'
+ assert_equal 'get#show', @response.body
+
+ get '/get/second'
+ assert_equal 'get#show', @response.body
+
+ get '/get/third'
+ assert_equal 'get#show', @response.body
+ end
+
def test_match_shorthand_with_no_scope
+ draw do
+ get 'account/overview'
+ end
+
assert_equal '/account/overview', account_overview_path
get '/account/overview'
assert_equal 'account#overview', @response.body
end
def test_match_shorthand_inside_namespace
+ draw do
+ namespace :account do
+ get 'shorthand'
+ end
+ end
+
assert_equal '/account/shorthand', account_shorthand_path
get '/account/shorthand'
assert_equal 'account#shorthand', @response.body
end
- def test_match_shorthand_with_module
- assert_equal '/api/version', api_version_path
- get '/api/version'
- assert_equal 'api/api#version', @response.body
+ def test_match_shorthand_with_multiple_paths_inside_namespace
+ draw do
+ namespace :proposals do
+ put 'activate', 'inactivate'
+ end
+ end
+
+ put '/proposals/activate'
+ assert_equal 'proposals#activate', @response.body
+
+ put '/proposals/inactivate'
+ assert_equal 'proposals#inactivate', @response.body
+ end
+
+ def test_match_shorthand_inside_namespace_with_controller
+ draw do
+ namespace :api do
+ get "products/list"
+ end
+ end
+
+ assert_equal '/api/products/list', api_products_list_path
+ get '/api/products/list'
+ assert_equal 'api/products#list', @response.body
+ end
+
+ def test_match_shorthand_inside_scope_with_variables_with_controller
+ draw do
+ scope ':locale' do
+ match 'questions/new', via: [:get]
+ end
+ end
+
+ get '/de/questions/new'
+ assert_equal 'questions#new', @response.body
+ assert_equal 'de', @request.params[:locale]
+ end
+
+ def test_match_shorthand_inside_nested_namespaces_and_scopes_with_controller
+ draw do
+ namespace :api do
+ namespace :v3 do
+ scope ':locale' do
+ get "products/list"
+ end
+ end
+ end
+ end
+
+ get '/api/v3/en/products/list'
+ assert_equal 'api/v3/products#list', @response.body
+ end
+
+ def test_controller_option_with_nesting_and_leading_slash
+ draw do
+ scope '/job', controller: 'job' do
+ scope ':id', action: 'manage_applicant' do
+ get "/active"
+ end
+ end
+ end
+
+ get '/job/5/active'
+ assert_equal 'job#manage_applicant', @response.body
end
def test_dynamically_generated_helpers_on_collection_do_not_clobber_resources_url_helper
+ draw do
+ resources :replies do
+ collection do
+ get 'page/:page' => 'replies#index', :page => %r{\d+}
+ get ':page' => 'replies#index', :page => %r{\d+}
+ end
+ end
+ end
+
assert_equal '/replies', replies_path
end
def test_scoped_controller_with_namespace_and_action
+ draw do
+ namespace :account do
+ get ':action/callback', :action => /twitter|github/, :to => "callbacks", :as => :callback
+ end
+ end
+
assert_equal '/account/twitter/callback', account_callback_path("twitter")
get '/account/twitter/callback'
assert_equal 'account/callbacks#twitter', @response.body
@@ -1321,23 +1308,39 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_convention_match_nested_and_with_leading_slash
+ draw do
+ get '/account/nested/overview'
+ end
+
assert_equal '/account/nested/overview', account_nested_overview_path
get '/account/nested/overview'
assert_equal 'account/nested#overview', @response.body
end
def test_convention_with_explicit_end
+ draw do
+ get 'sign_in' => "sessions#new"
+ end
+
get '/sign_in'
assert_equal 'sessions#new', @response.body
assert_equal '/sign_in', sign_in_path
end
def test_redirect_with_complete_url_and_status
+ draw do
+ get 'account/google' => redirect('http://www.google.com/', :status => 302)
+ end
+
get '/account/google'
verify_redirect 'http://www.google.com/', 302
end
def test_redirect_with_port
+ draw do
+ get 'account/login', :to => redirect("/login")
+ end
+
previous_host, self.host = self.host, 'www.example.com:3000'
get '/account/login'
@@ -1347,6 +1350,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_normalize_namespaced_matches
+ draw do
+ namespace :account do
+ get 'description', :to => :description, :as => "description"
+ end
+ end
+
assert_equal '/account/description', account_description_path
get '/account/description'
@@ -1354,18 +1363,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_namespaced_roots
+ draw do
+ namespace :account do
+ root :to => "account#index"
+ end
+ end
+
assert_equal '/account', account_root_path
get '/account'
assert_equal 'account/account#index', @response.body
end
def test_optional_scoped_root
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ root :to => 'projects#index'
+ end
+ end
+
assert_equal '/en', root_path("en")
get '/en'
assert_equal 'projects#index', @response.body
end
def test_optional_scoped_path
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ resources :descriptions
+ end
+ end
+
assert_equal '/en/descriptions', descriptions_path("en")
assert_equal '/descriptions', descriptions_path(nil)
assert_equal '/en/descriptions/1', description_path("en", 1)
@@ -1385,6 +1412,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_nested_optional_scoped_path
+ draw do
+ namespace :admin do
+ scope '(:locale)', :locale => /en|pl/ do
+ resources :descriptions
+ end
+ end
+ end
+
assert_equal '/admin/en/descriptions', admin_descriptions_path("en")
assert_equal '/admin/descriptions', admin_descriptions_path(nil)
assert_equal '/admin/en/descriptions/1', admin_description_path("en", 1)
@@ -1404,6 +1439,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_nested_optional_path_shorthand
+ draw do
+ scope '(:locale)', :locale => /en|pl/ do
+ get "registrations/new"
+ end
+ end
+
get '/registrations/new'
assert_nil @request.params[:locale]
@@ -1411,7 +1452,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'en', @request.params[:locale]
end
- def test_default_params
+ def test_default_string_params
+ draw do
+ get 'inline_pages/(:id)', :to => 'pages#show', :id => 'home'
+ get 'default_pages/(:id)', :to => 'pages#show', :defaults => { :id => 'home' }
+
+ defaults :id => 'home' do
+ get 'scoped_pages/(:id)', :to => 'pages#show'
+ end
+ end
+
get '/inline_pages'
assert_equal 'home', @request.params[:id]
@@ -1422,7 +1472,37 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'home', @request.params[:id]
end
+ def test_default_integer_params
+ draw do
+ get 'inline_pages/(:page)', to: 'pages#show', page: 1
+ get 'default_pages/(:page)', to: 'pages#show', defaults: { page: 1 }
+
+ defaults page: 1 do
+ get 'scoped_pages/(:page)', to: 'pages#show'
+ end
+ end
+
+ get '/inline_pages'
+ assert_equal 1, @request.params[:page]
+
+ get '/default_pages'
+ assert_equal 1, @request.params[:page]
+
+ get '/scoped_pages'
+ assert_equal 1, @request.params[:page]
+ end
+
def test_resource_constraints
+ draw do
+ resources :products, :constraints => { :id => /\d{4}/ } do
+ root :to => "products#root"
+ get :favorite, :on => :collection
+ resources :images
+ end
+
+ resource :dashboard, :constraints => { :ip => /192\.168\.1\.\d{1,3}/ }
+ end
+
get '/products/1'
assert_equal 'pass', @response.headers['X-Cascade']
get '/products'
@@ -1446,18 +1526,35 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_root_works_in_the_resources_scope
+ draw do
+ resources :products do
+ root :to => "products#root"
+ end
+ end
+
get '/products'
assert_equal 'products#root', @response.body
assert_equal '/products', products_root_path
end
def test_module_scope
+ draw do
+ resource :token, :module => :api
+ end
+
get '/token'
assert_equal 'api/tokens#show', @response.body
assert_equal '/token', token_path
end
def test_path_scope
+ draw do
+ scope :path => 'api' do
+ resource :me
+ get '/' => 'mes#index'
+ end
+ end
+
get '/api/me'
assert_equal 'mes#show', @response.body
assert_equal '/api/me', me_path
@@ -1466,7 +1563,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_equal 'mes#index', @response.body
end
+ def test_symbol_scope
+ draw do
+ scope :path => 'api' do
+ scope :v2 do
+ resource :me, as: 'v2_me'
+ get '/' => 'mes#index'
+ end
+
+ scope :v3, :admin do
+ resource :me, as: 'v3_me'
+ end
+ end
+ end
+
+ get '/api/v2/me'
+ assert_equal 'mes#show', @response.body
+ assert_equal '/api/v2/me', v2_me_path
+
+ get '/api/v2'
+ assert_equal 'mes#index', @response.body
+
+ get '/api/v3/admin/me'
+ assert_equal 'mes#show', @response.body
+ end
+
def test_url_generator_for_generic_route
+ draw do
+ get "whatever/:controller(/:action(/:id))"
+ end
+
get 'whatever/foo/bar'
assert_equal 'foo#bar', @response.body
@@ -1475,6 +1601,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_url_generator_for_namespaced_generic_route
+ draw do
+ get "whatever/:controller(/:action(/:id))", :id => /\d+/
+ end
+
get 'whatever/foo/bar/show'
assert_equal 'foo/bar#show', @response.body
@@ -1488,11 +1618,37 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
url_for(:controller => "foo/bar", :action => "show", :id => '1')
end
- def test_assert_recognizes_account_overview
- assert_recognizes({:controller => "account", :action => "overview"}, "/account/overview")
- end
-
def test_resource_new_actions
+ draw do
+ resources :replies do
+ new do
+ post :preview
+ end
+ end
+
+ scope 'pt', :as => 'pt' do
+ resources :projects, :path_names => { :new => 'novo' }, :path => 'projetos' do
+ post :preview, :on => :new
+ end
+
+ resource :admin, :path_names => { :new => 'novo' }, :path => 'administrador' do
+ post :preview, :on => :new
+ end
+
+ resources :products, :path_names => { :new => 'novo' } do
+ new do
+ post :preview
+ end
+ end
+ end
+
+ resource :profile do
+ new do
+ post :preview
+ end
+ end
+ end
+
assert_equal '/replies/new/preview', preview_new_reply_path
assert_equal '/pt/projetos/novo/preview', preview_new_pt_project_path
assert_equal '/pt/administrador/novo/preview', preview_new_pt_admin_path
@@ -1516,13 +1672,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resource_merges_options_from_scope
- assert_raise(NameError) { new_account_path }
+ draw do
+ scope :only => :show do
+ resource :account
+ end
+ end
+
+ assert_raise(NoMethodError) { new_account_path }
get '/account/new'
assert_equal 404, status
end
def test_resources_merges_options_from_scope
+ draw do
+ scope :only => [:index, :show] do
+ resources :products do
+ resources :images
+ end
+ end
+ end
+
assert_raise(NoMethodError) { edit_product_path('1') }
get '/products/1/edit'
@@ -1535,6 +1705,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_shallow_nested_resources
+ draw do
+ shallow do
+ namespace :api do
+ resources :teams do
+ resources :players
+ resource :captain
+ end
+ end
+ end
+
+ resources :threads, :shallow => true do
+ resource :owner
+ resources :messages do
+ resources :comments do
+ member do
+ post :preview
+ end
+ end
+ end
+ end
+ end
+
get '/api/teams'
assert_equal 'api/teams#index', @response.body
assert_equal '/api/teams', api_teams_path
@@ -1637,6 +1829,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_shallow_nested_resources_within_scope
+ draw do
+ scope '/hello' do
+ shallow do
+ resources :notes do
+ resources :trackbacks
+ end
+ end
+ end
+ end
+
get '/hello/notes/1/trackbacks'
assert_equal 'trackbacks#index', @response.body
assert_equal '/hello/notes/1/trackbacks', note_trackbacks_path(:note_id => 1)
@@ -1688,6 +1890,36 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_custom_resource_routes_are_scoped
+ draw do
+ resources :customers do
+ get :recent, :on => :collection
+ get "profile", :on => :member
+ get "secret/profile" => "customers#secret", :on => :member
+ post "preview" => "customers#preview", :as => :another_preview, :on => :new
+ resource :avatar do
+ get "thumbnail" => "avatars#thumbnail", :as => :thumbnail, :on => :member
+ end
+ resources :invoices do
+ get "outstanding" => "invoices#outstanding", :on => :collection
+ get "overdue", :to => :overdue, :on => :collection
+ get "print" => "invoices#print", :as => :print, :on => :member
+ post "preview" => "invoices#preview", :as => :preview, :on => :new
+ end
+ resources :notes, :shallow => true do
+ get "preview" => "notes#preview", :as => :preview, :on => :new
+ get "print" => "notes#print", :as => :print, :on => :member
+ end
+ end
+
+ namespace :api do
+ resources :customers do
+ get "recent" => "customers#recent", :as => :recent, :on => :collection
+ get "profile" => "customers#profile", :as => :profile, :on => :member
+ post "preview" => "customers#preview", :as => :preview, :on => :new
+ end
+ end
+ end
+
assert_equal '/customers/recent', recent_customers_path
assert_equal '/customers/1/profile', profile_customer_path(:id => '1')
assert_equal '/customers/1/secret/profile', secret_profile_customer_path(:id => '1')
@@ -1710,6 +1942,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_shallow_nested_routes_ignore_module
+ draw do
+ scope :module => :api do
+ resources :errors, :shallow => true do
+ resources :notices
+ end
+ end
+ end
+
get '/errors/1/notices'
assert_equal 'api/notices#index', @response.body
assert_equal '/errors/1/notices', error_notices_path(:error_id => '1')
@@ -1720,6 +1960,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_non_greedy_regexp
+ draw do
+ namespace :api do
+ scope(':version', :version => /.+/) do
+ resources :users, :id => /.+?/, :format => /json|xml/
+ end
+ end
+ end
+
get '/api/1.0/users'
assert_equal 'api/users#index', @response.body
assert_equal '/api/1.0/users', api_users_path(:version => '1.0')
@@ -1742,16 +1990,28 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_glob_parameter_accepts_regexp
+ draw do
+ get '/:locale/*file.:format', :to => 'files#show', :file => /path\/to\/existing\/file/
+ end
+
get '/en/path/to/existing/file.html'
assert_equal 200, @response.status
end
def test_resources_controller_name_is_not_pluralized
+ draw do
+ resources :content
+ end
+
get '/content'
assert_equal 'content#index', @response.body
end
def test_url_generator_for_optional_prefix_dynamic_segment
+ draw do
+ get "(/:username)/followers" => "followers#index"
+ end
+
get '/bob/followers'
assert_equal 'followers#index', @response.body
assert_equal 'http://www.example.com/bob/followers',
@@ -1764,6 +2024,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_url_generator_for_optional_suffix_static_and_dynamic_segment
+ draw do
+ get "/groups(/user/:username)" => "groups#index"
+ end
+
get '/groups/user/bob'
assert_equal 'groups#index', @response.body
assert_equal 'http://www.example.com/groups/user/bob',
@@ -1776,6 +2040,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_url_generator_for_optional_prefix_static_and_dynamic_segment
+ draw do
+ get "(/user/:username)/photos" => "photos#index"
+ end
+
get 'user/bob/photos'
assert_equal 'photos#index', @response.body
assert_equal 'http://www.example.com/user/bob/photos',
@@ -1788,6 +2056,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_url_recognition_for_optional_static_segments
+ draw do
+ scope '(groups)' do
+ scope '(discussions)' do
+ resources :messages
+ end
+ end
+ end
+
get '/groups/discussions/messages'
assert_equal 'messages#index', @response.body
@@ -1814,12 +2090,27 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_router_removes_invalid_conditions
+ draw do
+ scope :constraints => { :id => /\d+/ } do
+ get '/tickets', :to => 'tickets#index', :as => :tickets
+ end
+ end
+
get '/tickets'
assert_equal 'tickets#index', @response.body
assert_equal '/tickets', tickets_path
end
def test_constraints_are_merged_from_scope
+ draw do
+ scope :constraints => { :id => /\d{4}/ } do
+ resources :movies do
+ resources :reviews
+ resource :trailer
+ end
+ end
+ end
+
get '/movies/0001'
assert_equal 'movies#show', @response.body
assert_equal '/movies/0001', movie_path(:id => '0001')
@@ -1854,6 +2145,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_should_be_read_from_scope
+ draw do
+ scope :only => [:index, :show] do
+ namespace :only do
+ resources :clubs do
+ resources :players
+ resource :chairman
+ end
+ end
+ end
+ end
+
get '/only/clubs'
assert_equal 'only/clubs#index', @response.body
assert_equal '/only/clubs', only_clubs_path
@@ -1880,6 +2182,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_should_be_read_from_scope
+ draw do
+ scope :except => [:new, :create, :edit, :update, :destroy] do
+ namespace :except do
+ resources :clubs do
+ resources :players
+ resource :chairman
+ end
+ end
+ end
+ end
+
get '/except/clubs'
assert_equal 'except/clubs#index', @response.body
assert_equal '/except/clubs', except_clubs_path
@@ -1906,6 +2219,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_option_should_override_scope
+ draw do
+ scope :only => :show do
+ namespace :only do
+ resources :sectors, :only => :index
+ end
+ end
+ end
+
get '/only/sectors'
assert_equal 'only/sectors#index', @response.body
assert_equal '/only/sectors', only_sectors_path
@@ -1916,6 +2237,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_option_should_not_inherit
+ draw do
+ scope :only => :show do
+ namespace :only do
+ resources :sectors, :only => :index do
+ resources :companies
+ resource :leader
+ end
+ end
+ end
+ end
+
get '/only/sectors/1/companies/2'
assert_equal 'only/companies#show', @response.body
assert_equal '/only/sectors/1/companies/2', only_sector_company_path(:sector_id => '1', :id => '2')
@@ -1926,6 +2258,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_option_should_override_scope
+ draw do
+ scope :except => :index do
+ namespace :except do
+ resources :sectors, :except => [:show, :update, :destroy]
+ end
+ end
+ end
+
get '/except/sectors'
assert_equal 'except/sectors#index', @response.body
assert_equal '/except/sectors', except_sectors_path
@@ -1936,6 +2276,17 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_option_should_not_inherit
+ draw do
+ scope :except => :index do
+ namespace :except do
+ resources :sectors, :except => [:show, :update, :destroy] do
+ resources :companies
+ resource :leader
+ end
+ end
+ end
+ end
+
get '/except/sectors/1/companies/2'
assert_equal 'except/companies#show', @response.body
assert_equal '/except/sectors/1/companies/2', except_sector_company_path(:sector_id => '1', :id => '2')
@@ -1946,6 +2297,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_option_should_override_scoped_only
+ draw do
+ scope :only => :show do
+ namespace :only do
+ resources :sectors, :only => :index do
+ resources :managers, :except => [:show, :update, :destroy]
+ end
+ end
+ end
+ end
+
get '/only/sectors/1/managers'
assert_equal 'only/managers#index', @response.body
assert_equal '/only/sectors/1/managers', only_sector_managers_path(:sector_id => '1')
@@ -1956,6 +2317,16 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_option_should_override_scoped_except
+ draw do
+ scope :except => :index do
+ namespace :except do
+ resources :sectors, :except => [:show, :update, :destroy] do
+ resources :managers, :only => :index
+ end
+ end
+ end
+ end
+
get '/except/sectors/1/managers'
assert_equal 'except/managers#index', @response.body
assert_equal '/except/sectors/1/managers', except_sector_managers_path(:sector_id => '1')
@@ -1966,6 +2337,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_scope_should_override_parent_scope
+ draw do
+ scope :only => :show do
+ namespace :only do
+ resources :sectors, :only => :index do
+ resources :companies do
+ scope :only => :index do
+ resources :divisions
+ end
+ end
+ end
+ end
+ end
+ end
+
get '/only/sectors/1/companies/2/divisions'
assert_equal 'only/divisions#index', @response.body
assert_equal '/only/sectors/1/companies/2/divisions', only_sector_company_divisions_path(:sector_id => '1', :company_id => '2')
@@ -1976,6 +2361,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_scope_should_override_parent_scope
+ draw do
+ scope :except => :index do
+ namespace :except do
+ resources :sectors, :except => [:show, :update, :destroy] do
+ resources :companies do
+ scope :except => [:show, :update, :destroy] do
+ resources :divisions
+ end
+ end
+ end
+ end
+ end
+ end
+
get '/except/sectors/1/companies/2/divisions'
assert_equal 'except/divisions#index', @response.body
assert_equal '/except/sectors/1/companies/2/divisions', except_sector_company_divisions_path(:sector_id => '1', :company_id => '2')
@@ -1986,6 +2385,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_except_scope_should_override_parent_only_scope
+ draw do
+ scope :only => :show do
+ namespace :only do
+ resources :sectors, :only => :index do
+ resources :companies do
+ scope :except => [:show, :update, :destroy] do
+ resources :departments
+ end
+ end
+ end
+ end
+ end
+ end
+
get '/only/sectors/1/companies/2/departments'
assert_equal 'only/departments#index', @response.body
assert_equal '/only/sectors/1/companies/2/departments', only_sector_company_departments_path(:sector_id => '1', :company_id => '2')
@@ -1996,6 +2409,20 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_only_scope_should_override_parent_except_scope
+ draw do
+ scope :except => :index do
+ namespace :except do
+ resources :sectors, :except => [:show, :update, :destroy] do
+ resources :companies do
+ scope :only => :index do
+ resources :departments
+ end
+ end
+ end
+ end
+ end
+ end
+
get '/except/sectors/1/companies/2/departments'
assert_equal 'except/departments#index', @response.body
assert_equal '/except/sectors/1/companies/2/departments', except_sector_company_departments_path(:sector_id => '1', :company_id => '2')
@@ -2006,6 +2433,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resources_are_not_pluralized
+ draw do
+ namespace :transport do
+ resources :taxis
+ end
+ end
+
get '/transport/taxis'
assert_equal 'transport/taxis#index', @response.body
assert_equal '/transport/taxis', transport_taxis_path
@@ -2033,6 +2466,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_singleton_resources_are_not_singularized
+ draw do
+ namespace :medical do
+ resource :taxis
+ end
+ end
+
get '/medical/taxis/new'
assert_equal 'medical/taxis#new', @response.body
assert_equal '/medical/taxis/new', new_medical_taxis_path
@@ -2056,6 +2495,12 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_greedy_resource_id_regexp_doesnt_match_edit_and_custom_action
+ draw do
+ resources :sections, :id => /.+/ do
+ get :preview, :on => :member
+ end
+ end
+
get '/sections/1/edit'
assert_equal 'sections#edit', @response.body
assert_equal '/sections/1/edit', edit_section_path(:id => '1')
@@ -2066,6 +2511,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resource_constraints_are_pushed_to_scope
+ draw do
+ namespace :wiki do
+ resources :articles, :id => /[^\/]+/ do
+ resources :comments, :only => [:create, :new]
+ end
+ end
+ end
+
get '/wiki/articles/Ruby_on_Rails_3.0'
assert_equal 'wiki/articles#show', @response.body
assert_equal '/wiki/articles/Ruby_on_Rails_3.0', wiki_article_path(:id => 'Ruby_on_Rails_3.0')
@@ -2080,6 +2533,11 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_resources_path_can_be_a_symbol
+ draw do
+ resources :wiki_pages, :path => :pages
+ resource :wiki_account, :path => :my_account
+ end
+
get '/pages'
assert_equal 'wiki_pages#index', @response.body
assert_equal '/pages', wiki_pages_path
@@ -2094,6 +2552,10 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_redirect_https
+ draw do
+ get 'secure', :to => redirect("/secure/login")
+ end
+
with_https do
get '/secure'
verify_redirect 'https://www.example.com/secure/login'
@@ -2101,6 +2563,15 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_symbolized_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'
+ get '/cities', :to => 'countries#cities'
+ end
+
+ get '/countries/:country/(*other)', :to => redirect{ |params, req| params[:other] ? "/countries/all/#{params[:other]}" : '/countries/all' }
+ end
+
get '/countries/France'
assert_equal 'countries#index', @response.body
@@ -2115,6 +2586,14 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_constraints_block_not_carried_to_following_routes
+ draw do
+ scope '/italians' do
+ get '/writers', :to => 'italians#writers', :constraints => ::TestRoutingMapper::IpRestrictor
+ get '/sculptors', :to => 'italians#sculptors'
+ get '/painters/:painter', :to => 'italians#painters', :constraints => {:painter => /michelangelo/}
+ end
+ end
+
get '/italians/writers'
assert_equal 'Not Found', @response.body
@@ -2129,6 +2608,18 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_custom_resource_actions_defined_using_string
+ draw do
+ resources :customers do
+ resources :invoices do
+ get "aged/:months", :on => :collection, :action => :aged, :as => :aged
+ end
+
+ get "inactive", :on => :collection
+ post "deactivate", :on => :member
+ get "old", :on => :collection, :as => :stale
+ end
+ end
+
get '/customers/inactive'
assert_equal 'customers#inactive', @response.body
assert_equal '/customers/inactive', inactive_customers_path
@@ -2147,18 +2638,38 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_route_defined_in_resources_scope_level
+ draw do
+ resources :customers do
+ get "export"
+ end
+ end
+
get '/customers/1/export'
assert_equal 'customers#export', @response.body
assert_equal '/customers/1/export', customer_export_path(:customer_id => '1')
end
def test_named_character_classes_in_regexp_constraints
+ draw do
+ get '/purchases/:token/:filename',
+ :to => 'purchases#fetch',
+ :token => /[[:alnum:]]{10}/,
+ :filename => /(.+)/,
+ :as => :purchase
+ end
+
get '/purchases/315004be7e/Ruby_on_Rails_3.pdf'
assert_equal 'purchases#fetch', @response.body
assert_equal '/purchases/315004be7e/Ruby_on_Rails_3.pdf', purchase_path(:token => '315004be7e', :filename => 'Ruby_on_Rails_3.pdf')
end
def test_nested_resource_constraints
+ draw do
+ resources :lists, :id => /([A-Za-z0-9]{25})|default/ do
+ resources :todos, :id => /\d+/
+ end
+ end
+
get '/lists/01234012340123401234fffff'
assert_equal 'lists#show', @response.body
assert_equal '/lists/01234012340123401234fffff', list_path(:id => '01234012340123401234fffff')
@@ -2172,97 +2683,134 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
assert_raises(ActionController::UrlGenerationError){ list_todo_path(:list_id => '2', :id => '1') }
end
- def test_named_routes_collision_is_avoided_unless_explicitly_given_as
- assert_equal "/c/1", routes_collision_path(1)
- assert_equal "/fc/1", routes_forced_collision_path(1)
- end
-
def test_redirect_argument_error
routes = Class.new { include ActionDispatch::Routing::Redirection }.new
assert_raises(ArgumentError) { routes.redirect Object.new }
end
+ def test_named_route_check
+ before, after = nil
+
+ draw do
+ before = has_named_route?(:hello)
+ get "/hello", as: :hello, to: "hello#world"
+ after = has_named_route?(:hello)
+ end
+
+ assert !before, "expected to not have named route :hello before route definition"
+ assert after, "expected to have named route :hello after route definition"
+ end
+
def test_explicitly_avoiding_the_named_route
+ draw do
+ scope :as => "routes" do
+ get "/c/:id", :as => :collision, :to => "collision#show"
+ get "/collision", :to => "collision#show"
+ get "/no_collision", :to => "collision#show", :as => nil
+ end
+ end
+
assert !respond_to?(:routes_no_collision_path)
end
def test_controller_name_with_leading_slash_raise_error
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/feeds/:service', :to => '/feeds#show' }
- end
+ draw { get '/feeds/:service', :to => '/feeds#show' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' }
- end
+ draw { get '/feeds/:service', :controller => '/feeds', :action => 'show' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/api/feeds/:service', :to => '/api/feeds#show' }
- end
+ draw { get '/api/feeds/:service', :to => '/api/feeds#show' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
- end
+ draw { controller("/feeds") { get '/feeds/:service', :to => :show } }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { resources :feeds, :controller => '/feeds' }
- end
+ draw { resources :feeds, :controller => '/feeds' }
end
end
def test_invalid_route_name_raises_error
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/products', :to => 'products#index', :as => 'products ' }
- end
+ draw { get '/products', :to => 'products#index', :as => 'products ' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/products', :to => 'products#index', :as => ' products' }
- end
+ draw { get '/products', :to => 'products#index', :as => ' products' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/products', :to => 'products#index', :as => 'products!' }
- end
+ draw { get '/products', :to => 'products#index', :as => 'products!' }
end
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/products', :to => 'products#index', :as => 'products index' }
+ draw { get '/products', :to => 'products#index', :as => 'products index' }
+ end
+
+ assert_raise(ArgumentError) do
+ draw { get '/products', :to => 'products#index', :as => '1products' }
+ end
+ end
+
+ def test_duplicate_route_name_raises_error
+ assert_raise(ArgumentError) do
+ draw do
+ get '/collision', :to => 'collision#show', :as => 'collision'
+ get '/duplicate', :to => 'duplicate#show', :as => 'collision'
end
end
+ end
+ def test_duplicate_route_name_via_resources_raises_error
assert_raise(ArgumentError) do
- self.class.stub_controllers do |routes|
- routes.draw { get '/products', :to => 'products#index', :as => '1products' }
+ draw do
+ resources :collisions
+ get '/collision', :to => 'collision#show', :as => 'collision'
end
end
end
def test_nested_route_in_nested_resource
+ draw do
+ resources :posts, :only => [:index, :show] do
+ resources :comments, :except => :destroy do
+ get "views" => "comments#views", :as => :views
+ end
+ end
+ end
+
get "/posts/1/comments/2/views"
assert_equal "comments#views", @response.body
assert_equal "/posts/1/comments/2/views", post_comment_views_path(:post_id => '1', :comment_id => '2')
end
def test_root_in_deeply_nested_scope
+ draw do
+ resources :posts, :only => [:index, :show] do
+ namespace :admin do
+ root :to => "index#index"
+ end
+ end
+ end
+
get "/posts/1/admin"
assert_equal "admin/index#index", @response.body
assert_equal "/posts/1/admin", post_admin_root_path(:post_id => '1')
end
def test_custom_param
+ draw do
+ resources :profiles, :param => :username do
+ get :details, :on => :member
+ resources :messages
+ end
+ end
+
get '/profiles/bob'
assert_equal 'profiles#show', @response.body
assert_equal 'bob', @request.params[:username]
@@ -2276,6 +2824,13 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_custom_param_constraint
+ draw do
+ resources :profiles, :param => :username, :username => /[a-z]+/ do
+ get :details, :on => :member
+ resources :messages
+ end
+ end
+
get '/profiles/bob1'
assert_equal 404, @response.status
@@ -2287,12 +2842,50 @@ class TestRoutingMapper < ActionDispatch::IntegrationTest
end
def test_shallow_custom_param
+ draw do
+ resources :orders do
+ constraints :download => /[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}/ do
+ resources :downloads, :param => :download, :shallow => true
+ end
+ end
+ end
+
get '/downloads/0c0c0b68-d24b-11e1-a861-001ff3fffe6f.zip'
assert_equal 'downloads#show', @response.body
assert_equal '0c0c0b68-d24b-11e1-a861-001ff3fffe6f', @request.params[:download]
end
+ def test_action_from_path_is_not_frozen
+ draw do
+ get 'search' => 'search'
+ end
+
+ get '/search'
+ assert !@request.params[:action].frozen?
+ end
+
private
+
+ def draw(&block)
+ self.class.stub_controllers do |routes|
+ @app = routes
+ @app.default_url_options = { host: 'www.example.com' }
+ @app.draw(&block)
+ end
+ end
+
+ def url_for(options = {})
+ @app.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)
+ else
+ super
+ end
+ end
+
def with_https
old_https = https?
https!
@@ -2312,6 +2905,67 @@ private
end
end
+class TestAltApp < ActionDispatch::IntegrationTest
+ class AltRequest
+ def initialize(env)
+ @env = env
+ end
+
+ def path_info
+ "/"
+ end
+
+ def request_method
+ "GET"
+ end
+
+ def ip
+ "127.0.0.1"
+ end
+
+ def x_header
+ @env["HTTP_X_HEADER"] || ""
+ end
+ end
+
+ class XHeader
+ def call(env)
+ [200, {"Content-Type" => "text/html"}, ["XHeader"]]
+ end
+ end
+
+ class AltApp
+ def call(env)
+ [200, {"Content-Type" => "text/html"}, ["Alternative App"]]
+ end
+ end
+
+ AltRoutes = ActionDispatch::Routing::RouteSet.new(AltRequest)
+ AltRoutes.draw do
+ get "/" => TestAltApp::XHeader.new, :constraints => {:x_header => /HEADER/}
+ get "/" => TestAltApp::AltApp.new
+ end
+
+ def app
+ AltRoutes
+ end
+
+ def test_alt_request_without_header
+ get "/"
+ assert_equal "Alternative App", @response.body
+ end
+
+ def test_alt_request_with_matched_header
+ get "/", {}, "HTTP_X_HEADER" => "HEADER"
+ assert_equal "XHeader", @response.body
+ end
+
+ def test_alt_request_with_unmatched_header
+ get "/", {}, "HTTP_X_HEADER" => "NON_MATCH"
+ assert_equal "Alternative App", @response.body
+ end
+end
+
class TestAppendingRoutes < ActionDispatch::IntegrationTest
def simple_app(resp)
lambda { |e| [ 200, { 'Content-Type' => 'text/plain' }, [resp] ] }
@@ -2356,21 +3010,52 @@ class TestNamespaceWithControllerOption < ActionDispatch::IntegrationTest
end
end
- DefaultScopeRoutes = ActionDispatch::Routing::RouteSet.new
- DefaultScopeRoutes.draw do
- namespace :admin do
- resources :storage_files, :controller => "StorageFiles"
- end
+ def draw(&block)
+ @app = ActionDispatch::Routing::RouteSet.new
+ @app.draw(&block)
end
- def app
- DefaultScopeRoutes
- end
+ def test_valid_controller_options_inside_namespace
+ draw do
+ namespace :admin do
+ resources :storage_files, :controller => "storage_files"
+ end
+ end
- def test_controller_options
get '/admin/storage_files'
assert_equal "admin/storage_files#index", @response.body
end
+
+ def test_resources_with_valid_namespaced_controller_option
+ draw do
+ resources :storage_files, :controller => 'admin/storage_files'
+ end
+
+ get 'storage_files'
+ assert_equal "admin/storage_files#index", @response.body
+ end
+
+ def test_warn_with_ruby_constant_syntax_controller_option
+ e = assert_raise(ArgumentError) do
+ draw do
+ namespace :admin do
+ resources :storage_files, :controller => "StorageFiles"
+ end
+ end
+ end
+
+ assert_match "'admin/StorageFiles' is not a supported controller name", e.message
+ end
+
+ def test_warn_with_ruby_constant_syntax_namespaced_controller_option
+ e = assert_raise(ArgumentError) do
+ draw do
+ resources :storage_files, :controller => 'Admin::StorageFiles'
+ end
+ end
+
+ assert_match "'Admin::StorageFiles' is not a supported controller name", e.message
+ end
end
class TestDefaultScope < ActionDispatch::IntegrationTest
@@ -2597,11 +3282,41 @@ class TestConstraintsAccessingParameters < ActionDispatch::IntegrationTest
end
end
+class TestGlobRoutingMapper < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+
+ get "/*id" => redirect("/not_cars"), :constraints => {id: /dummy/}
+ get "/cars" => ok
+ end
+ end
+
+ #include Routes.url_helpers
+ def app; Routes end
+
+ def test_glob_constraint
+ get "/dummy"
+ assert_equal "301", @response.code
+ assert_equal "/not_cars", @response.header['Location'].match('/[^/]+$')[0]
+ end
+
+ def test_glob_constraint_skip_route
+ get "/cars"
+ assert_equal "200", @response.code
+ end
+ def test_glob_constraint_skip_all
+ get "/missing"
+ assert_equal "404", @response.code
+ end
+end
+
class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
app.draw do
ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
get '/foo' => ok, as: :foo
+ get '/post(/:action(/:id))' => ok, as: :posts
end
end
@@ -2619,6 +3334,11 @@ class TestOptimizedNamedRoutes < ActionDispatch::IntegrationTest
test 'named route called on included module' do
assert_equal '/foo', foo_path
end
+
+ test 'nested optional segments are removed' do
+ assert_equal '/post', Routes.url_helpers.posts_path
+ assert_equal '/post', posts_path
+ end
end
class TestNamedRouteUrlHelpers < ActionDispatch::IntegrationTest
@@ -2670,6 +3390,10 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest
end
get '/' => ok, :as => :alternate_root, :constraints => { :port => 8080 }
+
+ get '/search' => ok, :constraints => { :subdomain => false }
+
+ get '/logs' => ok, :constraints => { :subdomain => true }
end
end
@@ -2696,6 +3420,24 @@ class TestUrlConstraints < ActionDispatch::IntegrationTest
get 'http://www.example.com:8080/'
assert_response :success
end
+
+ test "false constraint expressions check for absence of values" do
+ get 'http://example.com/search'
+ assert_response :success
+ assert_equal 'http://example.com/search', search_url
+
+ get 'http://api.example.com/search'
+ assert_response :not_found
+ end
+
+ test "true constraint expressions check for presence of values" do
+ get 'http://api.example.com/logs'
+ assert_response :success
+ assert_equal 'http://api.example.com/logs', logs_url
+
+ get 'http://example.com/logs'
+ assert_response :not_found
+ end
end
class TestInvalidUrls < ActionDispatch::IntegrationTest
@@ -2755,3 +3497,192 @@ class TestOptionalRootSegments < ActionDispatch::IntegrationTest
assert_equal '/page/1', root_path(:page => '1')
end
end
+
+class TestPortConstraints < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+
+ get '/integer', to: ok, constraints: { :port => 8080 }
+ get '/string', to: ok, constraints: { :port => '8080' }
+ get '/array', to: ok, constraints: { :port => [8080] }
+ get '/regexp', to: ok, constraints: { :port => /8080/ }
+ end
+ end
+
+ include Routes.url_helpers
+ def app; Routes end
+
+ def test_integer_port_constraints
+ get 'http://www.example.com/integer'
+ assert_response :not_found
+
+ get 'http://www.example.com:8080/integer'
+ assert_response :success
+ end
+
+ def test_string_port_constraints
+ get 'http://www.example.com/string'
+ assert_response :not_found
+
+ get 'http://www.example.com:8080/string'
+ assert_response :success
+ end
+
+ def test_array_port_constraints
+ get 'http://www.example.com/array'
+ assert_response :not_found
+
+ get 'http://www.example.com:8080/array'
+ assert_response :success
+ end
+
+ def test_regexp_port_constraints
+ get 'http://www.example.com/regexp'
+ assert_response :not_found
+
+ get 'http://www.example.com:8080/regexp'
+ assert_response :success
+ end
+end
+
+class TestFormatConstraints < ActionDispatch::IntegrationTest
+ Routes = ActionDispatch::Routing::RouteSet.new.tap do |app|
+ app.draw do
+ ok = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+
+ get '/string', to: ok, constraints: { format: 'json' }
+ get '/regexp', to: ok, constraints: { format: /json/ }
+ get '/json_only', to: ok, format: true, constraints: { format: /json/ }
+ get '/xml_only', to: ok, format: 'xml'
+ end
+ end
+
+ include Routes.url_helpers
+ def app; Routes end
+
+ def test_string_format_constraints
+ get 'http://www.example.com/string'
+ assert_response :success
+
+ get 'http://www.example.com/string.json'
+ assert_response :success
+
+ get 'http://www.example.com/string.html'
+ assert_response :not_found
+ end
+
+ def test_regexp_format_constraints
+ get 'http://www.example.com/regexp'
+ assert_response :success
+
+ get 'http://www.example.com/regexp.json'
+ assert_response :success
+
+ get 'http://www.example.com/regexp.html'
+ assert_response :not_found
+ end
+
+ def test_enforce_with_format_true_with_constraint
+ get 'http://www.example.com/json_only.json'
+ assert_response :success
+
+ get 'http://www.example.com/json_only.html'
+ assert_response :not_found
+
+ get 'http://www.example.com/json_only'
+ assert_response :not_found
+ end
+
+ def test_enforce_with_string
+ get 'http://www.example.com/xml_only.xml'
+ assert_response :success
+
+ get 'http://www.example.com/xml_only'
+ assert_response :success
+
+ get 'http://www.example.com/xml_only.json'
+ assert_response :not_found
+ end
+end
+
+class TestRouteDefaults < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ resources :posts, bucket_type: 'post'
+ resources :projects, defaults: { bucket_type: 'project' }
+ end
+ end
+
+ def app
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_route_options_are_required_for_url_for
+ assert_raises(ActionController::UrlGenerationError) do
+ assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, only_path: true)
+ end
+
+ assert_equal '/posts/1', url_for(controller: 'posts', action: 'show', id: 1, bucket_type: 'post', only_path: true)
+ end
+
+ def test_route_defaults_are_not_required_for_url_for
+ assert_equal '/projects/1', url_for(controller: 'projects', action: 'show', id: 1, only_path: true)
+ end
+end
+
+class TestRackAppRouteGeneration < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ rack_app = lambda { |env| [200, { 'Content-Type' => 'text/plain' }, []] }
+ mount rack_app, at: '/account', as: 'account'
+ mount rack_app, at: '/:locale/account', as: 'localized_account'
+ end
+ end
+
+ def app
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_mounted_application_doesnt_match_unnamed_route
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ end
+ end
+end
+
+class TestRedirectRouteGeneration < ActionDispatch::IntegrationTest
+ stub_controllers do |routes|
+ Routes = routes
+ Routes.draw do
+ get '/account', to: redirect('/myaccount'), as: 'account'
+ get '/:locale/account', to: redirect('/%{locale}/myaccount'), as: 'localized_account'
+ end
+ end
+
+ def app
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_redirect_doesnt_match_unnamed_route
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/account?controller=products', url_for(controller: 'products', action: 'index', only_path: true)
+ end
+
+ assert_raise(ActionController::UrlGenerationError) do
+ assert_equal '/de/account?controller=products', url_for(controller: 'products', action: 'index', :locale => 'de', only_path: true)
+ end
+ end
+end
diff --git a/actionpack/test/dispatch/session/abstract_store_test.rb b/actionpack/test/dispatch/session/abstract_store_test.rb
index 8daf3d3f5e..fe1a7b4f86 100644
--- a/actionpack/test/dispatch/session/abstract_store_test.rb
+++ b/actionpack/test/dispatch/session/abstract_store_test.rb
@@ -42,7 +42,7 @@ module ActionDispatch
as.call(@env)
session1 = Request::Session.find @env
- refute_equal session, session1
+ assert_not_equal session, session1
assert_equal session.to_hash, session1.to_hash
end
diff --git a/actionpack/test/dispatch/session/cookie_store_test.rb b/actionpack/test/dispatch/session/cookie_store_test.rb
index 41fa036a92..e99ff46edf 100644
--- a/actionpack/test/dispatch/session/cookie_store_test.rb
+++ b/actionpack/test/dispatch/session/cookie_store_test.rb
@@ -1,9 +1,11 @@
require 'abstract_unit'
require 'stringio'
+require 'active_support/key_generator'
class CookieStoreTest < ActionDispatch::IntegrationTest
SessionKey = '_myapp_session'
SessionSecret = 'b3c631c314c0bbca50c1b2843150fe33'
+ Generator = ActiveSupport::LegacyKeyGenerator.new(SessionSecret)
Verifier = ActiveSupport::MessageVerifier.new(SessionSecret, :digest => 'SHA1')
SignedBar = Verifier.generate(:foo => "bar", :session_id => SecureRandom.hex(16))
@@ -273,7 +275,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
# First request accesses the session
time = Time.local(2008, 4, 24)
Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
cookies[SessionKey] = SignedBar
@@ -287,7 +289,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
# Second request does not access the session
time = Time.local(2008, 4, 25)
Time.stubs(:now).returns(time)
- expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d-%b-%Y %H:%M:%S GMT")
+ expected_expiry = (time + 5.hours).gmtime.strftime("%a, %d %b %Y %H:%M:%S -0000")
get '/no_session_access'
assert_response :success
@@ -330,7 +332,7 @@ class CookieStoreTest < ActionDispatch::IntegrationTest
# Overwrite get to send SessionSecret in env hash
def get(path, parameters = nil, env = {})
- env["action_dispatch.secret_token"] ||= SessionSecret
+ env["action_dispatch.key_generator"] ||= Generator
super
end
diff --git a/actionpack/test/dispatch/session/test_session_test.rb b/actionpack/test/dispatch/session/test_session_test.rb
index 904398f563..d30461a623 100644
--- a/actionpack/test/dispatch/session/test_session_test.rb
+++ b/actionpack/test/dispatch/session/test_session_test.rb
@@ -2,8 +2,8 @@ require 'abstract_unit'
require 'stringio'
class ActionController::TestSessionTest < ActiveSupport::TestCase
- def test_ctor_allows_setting
- session = ActionController::TestSession.new({:one => 'one', :two => 'two'})
+ def test_initialize_with_values
+ session = ActionController::TestSession.new(one: 'one', two: 'two')
assert_equal('one', session[:one])
assert_equal('two', session[:two])
end
@@ -23,15 +23,21 @@ class ActionController::TestSessionTest < ActiveSupport::TestCase
end
def test_calling_update_with_params_passes_to_attributes
- session = ActionController::TestSession.new()
+ session = ActionController::TestSession.new
session.update('key' => 'value')
assert_equal('value', session[:key])
end
- def test_clear_emptys_session
- session = ActionController::TestSession.new({:one => 'one', :two => 'two'})
+ def test_clear_empties_session
+ session = ActionController::TestSession.new(one: 'one', two: 'two')
session.clear
assert_nil(session[:one])
assert_nil(session[:two])
end
+
+ def test_keys_and_values
+ session = ActionController::TestSession.new(one: '1', two: '2')
+ assert_equal %w(one two), session.keys
+ assert_equal %w(1 2), session.values
+ end
end
diff --git a/actionpack/test/dispatch/show_exceptions_test.rb b/actionpack/test/dispatch/show_exceptions_test.rb
index 45f8fc11b3..38bd234f37 100644
--- a/actionpack/test/dispatch/show_exceptions_test.rb
+++ b/actionpack/test/dispatch/show_exceptions_test.rb
@@ -8,8 +8,12 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
case req.path
when "/not_found"
raise AbstractController::ActionNotFound
+ when "/bad_params"
+ raise ActionDispatch::ParamsParser::ParseError.new("", StandardError.new)
when "/method_not_allowed"
raise ActionController::MethodNotAllowed
+ when "/unknown_http_method"
+ raise ActionController::UnknownHttpMethod
when "/not_found_original_exception"
raise ActionView::Template::Error.new('template', AbstractController::ActionNotFound.new)
else
@@ -33,6 +37,10 @@ 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
get "/not_found", {}, {'action_dispatch.show_exceptions' => true}
assert_response 404
@@ -41,6 +49,10 @@ class ShowExceptionsTest < ActionDispatch::IntegrationTest
get "/method_not_allowed", {}, {'action_dispatch.show_exceptions' => true}
assert_response 405
assert_equal "", body
+
+ get "/unknown_http_method", {}, {'action_dispatch.show_exceptions' => true}
+ assert_response 405
+ assert_equal "", body
end
test "localize rescue error page" do
diff --git a/actionpack/test/dispatch/spec_type_test.rb b/actionpack/test/dispatch/spec_type_test.rb
deleted file mode 100644
index 6cd19fd333..0000000000
--- a/actionpack/test/dispatch/spec_type_test.rb
+++ /dev/null
@@ -1,41 +0,0 @@
-require "abstract_unit"
-
-class SpecTypeTest < ActiveSupport::TestCase
- def assert_dispatch actual
- assert_equal ActionDispatch::IntegrationTest, actual
- end
-
- def refute_dispatch actual
- refute_equal ActionDispatch::IntegrationTest, actual
- end
-
- def test_spec_type_resolves_for_matching_acceptance_strings
- assert_dispatch MiniTest::Spec.spec_type("WidgetAcceptanceTest")
- assert_dispatch MiniTest::Spec.spec_type("Widget Acceptance Test")
- assert_dispatch MiniTest::Spec.spec_type("widgetacceptancetest")
- assert_dispatch MiniTest::Spec.spec_type("widget acceptance test")
- end
-
- def test_spec_type_wont_match_non_space_characters_acceptance
- refute_dispatch MiniTest::Spec.spec_type("Widget Acceptance\tTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Acceptance\rTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Acceptance\nTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Acceptance\fTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget AcceptanceXTest")
- end
-
- def test_spec_type_resolves_for_matching_integration_strings
- assert_dispatch MiniTest::Spec.spec_type("WidgetIntegrationTest")
- assert_dispatch MiniTest::Spec.spec_type("Widget Integration Test")
- assert_dispatch MiniTest::Spec.spec_type("widgetintegrationtest")
- assert_dispatch MiniTest::Spec.spec_type("widget integration test")
- end
-
- def test_spec_type_wont_match_non_space_characters_integration
- refute_dispatch MiniTest::Spec.spec_type("Widget Integration\tTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Integration\rTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Integration\nTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget Integration\fTest")
- refute_dispatch MiniTest::Spec.spec_type("Widget IntegrationXTest")
- end
-end
diff --git a/actionpack/test/dispatch/ssl_test.rb b/actionpack/test/dispatch/ssl_test.rb
index 6f075a9074..94969f795a 100644
--- a/actionpack/test/dispatch/ssl_test.rb
+++ b/actionpack/test/dispatch/ssl_test.rb
@@ -37,6 +37,11 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Strict-Transport-Security']
end
+ def test_no_hsts_with_insecure_connection
+ get "http://example.org/"
+ assert_not response.headers['Strict-Transport-Security']
+ end
+
def test_hsts_header
self.app = ActionDispatch::SSL.new(default_app, :hsts => true)
get "https://example.org/"
@@ -47,7 +52,7 @@ class SSLTest < ActionDispatch::IntegrationTest
def test_disable_hsts_header
self.app = ActionDispatch::SSL.new(default_app, :hsts => false)
get "https://example.org/"
- refute response.headers['Strict-Transport-Security']
+ assert_not response.headers['Strict-Transport-Security']
end
def test_hsts_expires
@@ -57,6 +62,13 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Strict-Transport-Security']
end
+ def test_hsts_expires_with_duration
+ self.app = ActionDispatch::SSL.new(default_app, :hsts => { :expires => 1.year })
+ get "https://example.org/"
+ assert_equal "max-age=31557600",
+ response.headers['Strict-Transport-Security']
+ end
+
def test_hsts_include_subdomains
self.app = ActionDispatch::SSL.new(default_app, :hsts => { :subdomains => true })
get "https://example.org/"
@@ -112,6 +124,49 @@ class SSLTest < ActionDispatch::IntegrationTest
response.headers['Set-Cookie'].split("\n")
end
+
+ def test_flag_cookies_as_secure_with_has_not_spaces_before
+ self.app = ActionDispatch::SSL.new(lambda { |env|
+ headers = {
+ 'Content-Type' => "text/html",
+ 'Set-Cookie' => "problem=def; path=/;secure; HttpOnly"
+ }
+ [200, headers, ["OK"]]
+ })
+
+ get "https://example.org/"
+ assert_equal ["problem=def; path=/;secure; HttpOnly"],
+ response.headers['Set-Cookie'].split("\n")
+ end
+
+ def test_flag_cookies_as_secure_with_has_not_spaces_after
+ self.app = ActionDispatch::SSL.new(lambda { |env|
+ headers = {
+ 'Content-Type' => "text/html",
+ 'Set-Cookie' => "problem=def; path=/; secure;HttpOnly"
+ }
+ [200, headers, ["OK"]]
+ })
+
+ get "https://example.org/"
+ assert_equal ["problem=def; path=/; secure;HttpOnly"],
+ response.headers['Set-Cookie'].split("\n")
+ end
+
+ def test_flag_cookies_as_secure_with_ignore_case
+ self.app = ActionDispatch::SSL.new(lambda { |env|
+ headers = {
+ 'Content-Type' => "text/html",
+ 'Set-Cookie' => "problem=def; path=/; Secure; HttpOnly"
+ }
+ [200, headers, ["OK"]]
+ })
+
+ get "https://example.org/"
+ assert_equal ["problem=def; path=/; Secure; HttpOnly"],
+ response.headers['Set-Cookie'].split("\n")
+ end
+
def test_no_cookies
self.app = ActionDispatch::SSL.new(lambda { |env|
[200, {'Content-Type' => "text/html"}, ["OK"]]
diff --git a/actionpack/test/dispatch/test_request_test.rb b/actionpack/test/dispatch/test_request_test.rb
index 6047631ba3..65ad8677f3 100644
--- a/actionpack/test/dispatch/test_request_test.rb
+++ b/actionpack/test/dispatch/test_request_test.rb
@@ -18,7 +18,7 @@ class TestRequestTest < ActiveSupport::TestCase
assert_equal "0.0.0.0", env.delete("REMOTE_ADDR")
assert_equal "Rails Testing", env.delete("HTTP_USER_AGENT")
- assert_equal [1, 1], env.delete("rack.version")
+ assert_equal [1, 2], env.delete("rack.version")
assert_equal "", env.delete("rack.input").string
assert_kind_of StringIO, env.delete("rack.errors")
assert_equal true, env.delete("rack.multithread")
@@ -40,10 +40,10 @@ class TestRequestTest < ActiveSupport::TestCase
req.cookie_jar["login"] = "XJ-122"
assert_cookies({"user_name" => "david", "login" => "XJ-122"}, req.cookie_jar)
- assert_nothing_raised do
+ assert_nothing_raised do
req.cookie_jar["login"] = nil
assert_cookies({"user_name" => "david", "login" => nil}, req.cookie_jar)
- end
+ end
req.cookie_jar.delete(:login)
assert_cookies({"user_name" => "david"}, req.cookie_jar)
@@ -62,6 +62,36 @@ class TestRequestTest < ActiveSupport::TestCase
assert_equal false, req.env.empty?
end
+ test "default remote address is 0.0.0.0" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal '0.0.0.0', req.remote_addr
+ end
+
+ test "allows remote address to be overridden" do
+ req = ActionDispatch::TestRequest.new('REMOTE_ADDR' => '127.0.0.1')
+ assert_equal '127.0.0.1', req.remote_addr
+ end
+
+ test "default host is test.host" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal 'test.host', req.host
+ end
+
+ test "allows host to be overridden" do
+ req = ActionDispatch::TestRequest.new('HTTP_HOST' => 'www.example.com')
+ assert_equal 'www.example.com', req.host
+ end
+
+ test "default user agent is 'Rails Testing'" do
+ req = ActionDispatch::TestRequest.new
+ assert_equal 'Rails Testing', req.user_agent
+ end
+
+ test "allows user agent to be overridden" do
+ req = ActionDispatch::TestRequest.new('HTTP_USER_AGENT' => 'GoogleBot')
+ assert_equal 'GoogleBot', req.user_agent
+ end
+
private
def assert_cookies(expected, cookie_jar)
assert_equal(expected, cookie_jar.instance_variable_get("@cookies"))
diff --git a/actionpack/test/dispatch/url_generation_test.rb b/actionpack/test/dispatch/url_generation_test.rb
index e56e8ddc57..f919592d24 100644
--- a/actionpack/test/dispatch/url_generation_test.rb
+++ b/actionpack/test/dispatch/url_generation_test.rb
@@ -48,6 +48,55 @@ module TestUrlGeneration
https!
assert_equal "http://www.example.com/foo", foo_url(:protocol => "http")
end
+
+ test "extracting protocol from host when protocol not present" do
+ assert_equal "httpz://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: nil)
+ end
+
+ test "formatting host when protocol is present" do
+ assert_equal "http://www.example.com/foo", foo_url(host: "httpz://www.example.com", protocol: "http://")
+ end
+
+ test "default ports are removed from the host" do
+ assert_equal "http://www.example.com/foo", foo_url(host: "www.example.com:80", protocol: "http://")
+ assert_equal "https://www.example.com/foo", foo_url(host: "www.example.com:443", protocol: "https://")
+ end
+
+ 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://")
+ end
+
+ test "port option overides the host" do
+ assert_equal "http://www.example.com:8080/foo", foo_url(host: "www.example.com:8443", protocol: "http://", port: 8080)
+ 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)
+ 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)
+ end
+
+ test "keep subdomain when key is true" do
+ assert_equal "http://www.example.com/foo", foo_url(subdomain: true)
+ end
+
+ test "keep subdomain when key is missing" do
+ assert_equal "http://www.example.com/foo", foo_url
+ end
+
+ test "omit subdomain when key is nil" do
+ assert_equal "http://example.com/foo", foo_url(subdomain: nil)
+ end
+
+ test "omit subdomain when key is false" do
+ assert_equal "http://example.com/foo", foo_url(subdomain: false)
+ end
+
+ test "omit subdomain when key is blank" do
+ assert_equal "http://example.com/foo", foo_url(subdomain: "")
+ end
end
end
diff --git a/actionpack/test/fixtures/developer.rb b/actionpack/test/fixtures/developer.rb
deleted file mode 100644
index dd14548fac..0000000000
--- a/actionpack/test/fixtures/developer.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-class Developer < ActiveRecord::Base
- has_and_belongs_to_many :projects
- has_many :replies
- has_many :topics, :through => :replies
-end
-
-class DeVeLoPeR < ActiveRecord::Base
- self.table_name = "developers"
-end
diff --git a/actionpack/test/fixtures/digestor/messages/show.html.erb b/actionpack/test/fixtures/digestor/messages/show.html.erb
deleted file mode 100644
index 9f73345a9f..0000000000
--- a/actionpack/test/fixtures/digestor/messages/show.html.erb
+++ /dev/null
@@ -1,9 +0,0 @@
-<%# Template Dependency: messages/message %>
-<%= render "header" %>
-<%= render "comments/comments" %>
-
-<%= render "messages/actions/move" %>
-
-<%= render @message.history.events %>
-
-<%# render "something_missing" %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
new file mode 100644
index 0000000000..3125583a28
--- /dev/null
+++ b/actionpack/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
@@ -0,0 +1,3 @@
+<body>
+<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %>
+</body>
diff --git a/actionpack/test/fixtures/helpers/abc_helper.rb b/actionpack/test/fixtures/helpers/abc_helper.rb
index 7104ff3730..cf2774bb5f 100644
--- a/actionpack/test/fixtures/helpers/abc_helper.rb
+++ b/actionpack/test/fixtures/helpers/abc_helper.rb
@@ -1,5 +1,3 @@
module AbcHelper
def bare_a() end
- def bare_b() end
- def bare_c() end
end
diff --git a/actionpack/test/fixtures/layout_tests/layouts/symlinked b/actionpack/test/fixtures/layout_tests/layouts/symlinked
deleted file mode 120000
index 0ddc1154ab..0000000000
--- a/actionpack/test/fixtures/layout_tests/layouts/symlinked
+++ /dev/null
@@ -1 +0,0 @@
-../../symlink_parent \ No newline at end of file
diff --git a/actionpack/test/fixtures/multipart/bracketed_utf8_param b/actionpack/test/fixtures/multipart/bracketed_utf8_param
new file mode 100644
index 0000000000..976ca44a45
--- /dev/null
+++ b/actionpack/test/fixtures/multipart/bracketed_utf8_param
@@ -0,0 +1,5 @@
+--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/multipart/single_utf8_param b/actionpack/test/fixtures/multipart/single_utf8_param
new file mode 100644
index 0000000000..b86f62d1e1
--- /dev/null
+++ b/actionpack/test/fixtures/multipart/single_utf8_param
@@ -0,0 +1,5 @@
+--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/public/400.html b/actionpack/test/fixtures/public/400.html
new file mode 100644
index 0000000000..03be6bedaf
--- /dev/null
+++ b/actionpack/test/fixtures/public/400.html
@@ -0,0 +1 @@
+400 error fixture
diff --git a/actionpack/test/fixtures/public/images/rails.png b/actionpack/test/fixtures/public/images/rails.png
deleted file mode 100644
index b8441f182e..0000000000
--- a/actionpack/test/fixtures/public/images/rails.png
+++ /dev/null
Binary files differ
diff --git a/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb
new file mode 100644
index 0000000000..9f1f855269
--- /dev/null
+++ b/actionpack/test/fixtures/respond_to/using_defaults_with_all.html.erb
@@ -0,0 +1 @@
+HTML!
diff --git a/actionpack/test/fixtures/digestor/events/_event.html.erb b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/events/_event.html.erb
+++ b/actionpack/test/fixtures/respond_with/respond_with_additional_params.html.erb
diff --git a/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb
new file mode 100644
index 0000000000..1cc8d41475
--- /dev/null
+++ b/actionpack/test/fixtures/test/_directory/_partial_with_locales.html.erb
@@ -0,0 +1 @@
+Hello <%= name %>
diff --git a/actionpack/test/fixtures/test/_partial_name_local_variable.erb b/actionpack/test/fixtures/test/_partial_name_local_variable.erb
new file mode 100644
index 0000000000..cc3a91c89f
--- /dev/null
+++ b/actionpack/test/fixtures/test/_partial_name_local_variable.erb
@@ -0,0 +1 @@
+<%= partial_name_local_variable %>
diff --git a/actionpack/test/fixtures/test/change_priorty.html.erb b/actionpack/test/fixtures/test/change_priority.html.erb
index 5618977d05..5618977d05 100644
--- a/actionpack/test/fixtures/test/change_priorty.html.erb
+++ b/actionpack/test/fixtures/test/change_priority.html.erb
diff --git a/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb
new file mode 100644
index 0000000000..1461b95186
--- /dev/null
+++ b/actionpack/test/fixtures/test/render_partial_inside_directory.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %>
diff --git a/actionpack/test/journey/gtg/builder_test.rb b/actionpack/test/journey/gtg/builder_test.rb
new file mode 100644
index 0000000000..c1da374007
--- /dev/null
+++ b/actionpack/test/journey/gtg/builder_test.rb
@@ -0,0 +1,79 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module GTG
+ class TestBuilder < ActiveSupport::TestCase
+ def test_following_states_multi
+ table = tt ['a|a']
+ assert_equal 1, table.move([0], 'a').length
+ end
+
+ def test_following_states_multi_regexp
+ table = tt [':a|b']
+ assert_equal 1, table.move([0], 'fooo').length
+ assert_equal 2, table.move([0], 'b').length
+ end
+
+ def test_multi_path
+ table = tt ['/:a/d', '/b/c']
+
+ [
+ [1, '/'],
+ [2, 'b'],
+ [2, '/'],
+ [1, 'c'],
+ ].inject([0]) { |state, (exp, sym)|
+ new = table.move(state, sym)
+ assert_equal exp, new.length
+ new
+ }
+ end
+
+ def test_match_data_ambiguous
+ table = tt %w{
+ /articles(.:format)
+ /articles/new(.:format)
+ /articles/:id/edit(.:format)
+ /articles/:id(.:format)
+ }
+
+ sim = NFA::Simulator.new table
+
+ match = sim.match '/articles/new'
+ assert_equal 2, match.memos.length
+ end
+
+ ##
+ # Identical Routes may have different restrictions.
+ def test_match_same_paths
+ table = tt %w{
+ /articles/new(.:format)
+ /articles/new(.:format)
+ }
+
+ sim = NFA::Simulator.new table
+
+ match = sim.match '/articles/new'
+ assert_equal 2, match.memos.length
+ end
+
+ private
+ def ast strings
+ parser = Journey::Parser.new
+ asts = strings.map { |string|
+ memo = Object.new
+ ast = parser.parse string
+ ast.each { |n| n.memo = memo }
+ ast
+ }
+ Nodes::Or.new asts
+ end
+
+ def tt strings
+ Builder.new(ast(strings)).transition_table
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/gtg/transition_table_test.rb b/actionpack/test/journey/gtg/transition_table_test.rb
new file mode 100644
index 0000000000..33acba8b65
--- /dev/null
+++ b/actionpack/test/journey/gtg/transition_table_test.rb
@@ -0,0 +1,115 @@
+require 'abstract_unit'
+require 'json'
+
+module ActionDispatch
+ module Journey
+ module GTG
+ class TestGeneralizedTable < ActiveSupport::TestCase
+ def test_to_json
+ table = tt %w{
+ /articles(.:format)
+ /articles/new(.:format)
+ /articles/:id/edit(.:format)
+ /articles/:id(.:format)
+ }
+
+ json = JSON.load table.to_json
+ assert json['regexp_states']
+ assert json['string_states']
+ assert json['accepting']
+ end
+
+ if system("dot -V 2>/dev/null")
+ def test_to_svg
+ table = tt %w{
+ /articles(.:format)
+ /articles/new(.:format)
+ /articles/:id/edit(.:format)
+ /articles/:id(.:format)
+ }
+ svg = table.to_svg
+ assert svg
+ assert_no_match(/DOCTYPE/, svg)
+ end
+ end
+
+ def test_simulate_gt
+ sim = simulator_for ['/foo', '/bar']
+ assert_match sim, '/foo'
+ end
+
+ def test_simulate_gt_regexp
+ sim = simulator_for [':foo']
+ assert_match sim, 'foo'
+ end
+
+ def test_simulate_gt_regexp_mix
+ sim = simulator_for ['/get', '/:method/foo']
+ assert_match sim, '/get'
+ assert_match sim, '/get/foo'
+ end
+
+ def test_simulate_optional
+ sim = simulator_for ['/foo(/bar)']
+ assert_match sim, '/foo'
+ assert_match sim, '/foo/bar'
+ assert_no_match sim, '/foo/'
+ end
+
+ def test_match_data
+ path_asts = asts %w{ /get /:method/foo }
+ paths = path_asts.dup
+
+ builder = GTG::Builder.new Nodes::Or.new path_asts
+ tt = builder.transition_table
+
+ sim = GTG::Simulator.new tt
+
+ match = sim.match '/get'
+ assert_equal [paths.first], match.memos
+
+ match = sim.match '/get/foo'
+ assert_equal [paths.last], match.memos
+ end
+
+ def test_match_data_ambiguous
+ path_asts = asts %w{
+ /articles(.:format)
+ /articles/new(.:format)
+ /articles/:id/edit(.:format)
+ /articles/:id(.:format)
+ }
+
+ paths = path_asts.dup
+ ast = Nodes::Or.new path_asts
+
+ builder = GTG::Builder.new ast
+ sim = GTG::Simulator.new builder.transition_table
+
+ match = sim.match '/articles/new'
+ assert_equal [paths[1], paths[3]], match.memos
+ end
+
+ private
+ def asts paths
+ parser = Journey::Parser.new
+ paths.map { |x|
+ ast = parser.parse x
+ ast.each { |n| n.memo = ast}
+ ast
+ }
+ end
+
+ def tt paths
+ x = asts paths
+ builder = GTG::Builder.new Nodes::Or.new x
+ builder.transition_table
+ end
+
+ def simulator_for paths
+ GTG::Simulator.new tt(paths)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/nfa/simulator_test.rb b/actionpack/test/journey/nfa/simulator_test.rb
new file mode 100644
index 0000000000..673a491fe5
--- /dev/null
+++ b/actionpack/test/journey/nfa/simulator_test.rb
@@ -0,0 +1,98 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module NFA
+ class TestSimulator < ActiveSupport::TestCase
+ def test_simulate_simple
+ sim = simulator_for ['/foo']
+ assert_match sim, '/foo'
+ end
+
+ def test_simulate_simple_no_match
+ sim = simulator_for ['/foo']
+ assert_no_match sim, 'foo'
+ end
+
+ def test_simulate_simple_no_match_too_long
+ sim = simulator_for ['/foo']
+ assert_no_match sim, '/foo/bar'
+ end
+
+ def test_simulate_simple_no_match_wrong_string
+ sim = simulator_for ['/foo']
+ assert_no_match sim, '/bar'
+ end
+
+ def test_simulate_regex
+ sim = simulator_for ['/:foo/bar']
+ assert_match sim, '/bar/bar'
+ assert_match sim, '/foo/bar'
+ end
+
+ def test_simulate_or
+ sim = simulator_for ['/foo', '/bar']
+ assert_match sim, '/bar'
+ assert_match sim, '/foo'
+ assert_no_match sim, '/baz'
+ end
+
+ def test_simulate_optional
+ sim = simulator_for ['/foo(/bar)']
+ assert_match sim, '/foo'
+ assert_match sim, '/foo/bar'
+ assert_no_match sim, '/foo/'
+ end
+
+ def test_matchdata_has_memos
+ paths = %w{ /foo /bar }
+ parser = Journey::Parser.new
+ asts = paths.map { |x|
+ ast = parser.parse x
+ ast.each { |n| n.memo = ast}
+ ast
+ }
+
+ expected = asts.first
+
+ builder = Builder.new Nodes::Or.new asts
+
+ sim = Simulator.new builder.transition_table
+
+ md = sim.match '/foo'
+ assert_equal [expected], md.memos
+ end
+
+ def test_matchdata_memos_on_merge
+ parser = Journey::Parser.new
+ routes = [
+ '/articles(.:format)',
+ '/articles/new(.:format)',
+ '/articles/:id/edit(.:format)',
+ '/articles/:id(.:format)',
+ ].map { |path|
+ ast = parser.parse path
+ ast.each { |n| n.memo = ast }
+ ast
+ }
+
+ asts = routes.dup
+
+ ast = Nodes::Or.new routes
+
+ nfa = Journey::NFA::Builder.new ast
+ sim = Simulator.new nfa.transition_table
+ md = sim.match '/articles'
+ assert_equal [asts.first], md.memos
+ end
+
+ def simulator_for paths
+ parser = Journey::Parser.new
+ asts = paths.map { |x| parser.parse x }
+ builder = Builder.new Nodes::Or.new asts
+ Simulator.new builder.transition_table
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/nfa/transition_table_test.rb b/actionpack/test/journey/nfa/transition_table_test.rb
new file mode 100644
index 0000000000..1248082c03
--- /dev/null
+++ b/actionpack/test/journey/nfa/transition_table_test.rb
@@ -0,0 +1,72 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module NFA
+ class TestTransitionTable < ActiveSupport::TestCase
+ def setup
+ @parser = Journey::Parser.new
+ end
+
+ def test_eclosure
+ table = tt '/'
+ assert_equal [0], table.eclosure(0)
+
+ table = tt ':a|:b'
+ assert_equal 3, table.eclosure(0).length
+
+ table = tt '(:a|:b)'
+ assert_equal 5, table.eclosure(0).length
+ assert_equal 5, table.eclosure([0]).length
+ end
+
+ def test_following_states_one
+ table = tt '/'
+
+ assert_equal [1], table.following_states(0, '/')
+ assert_equal [1], table.following_states([0], '/')
+ end
+
+ def test_following_states_group
+ table = tt 'a|b'
+ states = table.eclosure 0
+
+ assert_equal 1, table.following_states(states, 'a').length
+ assert_equal 1, table.following_states(states, 'b').length
+ end
+
+ def test_following_states_multi
+ table = tt 'a|a'
+ states = table.eclosure 0
+
+ assert_equal 2, table.following_states(states, 'a').length
+ assert_equal 0, table.following_states(states, 'b').length
+ end
+
+ def test_following_states_regexp
+ table = tt 'a|:a'
+ states = table.eclosure 0
+
+ assert_equal 1, table.following_states(states, 'a').length
+ assert_equal 1, table.following_states(states, /[^\.\/\?]+/).length
+ assert_equal 0, table.following_states(states, 'b').length
+ end
+
+ def test_alphabet
+ table = tt 'a|:a'
+ assert_equal [/[^\.\/\?]+/, 'a'], table.alphabet
+
+ table = tt 'a|a'
+ assert_equal ['a'], table.alphabet
+ end
+
+ private
+ def tt string
+ ast = @parser.parse string
+ builder = Builder.new ast
+ builder.transition_table
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/nodes/symbol_test.rb b/actionpack/test/journey/nodes/symbol_test.rb
new file mode 100644
index 0000000000..d411a5018a
--- /dev/null
+++ b/actionpack/test/journey/nodes/symbol_test.rb
@@ -0,0 +1,17 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module Nodes
+ class TestSymbol < ActiveSupport::TestCase
+ def test_default_regexp?
+ sym = Symbol.new nil
+ assert sym.default_regexp?
+
+ sym.regexp = nil
+ assert_not sym.default_regexp?
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/path/pattern_test.rb b/actionpack/test/journey/path/pattern_test.rb
new file mode 100644
index 0000000000..ce02104181
--- /dev/null
+++ b/actionpack/test/journey/path/pattern_test.rb
@@ -0,0 +1,288 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module Path
+ class TestPattern < ActiveSupport::TestCase
+ x = /.+/
+ {
+ '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?\Z},
+ '/:controller/foo' => %r{\A/(#{x})/foo\Z},
+ '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)\Z},
+ '/:controller' => %r{\A/(#{x})\Z},
+ '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?\Z},
+ '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml\Z},
+ '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)\Z},
+ '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?\Z},
+ '/:controller/*foo' => %r{\A/(#{x})/(.+)\Z},
+ '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar\Z},
+ }.each do |path, expected|
+ define_method(:"test_to_regexp_#{path}") do
+ strexp = Router::Strexp.new(
+ path,
+ { :controller => /.+/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_equal(expected, path.to_regexp)
+ end
+ end
+
+ {
+ '/:controller(/:action)' => %r{\A/(#{x})(?:/([^/.?]+))?},
+ '/:controller/foo' => %r{\A/(#{x})/foo},
+ '/:controller/:action' => %r{\A/(#{x})/([^/.?]+)},
+ '/:controller' => %r{\A/(#{x})},
+ '/:controller(/:action(/:id))' => %r{\A/(#{x})(?:/([^/.?]+)(?:/([^/.?]+))?)?},
+ '/:controller/:action.xml' => %r{\A/(#{x})/([^/.?]+)\.xml},
+ '/:controller.:format' => %r{\A/(#{x})\.([^/.?]+)},
+ '/:controller(.:format)' => %r{\A/(#{x})(?:\.([^/.?]+))?},
+ '/:controller/*foo' => %r{\A/(#{x})/(.+)},
+ '/:controller/*foo/bar' => %r{\A/(#{x})/(.+)/bar},
+ }.each do |path, expected|
+ define_method(:"test_to_non_anchored_regexp_#{path}") do
+ strexp = Router::Strexp.new(
+ path,
+ { :controller => /.+/ },
+ ["/", ".", "?"],
+ false
+ )
+ path = Pattern.new strexp
+ assert_equal(expected, path.to_regexp)
+ end
+ end
+
+ {
+ '/:controller(/:action)' => %w{ controller action },
+ '/:controller/foo' => %w{ controller },
+ '/:controller/:action' => %w{ controller action },
+ '/:controller' => %w{ controller },
+ '/:controller(/:action(/:id))' => %w{ controller action id },
+ '/:controller/:action.xml' => %w{ controller action },
+ '/:controller.:format' => %w{ controller format },
+ '/:controller(.:format)' => %w{ controller format },
+ '/:controller/*foo' => %w{ controller foo },
+ '/:controller/*foo/bar' => %w{ controller foo },
+ }.each do |path, expected|
+ define_method(:"test_names_#{path}") do
+ strexp = Router::Strexp.new(
+ path,
+ { :controller => /.+/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_equal(expected, path.names)
+ 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(
+ '/page/:name',
+ { :name => /
+ #ROFL
+ (tender|love
+ #MAO
+ )/x },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_match(path, '/page/tender')
+ assert_match(path, '/page/love')
+ assert_no_match(path, '/page/loving')
+ end
+
+ def test_optional_names
+ [
+ ['/:foo(/:bar(/:baz))', %w{ bar baz }],
+ ['/:foo(/:bar)', %w{ bar }],
+ ['/:foo(/:bar)/:lol(/:baz)', %w{ bar baz }],
+ ].each do |pattern, list|
+ path = Pattern.new pattern
+ assert_equal list.sort, path.optional_names.sort
+ end
+ end
+
+ def test_to_regexp_match_non_optional
+ strexp = Router::Strexp.new(
+ '/:name',
+ { :name => /\d+/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_match(path, '/123')
+ assert_no_match(path, '/')
+ end
+
+ def test_to_regexp_with_group
+ strexp = Router::Strexp.new(
+ '/page/:name',
+ { :name => /(tender|love)/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_match(path, '/page/tender')
+ assert_match(path, '/page/love')
+ assert_no_match(path, '/page/loving')
+ end
+
+ def test_ast_sets_regular_expressions
+ requirements = { :name => /(tender|love)/, :value => /./ }
+ strexp = Router::Strexp.new(
+ '/page/:name/:value',
+ requirements,
+ ["/", ".", "?"]
+ )
+
+ assert_equal requirements, strexp.requirements
+
+ path = Pattern.new strexp
+ nodes = path.ast.grep(Nodes::Symbol)
+ assert_equal 2, nodes.length
+ nodes.each do |node|
+ assert_equal requirements[node.to_sym], node.regexp
+ end
+ end
+
+ def test_match_data_with_group
+ strexp = Router::Strexp.new(
+ '/page/:name',
+ { :name => /(tender|love)/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ match = path.match '/page/tender'
+ assert_equal 'tender', match[1]
+ assert_equal 2, match.length
+ end
+
+ def test_match_data_with_multi_group
+ strexp = Router::Strexp.new(
+ '/page/:name/:id',
+ { :name => /t(((ender|love)))()/ },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ match = path.match '/page/tender/10'
+ assert_equal 'tender', match[1]
+ assert_equal '10', match[2]
+ assert_equal 3, match.length
+ assert_equal %w{ tender 10 }, match.captures
+ end
+
+ def test_star_with_custom_re
+ z = /\d+/
+ strexp = Router::Strexp.new(
+ '/page/*foo',
+ { :foo => z },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_equal(%r{\A/page/(#{z})\Z}, path.to_regexp)
+ end
+
+ def test_insensitive_regexp_with_group
+ strexp = Router::Strexp.new(
+ '/page/:name/aaron',
+ { :name => /(tender|love)/i },
+ ["/", ".", "?"]
+ )
+ path = Pattern.new strexp
+ assert_match(path, '/page/TENDER/aaron')
+ assert_match(path, '/page/loVE/aaron')
+ assert_no_match(path, '/page/loVE/AAron')
+ end
+
+ def test_to_regexp_with_strexp
+ strexp = Router::Strexp.new('/:controller', { }, ["/", ".", "?"])
+ path = Pattern.new strexp
+ x = %r{\A/([^/.?]+)\Z}
+
+ assert_equal(x.source, path.source)
+ end
+
+ def test_to_regexp_defaults
+ path = Pattern.new '/: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)))'
+ uri = 'content'
+
+ assert_not path =~ uri
+ end
+
+ def test_match_controller
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ uri = '/content'
+
+ match = path =~ uri
+ assert_equal %w{ controller action id format }, match.names
+ assert_equal 'content', match[1]
+ assert_nil match[2]
+ assert_nil match[3]
+ assert_nil match[4]
+ end
+
+ def test_match_controller_action
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ uri = '/content/list'
+
+ match = path =~ uri
+ assert_equal %w{ controller action id format }, match.names
+ assert_equal 'content', match[1]
+ assert_equal 'list', match[2]
+ assert_nil match[3]
+ assert_nil match[4]
+ end
+
+ def test_match_controller_action_id
+ path = Pattern.new '/:controller(/:action(/:id(.:format)))'
+ uri = '/content/list/10'
+
+ match = path =~ uri
+ assert_equal %w{ controller action id format }, match.names
+ assert_equal 'content', match[1]
+ assert_equal 'list', match[2]
+ assert_equal '10', match[3]
+ assert_nil match[4]
+ end
+
+ def test_match_literal
+ path = Path::Pattern.new "/books(/:action(.:format))"
+
+ uri = '/books'
+ match = path =~ uri
+ assert_equal %w{ action format }, match.names
+ assert_nil match[1]
+ assert_nil match[2]
+ end
+
+ def test_match_literal_with_action
+ path = Path::Pattern.new "/books(/:action(.:format))"
+
+ uri = '/books/list'
+ match = path =~ uri
+ assert_equal %w{ action format }, match.names
+ assert_equal 'list', match[1]
+ assert_nil match[2]
+ end
+
+ def test_match_literal_with_action_and_format
+ path = Path::Pattern.new "/books(/:action(.:format))"
+
+ uri = '/books/list.rss'
+ match = path =~ uri
+ assert_equal %w{ action format }, match.names
+ assert_equal 'list', match[1]
+ assert_equal 'rss', match[2]
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/route/definition/parser_test.rb b/actionpack/test/journey/route/definition/parser_test.rb
new file mode 100644
index 0000000000..d7d7172a40
--- /dev/null
+++ b/actionpack/test/journey/route/definition/parser_test.rb
@@ -0,0 +1,110 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module Definition
+ class TestParser < ActiveSupport::TestCase
+ def setup
+ @parser = Parser.new
+ end
+
+ def test_slash
+ assert_equal :SLASH, @parser.parse('/').type
+ assert_round_trip '/'
+ end
+
+ def test_segment
+ assert_round_trip '/foo'
+ end
+
+ def test_segments
+ assert_round_trip '/foo/bar'
+ end
+
+ def test_segment_symbol
+ assert_round_trip '/foo/:id'
+ end
+
+ def test_symbol
+ assert_round_trip '/:foo'
+ end
+
+ def test_group
+ assert_round_trip '(/:foo)'
+ end
+
+ def test_groups
+ assert_round_trip '(/:foo)(/:bar)'
+ end
+
+ def test_nested_groups
+ assert_round_trip '(/:foo(/:bar))'
+ end
+
+ def test_dot_symbol
+ assert_round_trip('.:format')
+ end
+
+ def test_dot_literal
+ assert_round_trip('.xml')
+ end
+
+ def test_segment_dot
+ assert_round_trip('/foo.:bar')
+ end
+
+ def test_segment_group_dot
+ assert_round_trip('/foo(.:bar)')
+ end
+
+ def test_segment_group
+ assert_round_trip('/foo(/:action)')
+ end
+
+ def test_segment_groups
+ assert_round_trip('/foo(/:action)(/:bar)')
+ end
+
+ def test_segment_nested_groups
+ assert_round_trip('/foo(/:action(/:bar))')
+ end
+
+ def test_group_followed_by_path
+ assert_round_trip('/foo(/:action)/:bar')
+ end
+
+ def test_star
+ assert_round_trip('*foo')
+ assert_round_trip('/*foo')
+ assert_round_trip('/bar/*foo')
+ assert_round_trip('/bar/(*foo)')
+ end
+
+ def test_or
+ assert_round_trip('a|b')
+ assert_round_trip('a|b|c')
+ assert_round_trip('(a|b)|c')
+ assert_round_trip('a|(b|c)')
+ assert_round_trip('*a|(b|c)')
+ assert_round_trip('*a|:b|c')
+ end
+
+ def test_arbitrary
+ assert_round_trip('/bar/*foo#')
+ end
+
+ def test_literal_dot_paren
+ assert_round_trip "/sprockets.js(.:format)"
+ end
+
+ def test_groups_with_dot
+ assert_round_trip "/(:locale)(.:format)"
+ end
+
+ def assert_round_trip str
+ assert_equal str, @parser.parse(str).to_s
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/route/definition/scanner_test.rb b/actionpack/test/journey/route/definition/scanner_test.rb
new file mode 100644
index 0000000000..624e6df51a
--- /dev/null
+++ b/actionpack/test/journey/route/definition/scanner_test.rb
@@ -0,0 +1,56 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ module Definition
+ class TestScanner < ActiveSupport::TestCase
+ def setup
+ @scanner = Scanner.new
+ end
+
+ # /page/:id(/:action)(.:format)
+ def test_tokens
+ [
+ ['/', [[:SLASH, '/']]],
+ ['*omg', [[:STAR, '*omg']]],
+ ['/page', [[:SLASH, '/'], [:LITERAL, 'page']]],
+ ['/~page', [[:SLASH, '/'], [:LITERAL, '~page']]],
+ ['/pa-ge', [[:SLASH, '/'], [:LITERAL, 'pa-ge']]],
+ ['/:page', [[:SLASH, '/'], [:SYMBOL, ':page']]],
+ ['/(:page)', [
+ [:SLASH, '/'],
+ [:LPAREN, '('],
+ [:SYMBOL, ':page'],
+ [:RPAREN, ')'],
+ ]],
+ ['(/:action)', [
+ [:LPAREN, '('],
+ [:SLASH, '/'],
+ [:SYMBOL, ':action'],
+ [:RPAREN, ')'],
+ ]],
+ ['(())', [[:LPAREN, '('],
+ [:LPAREN, '('], [:RPAREN, ')'], [:RPAREN, ')']]],
+ ['(.:format)', [
+ [:LPAREN, '('],
+ [:DOT, '.'],
+ [:SYMBOL, ':format'],
+ [:RPAREN, ')'],
+ ]],
+ ].each do |str, expected|
+ @scanner.scan_setup str
+ assert_tokens expected, @scanner
+ end
+ end
+
+ def assert_tokens tokens, scanner
+ toks = []
+ while tok = scanner.next_token
+ toks << tok
+ end
+ assert_equal tokens, toks
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/route_test.rb b/actionpack/test/journey/route_test.rb
new file mode 100644
index 0000000000..cbe6284714
--- /dev/null
+++ b/actionpack/test/journey/route_test.rb
@@ -0,0 +1,106 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class TestRoute < ActiveSupport::TestCase
+ def test_initialize
+ app = Object.new
+ path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ defaults = {}
+ route = Route.new("name", app, path, {}, defaults)
+
+ assert_equal app, route.app
+ assert_equal path, route.path
+ assert_same defaults, route.defaults
+ end
+
+ def test_route_adds_itself_as_memo
+ app = Object.new
+ path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ defaults = {}
+ route = Route.new("name", app, path, {}, defaults)
+
+ route.ast.grep(Nodes::Terminal).each do |node|
+ assert_equal route, node.memo
+ end
+ end
+
+ def test_ip_address
+ path = Path::Pattern.new '/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)'
+ 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'
+ route = Route.new("name", nil, path, {},
+ { :controller => 'foo', :action => 'bar' })
+ assert_equal '/foo/himom', route.format({
+ :controller => 'foo',
+ :extra => 'himom',
+ })
+ end
+
+ def test_connects_all_match
+ path = Path::Pattern.new '/:controller(/:action(/:id(.:format)))'
+ route = Route.new("name", nil, path, {:action => 'bar'}, { :controller => 'foo' })
+
+ assert_equal '/foo/bar/10', route.format({
+ :controller => 'foo',
+ :action => 'bar',
+ :id => 10
+ })
+ end
+
+ def test_extras_are_not_included_if_optional
+ path = Path::Pattern.new '/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'
+ 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'
+ route = Route.new("name", nil, path, { }, { :action => 'show' })
+
+ assert_equal '/pages/10', route.format({:id => 10, :section => nil})
+ end
+
+ def test_score
+ constraints = {:required_defaults => [:controller, :action]}
+ defaults = {:controller=>"pages", :action=>"show"}
+
+ path = Path::Pattern.new "/page/:id(/:action)(.:format)"
+ specific = Route.new "name", nil, path, constraints, defaults
+
+ path = Path::Pattern.new "/:controller(/:action(/:id))(.:format)"
+ generic = Route.new "name", nil, path, constraints
+
+ knowledge = {:id=>20, :controller=>"pages", :action=>"show"}
+
+ routes = [specific, generic]
+
+ assert_not_equal specific.score(knowledge), generic.score(knowledge)
+
+ found = routes.sort_by { |r| r.score(knowledge) }.last
+
+ assert_equal specific, found
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/router/strexp_test.rb b/actionpack/test/journey/router/strexp_test.rb
new file mode 100644
index 0000000000..7ccdfb7b4d
--- /dev/null
+++ b/actionpack/test/journey/router/strexp_test.rb
@@ -0,0 +1,32 @@
+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
new file mode 100644
index 0000000000..93348f4647
--- /dev/null
+++ b/actionpack/test/journey/router/utils_test.rb
@@ -0,0 +1,29 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class Router
+ class TestUtils < ActiveSupport::TestCase
+ def test_path_escape
+ assert_equal "a/b%20c+d", Utils.escape_path("a/b c+d")
+ end
+
+ def test_fragment_escape
+ assert_equal "a/b%20c+d?e", Utils.escape_fragment("a/b c+d?e")
+ end
+
+ def test_uri_unescape
+ assert_equal "a/b c+d", Utils.unescape_uri("a%2Fb%20c+d")
+ end
+
+ def test_normalize_path_not_greedy
+ assert_equal "/foo%20bar%20baz", Utils.normalize_path("/foo%20bar%20baz")
+ end
+
+ def test_normalize_path_uppercase
+ assert_equal "/foo%AAbar%AAbaz", Utils.normalize_path("/foo%aabar%aabaz")
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/router_test.rb b/actionpack/test/journey/router_test.rb
new file mode 100644
index 0000000000..a286f77633
--- /dev/null
+++ b/actionpack/test/journey/router_test.rb
@@ -0,0 +1,579 @@
+# encoding: UTF-8
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class TestRouter < ActiveSupport::TestCase
+ # TODO : clean up routing tests so we don't need this hack
+ class StubDispatcher < Routing::RouteSet::Dispatcher; end
+
+ attr_reader :routes
+
+ def setup
+ @app = StubDispatcher.new
+ @routes = Routes.new
+ @router = Router.new(@routes, {})
+ @formatter = Formatter.new(@routes)
+ 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
+ self
+ end
+
+ def hello
+ self.called = true
+ 'world'
+ end
+
+ def path_info; env['PATH_INFO']; end
+ def request_method; env['REQUEST_METHOD']; end
+ def ip; env['REMOTE_ADDR']; end
+ end
+
+ def test_dashes
+ router = Router.new(routes, {})
+
+ exp = Router::Strexp.new '/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|
+ called = true
+ end
+ assert called
+ end
+
+ def test_unicode
+ router = Router.new(routes, {})
+
+ #match the escaped version of /ã»ã’
+ exp = Router::Strexp.new '/%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|
+ called = true
+ end
+ assert called
+ end
+
+ def test_request_class_and_requirements_success
+ klass = FakeRequestFeeler.new nil
+ router = Router.new(routes, {:request_class => klass })
+
+ requirements = { :hello => /world/ }
+
+ exp = Router::Strexp.new '/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|
+ assert_equal({:id => '10'}, params)
+ end
+
+ assert klass.called, 'hello should have been called'
+ assert_equal env.env, klass.env
+ end
+
+ def test_request_class_and_requirements_fail
+ klass = FakeRequestFeeler.new nil
+ router = Router.new(routes, {:request_class => klass })
+
+ requirements = { :hello => /mom/ }
+
+ exp = Router::Strexp.new '/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|
+ flunk 'route should not be found'
+ end
+
+ assert klass.called, 'hello should have been called'
+ assert_equal env.env, klass.env
+ end
+
+ class CustomPathRequest < Router::NullReq
+ def path_info
+ env['custom.path_info']
+ end
+ end
+
+ def test_request_class_overrides_path_info
+ router = Router.new(routes, {:request_class => CustomPathRequest })
+
+ exp = Router::Strexp.new '/bar', {}, ['/.?']
+ path = Path::Pattern.new exp
+
+ routes.add_route nil, path, {}, {}, {}
+
+ env = rails_env 'PATH_INFO' => '/foo', 'custom.path_info' => '/bar'
+
+ recognized = false
+ router.recognize(env) do |r, _, params|
+ recognized = true
+ end
+
+ assert recognized, "route should have been recognized"
+ end
+
+ def test_regexp_first_precedence
+ add_routes @router, [
+ Router::Strexp.new("/whois/:domain", {:domain => /\w+\.[\w\.]+/}, ['/', '.', '?']),
+ Router::Strexp.new("/whois/:id(.:format)", {}, ['/', '.', '?'])
+ ]
+
+ env = rails_env 'PATH_INFO' => '/whois/example.com'
+
+ list = []
+ @router.recognize(env) do |r, _, params|
+ list << r
+ end
+ assert_equal 2, list.length
+
+ r = list.first
+
+ assert_equal '/whois/:domain', r.path.spec.to_s
+ end
+
+ def test_required_parts_verified_are_anchored
+ add_routes @router, [
+ Router::Strexp.new("/foo/:id", { :id => /\d/ }, ['/', '.', '?'], false)
+ ]
+
+ assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ end
+ end
+
+ def test_required_parts_are_verified_when_building
+ add_routes @router, [
+ Router::Strexp.new("/foo/:id", { :id => /\d+/ }, ['/', '.', '?'], false)
+ ]
+
+ path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ assert_equal '/foo/10', path
+
+ assert_raises(ActionController::UrlGenerationError) do
+ @formatter.generate(:path_info, nil, { :id => 'aa' }, { })
+ end
+ end
+
+ def test_only_required_parts_are_verified
+ add_routes @router, [
+ Router::Strexp.new("/foo(/:id)", {:id => /\d/}, ['/', '.', '?'], false)
+ ]
+
+ path, _ = @formatter.generate(:path_info, nil, { :id => '10' }, { })
+ assert_equal '/foo/10', path
+
+ path, _ = @formatter.generate(:path_info, nil, { }, { })
+ assert_equal '/foo', path
+
+ path, _ = @formatter.generate(:path_info, 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)
+ 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, { }, { })
+ end
+
+ assert_match(/missing required keys: \[:id\]/, error.message)
+ end
+
+ def test_X_Cascade
+ add_routes @router, [ "/messages(.:format)" ]
+ resp = @router.call({ '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
+ app = lambda { |env| [200, {}, ['success!']] }
+ @router.routes.add_route(app, path, {}, {}, {})
+
+ env = rack_env('SCRIPT_NAME' => '', 'PATH_INFO' => '/weblog')
+ resp = @router.call(env)
+ assert_equal ['success!'], resp.last
+ assert_equal '', env['SCRIPT_NAME']
+ end
+
+ def test_defaults_merge_correctly
+ path = Path::Pattern.new '/foo(/:id)'
+ @router.routes.add_route nil, path, {}, {:id => nil}, {}
+
+ env = rails_env 'PATH_INFO' => '/foo/10'
+ @router.recognize(env) do |r, _, params|
+ assert_equal({:id => '10'}, params)
+ end
+
+ env = rails_env 'PATH_INFO' => '/foo'
+ @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)
+ ]
+
+ env = rails_env 'PATH_INFO' => '/foo/bar'
+
+ @router.recognize(env) { |*_| }
+
+ assert_equal '/foo', env.env['SCRIPT_NAME']
+ assert_equal '/bar', env.env['PATH_INFO']
+ end
+
+ def test_bound_regexp_keeps_path_info
+ add_routes @router, [
+ Router::Strexp.new("/foo", { }, ['/', '.', '?'], true)
+ ]
+
+ env = rails_env 'PATH_INFO' => '/foo'
+
+ before = env.env['SCRIPT_NAME']
+
+ @router.recognize(env) { |*_| }
+
+ assert_equal before, env.env['SCRIPT_NAME']
+ assert_equal '/foo', env.env['PATH_INFO']
+ end
+
+ def test_path_not_found
+ add_routes @router, [
+ "/messages(.:format)",
+ "/messages/new(.:format)",
+ "/messages/:id/edit(.:format)",
+ "/messages/:id(.:format)"
+ ]
+ env = rails_env 'PATH_INFO' => '/messages/unknown/path'
+ yielded = false
+
+ @router.recognize(env) do |*whatever|
+ yielded = true
+ end
+ assert_not yielded
+ end
+
+ def test_required_part_in_recall
+ add_routes @router, [ "/messages/:a/:b" ]
+
+ path, _ = @formatter.generate(:path_info, 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' })
+ assert_equal "/b", path
+ end
+
+ def test_recall_should_be_used_when_scoring
+ add_routes @router, [
+ "/messages/:action(/:id(.:format))",
+ "/messages/:id(.:format)"
+ ]
+
+ path, _ = @formatter.generate(:path_info, 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))"
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ params = { :controller => "tasks", :format => nil }
+ extras = { :action => 'lol' }
+
+ path, _ = @formatter.generate(:path_info, nil, params, extras)
+ assert_equal '/tasks', path
+ end
+
+ def test_generate_slash
+ params = [ [:controller, "tasks"],
+ [:action, "show"] ]
+ str = Router::Strexp.new("/", Hash[params], ['/', '.', '?'], true)
+ path = Path::Pattern.new str
+
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(:path_info, nil, Hash[params], {})
+ assert_equal '/', path
+ end
+
+ def test_generate_calls_param_proc
+ path = Path::Pattern.new '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ parameterized = []
+ params = [ [:controller, "tasks"],
+ [:action, "show"] ]
+
+ @formatter.generate(
+ :path_info,
+ nil,
+ Hash[params],
+ {},
+ lambda { |k,v| parameterized << [k,v]; v })
+
+ assert_equal params.map(&:to_s).sort, parameterized.map(&:to_s).sort
+ end
+
+ def test_generate_id
+ path = Path::Pattern.new '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(
+ :path_info, 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)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, _ = @formatter.generate(:path_info,
+ nil, { :controller => "tasks",
+ :action => "a/b c+d",
+ }, {})
+ assert_equal '/tasks/a/b%20c+d', path
+ end
+
+ def test_generate_extra_params
+ path = Path::Pattern.new '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(:path_info,
+ nil, { :id => 1,
+ :controller => "tasks",
+ :action => "show",
+ :relative_url_root => nil
+ }, {})
+ assert_equal '/tasks/show', path
+ assert_equal({:id => 1, :relative_url_root => nil}, params)
+ end
+
+ def test_generate_uses_recall_if_needed
+ path = Path::Pattern.new '/:controller(/:action(/:id))'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(:path_info,
+ nil,
+ {:controller =>"tasks", :id => 10},
+ {:action =>"index"})
+ assert_equal '/tasks/index/10', path
+ assert_equal({}, params)
+ end
+
+ def test_generate_with_name
+ path = Path::Pattern.new '/:controller(/:action)'
+ @router.routes.add_route @app, path, {}, {}, {}
+
+ path, params = @formatter.generate(:path_info,
+ "tasks",
+ {:controller=>"tasks"},
+ {:controller=>"tasks", :action=>"index"})
+ assert_equal '/tasks', path
+ assert_equal({}, params)
+ end
+
+ {
+ '/content' => { :controller => 'content' },
+ '/content/list' => { :controller => 'content', :action => 'list' },
+ '/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))"
+ 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|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+
+ assert called
+ end
+ end
+
+ {
+ :segment => ['/a%2Fb%20c+d/splat', { :segment => 'a/b c+d', :splat => 'splat' }],
+ :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'
+ 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|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+
+ assert called
+ end
+ end
+
+ def test_namespaced_controller
+ strexp = Router::Strexp.new(
+ "/:controller(/:action(/:id))",
+ { :controller => /.+?/ },
+ ["/", ".", "?"]
+ )
+ path = Path::Pattern.new strexp
+ app = Object.new
+ route = @router.routes.add_route(app, path, {}, {}, {})
+
+ env = rails_env 'PATH_INFO' => '/admin/users/show/10'
+ called = false
+ expected = {
+ :controller => 'admin/users',
+ :action => 'show',
+ :id => '10'
+ }
+
+ @router.recognize(env) do |r, _, params|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+ assert called
+ end
+
+ def test_recognize_literal
+ path = Path::Pattern.new "/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|
+ assert_equal route, r
+ assert_equal(expected, params)
+ called = true
+ end
+
+ assert called
+ end
+
+ def test_recognize_head_request_as_get_route
+ path = Path::Pattern.new "/books(/:action(.:format))"
+ app = Object.new
+ conditions = {
+ :request_method => 'GET'
+ }
+ @router.routes.add_route(app, path, conditions, {})
+
+ env = rails_env 'PATH_INFO' => '/books/list.rss',
+ "REQUEST_METHOD" => "HEAD"
+
+ called = false
+ @router.recognize(env) do |r, _, params|
+ called = true
+ end
+
+ assert called
+ end
+
+ def test_recognize_cares_about_verbs
+ path = Path::Pattern.new "/books(/:action(.:format))"
+ app = Object.new
+ conditions = {
+ :request_method => 'GET'
+ }
+ @router.routes.add_route(app, path, conditions, {})
+
+ conditions = conditions.dup
+ conditions[:request_method] = 'POST'
+
+ post = @router.routes.add_route(app, path, conditions, {})
+
+ env = rails_env 'PATH_INFO' => '/books/list.rss',
+ "REQUEST_METHOD" => "POST"
+
+ called = false
+ @router.recognize(env) do |r, _, params|
+ assert_equal post, r
+ called = true
+ end
+
+ assert called
+ end
+
+ private
+
+ def add_routes router, paths
+ paths.each do |path|
+ path = Path::Pattern.new path
+ router.routes.add_route @app, path, {}, {}, {}
+ end
+ end
+
+ RailsEnv = Struct.new(:env)
+
+ def rails_env env
+ RailsEnv.new rack_env env
+ end
+
+ def rack_env env
+ {
+ "rack.version" => [1, 1],
+ "rack.input" => StringIO.new,
+ "rack.errors" => StringIO.new,
+ "rack.multithread" => true,
+ "rack.multiprocess" => true,
+ "rack.run_once" => false,
+ "REQUEST_METHOD" => "GET",
+ "SERVER_NAME" => "example.org",
+ "SERVER_PORT" => "80",
+ "QUERY_STRING" => "",
+ "PATH_INFO" => "/content",
+ "rack.url_scheme" => "http",
+ "HTTPS" => "off",
+ "SCRIPT_NAME" => "",
+ "CONTENT_LENGTH" => "0"
+ }.merge env
+ end
+ end
+ end
+end
diff --git a/actionpack/test/journey/routes_test.rb b/actionpack/test/journey/routes_test.rb
new file mode 100644
index 0000000000..25e0321d31
--- /dev/null
+++ b/actionpack/test/journey/routes_test.rb
@@ -0,0 +1,53 @@
+require 'abstract_unit'
+
+module ActionDispatch
+ module Journey
+ class TestRoutes < ActiveSupport::TestCase
+ def test_clear
+ routes = Routes.new
+ exp = Router::Strexp.new '/foo(/:id)', {}, ['/.?']
+ path = Path::Pattern.new exp
+ requirements = { :hello => /world/ }
+
+ routes.add_route nil, path, requirements, {:id => nil}, {}
+ assert_equal 1, routes.length
+
+ routes.clear
+ assert_equal 0, routes.length
+ end
+
+ def test_ast
+ routes = Routes.new
+ path = Path::Pattern.new '/hello'
+
+ routes.add_route nil, path, {}, {}, {}
+ ast = routes.ast
+ routes.add_route nil, path, {}, {}, {}
+ assert_not_equal ast, routes.ast
+ end
+
+ def test_simulator_changes
+ routes = Routes.new
+ path = Path::Pattern.new '/hello'
+
+ routes.add_route nil, path, {}, {}, {}
+ sim = routes.simulator
+ routes.add_route nil, path, {}, {}, {}
+ assert_not_equal sim, routes.simulator
+ end
+
+ def test_first_name_wins
+ #def add_route app, path, conditions, defaults, name = nil
+ routes = Routes.new
+
+ one = Path::Pattern.new '/hello'
+ two = Path::Pattern.new '/aaron'
+
+ routes.add_route nil, one, {}, {}, 'aaron'
+ routes.add_route nil, two, {}, {}, 'aaron'
+
+ assert_equal '/hello', routes.named_routes['aaron'].path.spec.to_s
+ end
+ end
+ end
+end
diff --git a/actionpack/test/lib/controller/fake_models.rb b/actionpack/test/lib/controller/fake_models.rb
index 82f38b5309..08af187311 100644
--- a/actionpack/test/lib/controller/fake_models.rb
+++ b/actionpack/test/lib/controller/fake_models.rb
@@ -28,12 +28,6 @@ class Customer < Struct.new(:name, :id)
end
end
-class BadCustomer < Customer
-end
-
-class GoodCustomer < Customer
-end
-
class ValidatedCustomer < Customer
def errors
if name =~ /Sikachu/i
@@ -102,88 +96,6 @@ class Comment
attr_accessor :body
end
-class Tag
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :post_id
- def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @post_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-
- attr_accessor :relevances
- def relevances_attributes=(attributes); end
-
-end
-
-class CommentRelevance
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :comment_id
- def initialize(id = nil, comment_id = nil); @id, @comment_id = id, comment_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @comment_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-end
-
-class Sheep
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- def to_key; id ? [id] : nil end
- def save; @id = 1 end
- def new_record?; @id.nil? end
- def name
- @id.nil? ? 'new sheep' : "sheep ##{@id}"
- end
-end
-
-
-class TagRelevance
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- attr_reader :id
- attr_reader :tag_id
- def initialize(id = nil, tag_id = nil); @id, @tag_id = id, tag_id end
- def to_key; id ? [id] : nil end
- def save; @id = 1; @tag_id = 1 end
- def persisted?; @id.present? end
- def to_param; @id; end
- def value
- @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
- end
-end
-
-class Author < Comment
- attr_accessor :post
- def post_attributes=(attributes); end
-end
-
-class HashBackedAuthor < Hash
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- def persisted?; false; end
-
- def name
- "hash backed author"
- end
-end
-
module Blog
def self.use_relative_model_naming?
true
@@ -199,21 +111,8 @@ module Blog
end
end
-class ArelLike
- def to_ary
- true
- end
- def each
- a = Array.new(2) { |id| Comment.new(id + 1) }
- a.each { |i| yield i }
- end
-end
-
class RenderJsonTestException < Exception
def to_json(options = nil)
return { :error => self.class.name, :message => self.to_s }.to_json
end
end
-
-class Car < Struct.new(:color)
-end
diff --git a/actionpack/test/routing/helper_test.rb b/actionpack/test/routing/helper_test.rb
index a5588d95fa..0028aaa629 100644
--- a/actionpack/test/routing/helper_test.rb
+++ b/actionpack/test/routing/helper_test.rb
@@ -22,7 +22,7 @@ module ActionDispatch
x = Class.new {
include rs.url_helpers
}
- assert_raises ActionController::RoutingError do
+ assert_raises ActionController::UrlGenerationError do
x.new.pond_duck_path Duck.new
end
end
diff --git a/actionpack/test/template/asset_tag_helper_test.rb b/actionpack/test/template/asset_tag_helper_test.rb
deleted file mode 100644
index 754622c883..0000000000
--- a/actionpack/test/template/asset_tag_helper_test.rb
+++ /dev/null
@@ -1,670 +0,0 @@
-require 'zlib'
-require 'abstract_unit'
-require 'active_support/ordered_options'
-
-class FakeController
- attr_accessor :request
-
- def config
- @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config)
- end
-end
-
-class AssetTagHelperTest < ActionView::TestCase
- tests ActionView::Helpers::AssetTagHelper
-
- def setup
- super
- silence_warnings do
- ActionView::Helpers::AssetTagHelper.send(
- :const_set,
- :JAVASCRIPTS_DIR,
- File.dirname(__FILE__) + "/../fixtures/public/javascripts"
- )
-
- ActionView::Helpers::AssetTagHelper.send(
- :const_set,
- :STYLESHEETS_DIR,
- File.dirname(__FILE__) + "/../fixtures/public/stylesheets"
- )
-
- ActionView::Helpers::AssetTagHelper.send(
- :const_set,
- :ASSETS_DIR,
- File.dirname(__FILE__) + "/../fixtures/public"
- )
- end
-
- @controller = BasicController.new
-
- @request = Class.new do
- attr_accessor :script_name
- def protocol() 'http://' end
- def ssl?() false end
- def host_with_port() 'localhost' end
- end.new
-
- @controller.request = @request
- end
-
- def url_for(*args)
- "http://www.example.com"
- end
-
- def teardown
- ENV.delete('RAILS_ASSET_ID')
- end
-
- AutoDiscoveryToTag = {
- %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />),
- %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />),
- %(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="" type="text/html" />),
- %(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="No stream.. really" type="text/html" />),
- %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="text/html" />),
- %(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(<link href="http://www.example.com" rel="Not so alternate" title="ATOM" type="application/atom+xml" />),
- }
-
- JavascriptPathToTag = {
- %(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js),
- %(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js),
- %(javascript_path("/super/xmlhr.js")) => %(/super/xmlhr.js)
- }
-
- PathToJavascriptToTag = {
- %(path_to_javascript("xmlhr")) => %(/javascripts/xmlhr.js),
- %(path_to_javascript("super/xmlhr")) => %(/javascripts/super/xmlhr.js),
- %(path_to_javascript("/super/xmlhr.js")) => %(/super/xmlhr.js)
- }
-
- JavascriptUrlToTag = {
- %(javascript_url("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js),
- %(javascript_url("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js),
- %(javascript_url("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js)
- }
-
- UrlToJavascriptToTag = {
- %(url_to_javascript("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js),
- %(url_to_javascript("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js),
- %(url_to_javascript("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js)
- }
-
- JavascriptIncludeToTag = {
- %(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>),
- %(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>),
- %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>),
- %(javascript_include_tag("common.javascript", "/elsewhere/cools")) => %(<script src="/javascripts/common.javascript" ></script>\n<script src="/elsewhere/cools.js" ></script>),
-
- %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>),
- %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>),
- %(javascript_include_tag("//example.com/all.js")) => %(<script src="//example.com/all.js"></script>),
- }
-
- StylePathToTag = {
- %(stylesheet_path("bank")) => %(/stylesheets/bank.css),
- %(stylesheet_path("bank.css")) => %(/stylesheets/bank.css),
- %(stylesheet_path('subdir/subdir')) => %(/stylesheets/subdir/subdir.css),
- %(stylesheet_path('/subdir/subdir.css')) => %(/subdir/subdir.css)
- }
-
- PathToStyleToTag = {
- %(path_to_stylesheet("style")) => %(/stylesheets/style.css),
- %(path_to_stylesheet("style.css")) => %(/stylesheets/style.css),
- %(path_to_stylesheet('dir/file')) => %(/stylesheets/dir/file.css),
- %(path_to_stylesheet('/dir/file.rcss')) => %(/dir/file.rcss)
- }
-
- StyleUrlToTag = {
- %(stylesheet_url("bank")) => %(http://www.example.com/stylesheets/bank.css),
- %(stylesheet_url("bank.css")) => %(http://www.example.com/stylesheets/bank.css),
- %(stylesheet_url('subdir/subdir')) => %(http://www.example.com/stylesheets/subdir/subdir.css),
- %(stylesheet_url('/subdir/subdir.css')) => %(http://www.example.com/subdir/subdir.css)
- }
-
- UrlToStyleToTag = {
- %(url_to_stylesheet("style")) => %(http://www.example.com/stylesheets/style.css),
- %(url_to_stylesheet("style.css")) => %(http://www.example.com/stylesheets/style.css),
- %(url_to_stylesheet('dir/file')) => %(http://www.example.com/stylesheets/dir/file.css),
- %(url_to_stylesheet('/dir/file.rcss')) => %(http://www.example.com/dir/file.rcss)
- }
-
- StyleLinkToTag = {
- %(stylesheet_link_tag("bank")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("bank.css")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />),
- %(stylesheet_link_tag("random.styles", "/elsewhere/file")) => %(<link href="/stylesheets/random.styles" media="screen" rel="stylesheet" />\n<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />),
-
- %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />),
- %(stylesheet_link_tag("//www.example.com/styles/style.css")) => %(<link href="//www.example.com/styles/style.css" media="screen" rel="stylesheet" />),
- }
-
- ImagePathToTag = {
- %(image_path("xml")) => %(/images/xml),
- %(image_path("xml.png")) => %(/images/xml.png),
- %(image_path("dir/xml.png")) => %(/images/dir/xml.png),
- %(image_path("/dir/xml.png")) => %(/dir/xml.png)
- }
-
- PathToImageToTag = {
- %(path_to_image("xml")) => %(/images/xml),
- %(path_to_image("xml.png")) => %(/images/xml.png),
- %(path_to_image("dir/xml.png")) => %(/images/dir/xml.png),
- %(path_to_image("/dir/xml.png")) => %(/dir/xml.png)
- }
-
- ImageUrlToTag = {
- %(image_url("xml")) => %(http://www.example.com/images/xml),
- %(image_url("xml.png")) => %(http://www.example.com/images/xml.png),
- %(image_url("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png),
- %(image_url("/dir/xml.png")) => %(http://www.example.com/dir/xml.png)
- }
-
- UrlToImageToTag = {
- %(url_to_image("xml")) => %(http://www.example.com/images/xml),
- %(url_to_image("xml.png")) => %(http://www.example.com/images/xml.png),
- %(url_to_image("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png),
- %(url_to_image("/dir/xml.png")) => %(http://www.example.com/dir/xml.png)
- }
-
- ImageLinkToTag = {
- %(image_tag("xml.png")) => %(<img alt="Xml" src="/images/xml.png" />),
- %(image_tag("rss.gif", :alt => "rss syndication")) => %(<img alt="rss syndication" src="/images/rss.gif" />),
- %(image_tag("gold.png", :size => "20")) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
- %(image_tag("gold.png", :size => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
- %(image_tag("gold.png", "size" => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
- %(image_tag("error.png", "size" => "45 x 70")) => %(<img alt="Error" src="/images/error.png" />),
- %(image_tag("error.png", "size" => "x")) => %(<img alt="Error" src="/images/error.png" />),
- %(image_tag("google.com.png")) => %(<img alt="Google.com" src="/images/google.com.png" />),
- %(image_tag("slash..png")) => %(<img alt="Slash." src="/images/slash..png" />),
- %(image_tag(".pdf.png")) => %(<img alt=".pdf" src="/images/.pdf.png" />),
- %(image_tag("http://www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="http://www.rubyonrails.com/images/rails.png" />),
- %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />),
- %(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />),
- %(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />),
- %(image_tag("")) => %(<img src="" />)
- }
-
- 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 '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" />)
- }
-
- VideoPathToTag = {
- %(video_path("xml")) => %(/videos/xml),
- %(video_path("xml.ogg")) => %(/videos/xml.ogg),
- %(video_path("dir/xml.ogg")) => %(/videos/dir/xml.ogg),
- %(video_path("/dir/xml.ogg")) => %(/dir/xml.ogg)
- }
-
- PathToVideoToTag = {
- %(path_to_video("xml")) => %(/videos/xml),
- %(path_to_video("xml.ogg")) => %(/videos/xml.ogg),
- %(path_to_video("dir/xml.ogg")) => %(/videos/dir/xml.ogg),
- %(path_to_video("/dir/xml.ogg")) => %(/dir/xml.ogg)
- }
-
- VideoUrlToTag = {
- %(video_url("xml")) => %(http://www.example.com/videos/xml),
- %(video_url("xml.ogg")) => %(http://www.example.com/videos/xml.ogg),
- %(video_url("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg),
- %(video_url("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg)
- }
-
- UrlToVideoToTag = {
- %(url_to_video("xml")) => %(http://www.example.com/videos/xml),
- %(url_to_video("xml.ogg")) => %(http://www.example.com/videos/xml.ogg),
- %(url_to_video("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg),
- %(url_to_video("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg)
- }
-
- VideoLinkToTag = {
- %(video_tag("xml.ogg")) => %(<video src="/videos/xml.ogg"></video>),
- %(video_tag("rss.m4v", :autoplay => true, :controls => true)) => %(<video autoplay="autoplay" controls="controls" src="/videos/rss.m4v"></video>),
- %(video_tag("rss.m4v", :autobuffer => true)) => %(<video autobuffer="autobuffer" src="/videos/rss.m4v"></video>),
- %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>),
- %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>),
- %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>),
- %(video_tag("error.avi", "size" => "100")) => %(<video src="/videos/error.avi"></video>),
- %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>),
- %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>),
- %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>),
- %(video_tag("//media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="//media.rubyonrails.org/video/rails_blog_2.mov"></video>),
- %(video_tag("multiple.ogg", "multiple.avi")) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>),
- %(video_tag(["multiple.ogg", "multiple.avi"])) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>),
- %(video_tag(["multiple.ogg", "multiple.avi"], :size => "160x120", :controls => true)) => %(<video controls="controls" height="120" width="160"><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>)
- }
-
- AudioPathToTag = {
- %(audio_path("xml")) => %(/audios/xml),
- %(audio_path("xml.wav")) => %(/audios/xml.wav),
- %(audio_path("dir/xml.wav")) => %(/audios/dir/xml.wav),
- %(audio_path("/dir/xml.wav")) => %(/dir/xml.wav)
- }
-
- PathToAudioToTag = {
- %(path_to_audio("xml")) => %(/audios/xml),
- %(path_to_audio("xml.wav")) => %(/audios/xml.wav),
- %(path_to_audio("dir/xml.wav")) => %(/audios/dir/xml.wav),
- %(path_to_audio("/dir/xml.wav")) => %(/dir/xml.wav)
- }
-
- AudioUrlToTag = {
- %(audio_url("xml")) => %(http://www.example.com/audios/xml),
- %(audio_url("xml.wav")) => %(http://www.example.com/audios/xml.wav),
- %(audio_url("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav),
- %(audio_url("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav)
- }
-
- UrlToAudioToTag = {
- %(url_to_audio("xml")) => %(http://www.example.com/audios/xml),
- %(url_to_audio("xml.wav")) => %(http://www.example.com/audios/xml.wav),
- %(url_to_audio("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav),
- %(url_to_audio("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav)
- }
-
- AudioLinkToTag = {
- %(audio_tag("xml.wav")) => %(<audio src="/audios/xml.wav"></audio>),
- %(audio_tag("rss.wav", :autoplay => true, :controls => true)) => %(<audio autoplay="autoplay" controls="controls" src="/audios/rss.wav"></audio>),
- %(audio_tag("http://media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="http://media.rubyonrails.org/audio/rails_blog_2.mov"></audio>),
- %(audio_tag("//media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="//media.rubyonrails.org/audio/rails_blog_2.mov"></audio>),
- %(audio_tag("audio.mp3", "audio.ogg")) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
- %(audio_tag(["audio.mp3", "audio.ogg"])) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
- %(audio_tag(["audio.mp3", "audio.ogg"], :autobuffer => true, :controls => true)) => %(<audio autobuffer="autobuffer" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>)
- }
-
- def test_autodiscovery_link_tag_deprecated_types
- result = nil
- assert_deprecated do
- result = auto_discovery_link_tag(:xml)
- end
-
- expected = %(<link href="http://www.example.com" rel="alternate" title="XML" type="application/xml" />)
- assert_equal expected, result
- end
-
- def test_auto_discovery_link_tag
- AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_javascript_path
- JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_path_to_javascript_alias_for_javascript_path
- PathToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_javascript_url
- JavascriptUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_url_to_javascript_alias_for_javascript_url
- UrlToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_javascript_include_tag_with_blank_asset_id
- ENV["RAILS_ASSET_ID"] = ""
- JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_javascript_include_tag_with_missing_source
- assert_nothing_raised {
- javascript_include_tag('missing_security_guard')
- }
-
- assert_nothing_raised {
- javascript_include_tag('http://example.com/css/missing_security_guard')
- }
- end
-
- def test_javascript_include_tag_is_html_safe
- assert javascript_include_tag("prototype").html_safe?
- end
-
- def test_all_javascript_expansion_not_include_application_js_if_not_exists
- FileUtils.mv(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'application.js'),
- File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'application.bak'))
- assert_no_match(/application\.js/, javascript_include_tag(:all))
- ensure
- FileUtils.mv(File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'application.bak'),
- File.join(ActionView::Helpers::AssetTagHelper::JAVASCRIPTS_DIR, 'application.js'))
- end
-
- def test_stylesheet_path
- ENV["RAILS_ASSET_ID"] = ""
- StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_path_to_stylesheet_alias_for_stylesheet_path
- PathToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_stylesheet_url
- ENV["RAILS_ASSET_ID"] = ""
- StyleUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_url_to_stylesheet_alias_for_stylesheet_url
- UrlToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_stylesheet_link_tag
- ENV["RAILS_ASSET_ID"] = ""
- StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_stylesheet_link_tag_with_missing_source
- assert_nothing_raised {
- stylesheet_link_tag('missing_security_guard')
- }
-
- assert_nothing_raised {
- stylesheet_link_tag('http://example.com/css/missing_security_guard')
- }
- end
-
- def test_stylesheet_link_tag_is_html_safe
- ENV["RAILS_ASSET_ID"] = ""
- assert stylesheet_link_tag('dir/file').html_safe?
- assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe?
- end
-
- def test_stylesheet_link_tag_escapes_options
- assert_dom_equal %(<link href="/file.css" media="&lt;script&gt;" rel="stylesheet" />), stylesheet_link_tag('/file', :media => '<script>')
- end
-
- def test_stylesheet_link_tag_should_not_output_the_same_asset_twice
- ENV["RAILS_ASSET_ID"] = ""
- assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam')
- end
-
- def test_image_path
- ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_path_to_image_alias_for_image_path
- PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_image_url
- ImageUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_url_to_image_alias_for_image_url
- UrlToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_image_alt
- [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix|
- assert_equal 'Rails', image_alt("#{prefix}rails.png")
- assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png")
- assert_equal 'Avatar-0000', image_alt("#{prefix}avatar-0000.png")
- end
- end
-
- def test_image_tag
- ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_image_tag_does_not_modify_options
- options = {:size => '16x10'}
- image_tag('icon', options)
- assert_equal({:size => '16x10'}, options)
- end
-
- def test_favicon_link_tag
- FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_image_tag_windows_behaviour
- old_asset_id, ENV["RAILS_ASSET_ID"] = ENV["RAILS_ASSET_ID"], "1"
- # This simulates the behavior of File#exist? on windows when testing a file ending in "."
- # If the file "rails.png" exists, windows will return true when asked if "rails.png." exists (notice trailing ".")
- # OS X, linux etc will return false in this case.
- File.stubs(:exist?).with('template/../fixtures/public/images/rails.png.').returns(true)
- assert_equal '<img alt="Rails" src="/images/rails.png?1" />', image_tag('rails.png')
- ensure
- if old_asset_id
- ENV["RAILS_ASSET_ID"] = old_asset_id
- else
- ENV.delete("RAILS_ASSET_ID")
- end
- end
-
- def test_video_path
- VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_path_to_video_alias_for_video_path
- PathToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_video_url
- VideoUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_url_to_video_alias_for_video_url
- UrlToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_video_tag
- VideoLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_audio_path
- AudioPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_path_to_audio_alias_for_audio_path
- PathToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_audio_url
- AudioUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_url_to_audio_alias_for_audio_url
- UrlToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_audio_tag
- AudioLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
- end
-
- def test_video_audio_tag_does_not_modify_options
- options = {:autoplay => true}
- video_tag('video', options)
- assert_equal({:autoplay => true}, options)
- audio_tag('audio', options)
- assert_equal({:autoplay => true}, options)
- end
-
- def test_timebased_asset_id
- expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s
- assert_equal %(<img alt="Rails" src="/images/rails.png?#{expected_time}" />), image_tag("rails.png")
- end
-
- def test_string_asset_id
- @controller.config.asset_path = "/assets.v12345%s"
-
- expected_path = "/assets.v12345/images/rails.png"
- assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png")
- end
-
- def test_proc_asset_id
- @controller.config.asset_path = Proc.new do |asset_path|
- "/assets.v12345#{asset_path}"
- end
-
- expected_path = "/assets.v12345/images/rails.png"
- assert_equal %(<img alt="Rails" src="#{expected_path}" />), image_tag("rails.png")
- end
-
- def test_image_tag_interpreting_email_cid_correctly
- # An inline image has no need for an alt tag to be automatically generated from the cid:
- assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid")
- end
-
- def test_image_tag_interpreting_email_adding_optional_alt_tag
- assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", :alt => "Image")
- end
-
- def test_timebased_asset_id_with_relative_url_root
- @controller.config.relative_url_root = "/collaboration/hieraki"
- expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s
- assert_equal %(<img alt="Rails" src="#{@controller.config.relative_url_root}/images/rails.png?#{expected_time}" />), image_tag("rails.png")
- end
-
- # Same as above, but with script_name
- def test_timebased_asset_id_with_script_name
- @request.script_name = "/collaboration/hieraki"
- expected_time = File.mtime(File.expand_path(File.dirname(__FILE__) + "/../fixtures/public/images/rails.png")).to_i.to_s
- assert_equal %(<img alt="Rails" src="#{@request.script_name}/images/rails.png?#{expected_time}" />), image_tag("rails.png")
- end
-
- def test_should_skip_asset_id_on_complete_url
- assert_equal %(<img alt="Rails" src="http://www.example.com/rails.png" />), image_tag("http://www.example.com/rails.png")
- end
-
- def test_should_skip_asset_id_on_scheme_relative_url
- assert_equal %(<img alt="Rails" src="//www.example.com/rails.png" />), image_tag("//www.example.com/rails.png")
- end
-
- def test_should_use_preset_asset_id
- ENV["RAILS_ASSET_ID"] = "4500"
- assert_equal %(<img alt="Rails" src="/images/rails.png?4500" />), image_tag("rails.png")
- end
-
- def test_preset_empty_asset_id
- ENV["RAILS_ASSET_ID"] = ""
- assert_equal %(<img alt="Rails" src="/images/rails.png" />), image_tag("rails.png")
- end
-
- def test_should_not_modify_source_string
- source = '/images/rails.png'
- copy = source.dup
- image_tag(source)
- assert_equal copy, source
- end
-
- def test_caching_image_path_with_caching_and_proc_asset_host_using_request
- ENV['RAILS_ASSET_ID'] = ''
- @controller.config.asset_host = Proc.new do |source, request|
- if request.ssl?
- "#{request.protocol}#{request.host_with_port}"
- else
- "#{request.protocol}assets#{source.length}.example.com"
- end
- end
-
- @controller.request.stubs(:ssl?).returns(false)
- assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
-
- @controller.request.stubs(:ssl?).returns(true)
- assert_equal "http://localhost/images/xml.png", image_path("xml.png")
- end
-end
-
-class AssetTagHelperNonVhostTest < ActionView::TestCase
- tests ActionView::Helpers::AssetTagHelper
-
- def setup
- super
- @controller = BasicController.new
- @controller.config.relative_url_root = "/collaboration/hieraki"
-
- @request = Struct.new(:protocol).new("gopher://")
- @controller.request = @request
- end
-
- def url_for(options)
- "http://www.example.com/collaboration/hieraki"
- end
-
- def test_should_compute_proper_path
- assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
- assert_dom_equal(%(/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
- assert_dom_equal(%(/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
- assert_dom_equal(%(/collaboration/hieraki/images/xml.png), image_path("xml.png"))
- end
-
- def test_should_ignore_relative_root_path_on_complete_url
- assert_dom_equal(%(http://www.example.com/images/xml.png), image_path("http://www.example.com/images/xml.png"))
- end
-
- def test_should_compute_proper_path_with_asset_host
- @controller.config.asset_host = "assets.example.com"
- assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png"))
- end
-
- def test_should_compute_proper_path_with_asset_host_and_default_protocol
- @controller.config.asset_host = "assets.example.com"
- @controller.config.default_asset_host_protocol = :request
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png"))
- end
-
- def test_should_compute_proper_url_with_asset_host
- @controller.config.asset_host = "assets.example.com"
- assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png"))
- end
-
- def test_should_compute_proper_url_with_asset_host_and_default_protocol
- @controller.config.asset_host = "assets.example.com"
- @controller.config.default_asset_host_protocol = :request
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style"))
- assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png"))
- end
-
- def test_should_ignore_asset_host_on_complete_url
- @controller.config.asset_host = "http://assets.example.com"
- assert_dom_equal(%(<link href="http://bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("http://bar.example.com/stylesheets/style.css"))
- end
-
- def test_should_ignore_asset_host_on_scheme_relative_url
- @controller.config.asset_host = "http://assets.example.com"
- assert_dom_equal(%(<link href="//bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("//bar.example.com/stylesheets/style.css"))
- end
-
- def test_should_wildcard_asset_host_between_zero_and_four
- @controller.config.asset_host = 'http://a%d.example.com'
- assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png'))
- assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url('xml.png'))
- end
-
- def test_asset_host_without_protocol_should_be_protocol_relative
- @controller.config.asset_host = 'a.example.com'
- assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_path('xml.png')
- assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_url('xml.png')
- end
-
- def test_asset_host_without_protocol_should_be_protocol_relative_even_if_path_present
- @controller.config.asset_host = 'a.example.com/files/go/here'
- assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_path('xml.png')
- assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_url('xml.png')
- end
-
- def test_assert_css_and_js_of_the_same_name_return_correct_extension
- assert_dom_equal(%(/collaboration/hieraki/javascripts/foo.js), javascript_path("foo"))
- assert_dom_equal(%(/collaboration/hieraki/stylesheets/foo.css), stylesheet_path("foo"))
- end
-end
diff --git a/actionpack/test/template/atom_feed_helper_test.rb b/actionpack/test/template/atom_feed_helper_test.rb
deleted file mode 100644
index 89aae4ac56..0000000000
--- a/actionpack/test/template/atom_feed_helper_test.rb
+++ /dev/null
@@ -1,349 +0,0 @@
-require 'abstract_unit'
-
-class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at)
- extend ActiveModel::Naming
- include ActiveModel::Conversion
-
- def persisted?
- false
- end
-end
-
-class ScrollsController < ActionController::Base
- FEEDS = {}
- FEEDS["defaults"] = <<-EOT
- atom_feed(:schema_date => '2008') do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["entry_options"] = <<-EOT
- atom_feed do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["entry_type_options"] = <<-EOT
- atom_feed(:schema_date => '2008') do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll, :type => 'text/xml') do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["xml_block"] = <<-EOT
- atom_feed do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- feed.author do |author|
- author.name("DHH")
- end
-
- @scrolls.each do |scroll|
- feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
- end
- end
- end
- EOT
- FEEDS["feed_with_atomPub_namespace"] = <<-EOT
- atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
- 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
- entry.tag!('app:edited', Time.now)
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["feed_with_overridden_ids"] = <<-EOT
- atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
- entry.tag!('app:edited', Time.now)
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["feed_with_xml_processing_instructions"] = <<-EOT
- atom_feed(:schema_date => '2008',
- :instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["feed_with_xml_processing_instructions_duplicate_targets"] = <<-EOT
- atom_feed(:schema_date => '2008',
- :instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["feed_with_xhtml_content"] = <<-'EOT'
- atom_feed do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.summary(:type => 'xhtml') do |xhtml|
- xhtml.p "before #{scroll.id}"
- xhtml.p {xhtml << scroll.body}
- xhtml.p "after #{scroll.id}"
- end
- entry.tag!('app:edited', Time.now)
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- FEEDS["provide_builder"] = <<-'EOT'
- # we pass in the new_xml to the helper so it doesn't
- # call anything on the original builder
- new_xml = Builder::XmlMarkup.new(:target=>'')
- atom_feed(:xml => new_xml) do |feed|
- feed.title("My great blog!")
- feed.updated((@scrolls.first.created_at))
-
- @scrolls.each do |scroll|
- feed.entry(scroll) do |entry|
- entry.title(scroll.title)
- entry.content(scroll.body, :type => 'html')
-
- entry.author do |author|
- author.name("DHH")
- end
- end
- end
- end
- EOT
- def index
- @scrolls = [
- Scroll.new(1, "1", "Hello One", "Something <i>COOL!</i>", Time.utc(2007, 12, 12, 15), Time.utc(2007, 12, 12, 15)),
- Scroll.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)),
- ]
-
- render :inline => FEEDS[params[:id]], :type => :builder
- end
-end
-
-class AtomFeedTest < ActionController::TestCase
- tests ScrollsController
-
- def setup
- super
- @request.host = "www.nextangle.com"
- end
-
- def test_feed_should_use_default_language_if_none_is_given
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_match(%r{xml:lang="en-US"}, @response.body)
- end
- end
-
- def test_feed_should_include_two_entries
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "entry", 2
- end
- end
-
- def test_entry_should_only_use_published_if_created_at_is_present
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "published", 1
- end
- end
-
- def test_providing_builder_to_atom_feed
- with_restful_routing(:scrolls) do
- get :index, :id=>"provide_builder"
- # because we pass in the non-default builder, the content generated by the
- # helper should go 'nowhere'. Leaving the response body blank.
- assert_blank @response.body
- end
- end
-
- def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record
- with_restful_routing(:scrolls) do
- get :index, :id => "entry_options"
-
- assert_select "updated", Time.utc(2007, 1, 1).xmlschema
- assert_select "updated", Time.utc(2007, 1, 2).xmlschema
- end
- end
-
- def test_self_url_should_default_to_current_request_url
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]"
- end
- end
-
- def test_feed_id_should_be_a_valid_tag
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "id", :text => "tag:www.nextangle.com,2008:/scrolls?id=defaults"
- end
- end
-
- def test_entry_id_should_be_a_valid_tag
- with_restful_routing(:scrolls) do
- get :index, :id => "defaults"
- assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/1"
- assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/2"
- end
- end
-
- def test_feed_should_allow_nested_xml_blocks
- with_restful_routing(:scrolls) do
- get :index, :id => "xml_block"
- assert_select "author name", :text => "DHH"
- end
- end
-
- def test_feed_should_include_atomPub_namespace
- with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_atomPub_namespace"
- assert_match %r{xml:lang="en-US"}, @response.body
- assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body
- assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body
- end
- end
-
- def test_feed_should_allow_overriding_ids
- with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_overridden_ids"
- assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/"
- assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1"
- assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2"
- end
- end
-
- def test_feed_xml_processing_instructions
- with_restful_routing(:scrolls) do
- get :index, :id => 'feed_with_xml_processing_instructions'
- assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body
- assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body
- end
- end
-
- def test_feed_xml_processing_instructions_duplicate_targets
- with_restful_routing(:scrolls) do
- get :index, :id => 'feed_with_xml_processing_instructions_duplicate_targets'
- assert_match %r{<\?target1 (a="1" b="2"|b="2" a="1")\?>}, @response.body
- assert_match %r{<\?target1 (c="3" d="4"|d="4" c="3")\?>}, @response.body
- end
- end
-
- def test_feed_xhtml
- with_restful_routing(:scrolls) do
- get :index, :id => "feed_with_xhtml_content"
- assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
- assert_select "summary div p", :text => "Something Boring"
- assert_select "summary div p", :text => "after 2"
- end
- end
-
- def test_feed_entry_type_option_default_to_text_html
- with_restful_routing(:scrolls) do
- get :index, :id => 'defaults'
- assert_select "entry link[rel=alternate][type=text/html]"
- end
- end
-
- def test_feed_entry_type_option_specified
- with_restful_routing(:scrolls) do
- get :index, :id => 'entry_type_options'
- assert_select "entry link[rel=alternate][type=text/xml]"
- end
- end
-
- private
- def with_restful_routing(resources)
- with_routing do |set|
- set.draw do
- resources(resources)
- end
- yield
- end
- end
-end
diff --git a/actionpack/test/template/benchmark_helper_test.rb b/actionpack/test/template/benchmark_helper_test.rb
deleted file mode 100644
index 8c198d2562..0000000000
--- a/actionpack/test/template/benchmark_helper_test.rb
+++ /dev/null
@@ -1,24 +0,0 @@
-require 'abstract_unit'
-require 'stringio'
-
-class BenchmarkHelperTest < ActionView::TestCase
- include RenderERBUtils
- tests ActionView::Helpers::BenchmarkHelper
-
- def test_output_in_erb
- output = render_erb("Hello <%= benchmark do %>world<% end %>")
- expected = 'Hello world'
- assert_equal expected, output
- end
-
- def test_returns_value_from_block
- assert_equal 'test', benchmark { 'test' }
- end
-
- def test_default_message
- log = StringIO.new
- self.stubs(:logger).returns(Logger.new(log))
- benchmark {}
- assert_match(/Benchmarking \(\d+.\d+ms\)/, log.rewind && log.read)
- end
-end
diff --git a/actionpack/test/template/capture_helper_test.rb b/actionpack/test/template/capture_helper_test.rb
deleted file mode 100644
index 234ac3252d..0000000000
--- a/actionpack/test/template/capture_helper_test.rb
+++ /dev/null
@@ -1,238 +0,0 @@
-require 'abstract_unit'
-
-class CaptureHelperTest < ActionView::TestCase
- def setup
- super
- @av = ActionView::Base.new
- @view_flow = ActionView::OutputFlow.new
- end
-
- def test_capture_captures_the_temporary_output_buffer_in_its_block
- assert_nil @av.output_buffer
- string = @av.capture do
- @av.output_buffer << 'foo'
- @av.output_buffer << 'bar'
- end
- assert_nil @av.output_buffer
- assert_equal 'foobar', string
- end
-
- def test_capture_captures_the_value_returned_by_the_block_if_the_temporary_buffer_is_blank
- string = @av.capture('foo', 'bar') do |a, b|
- a + b
- end
- assert_equal 'foobar', string
- end
-
- def test_capture_returns_nil_if_the_returned_value_is_not_a_string
- assert_nil @av.capture { 1 }
- end
-
- def test_capture_escapes_html
- string = @av.capture { '<em>bar</em>' }
- assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
- end
-
- def test_capture_doesnt_escape_twice
- string = @av.capture { '&lt;em&gt;bar&lt;/em&gt;'.html_safe }
- assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
- end
-
- def test_capture_used_for_read
- content_for :foo, "foo"
- assert_equal "foo", content_for(:foo)
-
- content_for(:bar){ "bar" }
- assert_equal "bar", content_for(:bar)
- end
-
- def test_content_for_with_multiple_calls
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title, 'bar'
- assert_equal 'foobar', content_for(:title)
- end
-
- def test_content_for_with_multiple_calls_and_flush
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title, 'bar', flush: true
- assert_equal 'bar', content_for(:title)
- end
-
- def test_content_for_with_block
- assert ! content_for?(:title)
- content_for :title do
- output_buffer << 'foo'
- output_buffer << 'bar'
- nil
- end
- assert_equal 'foobar', content_for(:title)
- end
-
- def test_content_for_with_block_and_multiple_calls_with_flush
- assert ! content_for?(:title)
- content_for :title do
- 'foo'
- end
- content_for :title, flush: true do
- 'bar'
- end
- assert_equal 'bar', content_for(:title)
- end
-
- def test_content_for_with_block_and_multiple_calls_with_flush_nil_content
- assert ! content_for?(:title)
- content_for :title do
- 'foo'
- end
- content_for :title, nil, flush: true do
- 'bar'
- end
- assert_equal 'bar', content_for(:title)
- end
-
- def test_content_for_with_block_and_multiple_calls_without_flush
- assert ! content_for?(:title)
- content_for :title do
- 'foo'
- end
- content_for :title, flush: false do
- 'bar'
- end
- assert_equal 'foobar', content_for(:title)
- end
-
- def test_content_for_with_whitespace_block
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title do
- output_buffer << " \n "
- nil
- end
- content_for :title, 'bar'
- assert_equal 'foobar', content_for(:title)
- end
-
- def test_content_for_with_whitespace_block_and_flush
- assert ! content_for?(:title)
- content_for :title, 'foo'
- content_for :title, flush: true do
- output_buffer << " \n "
- nil
- end
- content_for :title, 'bar', flush: true
- assert_equal 'bar', content_for(:title)
- end
-
- def test_content_for_returns_nil_when_writing
- assert ! content_for?(:title)
- assert_equal nil, content_for(:title, 'foo')
- assert_equal nil, content_for(:title) { output_buffer << 'bar'; nil }
- assert_equal nil, content_for(:title) { output_buffer << " \n "; nil }
- assert_equal 'foobar', content_for(:title)
- assert_equal nil, content_for(:title, 'foo', flush: true)
- assert_equal nil, content_for(:title, flush: true) { output_buffer << 'bar'; nil }
- assert_equal nil, content_for(:title, flush: true) { output_buffer << " \n "; nil }
- assert_equal 'bar', content_for(:title)
- end
-
- def test_content_for_question_mark
- assert ! content_for?(:title)
- content_for :title, 'title'
- assert content_for?(:title)
- assert ! content_for?(:something_else)
- end
-
- def test_provide
- assert !content_for?(:title)
- provide :title, "hi"
- assert content_for?(:title)
- assert_equal "hi", content_for(:title)
- provide :title, "<p>title</p>"
- assert_equal "hi&lt;p&gt;title&lt;/p&gt;", content_for(:title)
-
- @view_flow = ActionView::OutputFlow.new
- provide :title, "hi"
- provide :title, "<p>title</p>".html_safe
- assert_equal "hi<p>title</p>", content_for(:title)
- end
-
- def test_with_output_buffer_swaps_the_output_buffer_given_no_argument
- assert_nil @av.output_buffer
- buffer = @av.with_output_buffer do
- @av.output_buffer << '.'
- end
- assert_equal '.', buffer
- assert_nil @av.output_buffer
- end
-
- def test_with_output_buffer_swaps_the_output_buffer_with_an_argument
- assert_nil @av.output_buffer
- buffer = ActionView::OutputBuffer.new('.')
- @av.with_output_buffer(buffer) do
- @av.output_buffer << '.'
- end
- assert_equal '..', buffer
- assert_nil @av.output_buffer
- end
-
- def test_with_output_buffer_restores_the_output_buffer
- buffer = ActionView::OutputBuffer.new
- @av.output_buffer = buffer
- @av.with_output_buffer do
- @av.output_buffer << '.'
- end
- assert buffer.equal?(@av.output_buffer)
- end
-
- def test_with_output_buffer_sets_proper_encoding
- @av.output_buffer = ActionView::OutputBuffer.new
-
- # Ensure we set the output buffer to an encoding different than the default one.
- alt_encoding = alt_encoding(@av.output_buffer)
- @av.output_buffer.force_encoding(alt_encoding)
-
- @av.with_output_buffer do
- assert_equal alt_encoding, @av.output_buffer.encoding
- end
- end
-
- def test_with_output_buffer_does_not_assume_there_is_an_output_buffer
- assert_nil @av.output_buffer
- 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
-
- def view_with_controller
- TestController.new.view_context.tap do |view|
- view.output_buffer = ActionView::OutputBuffer.new
- end
- end
-end
diff --git a/actionpack/test/template/compiled_templates_test.rb b/actionpack/test/template/compiled_templates_test.rb
deleted file mode 100644
index f5dc2fbb33..0000000000
--- a/actionpack/test/template/compiled_templates_test.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-
-class CompiledTemplatesTest < ActiveSupport::TestCase
- def setup
- # Clean up any details key cached to expose failures
- # that otherwise would appear just on isolated tests
- 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
- assert_equal "one", render(:file => "test/render_file_with_locals_and_default")
- assert_equal "two", render(:file => "test/render_file_with_locals_and_default", :locals => { :secret => "two" })
- end
-
- def test_template_changes_are_not_reflected_with_cached_templates
- assert_equal "Hello world!", render(:file => "test/hello_world")
- modify_template "test/hello_world.erb", "Goodbye world!" do
- assert_equal "Hello world!", render(:file => "test/hello_world")
- end
- assert_equal "Hello world!", render(:file => "test/hello_world")
- end
-
- def test_template_changes_are_reflected_with_uncached_templates
- assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
- modify_template "test/hello_world.erb", "Goodbye world!" do
- assert_equal "Goodbye world!", render_without_cache(:file => "test/hello_world")
- end
- assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
- end
-
- private
- def render(*args)
- render_with_cache(*args)
- end
-
- def render_with_cache(*args)
- view_paths = ActionController::Base.view_paths
- ActionView::Base.new(view_paths, {}).render(*args)
- end
-
- def render_without_cache(*args)
- path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
- view_paths = ActionView::PathSet.new([path])
- ActionView::Base.new(view_paths, {}).render(*args)
- end
-
- def modify_template(template, content)
- filename = "#{FIXTURE_LOAD_PATH}/#{template}"
- old_content = File.read(filename)
- begin
- File.open(filename, "wb+") { |f| f.write(content) }
- yield
- ensure
- File.open(filename, "wb+") { |f| f.write(old_content) }
- end
- end
-end
diff --git a/actionpack/test/template/date_helper_i18n_test.rb b/actionpack/test/template/date_helper_i18n_test.rb
deleted file mode 100644
index 63066d40cd..0000000000
--- a/actionpack/test/template/date_helper_i18n_test.rb
+++ /dev/null
@@ -1,134 +0,0 @@
-require 'abstract_unit'
-
-class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
- include ActionView::Helpers::DateHelper
- attr_reader :request
-
- def setup
- @from = Time.utc(2004, 6, 6, 21, 45, 0)
- end
-
- # distance_of_time_in_words
-
- def test_distance_of_time_in_words_calls_i18n
- { # with include_seconds
- [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5],
- [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10],
- [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20],
- [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil],
- [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1],
- [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1],
-
- # without include_seconds
- [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1],
- [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1],
- [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44],
- [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1],
- [24.hours, { :include_seconds => false }] => [:'x_days', 1],
- [30.days, { :include_seconds => false }] => [:'about_x_months', 1],
- [60.days, { :include_seconds => false }] => [:'x_months', 2],
- [1.year, { :include_seconds => false }] => [:'about_x_years', 1],
- [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3],
- [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4]
-
- }.each do |passed, expected|
- assert_distance_of_time_in_words_translates_key passed, expected
- end
- end
-
- def assert_distance_of_time_in_words_translates_key(passed, expected)
- diff, passed_options = *passed
- key, count = *expected
- to = @from + diff
-
- options = {:locale => 'en', :scope => :'datetime.distance_in_words'}
- options[:count] = count if count
-
- I18n.expects(:t).with(key, options)
- distance_of_time_in_words(@from, to, passed_options.merge(:locale => 'en'))
- end
-
- def test_time_ago_in_words_passes_locale
- I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru')
- time_ago_in_words(15.seconds.ago, :locale => 'ru')
- end
-
- def test_distance_of_time_pluralizations
- { [:'less_than_x_seconds', 1] => 'less than 1 second',
- [:'less_than_x_seconds', 2] => 'less than 2 seconds',
- [:'less_than_x_minutes', 1] => 'less than a minute',
- [:'less_than_x_minutes', 2] => 'less than 2 minutes',
- [:'x_minutes', 1] => '1 minute',
- [:'x_minutes', 2] => '2 minutes',
- [:'about_x_hours', 1] => 'about 1 hour',
- [:'about_x_hours', 2] => 'about 2 hours',
- [:'x_days', 1] => '1 day',
- [:'x_days', 2] => '2 days',
- [:'about_x_years', 1] => 'about 1 year',
- [:'about_x_years', 2] => 'about 2 years',
- [:'over_x_years', 1] => 'over 1 year',
- [:'over_x_years', 2] => 'over 2 years'
-
- }.each do |args, expected|
- key, count = *args
- assert_equal expected, I18n.t(key, :count => count, :scope => 'datetime.distance_in_words')
- end
- end
-end
-
-class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
- include ActionView::Helpers::DateHelper
- attr_reader :request
-
- def setup
- @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'}
-
- I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
- end
-
- # select_month
-
- def test_select_month_given_use_month_names_option_does_not_translate_monthnames
- I18n.expects(:translate).never
- select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES)
- end
-
- def test_select_month_translates_monthnames
- I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
- select_month(8, :locale => 'en')
- end
-
- def test_select_month_given_use_short_month_option_translates_abbr_monthnames
- I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES
- select_month(8, :locale => 'en', :use_short_month => true)
- end
-
- def test_date_or_time_select_translates_prompts
- @prompt_defaults.each do |key, prompt|
- I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt
- end
-
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day]
- datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true)
- end
-
- # date_or_time_select
-
- def test_date_or_time_select_given_an_order_options_does_not_translate_order
- I18n.expects(:translate).never
- datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en')
- end
-
- def test_date_or_time_select_given_no_order_options_translates_order
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day]
- datetime_select('post', 'updated_at', :locale => 'en')
- end
-
- def test_date_or_time_select_given_invalid_order
- I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:invalid, :month, :day]
-
- assert_raise StandardError do
- datetime_select('post', 'updated_at', :locale => 'en')
- end
- end
-end
diff --git a/actionpack/test/template/date_helper_test.rb b/actionpack/test/template/date_helper_test.rb
deleted file mode 100644
index a4da7cd4b0..0000000000
--- a/actionpack/test/template/date_helper_test.rb
+++ /dev/null
@@ -1,3046 +0,0 @@
-require 'abstract_unit'
-
-class DateHelperTest < ActionView::TestCase
- tests ActionView::Helpers::DateHelper
-
- silence_warnings do
- Post = Struct.new("Post", :id, :written_on, :updated_at)
- Post.class_eval do
- def id
- 123
- end
- def id_before_type_cast
- 123
- end
- def to_param
- '123'
- end
- end
- end
-
- def assert_distance_of_time_in_words(from, to=nil)
- to ||= from
-
- # 0..1 minute with :include_seconds => true
- assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true)
- assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true)
- assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true)
- assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true)
- assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true)
- assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true)
-
- # 0..1 minute with :include_seconds => false
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false)
-
- # Note that we are including a 30-second boundary around the interval we
- # want to test. For instance, "1 minute" is actually 30s to 1m29s. The
- # reason for doing this is simple -- in `distance_of_time_to_words`, when we
- # take the distance between our two Time objects in seconds and convert it
- # to minutes, we round the number. So 29s gets rounded down to 0m, 30s gets
- # rounded up to 1m, and 1m29s gets rounded down to 1m. A similar thing
- # happens with the other cases.
-
- # First case 0..1 minute
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds)
- assert_equal "less than a minute", distance_of_time_in_words(from, to + 29.seconds)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 30.seconds)
- assert_equal "1 minute", distance_of_time_in_words(from, to + 1.minutes + 29.seconds)
-
- # 2 minutes up to 45 minutes
- assert_equal "2 minutes", distance_of_time_in_words(from, to + 1.minutes + 30.seconds)
- assert_equal "44 minutes", distance_of_time_in_words(from, to + 44.minutes + 29.seconds)
-
- # 45 minutes up to 90 minutes
- assert_equal "about 1 hour", distance_of_time_in_words(from, to + 44.minutes + 30.seconds)
- assert_equal "about 1 hour", distance_of_time_in_words(from, to + 89.minutes + 29.seconds)
-
- # 90 minutes up to 24 hours
- assert_equal "about 2 hours", distance_of_time_in_words(from, to + 89.minutes + 30.seconds)
- assert_equal "about 24 hours", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 29.seconds)
-
- # 24 hours up to 42 hours
- assert_equal "1 day", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 30.seconds)
- assert_equal "1 day", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 29.seconds)
-
- # 42 hours up to 30 days
- assert_equal "2 days", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 30.seconds)
- assert_equal "3 days", distance_of_time_in_words(from, to + 2.days + 12.hours)
- assert_equal "30 days", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 29.seconds)
-
- # 30 days up to 60 days
- assert_equal "about 1 month", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 30.seconds)
- assert_equal "about 1 month", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 29.seconds)
- assert_equal "about 2 months", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 30.seconds)
- assert_equal "about 2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds)
-
- # 60 days up to 365 days
- assert_equal "2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 30.seconds)
- assert_equal "12 months", distance_of_time_in_words(from, to + 1.years - 31.seconds)
-
- # >= 365 days
- assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years - 30.seconds)
- assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years + 3.months - 1.day)
- assert_equal "over 1 year", distance_of_time_in_words(from, to + 1.years + 6.months)
-
- assert_equal "almost 2 years", distance_of_time_in_words(from, to + 2.years - 3.months + 1.day)
- assert_equal "about 2 years", distance_of_time_in_words(from, to + 2.years + 3.months - 1.day)
- assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 3.months + 1.day)
- assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 9.months - 1.day)
- assert_equal "almost 3 years", distance_of_time_in_words(from, to + 2.years + 9.months + 1.day)
-
- assert_equal "almost 5 years", distance_of_time_in_words(from, to + 5.years - 3.months + 1.day)
- assert_equal "about 5 years", distance_of_time_in_words(from, to + 5.years + 3.months - 1.day)
- assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 3.months + 1.day)
- assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 9.months - 1.day)
- assert_equal "almost 6 years", distance_of_time_in_words(from, to + 5.years + 9.months + 1.day)
-
- assert_equal "almost 10 years", distance_of_time_in_words(from, to + 10.years - 3.months + 1.day)
- assert_equal "about 10 years", distance_of_time_in_words(from, to + 10.years + 3.months - 1.day)
- assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 3.months + 1.day)
- assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 9.months - 1.day)
- assert_equal "almost 11 years", distance_of_time_in_words(from, to + 10.years + 9.months + 1.day)
-
- # test to < from
- assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to)
- assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false)
- end
-
- def test_distance_in_words
- from = Time.utc(2004, 6, 6, 21, 45, 0)
- assert_distance_of_time_in_words(from)
- end
-
- def test_time_ago_in_words_passes_include_seconds
- assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true)
- assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false)
- end
-
- def test_distance_in_words_with_time_zones
- from = Time.mktime(2004, 6, 6, 21, 45, 0)
- assert_distance_of_time_in_words(from.in_time_zone('Alaska'))
- assert_distance_of_time_in_words(from.in_time_zone('Hawaii'))
- end
-
- def test_distance_in_words_with_different_time_zones
- from = Time.mktime(2004, 6, 6, 21, 45, 0)
- assert_distance_of_time_in_words(
- from.in_time_zone('Alaska'),
- from.in_time_zone('Hawaii')
- )
- end
-
- def test_distance_in_words_with_dates
- start_date = Date.new 1975, 1, 31
- end_date = Date.new 1977, 1, 31
- assert_equal("about 2 years", distance_of_time_in_words(start_date, end_date))
-
- start_date = Date.new 1982, 12, 3
- end_date = Date.new 2010, 11, 30
- assert_equal("almost 28 years", distance_of_time_in_words(start_date, end_date))
- assert_equal("almost 28 years", distance_of_time_in_words(end_date, start_date))
- end
-
- def test_distance_in_words_with_integers
- assert_equal "1 minute", distance_of_time_in_words(59)
- assert_equal "about 1 hour", distance_of_time_in_words(60*60)
- assert_equal "1 minute", distance_of_time_in_words(0, 59)
- assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0)
- assert_equal "about 3 years", distance_of_time_in_words(10**8)
- assert_equal "about 3 years", distance_of_time_in_words(0, 10**8)
- end
-
- def test_distance_in_words_with_times
- assert_equal "1 minute", distance_of_time_in_words(30.seconds)
- assert_equal "1 minute", distance_of_time_in_words(59.seconds)
- assert_equal "2 minutes", distance_of_time_in_words(119.seconds)
- assert_equal "2 minutes", distance_of_time_in_words(1.minute + 59.seconds)
- assert_equal "3 minutes", distance_of_time_in_words(2.minute + 30.seconds)
- assert_equal "44 minutes", distance_of_time_in_words(44.minutes + 29.seconds)
- assert_equal "about 1 hour", distance_of_time_in_words(44.minutes + 30.seconds)
- assert_equal "about 1 hour", distance_of_time_in_words(60.minutes)
-
- # include seconds
- assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true)
- assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true)
- assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true)
- end
-
- def test_time_ago_in_words
- assert_equal "about 1 year", time_ago_in_words(1.year.ago - 1.day)
- end
-
- def test_select_day
- expected = %(<select id="date_day" name="date[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_day(Time.mktime(2003, 8, 16))
- assert_dom_equal expected, select_day(16)
- end
-
- def test_select_day_with_blank
- expected = %(<select id="date_day" name="date[day]">\n)
- expected << %(<option value=""></option>\n<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_day(Time.mktime(2003, 8, 16), :include_blank => true)
- assert_dom_equal expected, select_day(16, :include_blank => true)
- end
-
- def test_select_day_nil_with_blank
- expected = %(<select id="date_day" name="date[day]">\n)
- expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_day(nil, :include_blank => true)
- end
-
- def test_select_day_with_two_digit_numbers
- expected = %(<select id="date_day" name="date[day]">\n)
- expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), :use_two_digit_numbers => true)
- assert_dom_equal expected, select_day(2, :use_two_digit_numbers => true)
- end
-
- def test_select_day_with_html_options
- expected = %(<select id="date_day" name="date[day]" class="selector">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), {}, :class => 'selector')
- assert_dom_equal expected, select_day(16, {}, :class => 'selector')
- end
-
- def test_select_day_with_default_prompt
- expected = %(<select id="date_day" name="date[day]">\n)
- expected << %(<option value="">Day</option>\n<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_day(16, :prompt => true)
- end
-
- def test_select_day_with_custom_prompt
- expected = %(<select id="date_day" name="date[day]">\n)
- expected << %(<option value="">Choose day</option>\n<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_day(16, :prompt => 'Choose day')
- end
-
- def test_select_month
- expected = %(<select id="date_month" name="date[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"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16))
- assert_dom_equal expected, select_month(8)
- end
-
- def test_select_month_with_two_digit_numbers
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), :use_two_digit_numbers => true)
- assert_dom_equal expected, select_month(8, :use_two_digit_numbers => true)
- end
-
- def test_select_month_with_disabled
- expected = %(<select id="date_month" name="date[month]" disabled="disabled">\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"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true)
- assert_dom_equal expected, select_month(8, :disabled => true)
- end
-
- def test_select_month_with_field_name_override
- expected = %(<select id="date_mois" name="date[mois]">\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"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois')
- assert_dom_equal expected, select_month(8, :field_name => 'mois')
- end
-
- def test_select_month_with_blank
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value=""></option>\n<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"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true)
- assert_dom_equal expected, select_month(8, :include_blank => true)
- end
-
- def test_select_month_nil_with_blank
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value=""></option>\n<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">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"
-
- assert_dom_equal expected, select_month(nil, :include_blank => true)
- end
-
- def test_select_month_with_numbers
- expected = %(<select id="date_month" name="date[month]">\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" selected="selected">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)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
- assert_dom_equal expected, select_month(8, :use_month_numbers => true)
- end
-
- def test_select_month_with_numbers_and_names
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="1">1 - January</option>\n<option value="2">2 - February</option>\n<option value="3">3 - March</option>\n<option value="4">4 - April</option>\n<option value="5">5 - May</option>\n<option value="6">6 - June</option>\n<option value="7">7 - July</option>\n<option value="8" selected="selected">8 - August</option>\n<option value="9">9 - September</option>\n<option value="10">10 - October</option>\n<option value="11">11 - November</option>\n<option value="12">12 - December</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
- assert_dom_equal expected, select_month(8, :add_month_numbers => true)
- end
-
- def test_select_month_with_numbers_and_names_with_abbv
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true)
- assert_dom_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true)
- end
-
- def test_select_month_with_abbv
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="1">Jan</option>\n<option value="2">Feb</option>\n<option value="3">Mar</option>\n<option value="4">Apr</option>\n<option value="5">May</option>\n<option value="6">Jun</option>\n<option value="7">Jul</option>\n<option value="8" selected="selected">Aug</option>\n<option value="9">Sep</option>\n<option value="10">Oct</option>\n<option value="11">Nov</option>\n<option value="12">Dec</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true)
- assert_dom_equal expected, select_month(8, :use_short_month => true)
- end
-
- def test_select_month_with_custom_names
- month_names = %w(nil Januar Februar Marts April Maj Juni Juli August September Oktober November December)
-
- expected = %(<select id="date_month" name="date[month]">\n)
- 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month]}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
- assert_dom_equal expected, select_month(8, :use_month_names => month_names)
- end
-
- def test_select_month_with_zero_indexed_custom_names
- month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December)
-
- expected = %(<select id="date_month" name="date[month]">\n)
- 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month-1]}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
- assert_dom_equal expected, select_month(8, :use_month_names => month_names)
- end
-
- def test_select_month_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_month\" name=\"date[month]\" value=\"8\" />\n", select_month(8, :use_hidden => true)
- end
-
- def test_select_month_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_mois\" name=\"date[mois]\" value=\"8\" />\n", select_month(8, :use_hidden => true, :field_name => 'mois')
- end
-
- def test_select_month_with_html_options
- expected = %(<select id="date_month" name="date[month]" class="selector" accesskey="M">\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"
-
- assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
- #result = select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_month" name="date[month]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="1">January')
- end
-
- def test_select_month_with_default_prompt
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="">Month</option>\n<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"
-
- assert_dom_equal expected, select_month(8, :prompt => true)
- end
-
- def test_select_month_with_custom_prompt
- expected = %(<select id="date_month" name="date[month]">\n)
- expected << %(<option value="">Choose month</option>\n<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"
-
- assert_dom_equal expected, select_month(8, :prompt => 'Choose month')
- end
-
- def test_select_year
- expected = %(<select id="date_year" name="date[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"
-
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
- assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
- end
-
- def test_select_year_with_disabled
- expected = %(<select id="date_year" name="date[year]" disabled="disabled">\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"
-
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005)
- assert_dom_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005)
- end
-
- def test_select_year_with_field_name_override
- expected = %(<select id="date_annee" name="date[annee]">\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"
-
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee')
- assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee')
- end
-
- def test_select_year_with_type_discarding
- expected = %(<select id="date_year" name="date_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"
-
- assert_dom_equal expected, select_year(
- Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
- assert_dom_equal expected, select_year(
- 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
- end
-
- def test_select_year_descending
- expected = %(<select id="date_year" name="date[year]">\n)
- expected << %(<option value="2005" selected="selected">2005</option>\n<option value="2004">2004</option>\n<option value="2003">2003</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003)
- assert_dom_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003)
- end
-
- def test_select_year_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_year\" name=\"date[year]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true)
- end
-
- def test_select_year_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_anno\" name=\"date[anno]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true, :field_name => 'anno')
- end
-
- def test_select_year_with_html_options
- expected = %(<select id="date_year" name="date[year]" class="selector" accesskey="M">\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"
-
- assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
- #result = select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_year" name="date[year]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="2003"')
- end
-
- def test_select_year_with_default_prompt
- expected = %(<select id="date_year" name="date[year]">\n)
- expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => true)
- end
-
- def test_select_year_with_custom_prompt
- expected = %(<select id="date_year" name="date[year]">\n)
- expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year')
- end
-
- def test_select_hour
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18))
- end
-
- def test_select_hour_with_ampm
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :ampm => true)
- end
-
- def test_select_hour_with_disabled
- expected = %(<select id="date_hour" name="date[hour]" disabled="disabled">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
- end
-
- def test_select_hour_with_field_name_override
- expected = %(<select id="date_heure" name="date[heure]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure')
- end
-
- def test_select_hour_with_blank
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
- end
-
- def test_select_hour_nil_with_blank
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(nil, :include_blank => true)
- end
-
- def test_select_hour_with_html_options
- expected = %(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
- end
-
- def test_select_hour_with_default_prompt
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
- end
-
- def test_select_hour_with_custom_prompt
- expected = %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour')
- end
-
- def test_select_minute
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18))
- end
-
- def test_select_minute_with_disabled
- expected = %(<select id="date_minute" name="date[minute]" disabled="disabled">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
- end
-
- def test_select_minute_with_field_name_override
- expected = %(<select id="date_minuto" name="date[minuto]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto')
- end
-
- def test_select_minute_with_blank
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
- end
-
- def test_select_minute_with_blank_and_step
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 })
- end
-
- def test_select_minute_nil_with_blank
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(nil, :include_blank => true)
- end
-
- def test_select_minute_nil_with_blank_and_step
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 })
- end
-
- def test_select_minute_with_hidden
- assert_dom_equal "<input type=\"hidden\" id=\"date_minute\" name=\"date[minute]\" value=\"8\" />\n", select_minute(8, :use_hidden => true)
- end
-
- def test_select_minute_with_hidden_and_field_name
- assert_dom_equal "<input type=\"hidden\" id=\"date_minuto\" name=\"date[minuto]\" value=\"8\" />\n", select_minute(8, :use_hidden => true, :field_name => 'minuto')
- end
-
- def test_select_minute_with_html_options
- expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
-
- #result = select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_minute" name="date[minute]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="00">00')
- end
-
- def test_select_minute_with_default_prompt
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
- end
-
- def test_select_minute_with_custom_prompt
- expected = %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute')
- end
-
- def test_select_second
- expected = %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18))
- end
-
- def test_select_second_with_disabled
- expected = %(<select id="date_second" name="date[second]" disabled="disabled">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
- end
-
- def test_select_second_with_field_name_override
- expected = %(<select id="date_segundo" name="date[segundo]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo')
- end
-
- def test_select_second_with_blank
- expected = %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
- end
-
- def test_select_second_nil_with_blank
- expected = %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(nil, :include_blank => true)
- end
-
- def test_select_second_with_html_options
- expected = %(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
-
- #result = select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
- #assert result.include?('<select id="date_second" name="date[second]"')
- #assert result.include?('class="selector"')
- #assert result.include?('accesskey="M"')
- #assert result.include?('<option value="00">00')
- end
-
- def test_select_second_with_default_prompt
- expected = %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
- end
-
- def test_select_second_with_custom_prompt
- expected = %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds')
- end
-
- def test_select_date
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
- end
-
- def test_select_date_with_too_big_range_between_start_year_and_end_year
- assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 20000, :prefix => "date[first]", :order => [:month, :day, :year]) }
- assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => Date.today.year - 100.years, :end_year => 2000, :prefix => "date[first]", :order => [:month, :day, :year]) }
- end
-
- def test_select_date_can_have_more_then_1000_years_interval_if_forced_via_parameter
- assert_nothing_raised { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 3100, :max_years_allowed => 2000) }
- end
-
- def test_select_date_with_order
- expected = %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:month, :day, :year])
- end
-
- def test_select_date_with_incomplete_order
- # Since the order is incomplete nothing will be shown
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
- expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="1" />\n)
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day])
- end
-
- def test_select_date_with_disabled
- expected = %(<select id="date_first_year" name="date[first][year]" disabled="disabled">\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]" disabled="disabled">\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]" disabled="disabled">\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]", :disabled => true)
- end
-
- def test_select_date_with_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) do |y|
- if y == Date.today.year
- expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
- else
- expected << %(<option value="#{y}">#{y}</option>\n)
- end
- end
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(
- Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]"
- )
- end
-
- def test_select_date_with_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- 2003.upto(2008) do |y|
- if y == 2003
- expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
- else
- expected << %(<option value="#{y}">#{y}</option>\n)
- end
- end
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(
- Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]"
- )
- end
-
- def test_select_date_with_no_start_or_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) do |y|
- if y == Date.today.year
- expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
- else
- expected << %(<option value="#{y}">#{y}</option>\n)
- end
- end
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(
- Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]"
- )
- end
-
- def test_select_date_with_zero_value
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003">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]">\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">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
- end
-
- def test_select_date_with_zero_value_and_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
- end
-
- def test_select_date_with_zero_value_and_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- last_year = Time.now.year + 5
- 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
- end
-
- def test_select_date_with_zero_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :prefix => "date[first]")
- end
-
- def test_select_date_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
- end
-
- def test_select_date_with_html_options
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector")
- end
-
- def test_select_date_with_separator
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
- end
-
- def test_select_date_with_separator_and_discard_day
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
- end
-
- def test_select_date_with_separator_discard_month_and_day
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<input type="hidden" id="date_first_month" name="date[first][month]" value="8" />\n)
- expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
- end
-
- def test_select_date_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n)
- expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
-
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :prefix => "date[first]", :use_hidden => true })
- assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true })
- 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)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
- end
-
- def test_select_datetime_with_ampm
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :ampm => true)
- end
-
- def test_select_datetime_with_separators
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :datetime_separator => ' &mdash; ', :time_separator => ' : ')
- end
-
- def test_select_datetime_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
- end
-
- def test_select_datetime_with_html_options
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
-
- expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
- end
-
- def test_select_datetime_with_all_separators
- expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
- expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
- expected << "</select>\n"
-
- expected << "/"
-
- expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << "/"
-
- expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << "&mdash;"
-
- expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << ":"
-
- expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "&mdash;", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
- end
-
- def test_select_datetime_should_work_with_date
- assert_nothing_raised { select_datetime(Date.today) }
- end
-
- def test_select_datetime_with_default_prompt
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="">Year</option>\n<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]">\n)
- expected << %(<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005,
- :prefix => "date[first]", :prompt => true)
- end
-
- def test_select_datetime_with_custom_prompt
-
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="">Choose year</option>\n<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]">\n)
- expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]",
- :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
- end
-
- def test_select_datetime_with_custom_hours
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- expected << %(<option value="">Choose year</option>\n<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]">\n)
- expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="">Choose hour</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :start_hour => 1, :end_hour => 9, :prefix => "date[first]",
- :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
- end
-
- def test_select_datetime_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
- expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
- expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
-
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
- assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "&mdash;", :date_separator => "/",
- :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
- end
-
- def test_select_time
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18))
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false)
- end
-
- def test_select_time_with_ampm
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false, :ampm => true)
- end
-
- def test_select_time_with_separator
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ')
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ', :include_seconds => false)
- end
-
- def test_select_time_with_seconds
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << ' : '
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- expected << ' : '
-
- expected << %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true)
- end
-
- def test_select_time_with_seconds_and_separator
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :time_separator => ' : ')
- end
-
- def test_select_time_with_html_options
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]" class="selector">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector')
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector')
- end
-
- def test_select_time_should_work_with_date
- assert_nothing_raised { select_time(Date.today) }
- end
-
- def test_select_time_with_default_prompt
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :prompt => true)
- end
-
- def test_select_time_with_custom_prompt
- expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
- expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
- expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
-
- expected << %(<select id="date_hour" name="date[hour]">\n)
- expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_minute" name="date[minute]">\n)
- expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_second" name="date[second]">\n)
- expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true, :include_seconds => true,
- :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
- end
-
- def test_select_time_with_hidden
- expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
- expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
- expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
- expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
-
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
- assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
- end
-
- def test_date_select
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on")
- end
-
- def test_date_select_without_day
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :order => [ :month, :year ])
- end
-
- def test_date_select_without_day_and_month
- @post = Post.new
- @post.written_on = Date.new(2004, 2, 29)
-
- expected = "<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n"
- expected << "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
-
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :order => [ :year ])
- end
-
- def test_date_select_without_day_with_separator
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << "/"
-
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :date_separator => '/', :order => [ :month, :year ])
- end
-
- def test_date_select_without_day_and_with_disabled_html_option
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
-
- expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", { :order => [ :month, :year ] }, :disabled => true)
- end
-
- def test_date_select_within_fields_for
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- output_buffer = fields_for :post, @post do |f|
- concat f.date_select(:written_on)
- end
-
- expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
-
- assert_dom_equal(expected, output_buffer)
- end
-
- def test_date_select_within_fields_for_with_index
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
- id = 27
-
- output_buffer = fields_for :post, @post, :index => id do |f|
- concat f.date_select(:written_on)
- end
-
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
-
- assert_dom_equal(expected, output_buffer)
- end
-
- def test_date_select_within_fields_for_with_blank_index
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
- id = nil
-
- output_buffer = fields_for :post, @post, :index => id do |f|
- concat f.date_select(:written_on)
- end
-
- expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
-
- assert_dom_equal(expected, output_buffer)
- end
-
- def test_date_select_with_index
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
- id = 456
-
- expected = %{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_written_on_2i" name="post[#{id}][written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_written_on_3i" name="post[#{id}][written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :index => id)
- end
-
- def test_date_select_with_auto_index
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
- id = 123
-
- expected = %{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_123_written_on_2i" name="post[#{id}][written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_123_written_on_3i" name="post[#{id}][written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post[]", "written_on")
- end
-
- def test_date_select_with_different_order
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :order => [:day, :month, :year])
- end
-
- def test_date_select_with_nil
- @post = Post.new
-
- start_year = Time.now.year-5
- end_year = Time.now.year+5
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.month}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on")
- end
-
- def test_date_select_with_nil_and_blank
- @post = Post.new
-
- start_year = Time.now.year-5
- end_year = Time.now.year+5
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << "<option value=\"\"></option>\n"
- start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << "<option value=\"\"></option>\n"
- 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << "<option value=\"\"></option>\n"
- 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :include_blank => true)
- end
-
- def test_date_select_with_nil_and_blank_and_order
- @post = Post.new
-
- start_year = Time.now.year-5
- end_year = Time.now.year+5
-
- expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << "<option value=\"\"></option>\n"
- start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << "<option value=\"\"></option>\n"
- 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true)
- end
-
- def test_date_select_with_nil_and_blank_and_discard_month
- @post = Post.new
-
- start_year = Time.now.year-5
- end_year = Time.now.year+5
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << "<option value=\"\"></option>\n"
- start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
- expected << '<input name="post[written_on(2i)]" type="hidden" id="post_written_on_2i" value="1"/>' + "\n"
- expected << '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :discard_month => true, :include_blank=>true)
- end
-
- def test_date_select_with_nil_and_blank_and_discard_year
- @post = Post.new
-
- expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1" />' + "\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << "<option value=\"\"></option>\n"
- 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << "<option value=\"\"></option>\n"
- 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :discard_year => true, :include_blank=>true)
- end
-
- def test_date_select_cant_override_discard_hour
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :discard_hour => false)
- end
-
- def test_date_select_with_html_options
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector')
- end
-
- def test_date_select_with_html_options_within_fields_for
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- output_buffer = fields_for :post, @post do |f|
- concat f.date_select(:written_on, {}, :class => 'selector')
- end
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_date_select_with_separator
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " })
- end
-
- def test_date_select_with_separator_and_order
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " })
- end
-
- def test_date_select_with_separator_and_order_and_year_discarded
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
- expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
-
- assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " })
- end
-
- def test_date_select_with_default_prompt
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :prompt => true)
- end
-
- def test_date_select_with_custom_prompt
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
- expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
- expected << %{<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
- expected << %{<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
-
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'})
- end
-
- def test_time_select
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on")
- end
-
- def test_time_select_without_date_hidden_fields
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true)
- end
-
- def test_time_select_with_seconds
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true)
- end
-
- def test_time_select_with_html_options
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector')
- end
-
- def test_time_select_with_html_options_within_fields_for
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- output_buffer = fields_for :post, @post do |f|
- concat f.time_select(:written_on, {}, :class => 'selector')
- end
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_time_select_with_separator
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- expected << " - "
-
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- expected << " - "
-
- expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true })
- end
-
- def test_time_select_with_default_prompt
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- expected << %(<option value="">Hour</option>\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- expected << %(<option value="">Minute</option>\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", :prompt => true)
- end
-
- def test_time_select_with_custom_prompt
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
- expected << %(<option value="">Choose hour</option>\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
- expected << %(<option value="">Choose minute</option>\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'})
- end
-
- def test_time_select_with_disabled_html_option
- @post = Post.new
- @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_written_on_3i" disabled="disabled" name="post[written_on(3i)]" value="15" />\n}
-
- expected << %(<select id="post_written_on_4i" disabled="disabled" name="post[written_on(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %(<select id="post_written_on_5i" disabled="disabled" name="post[written_on(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, time_select("post", "written_on", {}, :disabled => true)
- end
-
- def test_datetime_select
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at")
- end
-
- def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set
- # The love zone is UTC+0
- mytz = Class.new(ActiveSupport::TimeZone) {
- attr_accessor :now
- }.create('tenderlove', 0)
-
- now = Time.mktime(2004, 6, 15, 16, 35, 0)
- mytz.now = now
- Time.zone = mytz
-
- assert_equal mytz, Time.zone
-
- @post = Post.new
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at")
- ensure
- Time.zone = nil
- end
-
- def test_datetime_select_with_html_options_within_fields_for
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
-
- output_buffer = fields_for :post, @post do |f|
- concat f.datetime_select(:updated_at, {}, :class => 'selector')
- end
-
- expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
- expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
- expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
- expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
- expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_datetime_select_with_separators
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << " / "
-
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " , "
-
- expected << %(<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n)
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- expected << " - "
-
- expected << %(<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- expected << " - "
-
- expected << %(<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n)
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true })
- end
-
- def test_datetime_select_with_integer
- @post = Post.new
- @post.updated_at = 3
- datetime_select("post", "updated_at")
- end
-
- def test_datetime_select_with_infinity # Float
- @post = Post.new
- @post.updated_at = (-1.0/0)
- datetime_select("post", "updated_at")
- end
-
- def test_datetime_select_with_default_prompt
- @post = Post.new
- @post.updated_at = nil
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %{<option value="">Month</option>\n<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">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="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %{<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- expected << %{<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- expected << %{<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => true)
- end
-
- def test_datetime_select_with_custom_prompt
- @post = Post.new
- @post.updated_at = nil
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %{<option value="">Choose month</option>\n<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">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="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %{<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- expected << %{<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- expected << %{<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'})
- end
-
- def test_date_select_with_zero_value_and_no_start_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
- end
-
- def test_date_select_with_zero_value_and_no_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- last_year = Time.now.year + 5
- 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
- end
-
- def test_date_select_with_zero_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(0, :prefix => "date[first]")
- end
-
- def test_date_select_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
- end
-
- def test_datetime_select_with_nil_value_and_no_start_and_end_year
- expected = %(<select id="date_first_year" name="date[first][year]">\n)
- (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
- expected << "</select>\n"
-
- expected << %(<select id="date_first_month" name="date[first][month]">\n)
- expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
- expected << "</select>\n"
-
- expected << %(<select id="date_first_day" name="date[first][day]">\n)
- expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
- expected << "</select>\n"
-
- expected << " : "
-
- expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
- expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
- expected << "</select>\n"
-
- assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
- end
-
- def test_datetime_select_with_options_index
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
- id = 456
-
- expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :index => id)
- end
-
- def test_datetime_select_within_fields_for_with_options_index
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
- id = 456
-
- output_buffer = fields_for :post, @post, :index => id do |f|
- concat f.datetime_select(:updated_at)
- end
-
- expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_datetime_select_with_auto_index
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
- id = @post.id
-
- expected = %{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_123_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_123_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_123_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_123_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post[]", "updated_at")
- end
-
- def test_datetime_select_with_seconds
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true)
- end
-
- def test_datetime_select_discard_year
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true)
- end
-
- def test_datetime_select_discard_month
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n}
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true)
- end
-
- def test_datetime_select_discard_year_and_month
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n}
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true)
- end
-
- def test_datetime_select_discard_year_and_month_with_disabled_html_option
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n}
- expected << %{<input type="hidden" id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]" value="6" />\n}
- expected << %{<input type="hidden" id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]" value="1" />\n}
-
- expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", { :discard_year => true, :discard_month => true }, :disabled => true)
- end
-
- def test_datetime_select_discard_hour
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_hour => true)
- end
-
- def test_datetime_select_discard_minute
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << %{<input type="hidden" id="post_updated_at_5i" name="post[updated_at(5i)]" value="16" />\n}
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true)
- end
-
- def test_datetime_select_disabled_and_discard_minute
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << %{<input type="hidden" id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]" value="16" />\n}
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true, :disabled => true)
- end
-
- def test_datetime_select_invalid_order
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second])
- end
-
- def test_datetime_select_discard_with_order
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
-
- expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month])
- end
-
- def test_datetime_select_with_default_value_as_time
- @post = Post.new
- @post.updated_at = nil
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- 2001.upto(2011) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2006}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 9}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 19}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35))
- end
-
- def test_include_blank_overrides_default_option
- @post = Post.new
- @post.updated_at = nil
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- expected << %(<option value=""></option>\n)
- (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- expected << %(<option value=""></option>\n)
- 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- expected << %(<option value=""></option>\n)
- 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, date_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true)
- end
-
- def test_datetime_select_with_default_value_as_hash
- @post = Post.new
- @post.updated_at = nil
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
- (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
- 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 10}>#{Date::MONTHNAMES[i]}</option>\n) }
- expected << "</select>\n"
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
- 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) }
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
- 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 9}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
- 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 42}>#{sprintf("%02d", i)}</option>\n) }
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 })
- end
-
- def test_datetime_select_with_html_options
- @post = Post.new
- @post.updated_at = Time.local(2004, 6, 15, 16, 35)
-
- expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n}
- expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n}
- expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
- expected << "</select>\n"
-
- expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n}
- expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
- expected << "</select>\n"
-
- expected << " &mdash; "
-
- expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
- expected << "</select>\n"
- expected << " : "
- expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n}
- expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
- expected << "</select>\n"
-
- assert_dom_equal expected, datetime_select("post", "updated_at", {}, :class => 'selector')
- end
-
- def test_date_select_should_not_change_passed_options_hash
- @post = Post.new
- @post.updated_at = Time.local(2008, 7, 16, 23, 30)
-
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- date_select(@post, :updated_at, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_datetime_select_should_not_change_passed_options_hash
- @post = Post.new
- @post.updated_at = Time.local(2008, 7, 16, 23, 30)
-
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- datetime_select(@post, :updated_at, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_time_select_should_not_change_passed_options_hash
- @post = Post.new
- @post.updated_at = Time.local(2008, 7, 16, 23, 30)
-
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- time_select(@post, :updated_at, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_select_date_should_not_change_passed_options_hash
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- select_date(Date.today, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_select_datetime_should_not_change_passed_options_hash
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- select_datetime(Time.now, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_select_time_should_not_change_passed_options_hash
- options = {
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }
- select_time(Time.now, options)
-
- # note: the literal hash is intentional to show that the actual options hash isn't modified
- # don't change this!
- assert_equal({
- :order => [ :year, :month, :day ],
- :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
- :discard_type => false,
- :include_blank => false,
- :ignore_date => false,
- :include_seconds => true
- }, options)
- end
-
- def test_select_html_safety
- assert select_day(16).html_safe?
- assert select_month(8).html_safe?
- assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
- assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
-
- assert select_minute(8, :use_hidden => true).html_safe?
- assert select_month(8, :prompt => 'Choose month').html_safe?
-
- assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector').html_safe?
- assert select_date(Time.mktime(2003, 8, 16), :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]").html_safe?
- end
-
- def test_object_select_html_safety
- @post = Post.new
- @post.written_on = Date.new(2004, 6, 15)
-
- assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe?
- assert time_select("post", "written_on", :ignore_date => true).html_safe?
- end
-
- def test_time_tag_with_date
- date = Date.today
- expected = "<time datetime=\"#{date.rfc3339}\">#{I18n.l(date, :format => :long)}</time>"
- assert_equal expected, time_tag(date)
- end
-
- def test_time_tag_with_time
- time = Time.now
- expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :long)}</time>"
- assert_equal expected, time_tag(time)
- end
-
- def test_time_tag_pubdate_option
- assert_match(/<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true))
- end
-
- def test_time_tag_with_given_text
- assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now'))
- end
-
- def test_time_tag_with_given_block
- assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ '<span>Right now</span>'.html_safe })
- end
-
- def test_time_tag_with_different_format
- time = Time.now
- expected = "<time datetime=\"#{time.xmlschema}\">#{I18n.l(time, :format => :short)}</time>"
- assert_equal expected, time_tag(time, :format => :short)
- 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/actionpack/test/template/digestor_test.rb b/actionpack/test/template/digestor_test.rb
deleted file mode 100644
index 8181aa11f7..0000000000
--- a/actionpack/test/template/digestor_test.rb
+++ /dev/null
@@ -1,166 +0,0 @@
-require 'abstract_unit'
-require 'fileutils'
-
-class FixtureTemplate
- attr_reader :source
-
- def initialize(template_path)
- @source = File.read(template_path)
- rescue Errno::ENOENT
- raise ActionView::MissingTemplate.new([], "", [], true, [])
- end
-end
-
-class FixtureFinder
- FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
- TMP_DIR = "#{File.dirname(__FILE__)}/../tmp"
-
- def find(logical_name, keys, partial, options)
- FixtureTemplate.new("#{TMP_DIR}/digestor/#{partial ? logical_name.gsub(%r|/([^/]+)$|, '/_\1') : logical_name}.#{options[:formats].first}.erb")
- end
-end
-
-class TemplateDigestorTest < ActionView::TestCase
- def setup
- FileUtils.cp_r FixtureFinder::FIXTURES_DIR, FixtureFinder::TMP_DIR
- end
-
- def teardown
- FileUtils.rm_r File.join(FixtureFinder::TMP_DIR, "digestor")
- ActionView::Digestor.cache.clear
- end
-
- def test_top_level_change_reflected
- assert_digest_difference("messages/show") do
- change_template("messages/show")
- end
- end
-
- def test_explicit_dependency
- assert_digest_difference("messages/show") do
- change_template("messages/_message")
- end
- end
-
- def test_second_level_dependency
- assert_digest_difference("messages/show") do
- change_template("comments/_comments")
- end
- end
-
- def test_second_level_dependency_within_same_directory
- assert_digest_difference("messages/show") do
- change_template("messages/_header")
- end
- end
-
- def test_third_level_dependency
- assert_digest_difference("messages/show") do
- change_template("comments/_comment")
- end
- end
-
- def test_directory_depth_dependency
- assert_digest_difference("level/below/index") do
- change_template("level/below/_header")
- end
- end
-
- def test_logging_of_missing_template
- assert_logged "Couldn't find template for digesting: messages/something_missing.html" do
- digest("messages/show")
- end
- end
-
- def test_nested_template_directory
- assert_digest_difference("messages/show") do
- change_template("messages/actions/_move")
- end
- end
-
- def test_dont_generate_a_digest_for_missing_templates
- assert_equal '', digest("nothing/there")
- end
-
- def test_collection_dependency
- assert_digest_difference("messages/index") do
- change_template("messages/_message")
- end
-
- assert_digest_difference("messages/index") do
- change_template("events/_event")
- end
- end
-
- def test_collection_derived_from_record_dependency
- assert_digest_difference("messages/show") do
- change_template("events/_event")
- end
- end
-
- def test_extra_whitespace_in_render_partial
- assert_digest_difference("messages/edit") do
- change_template("messages/_form")
- end
- end
-
- def test_extra_whitespace_in_render_named_partial
- assert_digest_difference("messages/edit") do
- change_template("messages/_header")
- end
- end
-
- def test_extra_whitespace_in_render_record
- assert_digest_difference("messages/edit") do
- change_template("messages/_message")
- end
- end
-
- def test_extra_whitespace_in_render_with_parenthesis
- assert_digest_difference("messages/edit") do
- change_template("events/_event")
- end
- end
-
- def test_old_style_hash_in_render_invocation
- assert_digest_difference("messages/edit") do
- change_template("comments/_comment")
- end
- end
-
- private
- def assert_logged(message)
- old_logger = ActionView::Base.logger
- log = StringIO.new
- ActionView::Base.logger = Logger.new(log)
-
- begin
- yield
-
- log.rewind
- assert_match message, log.read
- ensure
- ActionView::Base.logger = old_logger
- end
- end
-
- def assert_digest_difference(template_name)
- previous_digest = digest(template_name)
- ActionView::Digestor.cache.clear
-
- yield
-
- assert previous_digest != digest(template_name), "digest didn't change"
- ActionView::Digestor.cache.clear
- end
-
- def digest(template_name)
- ActionView::Digestor.digest(template_name, :html, FixtureFinder.new)
- end
-
- def change_template(template_name)
- File.open("#{FixtureFinder::TMP_DIR}/digestor/#{template_name}.html.erb", "w") do |f|
- f.write "\nTHIS WAS CHANGED!"
- end
- end
-end
diff --git a/actionpack/test/template/form_collections_helper_test.rb b/actionpack/test/template/form_collections_helper_test.rb
deleted file mode 100644
index c73e80ed88..0000000000
--- a/actionpack/test/template/form_collections_helper_test.rb
+++ /dev/null
@@ -1,336 +0,0 @@
-require 'abstract_unit'
-
-class Category < Struct.new(:id, :name)
-end
-
-class FormCollectionsHelperTest < ActionView::TestCase
- def assert_no_select(selector, value = nil)
- assert_select(selector, :text => value, :count => 0)
- end
-
- def with_collection_radio_buttons(*args, &block)
- @output_buffer = collection_radio_buttons(*args, &block)
- end
-
- def with_collection_check_boxes(*args, &block)
- @output_buffer = collection_check_boxes(*args, &block)
- end
-
- # COLLECTION RADIO BUTTONS
- test 'collection radio accepts a collection and generate inputs from value method' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
-
- assert_select 'input[type=radio][value=true]#user_active_true'
- assert_select 'input[type=radio][value=false]#user_active_false'
- end
-
- test 'collection radio accepts a collection and generate inputs from label method' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
-
- assert_select 'label[for=user_active_true]', 'true'
- assert_select 'label[for=user_active_false]', 'false'
- end
-
- test 'collection radio handles camelized collection values for labels correctly' do
- with_collection_radio_buttons :user, :active, ['Yes', 'No'], :to_s, :to_s
-
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
- end
-
- test 'colection radio should sanitize collection values for labels correctly' do
- with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
- assert_select 'label[for=user_name_099]', '$0.99'
- assert_select 'label[for=user_name_199]', '$1.99'
- end
-
- test 'collection radio accepts checked item' do
- with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => true
-
- assert_select 'input[type=radio][value=true][checked=checked]'
- assert_no_select 'input[type=radio][value=false][checked=checked]'
- end
-
- test 'collection radio accepts multiple disabled items' do
- collection = [[1, true], [0, false], [2, 'other']]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => [true, false]
-
- assert_select 'input[type=radio][value=true][disabled=disabled]'
- assert_select 'input[type=radio][value=false][disabled=disabled]'
- assert_no_select 'input[type=radio][value=other][disabled=disabled]'
- end
-
- test 'collection radio accepts single disable item' do
- collection = [[1, true], [0, false]]
- with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true
-
- assert_select 'input[type=radio][value=true][disabled=disabled]'
- assert_no_select 'input[type=radio][value=false][disabled=disabled]'
- 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'
-
- assert_select 'input[type=radio][value=true].special-radio#user_active_true'
- assert_select 'input[type=radio][value=false].special-radio#user_active_false'
- end
-
- test 'collection radio does not wrap input inside the label' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
-
- assert_select 'input[type=radio] + label'
- assert_no_select 'label input'
- end
-
- test 'collection radio accepts a block to render the label as radio button wrapper' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label { b.radio_button }
- end
-
- assert_select 'label[for=user_active_true] > input#user_active_true[type=radio]'
- assert_select 'label[for=user_active_false] > input#user_active_false[type=radio]'
- end
-
- test 'collection radio accepts a block to change the order of label and radio button' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label + b.radio_button
- end
-
- assert_select 'label[for=user_active_true] + input#user_active_true[type=radio]'
- assert_select 'label[for=user_active_false] + input#user_active_false[type=radio]'
- end
-
- test 'collection radio with block helpers accept extra html options' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => "radio_button") + b.radio_button(:class => "radio_button")
- end
-
- assert_select 'label.radio_button[for=user_active_true] + input#user_active_true.radio_button[type=radio]'
- assert_select 'label.radio_button[for=user_active_false] + input#user_active_false.radio_button[type=radio]'
- end
-
- test 'collection radio with block helpers allows access to current text and value' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:"data-value" => b.value) { b.radio_button + b.text }
- end
-
- assert_select 'label[for=user_active_true][data-value=true]', 'true' do
- assert_select 'input#user_active_true[type=radio]'
- end
- assert_select 'label[for=user_active_false][data-value=false]', 'false' do
- assert_select 'input#user_active_false[type=radio]'
- end
- end
-
- test 'collection radio with block helpers allows access to the current object item in the collection to access extra properties' do
- with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => b.object) { b.radio_button + b.text }
- end
-
- assert_select 'label.true[for=user_active_true]', 'true' do
- assert_select 'input#user_active_true[type=radio]'
- end
- assert_select 'label.false[for=user_active_false]', 'false' do
- assert_select 'input#user_active_false[type=radio]'
- end
- end
-
- test 'collection radio buttons with fields for' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- @output_buffer = fields_for(:post) do |p|
- p.collection_radio_buttons :category_id, collection, :id, :name
- end
-
- assert_select 'input#post_category_id_1[type=radio][value=1]'
- assert_select 'input#post_category_id_2[type=radio][value=2]'
-
- assert_select 'label[for=post_category_id_1]', 'Category 1'
- assert_select 'label[for=post_category_id_2]', 'Category 2'
- end
-
- # COLLECTION CHECK BOXES
- test 'collection check boxes accepts a collection and generate a serie of checkboxes for value method' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- with_collection_check_boxes :user, :category_ids, collection, :id, :name
-
- assert_select 'input#user_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#user_category_ids_2[type=checkbox][value=2]'
- end
-
- test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- with_collection_check_boxes :user, :category_ids, collection, :id, :name
-
- assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
- end
-
- test 'collection check boxes accepts a collection and generate a serie of checkboxes with labels for label method' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- with_collection_check_boxes :user, :category_ids, collection, :id, :name
-
- assert_select 'label[for=user_category_ids_1]', 'Category 1'
- assert_select 'label[for=user_category_ids_2]', 'Category 2'
- end
-
- test 'collection check boxes handles camelized collection values for labels correctly' do
- with_collection_check_boxes :user, :active, ['Yes', 'No'], :to_s, :to_s
-
- assert_select 'label[for=user_active_yes]', 'Yes'
- assert_select 'label[for=user_active_no]', 'No'
- end
-
- test 'colection check box should sanitize collection values for labels correctly' do
- with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
- assert_select 'label[for=user_name_099]', '$0.99'
- assert_select 'label[for=user_name_199]', '$1.99'
- end
-
- test 'collection check boxes accepts selected values as :checked option' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
-
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
- end
-
- test 'collection check boxes accepts selected string values as :checked option' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
-
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
- end
-
- test 'collection check boxes accepts a single checked value' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
-
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=1][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
- end
-
- test 'collection check boxes accepts selected values as :checked option and override the model values' do
- user = Struct.new(:category_ids).new(2)
- collection = (1..3).map{|i| [i, "Category #{i}"] }
-
- @output_buffer = fields_for(:user, user) do |p|
- p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
- end
-
- assert_select 'input[type=checkbox][value=1][checked=checked]'
- assert_select 'input[type=checkbox][value=3][checked=checked]'
- assert_no_select 'input[type=checkbox][value=2][checked=checked]'
- end
-
- test 'collection check boxes accepts multiple disabled items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
-
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
- end
-
- test 'collection check boxes accepts single disable item' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
-
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
- end
-
- test 'collection check boxes accepts a proc to disabled items' do
- collection = (1..3).map{|i| [i, "Category #{i}"] }
- with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
-
- assert_select 'input[type=checkbox][value=1][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
- assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
- 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'
-
- assert_select 'input.check[type=checkbox][value=1]'
- assert_select 'input.check[type=checkbox][value=2]'
- end
-
- test 'collection check boxes with fields for' do
- collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
- @output_buffer = fields_for(:post) do |p|
- p.collection_check_boxes :category_ids, collection, :id, :name
- end
-
- assert_select 'input#post_category_ids_1[type=checkbox][value=1]'
- assert_select 'input#post_category_ids_2[type=checkbox][value=2]'
-
- assert_select 'label[for=post_category_ids_1]', 'Category 1'
- assert_select 'label[for=post_category_ids_2]', 'Category 2'
- end
-
- test 'collection check boxes does not wrap input inside the label' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s
-
- assert_select 'input[type=checkbox] + label'
- assert_no_select 'label input'
- end
-
- test 'collection check boxes accepts a block to render the label as check box wrapper' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label { b.check_box }
- end
-
- assert_select 'label[for=user_active_true] > input#user_active_true[type=checkbox]'
- assert_select 'label[for=user_active_false] > input#user_active_false[type=checkbox]'
- end
-
- test 'collection check boxes accepts a block to change the order of label and check box' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label + b.check_box
- end
-
- assert_select 'label[for=user_active_true] + input#user_active_true[type=checkbox]'
- assert_select 'label[for=user_active_false] + input#user_active_false[type=checkbox]'
- end
-
- test 'collection check boxes with block helpers accept extra html options' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => "check_box") + b.check_box(:class => "check_box")
- end
-
- assert_select 'label.check_box[for=user_active_true] + input#user_active_true.check_box[type=checkbox]'
- assert_select 'label.check_box[for=user_active_false] + input#user_active_false.check_box[type=checkbox]'
- end
-
- test 'collection check boxes with block helpers allows access to current text and value' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:"data-value" => b.value) { b.check_box + b.text }
- end
-
- assert_select 'label[for=user_active_true][data-value=true]', 'true' do
- assert_select 'input#user_active_true[type=checkbox]'
- end
- assert_select 'label[for=user_active_false][data-value=false]', 'false' do
- assert_select 'input#user_active_false[type=checkbox]'
- end
- end
-
- test 'collection check boxes with block helpers allows access to the current object item in the collection to access extra properties' do
- with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
- b.label(:class => b.object) { b.check_box + b.text }
- end
-
- assert_select 'label.true[for=user_active_true]', 'true' do
- assert_select 'input#user_active_true[type=checkbox]'
- end
- assert_select 'label.false[for=user_active_false]', 'false' do
- assert_select 'input#user_active_false[type=checkbox]'
- end
- end
-end
diff --git a/actionpack/test/template/form_helper_test.rb b/actionpack/test/template/form_helper_test.rb
deleted file mode 100644
index 246c4bfada..0000000000
--- a/actionpack/test/template/form_helper_test.rb
+++ /dev/null
@@ -1,2713 +0,0 @@
-require 'abstract_unit'
-require 'controller/fake_models'
-
-class FormHelperTest < ActionView::TestCase
- include RenderERBUtils
-
- tests ActionView::Helpers::FormHelper
-
- def form_for(*)
- @output_buffer = super
- end
-
- def setup
- super
-
- # Create "label" locale for testing I18n label helpers
- I18n.backend.store_translations 'label', {
- :activemodel => {
- :attributes => {
- :post => {
- :cost => "Total cost"
- }
- }
- },
- :helpers => {
- :label => {
- :post => {
- :body => "Write entire text here",
- :color => {
- :red => "Rojo"
- },
- :comments => {
- :body => "Write body here"
- }
- },
- :tag => {
- :value => "Tag"
- }
- }
- }
- }
-
- # Create "submit" locale for testing I18n submit helpers
- I18n.backend.store_translations 'submit', {
- :helpers => {
- :submit => {
- :create => 'Create %{model}',
- :update => 'Confirm %{model} changes',
- :submit => 'Save changes',
- :another_post => {
- :update => 'Update your %{model}'
- }
- }
- }
- }
-
- @post = Post.new
- @comment = Comment.new
- def @post.errors()
- Class.new{
- def [](field); field == "author_name" ? ["can't be empty"] : [] end
- def empty?() false end
- def count() 1 end
- def full_messages() [ "Author name can't be empty" ] end
- }.new
- end
- def @post.to_key; [123]; end
- def @post.id_before_type_cast; 123; end
- def @post.to_param; '123'; end
-
- @post.persisted = true
- @post.title = "Hello World"
- @post.author_name = ""
- @post.body = "Back to the hill and over it again!"
- @post.secret = 1
- @post.written_on = Date.new(2004, 6, 15)
-
- @post.comments = []
- @post.comments << @comment
-
- @post.tags = []
- @post.tags << Tag.new
-
- @car = Car.new("#000FFF")
- end
-
- Routes = ActionDispatch::Routing::RouteSet.new
- Routes.draw do
- resources :posts do
- resources :comments
- end
-
- namespace :admin do
- resources :posts do
- resources :comments
- end
- end
-
- get "/foo", :to => "controller#action"
- root :to => "main#index"
- end
-
- def _routes
- Routes
- end
-
- include Routes.url_helpers
-
- def url_for(object)
- @url_for_options = object
- if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank?
- object.merge!(:controller => "main", :action => "index")
- end
- super
- end
-
- class FooTag < ActionView::Helpers::Tags::Base
- def initialize; end
- end
-
- def test_tags_base_child_without_render_method
- assert_raise(NotImplementedError) { FooTag.new.render }
- end
-
- def test_label
- assert_dom_equal('<label for="post_title">Title</label>', label("post", "title"))
- assert_dom_equal('<label for="post_title">The title goes here</label>', label("post", "title", "The title goes here"))
- assert_dom_equal(
- '<label class="title_label" for="post_title">Title</label>',
- label("post", "title", nil, :class => 'title_label')
- )
- assert_dom_equal('<label for="post_secret">Secret?</label>', label("post", "secret?"))
- end
-
- def test_label_with_symbols
- assert_dom_equal('<label for="post_title">Title</label>', label(:post, :title))
- assert_dom_equal('<label for="post_secret">Secret?</label>', label(:post, :secret?))
- 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
- 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
- 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
- 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
- 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
- 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)
- 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
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
- 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)
- end
- 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
- end
-
- def test_label_with_for_attribute_as_symbol
- assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, :for => "my_for"))
- end
-
- def test_label_with_for_attribute_as_string
- assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, "for" => "my_for"))
- end
-
- def test_label_does_not_generate_for_attribute_when_given_nil
- assert_dom_equal('<label>Title</label>', label(:post, :title, :for => nil))
- end
-
- def test_label_with_id_attribute_as_symbol
- assert_dom_equal('<label for="post_title" id="my_id">Title</label>', label(:post, :title, nil, :id => "my_id"))
- end
-
- def test_label_with_id_attribute_as_string
- assert_dom_equal('<label for="post_title" id="my_id">Title</label>', label(:post, :title, nil, "id" => "my_id"))
- end
-
- def test_label_with_for_and_id_attributes_as_symbol
- assert_dom_equal('<label for="my_for" id="my_id">Title</label>', label(:post, :title, nil, :for => "my_for", :id => "my_id"))
- end
-
- def test_label_with_for_and_id_attributes_as_string
- assert_dom_equal('<label for="my_for" id="my_id">Title</label>', label(:post, :title, nil, "for" => "my_for", "id" => "my_id"))
- end
-
- def test_label_for_radio_buttons_with_value
- assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great_title"))
- assert_dom_equal('<label for="post_title_great_title">The title goes here</label>', label("post", "title", "The title goes here", :value => "great title"))
- end
-
- def test_label_with_block
- assert_dom_equal('<label for="post_title">The title, please:</label>', label(:post, :title) { "The title, please:" })
- end
-
- def test_label_with_block_and_options
- assert_dom_equal('<label for="my_for">The title, please:</label>', label(:post, :title, "for" => "my_for") { "The title, please:" })
- end
-
- def test_label_with_block_in_erb
- assert_equal "<label for=\"post_message\">\n Message\n <input id=\"post_message\" name=\"post[message]\" type=\"text\" />\n</label>", view.render("test/label_with_block")
- end
-
- def test_text_field
- assert_dom_equal(
- '<input id="post_title" name="post[title]" type="text" value="Hello World" />', text_field("post", "title")
- )
- assert_dom_equal(
- '<input id="post_title" name="post[title]" type="password" />', password_field("post", "title")
- )
- assert_dom_equal(
- '<input id="post_title" name="post[title]" type="password" value="Hello World" />', password_field("post", "title", :value => @post.title)
- )
- assert_dom_equal(
- '<input id="person_name" name="person[name]" type="password" />', password_field("person", "name")
- )
- end
-
- def test_text_field_with_escapes
- @post.title = "<b>Hello World</b>"
- assert_dom_equal(
- '<input id="post_title" name="post[title]" type="text" value="&lt;b&gt;Hello World&lt;/b&gt;" />', text_field("post", "title")
- )
- end
-
- def test_text_field_with_html_entities
- @post.title = "The HTML Entity for & is &amp;"
- assert_dom_equal(
- '<input id="post_title" name="post[title]" type="text" value="The HTML Entity for &amp; is &amp;amp;" />',
- text_field("post", "title")
- )
- end
-
- def test_text_field_with_options
- expected = '<input id="post_title" name="post[title]" size="35" type="text" value="Hello World" />'
- assert_dom_equal expected, text_field("post", "title", "size" => 35)
- assert_dom_equal expected, text_field("post", "title", :size => 35)
- end
-
- def test_text_field_assuming_size
- expected = '<input id="post_title" maxlength="35" name="post[title]" size="35" type="text" value="Hello World" />'
- assert_dom_equal expected, text_field("post", "title", "maxlength" => 35)
- assert_dom_equal expected, text_field("post", "title", :maxlength => 35)
- end
-
- def test_text_field_removing_size
- expected = '<input id="post_title" maxlength="35" name="post[title]" type="text" value="Hello World" />'
- assert_dom_equal expected, text_field("post", "title", "maxlength" => 35, "size" => nil)
- assert_dom_equal expected, text_field("post", "title", :maxlength => 35, :size => nil)
- end
-
- def test_text_field_with_nil_value
- expected = '<input id="post_title" name="post[title]" type="text" />'
- assert_dom_equal expected, text_field("post", "title", :value => nil)
- end
-
- def test_text_field_with_nil_name
- expected = '<input id="post_title" type="text" value="Hello World" />'
- assert_dom_equal expected, text_field("post", "title", :name => nil)
- end
-
- def test_text_field_doesnt_change_param_values
- object_name = 'post[]'
- expected = '<input id="post_123_title" name="post[123][title]" type="text" value="Hello World" />'
- assert_equal expected, text_field(object_name, "title")
- assert_equal object_name, "post[]"
- end
-
- def test_file_field_has_no_size
- expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
- assert_dom_equal expected, file_field("user", "avatar")
- end
-
- def test_hidden_field
- assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="Hello World" />',
- hidden_field("post", "title")
- assert_dom_equal '<input id="post_secret" name="post[secret]" type="hidden" value="1" />',
- hidden_field("post", "secret?")
- end
-
- def test_hidden_field_with_escapes
- @post.title = "<b>Hello World</b>"
- assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="&lt;b&gt;Hello World&lt;/b&gt;" />',
- hidden_field("post", "title")
- end
-
- def test_hidden_field_with_nil_value
- expected = '<input id="post_title" name="post[title]" type="hidden" />'
- assert_dom_equal expected, hidden_field("post", "title", :value => nil)
- end
-
- def test_hidden_field_with_options
- assert_dom_equal '<input id="post_title" name="post[title]" type="hidden" value="Something Else" />',
- hidden_field("post", "title", :value => "Something Else")
- end
-
- def test_text_field_with_custom_type
- assert_dom_equal '<input id="user_email" name="user[email]" type="email" />',
- text_field("user", "email", :type => "email")
- end
-
- def test_check_box_is_html_safe
- assert check_box("post", "secret").html_safe?
- end
-
- def test_check_box_checked_if_object_value_is_same_that_check_value
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret")
- )
- end
-
- def test_check_box_not_checked_if_object_value_is_same_that_unchecked_value
- @post.secret = 0
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret")
- )
- end
-
- def test_check_box_checked_if_option_checked_is_present
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret" ,{"checked"=>"checked"})
- )
- end
-
- def test_check_box_checked_if_object_value_is_true
- @post.secret = true
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret")
- )
-
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret?")
- )
- end
-
- def test_check_box_checked_if_object_value_includes_checked_value
- @post.secret = ['0']
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret")
- )
-
- @post.secret = ['1']
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret")
- )
- end
-
- def test_check_box_with_include_hidden_false
- @post.secret = false
- assert_dom_equal('<input id="post_secret" name="post[secret]" type="checkbox" value="1" />', check_box("post", "secret", :include_hidden => false))
- end
-
- def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_string
- @post.secret = "on"
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="off" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />',
- check_box("post", "secret", {}, "on", "off")
- )
-
- @post.secret = "off"
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="off" /><input id="post_secret" name="post[secret]" type="checkbox" value="on" />',
- check_box("post", "secret", {}, "on", "off")
- )
- end
-
- def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_boolean
- @post.secret = false
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="true" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="false" />',
- check_box("post", "secret", {}, false, true)
- )
-
- @post.secret = true
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="true" /><input id="post_secret" name="post[secret]" type="checkbox" value="false" />',
- check_box("post", "secret", {}, false, true)
- )
- end
-
- def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_integer
- @post.secret = 0
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = 1
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = 2
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
- end
-
- def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_float
- @post.secret = 0.0
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = 1.1
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = 2.2
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
- end
-
- def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal
- @post.secret = BigDecimal.new(0)
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = BigDecimal.new(1)
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
-
- @post.secret = BigDecimal.new(2.2, 1)
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
- check_box("post", "secret", {}, 0, 1)
- )
- end
-
- def test_check_box_with_nil_unchecked_value
- @post.secret = "on"
- assert_dom_equal(
- '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />',
- check_box("post", "secret", {}, "on", nil)
- )
- end
-
- def test_check_box_with_nil_unchecked_value_is_html_safe
- assert check_box("post", "secret", {}, "on", nil).html_safe?
- end
-
- def test_check_box_with_multiple_behavior
- @post.comment_ids = [2,3]
- assert_dom_equal(
- '<input name="post[comment_ids][]" type="hidden" value="0" /><input id="post_comment_ids_1" name="post[comment_ids][]" type="checkbox" value="1" />',
- check_box("post", "comment_ids", { :multiple => true }, 1)
- )
- assert_dom_equal(
- '<input name="post[comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_comment_ids_3" name="post[comment_ids][]" type="checkbox" value="3" />',
- check_box("post", "comment_ids", { :multiple => true }, 3)
- )
- end
-
- def test_checkbox_disabled_disables_hidden_field
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" disabled="disabled"/><input checked="checked" disabled="disabled" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", { :disabled => true })
- )
- end
-
- def test_checkbox_form_html5_attribute
- assert_dom_equal(
- '<input form="new_form" name="post[secret]" type="hidden" value="0" /><input checked="checked" form="new_form" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", :form => "new_form")
- )
- end
-
- def test_radio_button
- assert_dom_equal('<input checked="checked" id="post_title_hello_world" name="post[title]" type="radio" value="Hello World" />',
- radio_button("post", "title", "Hello World")
- )
- assert_dom_equal('<input id="post_title_goodbye_world" name="post[title]" type="radio" value="Goodbye World" />',
- radio_button("post", "title", "Goodbye World")
- )
- assert_dom_equal('<input id="item_subobject_title_inside_world" name="item[subobject][title]" type="radio" value="inside world"/>',
- radio_button("item[subobject]", "title", "inside world")
- )
- end
-
- def test_radio_button_is_checked_with_integers
- assert_dom_equal('<input checked="checked" id="post_secret_1" name="post[secret]" type="radio" value="1" />',
- radio_button("post", "secret", "1")
- )
- end
-
- def test_radio_button_with_negative_integer_value
- assert_dom_equal('<input id="post_secret_-1" name="post[secret]" type="radio" value="-1" />',
- radio_button("post", "secret", "-1"))
- end
-
- def test_radio_button_respects_passed_in_id
- assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />',
- radio_button("post", "secret", "1", :id=>"foo")
- )
- end
-
- def test_radio_button_with_booleans
- assert_dom_equal('<input id="post_secret_true" name="post[secret]" type="radio" value="true" />',
- radio_button("post", "secret", true)
- )
-
- assert_dom_equal('<input id="post_secret_false" name="post[secret]" type="radio" value="false" />',
- radio_button("post", "secret", false)
- )
- end
-
- def test_text_area
- assert_dom_equal(
- %{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body")
- )
- end
-
- def test_text_area_with_escapes
- @post.body = "Back to <i>the</i> hill and over it again!"
- assert_dom_equal(
- %{<textarea id="post_body" name="post[body]">\nBack to &lt;i&gt;the&lt;/i&gt; hill and over it again!</textarea>},
- text_area("post", "body")
- )
- end
-
- def test_text_area_with_alternate_value
- assert_dom_equal(
- %{<textarea id="post_body" name="post[body]">\nTesting alternate values.</textarea>},
- text_area("post", "body", :value => 'Testing alternate values.')
- )
- end
-
- def test_text_area_with_html_entities
- @post.body = "The HTML Entity for & is &amp;"
- assert_dom_equal(
- %{<textarea id="post_body" name="post[body]">\nThe HTML Entity for &amp; is &amp;amp;</textarea>},
- text_area("post", "body")
- )
- end
-
- def test_text_area_with_size_option
- assert_dom_equal(
- %{<textarea cols="183" id="post_body" name="post[body]" rows="820">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", :size => "183x820")
- )
- end
-
- def test_color_field_with_valid_hex_color_string
- expected = %{<input id="car_color" name="car[color]" type="color" value="#000fff" />}
- assert_dom_equal(expected, color_field("car", "color"))
- end
-
- def test_color_field_with_invalid_hex_color_string
- expected = %{<input id="car_color" name="car[color]" type="color" value="#000000" />}
- @car.color = "#1234TR"
- assert_dom_equal(expected, color_field("car", "color"))
- end
-
- def test_search_field
- expected = %{<input id="contact_notes_query" name="contact[notes_query]" type="search" />}
- assert_dom_equal(expected, search_field("contact", "notes_query"))
- end
-
- def test_telephone_field
- expected = %{<input id="user_cell" name="user[cell]" type="tel" />}
- assert_dom_equal(expected, telephone_field("user", "cell"))
- end
-
- def test_date_field
- expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
- assert_dom_equal(expected, date_field("post", "written_on"))
- end
-
- def test_date_field_with_datetime_value
- 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)
- assert_dom_equal(expected, date_field("post", "written_on"))
- end
-
- def test_date_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="2" 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 = DateTime.new(2000, 6, 15)
- max_value = DateTime.new(2010, 8, 15)
- step = 2
- assert_dom_equal(expected, date_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_date_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, date_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_date_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="date" />}
- @post.written_on = nil
- assert_dom_equal(expected, date_field("post", "written_on"))
- 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"))
- end
-
- def test_time_field_with_datetime_value
- 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)
- assert_dom_equal(expected, time_field("post", "written_on"))
- end
-
- def test_time_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
- max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
- step = 60
- assert_dom_equal(expected, time_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_time_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />}
- @post.written_on = Time.zone.parse('2004-06-15 01:02:03')
- assert_dom_equal(expected, time_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_time_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="time" />}
- @post.written_on = nil
- assert_dom_equal(expected, time_field("post", "written_on"))
- 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"))
- end
-
- def test_datetime_field_with_datetime_value
- 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)
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- end
-
- def test_datetime_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
- max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
- step = 60
- assert_dom_equal(expected, datetime_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_datetime_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_datetime_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />}
- @post.written_on = nil
- assert_dom_equal(expected, datetime_field("post", "written_on"))
- 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"))
- end
-
- def test_datetime_local_field_with_datetime_value
- 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)
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- end
-
- def test_datetime_local_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
- max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
- step = 60
- assert_dom_equal(expected, datetime_local_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_datetime_local_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_datetime_local_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />}
- @post.written_on = nil
- assert_dom_equal(expected, datetime_local_field("post", "written_on"))
- 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"))
- end
-
- def test_month_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="month" />}
- @post.written_on = nil
- assert_dom_equal(expected, month_field("post", "written_on"))
- end
-
- def test_month_field_with_datetime_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- assert_dom_equal(expected, month_field("post", "written_on"))
- end
-
- def test_month_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="2" max="2010-12" min="2000-02" name="post[written_on]" type="month" value="2004-06" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = DateTime.new(2000, 2, 13)
- max_value = DateTime.new(2010, 12, 23)
- step = 2
- assert_dom_equal(expected, month_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_month_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, month_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_week_field
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
- assert_dom_equal(expected, week_field("post", "written_on"))
- end
-
- def test_week_field_with_nil_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" />}
- @post.written_on = nil
- assert_dom_equal(expected, week_field("post", "written_on"))
- end
-
- def test_week_field_with_datetime_value
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- assert_dom_equal(expected, week_field("post", "written_on"))
- end
-
- def test_week_field_with_extra_attrs
- expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />}
- @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
- min_value = DateTime.new(2000, 2, 13)
- max_value = DateTime.new(2010, 12, 23)
- step = 2
- assert_dom_equal(expected, week_field("post", "written_on", :min => min_value, :max => max_value, :step => step))
- end
-
- def test_week_field_with_timewithzone_value
- previous_time_zone, Time.zone = Time.zone, 'UTC'
- expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
- @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
- assert_dom_equal(expected, week_field("post", "written_on"))
- ensure
- Time.zone = previous_time_zone
- end
-
- def test_url_field
- expected = %{<input id="user_homepage" name="user[homepage]" type="url" />}
- assert_dom_equal(expected, url_field("user", "homepage"))
- end
-
- def test_email_field
- expected = %{<input id="user_address" name="user[address]" type="email" />}
- assert_dom_equal(expected, email_field("user", "address"))
- end
-
- def test_number_field
- expected = %{<input name="order[quantity]" max="9" id="order_quantity" type="number" min="1" />}
- assert_dom_equal(expected, number_field("order", "quantity", :in => 1...10))
- expected = %{<input name="order[quantity]" size="30" max="9" id="order_quantity" type="number" min="1" />}
- assert_dom_equal(expected, number_field("order", "quantity", :size => 30, :in => 1...10))
- end
-
- def test_range_input
- expected = %{<input name="hifi[volume]" step="0.1" max="11" id="hifi_volume" type="range" min="0" />}
- assert_dom_equal(expected, range_field("hifi", "volume", :in => 0..11, :step => 0.1))
- expected = %{<input name="hifi[volume]" step="0.1" size="30" max="11" id="hifi_volume" type="range" min="0" />}
- assert_dom_equal(expected, range_field("hifi", "volume", :size => 30, :in => 0..11, :step => 0.1))
- end
-
- def test_explicit_name
- assert_dom_equal(
- '<input id="post_title" name="dont guess" type="text" value="Hello World" />', text_field("post", "title", "name" => "dont guess")
- )
- assert_dom_equal(
- %{<textarea id="post_body" name="really!">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "name" => "really!")
- )
- assert_dom_equal(
- '<input name="i mean it" type="hidden" value="0" /><input checked="checked" id="post_secret" name="i mean it" type="checkbox" value="1" />',
- check_box("post", "secret", "name" => "i mean it")
- )
- assert_dom_equal text_field("post", "title", "name" => "dont guess"),
- text_field("post", "title", :name => "dont guess")
- assert_dom_equal text_area("post", "body", "name" => "really!"),
- text_area("post", "body", :name => "really!")
- assert_dom_equal check_box("post", "secret", "name" => "i mean it"),
- check_box("post", "secret", :name => "i mean it")
- end
-
- def test_explicit_id
- assert_dom_equal(
- '<input id="dont guess" name="post[title]" type="text" value="Hello World" />', text_field("post", "title", "id" => "dont guess")
- )
- assert_dom_equal(
- %{<textarea id="really!" name="post[body]">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "id" => "really!")
- )
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="i mean it" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", "id" => "i mean it")
- )
- assert_dom_equal text_field("post", "title", "id" => "dont guess"),
- text_field("post", "title", :id => "dont guess")
- assert_dom_equal text_area("post", "body", "id" => "really!"),
- text_area("post", "body", :id => "really!")
- assert_dom_equal check_box("post", "secret", "id" => "i mean it"),
- check_box("post", "secret", :id => "i mean it")
- end
-
- def test_nil_id
- assert_dom_equal(
- '<input name="post[title]" type="text" value="Hello World" />', text_field("post", "title", "id" => nil)
- )
- assert_dom_equal(
- %{<textarea name="post[body]">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "id" => nil)
- )
- assert_dom_equal(
- '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" name="post[secret]" type="checkbox" value="1" />',
- check_box("post", "secret", "id" => nil)
- )
- assert_dom_equal(
- '<input type="radio" name="post[secret]" value="0" />',
- radio_button("post", "secret", "0", "id" => nil)
- )
- assert_dom_equal(
- '<select name="post[secret]"></select>',
- select("post", "secret", [], {}, "id" => nil)
- )
- assert_dom_equal text_field("post", "title", "id" => nil),
- text_field("post", "title", :id => nil)
- assert_dom_equal text_area("post", "body", "id" => nil),
- text_area("post", "body", :id => nil)
- assert_dom_equal check_box("post", "secret", "id" => nil),
- check_box("post", "secret", :id => nil)
- assert_dom_equal radio_button("post", "secret", "0", "id" => nil),
- radio_button("post", "secret", "0", :id => nil)
- end
-
- def test_index
- assert_dom_equal(
- '<input name="post[5][title]" id="post_5_title" type="text" value="Hello World" />',
- text_field("post", "title", "index" => 5)
- )
- assert_dom_equal(
- %{<textarea name="post[5][body]" id="post_5_body">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "index" => 5)
- )
- assert_dom_equal(
- '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" id="post_5_secret" />',
- check_box("post", "secret", "index" => 5)
- )
- assert_dom_equal(
- text_field("post", "title", "index" => 5),
- text_field("post", "title", "index" => 5)
- )
- assert_dom_equal(
- text_area("post", "body", "index" => 5),
- text_area("post", "body", "index" => 5)
- )
- assert_dom_equal(
- check_box("post", "secret", "index" => 5),
- check_box("post", "secret", "index" => 5)
- )
- end
-
- def test_index_with_nil_id
- assert_dom_equal(
- '<input name="post[5][title]" type="text" value="Hello World" />',
- text_field("post", "title", "index" => 5, 'id' => nil)
- )
- assert_dom_equal(
- %{<textarea name="post[5][body]">\nBack to the hill and over it again!</textarea>},
- text_area("post", "body", "index" => 5, 'id' => nil)
- )
- assert_dom_equal(
- '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" />',
- check_box("post", "secret", "index" => 5, 'id' => nil)
- )
- assert_dom_equal(
- text_field("post", "title", "index" => 5, 'id' => nil),
- text_field("post", "title", :index => 5, :id => nil)
- )
- assert_dom_equal(
- text_area("post", "body", "index" => 5, 'id' => nil),
- text_area("post", "body", :index => 5, :id => nil)
- )
- assert_dom_equal(
- check_box("post", "secret", "index" => 5, 'id' => nil),
- check_box("post", "secret", :index => 5, :id => nil)
- )
- end
-
- def test_auto_index
- pid = 123
- assert_dom_equal(
- "<label for=\"post_#{pid}_title\">Title</label>",
- label("post[]", "title")
- )
- assert_dom_equal(
- "<input id=\"post_#{pid}_title\" name=\"post[#{pid}][title]\" type=\"text\" value=\"Hello World\" />", text_field("post[]","title")
- )
- assert_dom_equal(
- "<textarea id=\"post_#{pid}_body\" name=\"post[#{pid}][body]\">\nBack to the hill and over it again!</textarea>",
- text_area("post[]", "body")
- )
- assert_dom_equal(
- "<input name=\"post[#{pid}][secret]\" type=\"hidden\" value=\"0\" /><input checked=\"checked\" id=\"post_#{pid}_secret\" name=\"post[#{pid}][secret]\" type=\"checkbox\" value=\"1\" />",
- check_box("post[]", "secret")
- )
- assert_dom_equal(
- "<input checked=\"checked\" id=\"post_#{pid}_title_hello_world\" name=\"post[#{pid}][title]\" type=\"radio\" value=\"Hello World\" />",
- radio_button("post[]", "title", "Hello World")
- )
- assert_dom_equal("<input id=\"post_#{pid}_title_goodbye_world\" name=\"post[#{pid}][title]\" type=\"radio\" value=\"Goodbye World\" />",
- radio_button("post[]", "title", "Goodbye World")
- )
- end
-
- def test_auto_index_with_nil_id
- pid = 123
- assert_dom_equal(
- "<input name=\"post[#{pid}][title]\" type=\"text\" value=\"Hello World\" />",
- text_field("post[]","title", :id => nil)
- )
- assert_dom_equal(
- "<textarea name=\"post[#{pid}][body]\">\nBack to the hill and over it again!</textarea>",
- text_area("post[]", "body", :id => nil)
- )
- assert_dom_equal(
- "<input name=\"post[#{pid}][secret]\" type=\"hidden\" value=\"0\" /><input checked=\"checked\" name=\"post[#{pid}][secret]\" type=\"checkbox\" value=\"1\" />",
- check_box("post[]", "secret", :id => nil)
- )
- assert_dom_equal(
-"<input checked=\"checked\" name=\"post[#{pid}][title]\" type=\"radio\" value=\"Hello World\" />",
- radio_button("post[]", "title", "Hello World", :id => nil)
- )
- assert_dom_equal("<input name=\"post[#{pid}][title]\" type=\"radio\" value=\"Goodbye World\" />",
- radio_button("post[]", "title", "Goodbye World", :id => nil)
- )
- end
-
- def test_form_for_requires_block
- assert_raises(ArgumentError) do
- form_for(:post, @post, :html => { :id => 'create-post' })
- end
- end
-
- def test_form_for_requires_arguments
- error = assert_raises(ArgumentError) do
- form_for(nil, :html => { :id => 'create-post' }) do
- end
- end
- assert_equal "First argument in form cannot contain nil or be empty", error.message
-
- error = assert_raises(ArgumentError) do
- form_for([nil, nil], :html => { :id => 'create-post' }) do
- end
- end
- assert_equal "First argument in form cannot contain nil or be empty", error.message
- end
-
- def test_form_for
- form_for(@post, :html => { :id => 'create-post' }) do |f|
- concat f.label(:title) { "The Title" }
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- concat f.submit('Create post')
- concat f.button('Create post')
- concat f.button {
- concat content_tag(:span, 'Create post')
- }
- end
-
- expected = whole_form("/posts/123", "create-post" , "edit_post", :method => 'patch') do
- "<label for='post_title'>The Title</label>" +
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" +
- "<input name='commit' type='submit' value='Create post' />" +
- "<button name='button' type='submit'>Create post</button>" +
- "<button name='button' type='submit'><span>Create post</span></button>"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_collection_radio_buttons
- post = Post.new
- def post.active; false; end
- form_for(post) do |f|
- concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
- end
-
- expected = whole_form("/posts", "new_post" , "new_post") do
- "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
- "<label for='post_active_true'>true</label>" +
- "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
- "<label for='post_active_false'>false</label>"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_collection_check_boxes
- post = Post.new
- def post.tag_ids; [1, 3]; end
- collection = (1..3).map{|i| [i, "Tag #{i}"] }
- form_for(post) do |f|
- concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
- end
-
- expected = whole_form("/posts", "new_post" , "new_post") do
- "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
- "<label for='post_tag_ids_1'>Tag 1</label>" +
- "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
- "<label for='post_tag_ids_2'>Tag 2</label>" +
- "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
- "<label for='post_tag_ids_3'>Tag 3</label>" +
- "<input name='post[tag_ids][]' type='hidden' value='' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_file_field_generate_multipart
- Post.send :attr_accessor, :file
-
- form_for(@post, :html => { :id => 'create-post' }) do |f|
- concat f.file_field(:file)
- end
-
- expected = whole_form("/posts/123", "create-post" , "edit_post", :method => 'patch', :multipart => true) do
- "<input name='post[file]' type='file' id='post_file' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_with_file_field_generate_multipart
- Comment.send :attr_accessor, :file
-
- form_for(@post) do |f|
- concat f.fields_for(:comment, @post) { |c|
- concat c.file_field(:file)
- }
- end
-
- expected = whole_form("/posts/123", "edit_post_123" , "edit_post", :method => 'patch', :multipart => true) do
- "<input name='post[comment][file]' type='file' id='post_comment_file' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
-
- def test_form_for_with_format
- form_for(@post, :format => :json, :html => { :id => "edit_post_123", :class => "edit_post" }) do |f|
- concat f.label(:title)
- end
-
- expected = whole_form("/posts/123.json", "edit_post_123" , "edit_post", :method => 'patch') do
- "<label for='post_title'>Title</label>"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_model_using_relative_model_naming
- blog_post = Blog::Post.new("And his name will be forty and four.", 44)
-
- form_for(blog_post) do |f|
- concat f.text_field :title
- concat f.submit('Edit post')
- end
-
- expected = whole_form("/posts/44", "edit_post_44" , "edit_post", :method => 'patch') do
- "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" +
- "<input name='commit' type='submit' value='Edit post' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_symbol_object_name
- form_for(@post, :as => "other_name", :html => { :id => 'create-post' }) do |f|
- concat f.label(:title, :class => 'post_title')
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- concat f.submit('Create post')
- end
-
- expected = whole_form("/posts/123", "create-post", "edit_other_name", :method => 'patch') do
- "<label for='other_name_title' class='post_title'>Title</label>" +
- "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" +
- "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='other_name[secret]' value='0' type='hidden' />" +
- "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" +
- "<input name='commit' value='Create post' type='submit' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_method_as_part_of_html_options
- form_for(@post, :url => '/', :html => { :id => 'create-post', :method => :delete }) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/", "create-post", "edit_post", "delete") do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_method
- form_for(@post, :url => '/', :method => :delete, :html => { :id => 'create-post' }) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/", "create-post", "edit_post", "delete") do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_search_field
- # Test case for bug which would emit an "object" attribute
- # when used with form_for using a search_field form helper
- form_for(Post.new, :url => "/search", :html => { :id => 'search-post', :method => :get}) do |f|
- concat f.search_field(:title)
- end
-
- expected = whole_form("/search", "search-post", "new_post", "get") do
- "<input name='post[title]' type='search' id='post_title' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_remote
- form_for(@post, :url => '/', :remote => true, :html => { :id => 'create-post', :method => :patch}) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/", "create-post", "edit_post", :method => 'patch', :remote => true) do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_remote_in_html
- form_for(@post, :url => '/', :html => { :remote => true, :id => 'create-post', :method => :patch }) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/", "create-post", "edit_post", :method => 'patch', :remote => true) do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_remote_without_html
- @post.persisted = false
- @post.stubs(:to_key).returns(nil)
- form_for(@post, :remote => true) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/posts", 'new_post', 'new_post', :remote => true) do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_without_object
- form_for(:post, :html => { :id => 'create-post' }) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form("/", "create-post") do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_index
- form_for(@post, :as => "post[]") do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<label for='post_123_title'>Title</label>" +
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[123][secret]' type='hidden' value='0' />" +
- "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_nil_index_option_override
- form_for(@post, :as => "post[]", :index => nil) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" +
- "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[][secret]' type='hidden' value='0' />" +
- "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_label_error_wrapping
- form_for(@post) do |f|
- concat f.label(:author_name, :class => 'label')
- concat f.text_field(:author_name)
- concat f.submit('Create post')
- end
-
- expected = whole_form('/posts/123', 'edit_post_123' , 'edit_post', 'patch') do
- "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
- "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
- "<input name='commit' type='submit' value='Create post' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_label_error_wrapping_without_conventional_instance_variable
- post = remove_instance_variable :@post
-
- form_for(post) do |f|
- concat f.label(:author_name, :class => 'label')
- concat f.text_field(:author_name)
- concat f.submit('Create post')
- end
-
- expected = whole_form('/posts/123', 'edit_post_123' , 'edit_post', 'patch') do
- "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
- "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
- "<input name='commit' type='submit' value='Create post' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_label_error_wrapping_block_and_non_block_versions
- form_for(@post) do |f|
- concat f.label(:author_name, 'Name', :class => 'label')
- concat f.label(:author_name, :class => 'label') { 'Name' }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123' , 'edit_post', 'patch') do
- "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" +
- "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_namespace
- form_for(@post, :namespace => 'namespace') do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', 'patch') do
- "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='namespace_post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_namespace_with_date_select
- form_for(@post, :namespace => 'namespace') do |f|
- concat f.date_select(:written_on)
- end
-
- assert_select 'select#namespace_post_written_on_1i'
- end
-
- def test_form_for_with_namespace_with_label
- form_for(@post, :namespace => 'namespace') do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- end
-
- expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', 'patch') do
- "<label for='namespace_post_title'>Title</label>" +
- "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_two_form_for_with_namespace
- form_for(@post, :namespace => 'namespace_1') do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- end
-
- expected_1 = whole_form('/posts/123', 'namespace_1_edit_post_123', 'edit_post', 'patch') do
- "<label for='namespace_1_post_title'>Title</label>" +
- "<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />"
- end
-
- assert_dom_equal expected_1, output_buffer
-
- form_for(@post, :namespace => 'namespace_2') do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- end
-
- expected_2 = whole_form('/posts/123', 'namespace_2_edit_post_123', 'edit_post', 'patch') do
- "<label for='namespace_2_post_title'>Title</label>" +
- "<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />"
- end
-
- assert_dom_equal expected_2, output_buffer
- end
-
- def test_fields_for_with_namespace
- @comment.body = 'Hello World'
- form_for(@post, :namespace => 'namespace') do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.fields_for(@comment) { |c|
- concat c.text_field(:body)
- }
- end
-
- expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', 'patch') do
- "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_submit_with_object_as_new_record_and_locale_strings
- old_locale, I18n.locale = I18n.locale, :submit
-
- @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
-
- 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
-
- 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
-
- 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
-
- 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
-
- 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
-
- 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
-
- assert_dom_equal expected, output_buffer
- ensure
- I18n.locale = old_locale
- end
-
- def test_nested_fields_for
- @comment.body = 'Hello World'
- form_for(@post) do |f|
- concat f.fields_for(@comment) { |c|
- concat c.text_field(:body)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- "<input name='post[comment][body]' type='text' id='post_comment_body' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_nested_collections
- form_for(@post, :as => 'post[]') do |f|
- concat f.text_field(:title)
- concat f.fields_for('comment[]', @comment) { |c|
- concat c.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_index_and_parent_fields
- form_for(@post, :index => 1) do |c|
- concat c.text_field(:title)
- concat c.fields_for('comment', @comment, :index => 1) { |r|
- concat r.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'patch') do
- "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" +
- "<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_index_and_nested_fields_for
- output_buffer = form_for(@post, :index => 1) do |f|
- concat f.fields_for(:comment, @post) { |c|
- concat c.text_field(:title)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'patch') do
- "<input name='post[1][comment][title]' type='text' id='post_1_comment_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_index_on_both
- form_for(@post, :index => 1) do |f|
- concat f.fields_for(:comment, @post, :index => 5) { |c|
- concat c.text_field(:title)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'patch') do
- "<input name='post[1][comment][5][title]' type='text' id='post_1_comment_5_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_auto_index
- form_for(@post, :as => "post[]") do |f|
- concat f.fields_for(:comment, @post) { |c|
- concat c.text_field(:title)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<input name='post[123][comment][title]' type='text' id='post_123_comment_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_index_radio_button
- form_for(@post) do |f|
- concat f.fields_for(:comment, @post, :index => 5) { |c|
- concat c.radio_button(:title, "hello")
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'patch') do
- "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_auto_index_on_both
- form_for(@post, :as => "post[]") do |f|
- concat f.fields_for("comment[]", @post) { |c|
- concat c.text_field(:title)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<input name='post[123][comment][123][title]' type='text' id='post_123_comment_123_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_index_and_auto_index
- output_buffer = form_for(@post, :as => "post[]") do |f|
- concat f.fields_for(:comment, @post, :index => 5) { |c|
- concat c.text_field(:title)
- }
- end
-
- output_buffer << form_for(@post, :as => :post, :index => 1) do |f|
- concat f.fields_for("comment[]", @post) { |c|
- concat c.text_field(:title)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', 'patch') do
- "<input name='post[123][comment][5][title]' type='text' id='post_123_comment_5_title' value='Hello World' />"
- end + whole_form('/posts/123', 'edit_post', 'edit_post', 'patch') do
- "<input name='post[1][comment][123][title]' type='text' id='post_1_comment_123_title' value='Hello World' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association
- @post.author = Author.new
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- concat af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association
- form_for(@post) do |f|
- f.fields_for(:author, Author.new(123)) do |af|
- assert_not_nil af.object
- assert_equal 123, af.object.id
- end
- end
- end
-
- def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association
- @post.author = Author.new(321)
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- concat af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block
- @post.author = Author.new(321)
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id
- @post.author = Author.new(321)
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author, :include_id => false) { |af|
- af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited
- @post.author = Author.new(321)
-
- form_for(@post, :include_id => false) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override
- @post.author = Author.new(321)
-
- form_for(@post, :include_id => false) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author, :include_id => true) { |af|
- af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement
- @post.author = Author.new(321)
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- concat af.hidden_field(:id)
- concat af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
- @post.author = Author.new(321)
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- concat af.text_field(:name)
- }
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment, :include_id => false) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
- @post.author = Author.new(321)
-
- form_for(@post, :include_id => false) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author) { |af|
- concat af.text_field(:name)
- }
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
- @post.author = Author.new(321)
-
- form_for(@post, :include_id => false) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:author, :include_id => true) { |af|
- concat af.text_field(:name)
- }
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
- '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.hidden_field(:id)
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association
- @post.comments = [Comment.new, Comment.new]
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association
- @post.comments = [Comment.new(321), Comment.new]
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- @post.comments.each do |comment|
- concat f.fields_for(:comments, comment) { |cf|
- concat cf.text_field(:name)
- }
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_an_empty_supplied_attributes_collection
- form_for(@post) do |f|
- concat f.text_field(:title)
- f.fields_for(:comments, []) do |cf|
- concat cf.text_field(:name)
- end
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:comments, @post.comments) { |cf|
- concat cf.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_arel_like
- @post.comments = ArelLike.new
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:comments, @post.comments) { |cf|
- concat cf.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
- end
-
- assert_dom_equal expected, output_buffer
- 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 = []
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:comments, comments) { |cf|
- concat cf.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
- '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder
- @post.comments = [Comment.new(321), Comment.new]
- yielded_comments = []
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.fields_for(:comments) { |cf|
- concat cf.text_field(:name)
- yielded_comments << cf.object
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
- '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
- end
-
- assert_dom_equal expected, output_buffer
- assert_equal yielded_comments, @post.comments
- end
-
- def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association
- @post.comments = []
-
- form_for(@post) do |f|
- concat f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf|
- concat cf.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attributes_collection_association
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- expected = 0
- @post.comments.each do |comment|
- f.fields_for(:comments, comment) { |cf|
- assert_equal cf.index, expected
- expected += 1
- }
- end
- end
- end
-
- def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association
- @post.comments = [Comment.new(321), Comment.new]
-
- form_for(@post) do |f|
- expected = 0
- @post.comments.each do |comment|
- f.fields_for(:comments, comment) { |cf|
- assert_equal cf.index, expected
- expected += 1
- }
- end
- end
- end
-
- def test_nested_fields_for_index_method_with_existing_records_on_a_supplied_nested_attributes_collection
- @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
-
- form_for(@post) do |f|
- expected = 0
- f.fields_for(:comments, @post.comments) { |cf|
- assert_equal cf.index, expected
- expected += 1
- }
- end
- end
-
- def test_nested_fields_for_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association
- @post.comments = []
-
- form_for(@post) do |f|
- f.fields_for(:comments, Comment.new(321), :child_index => 'abc') { |cf|
- assert_equal cf.index, 'abc'
- }
- end
- end
-
- def test_nested_fields_uses_unique_indices_for_different_collection_associations
- @post.comments = [Comment.new(321)]
- @post.tags = [Tag.new(123), Tag.new(456)]
- @post.comments[0].relevances = []
- @post.tags[0].relevances = []
- @post.tags[1].relevances = []
-
- form_for(@post) do |f|
- concat f.fields_for(:comments, @post.comments[0]) { |cf|
- concat cf.text_field(:name)
- concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf|
- concat crf.text_field(:value)
- }
- }
- concat f.fields_for(:tags, @post.tags[0]) { |tf|
- concat tf.text_field(:value)
- concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf|
- concat trf.text_field(:value)
- }
- }
- concat f.fields_for('tags', @post.tags[1]) { |tf|
- concat tf.text_field(:value)
- concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf|
- concat trf.text_field(:value)
- }
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
- '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' +
- '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' +
- '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
- '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' +
- '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' +
- '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' +
- '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' +
- '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' +
- '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' +
- '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' +
- '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_nested_fields_for_with_hash_like_model
- @author = HashBackedAuthor.new
-
- form_for(@post) do |f|
- concat f.fields_for(:author, @author) { |af|
- concat af.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="hash backed author" />'
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for
- output_buffer = fields_for(:post, @post) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_with_index
- output_buffer = fields_for("post[]", @post) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
- "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[123][secret]' type='hidden' value='0' />" +
- "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_with_nil_index_option_override
- output_buffer = fields_for("post[]", @post, :index => nil) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" +
- "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[][secret]' type='hidden' value='0' />" +
- "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_with_index_option_override
- output_buffer = fields_for("post[]", @post, :index => "abc") do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" +
- "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[abc][secret]' type='hidden' value='0' />" +
- "<input name='post[abc][secret]' checked='checked' type='checkbox' id='post_abc_secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_without_object
- output_buffer = fields_for(:post) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_with_only_object
- output_buffer = fields_for(@post) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[secret]' type='hidden' value='0' />" +
- "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_fields_for_object_with_bracketed_name
- output_buffer = fields_for("author[post]", @post) do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- end
-
- assert_dom_equal "<label for=\"author_post_title\">Title</label>" +
- "<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />",
- output_buffer
- end
-
- def test_fields_for_object_with_bracketed_name_and_index
- output_buffer = fields_for("author[post]", @post, :index => 1) do |f|
- concat f.label(:title)
- concat f.text_field(:title)
- end
-
- assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" +
- "<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />",
- output_buffer
- end
-
- def test_form_builder_does_not_have_form_for_method
- assert !ActionView::Helpers::FormBuilder.instance_methods.include?(:form_for)
- end
-
- def test_form_for_and_fields_for
- form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form|
- concat post_form.text_field(:title)
- concat post_form.text_area(:body)
-
- concat fields_for(:parent_post, @post) { |parent_fields|
- concat parent_fields.check_box(:secret)
- }
- end
-
- expected = whole_form('/posts/123', 'create-post', 'edit_post', :method => 'patch') do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='parent_post[secret]' type='hidden' value='0' />" +
- "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_and_fields_for_with_object
- form_for(@post, :as => :post, :html => { :id => 'create-post' }) do |post_form|
- concat post_form.text_field(:title)
- concat post_form.text_area(:body)
-
- concat post_form.fields_for(@comment) { |comment_fields|
- concat comment_fields.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'create-post', 'edit_post', :method => 'patch') do
- "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
- "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
- "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_and_fields_for_with_non_nested_association_and_without_object
- form_for(@post) do |f|
- concat f.fields_for(:category) { |c|
- concat c.text_field(:name)
- }
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', 'patch') do
- "<input name='post[category][name]' type='text' id='post_category_name' />"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- class LabelledFormBuilder < ActionView::Helpers::FormBuilder
- (field_helpers - %w(hidden_field)).each do |selector|
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{selector}(field, *args, &proc)
- ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe
- end
- RUBY_EVAL
- end
- end
-
- def test_form_for_with_labelled_builder
- form_for(@post, :builder => LabelledFormBuilder) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
- "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_default_form_builder
- old_default_form_builder, ActionView::Base.default_form_builder =
- ActionView::Base.default_form_builder, LabelledFormBuilder
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
- "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
- end
-
- assert_dom_equal expected, output_buffer
- ensure
- ActionView::Base.default_form_builder = old_default_form_builder
- end
-
- def test_lazy_loading_default_form_builder
- old_default_form_builder, ActionView::Base.default_form_builder =
- ActionView::Base.default_form_builder, "FormHelperTest::LabelledFormBuilder"
-
- form_for(@post) do |f|
- concat f.text_field(:title)
- end
-
- expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', :method => 'patch') do
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>"
- end
-
- assert_dom_equal expected, output_buffer
- ensure
- ActionView::Base.default_form_builder = old_default_form_builder
- end
-
- def test_fields_for_with_labelled_builder
- output_buffer = fields_for(:post, @post, :builder => LabelledFormBuilder) do |f|
- concat f.text_field(:title)
- concat f.text_area(:body)
- concat f.check_box(:secret)
- end
-
- expected =
- "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
- "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
- "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash
- klass = nil
-
- form_for(@post, :builder => LabelledFormBuilder) do |f|
- f.fields_for(:comments, Comment.new) do |nested_fields|
- klass = nested_fields.class
- ''
- end
- end
-
- assert_equal LabelledFormBuilder, klass
- end
-
- def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash
- klass = nil
-
- form_for(@post, :builder => LabelledFormBuilder) do |f|
- f.fields_for(:comments, Comment.new, :index => 'foo') do |nested_fields|
- klass = nested_fields.class
- ''
- end
- end
-
- assert_equal LabelledFormBuilder, klass
- end
-
- def test_form_for_with_labelled_builder_path
- path = nil
-
- form_for(@post, :builder => LabelledFormBuilder) do |f|
- path = f.to_partial_path
- ''
- end
-
- assert_equal 'labelled_form', path
- end
-
- class LabelledFormBuilderSubclass < LabelledFormBuilder; end
-
- def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder
- klass = nil
-
- form_for(@post, :builder => LabelledFormBuilder) do |f|
- f.fields_for(:comments, Comment.new, :builder => LabelledFormBuilderSubclass) do |nested_fields|
- klass = nested_fields.class
- ''
- end
- end
-
- assert_equal LabelledFormBuilderSubclass, klass
- end
-
- def test_form_for_with_html_options_adds_options_to_form_tag
- form_for(@post, :html => {:id => 'some_form', :class => 'some_class'}) do |f| end
- expected = whole_form("/posts/123", "some_form", "some_class", 'patch')
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_string_url_option
- form_for(@post, :url => 'http://www.otherdomain.com') do |f| end
-
- assert_equal whole_form("http://www.otherdomain.com", 'edit_post_123', 'edit_post', 'patch'), output_buffer
- end
-
- def test_form_for_with_hash_url_option
- form_for(@post, :url => {:controller => 'controller', :action => 'action'}) do |f| end
-
- assert_equal 'controller', @url_for_options[:controller]
- assert_equal 'action', @url_for_options[:action]
- end
-
- def test_form_for_with_record_url_option
- form_for(@post, :url => @post) do |f| end
-
- expected = whole_form("/posts/123", 'edit_post_123', 'edit_post', 'patch')
- assert_equal expected, output_buffer
- end
-
- def test_form_for_with_existing_object
- form_for(@post) do |f| end
-
- expected = whole_form("/posts/123", "edit_post_123", "edit_post", 'patch')
- assert_equal expected, output_buffer
- end
-
- def test_form_for_with_new_object
- post = Post.new
- post.persisted = false
- def post.to_key; nil; end
-
- form_for(post) do |f| end
-
- expected = whole_form("/posts", "new_post", "new_post")
- assert_equal expected, output_buffer
- end
-
- def test_form_for_with_existing_object_in_list
- @comment.save
- form_for([@post, @comment]) {}
-
- expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", 'patch')
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_new_object_in_list
- form_for([@post, @comment]) {}
-
- expected = whole_form(post_comments_path(@post), "new_comment", "new_comment")
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_existing_object_and_namespace_in_list
- @comment.save
- form_for([:admin, @post, @comment]) {}
-
- expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", 'patch')
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_new_object_and_namespace_in_list
- form_for([:admin, @post, @comment]) {}
-
- expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment")
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_existing_object_and_custom_url
- form_for(@post, :url => "/super_posts") do |f| end
-
- expected = whole_form("/super_posts", "edit_post_123", "edit_post", 'patch')
- assert_equal expected, output_buffer
- end
-
- def test_form_for_with_default_method_as_patch
- form_for(@post) {}
- expected = whole_form("/posts/123", "edit_post_123", "edit_post", "patch")
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_for_with_data_attributes
- form_for(@post, data: { behavior: "stuff" }, remote: true) {}
- assert_match %r|data-behavior="stuff"|, output_buffer
- assert_match %r|data-remote="true"|, output_buffer
- end
-
- def test_fields_for_returns_block_result
- output = fields_for(Post.new) { |f| "fields" }
- assert_equal "fields", output
- end
-
- def test_form_builder_block_argument_deprecation
- builder_class = Class.new(ActionView::Helpers::FormBuilder) do
- def initialize(object_name, object, template, options, block)
- super
- end
- end
-
- assert_deprecated(/Giving a block to FormBuilder is deprecated and has no effect anymore/) do
- builder_class.new(:foo, nil, nil, {}, proc {})
- end
- end
-
- protected
-
- def hidden_fields(method = nil)
- txt = %{<div style="margin:0;padding:0;display:inline">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
- if method && !%w(get post).include?(method.to_s)
- txt << %{<input name="_method" type="hidden" value="#{method}" />}
- end
- txt << %{</div>}
- end
-
- def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
- txt = %{<form accept-charset="UTF-8" action="#{action}"}
- txt << %{ enctype="multipart/form-data"} if multipart
- txt << %{ data-remote="true"} if remote
- txt << %{ class="#{html_class}"} if html_class
- txt << %{ id="#{id}"} if id
- method = method.to_s == "get" ? "get" : "post"
- txt << %{ method="#{method}">}
- end
-
- def whole_form(action = "/", id = nil, html_class = nil, options = nil)
- contents = block_given? ? yield : ""
-
- if options.is_a?(Hash)
- method, remote, multipart = options.values_at(:method, :remote, :multipart)
- else
- method = options
- end
-
- form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
- end
-
- def protect_against_forgery?
- false
- end
-end
diff --git a/actionpack/test/template/form_options_helper_test.rb b/actionpack/test/template/form_options_helper_test.rb
deleted file mode 100644
index 54ab8b4d3a..0000000000
--- a/actionpack/test/template/form_options_helper_test.rb
+++ /dev/null
@@ -1,1259 +0,0 @@
-require 'abstract_unit'
-require 'tzinfo'
-
-class Map < Hash
- def category
- "<mus>"
- end
-end
-
-TZInfo::Timezone.cattr_reader :loaded_zones
-
-class FormOptionsHelperTest < ActionView::TestCase
- tests ActionView::Helpers::FormOptionsHelper
-
- silence_warnings do
- Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments)
- Continent = Struct.new('Continent', :continent_name, :countries)
- Country = Struct.new('Country', :country_id, :country_name)
- Firm = Struct.new('Firm', :time_zone)
- Album = Struct.new('Album', :id, :title, :genre)
- end
-
- def setup
- @fake_timezones = %w(A B C D E).inject([]) do |zones, id|
- tz = TZInfo::Timezone.loaded_zones[id] = stub(:name => id, :to_s => id)
- ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz)
- zones << tz
- end
- ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones)
- end
-
- def test_collection_options
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title")
- )
- end
-
-
- def test_collection_options_with_preselected_value
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", "Babe")
- )
- end
-
- def test_collection_options_with_preselected_value_array
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", [ "Babe", "Cabe" ])
- )
- end
-
- def test_collection_options_with_proc_for_selected
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", lambda{|p| p.author_name == 'Babe' })
- )
- end
-
- def test_collection_options_with_disabled_value
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => "Babe")
- )
- end
-
- def test_collection_options_with_disabled_array
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => [ "Babe", "Cabe" ])
- )
- end
-
- def test_collection_options_with_preselected_and_disabled_value
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :selected => "Cabe", :disabled => "Babe")
- )
- end
-
- def test_collection_options_with_proc_for_disabled
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda {|p| %w(Babe Cabe).include?(p.author_name)})
- )
- end
-
- def test_collection_options_with_proc_for_value_method
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, lambda { |p| p.author_name }, "title")
- )
- end
-
- def test_collection_options_with_proc_for_text_method
- assert_dom_equal(
- "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
- options_from_collection_for_select(dummy_posts, "author_name", lambda { |p| p.title })
- )
- end
-
- def test_string_options_for_select
- options = "<option value=\"Denmark\">Denmark</option><option value=\"USA\">USA</option><option value=\"Sweden\">Sweden</option>"
- assert_dom_equal(
- options,
- options_for_select(options)
- )
- end
-
- def test_array_options_for_select
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\">USA</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "<Denmark>", "USA", "Sweden" ])
- )
- 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>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], "<USA>")
- )
- end
-
- def test_array_options_for_select_with_selection_array
- assert_dom_equal(
- "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ])
- )
- end
-
- def test_array_options_for_select_with_disabled_value
- assert_dom_equal(
- "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => "<USA>")
- )
- end
-
- def test_array_options_for_select_with_disabled_array
- assert_dom_equal(
- "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\" disabled=\"disabled\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => ["<USA>", "Sweden"])
- )
- end
-
- def test_array_options_for_select_with_selection_and_disabled_value
- assert_dom_equal(
- "<option value=\"Denmark\" selected=\"selected\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "Denmark", "<USA>", "Sweden" ], :selected => "Denmark", :disabled => "<USA>")
- )
- end
-
- def test_boolean_array_options_for_select_with_selection_and_disabled_value
- assert_dom_equal(
- "<option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option>",
- options_for_select([ true, false ], :selected => false, :disabled => nil)
- )
- end
-
- def test_range_options_for_select
- assert_dom_equal(
- "<option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option>",
- options_for_select(1..3)
- )
- end
-
- def test_array_options_for_string_include_in_other_string_bug_fix
- assert_dom_equal(
- "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>",
- options_for_select([ "ruby", "rubyonrails" ], "rubyonrails")
- )
- assert_dom_equal(
- "<option value=\"ruby\" selected=\"selected\">ruby</option>\n<option value=\"rubyonrails\">rubyonrails</option>",
- options_for_select([ "ruby", "rubyonrails" ], "ruby")
- )
- assert_dom_equal(
- %(<option value="ruby" selected="selected">ruby</option>\n<option value="rubyonrails">rubyonrails</option>\n<option value=""></option>),
- options_for_select([ "ruby", "rubyonrails", nil ], "ruby")
- )
- end
-
- def test_hash_options_for_select
- assert_dom_equal(
- "<option value=\"Dollar\">$</option>\n<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>",
- options_for_select("$" => "Dollar", "<DKR>" => "<Kroner>").split("\n").join("\n")
- )
- assert_dom_equal(
- "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>",
- options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, "Dollar").split("\n").join("\n")
- )
- assert_dom_equal(
- "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"&lt;Kroner&gt;\" selected=\"selected\">&lt;DKR&gt;</option>",
- options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, [ "Dollar", "<Kroner>" ]).split("\n").join("\n")
- )
- end
-
- def test_ducktyped_options_for_select
- quack = Struct.new(:first, :last)
- assert_dom_equal(
- "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\">$</option>",
- options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")])
- )
- assert_dom_equal(
- "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
- options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], "Dollar")
- )
- assert_dom_equal(
- "<option value=\"&lt;Kroner&gt;\" selected=\"selected\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
- options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], ["Dollar", "<Kroner>"])
- )
- end
-
- def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer
- albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")]
- assert_dom_equal(
- %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => "1")
- )
- end
-
- def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string
- albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")]
-
- assert_dom_equal(
- %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => 1)
- )
- end
-
- def test_collection_options_with_preselected_value_as_string_and_option_value_is_float
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
-
- assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => "2.0")
- )
- end
-
- def test_collection_options_with_preselected_value_as_nil
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
-
- assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :selected => nil)
- )
- end
-
- def test_collection_options_with_disabled_value_as_nil
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
-
- assert_dom_equal(
- %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :disabled => nil)
- )
- end
-
- def test_collection_options_with_disabled_value_as_array
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
-
- assert_dom_equal(
- %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>),
- options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0])
- )
- end
-
- def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float
- albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ]
-
- assert_dom_equal(
- %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>),
- options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"])
- )
- end
-
- def test_option_groups_from_collection_for_select
- assert_dom_equal(
- "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
- option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk")
- )
- end
-
- def test_option_groups_from_collection_for_select_returns_html_safe_string
- assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe?
- end
-
- def test_grouped_options_for_select_with_array
- assert_dom_equal(
- "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
- grouped_options_for_select([
- ["North America",
- [['United States','US'],"Canada"]],
- ["Europe",
- [["Great Britain","GB"], "Germany"]]
- ])
- )
- end
-
- def test_grouped_options_for_select_with_optional_divider
- assert_dom_equal(
- "<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>",
-
- grouped_options_for_select([['US',"Canada"] , ["GB", "Germany"]], nil, divider: "----------")
- )
- end
-
- def test_grouped_options_for_select_with_selected_and_prompt_deprecated
- assert_deprecated 'Passing the prompt to grouped_options_for_select as an argument is deprecated. Please use an options hash like `{ prompt: "Choose a product..." }`.' do
- assert_dom_equal(
- "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", "Choose a product...")
- )
- end
- end
-
- def test_grouped_options_for_select_with_selected_and_prompt
- assert_dom_equal(
- "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...")
- )
- end
-
- def test_grouped_options_for_select_with_selected_and_prompt_true
- assert_dom_equal(
- "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: true)
- )
- end
-
- def test_grouped_options_for_select_returns_html_safe_string
- assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe?
- end
-
- def test_grouped_options_for_select_with_prompt_returns_html_escaped_string_deprecated
- ActiveSupport::Deprecation.silence do
- assert_dom_equal(
- "<option value=\"\">&lt;Choose One&gt;</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, '<Choose One>'))
- end
- end
-
- def test_grouped_options_for_select_with_prompt_returns_html_escaped_string
- assert_dom_equal(
- "<option value=\"\">&lt;Choose One&gt;</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
- grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, prompt: '<Choose One>'))
- end
-
- def test_optgroups_with_with_options_with_hash
- assert_dom_equal(
- "<optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup><optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup>",
- grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']})
- )
- end
-
- def test_time_zone_options_no_parms
- opts = time_zone_options_for_select
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
- "<option value=\"E\">E</option>",
- opts
- end
-
- def test_time_zone_options_with_selected
- opts = time_zone_options_for_select( "D" )
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>",
- opts
- end
-
- def test_time_zone_options_with_unknown_selected
- opts = time_zone_options_for_select( "K" )
- assert_dom_equal "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
- "<option value=\"E\">E</option>",
- opts
- end
-
- def test_time_zone_options_with_priority_zones
- zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
- opts = time_zone_options_for_select( nil, zones )
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>",
- opts
- end
-
- def test_time_zone_options_with_selected_priority_zones
- zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
- opts = time_zone_options_for_select( "E", zones )
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\" selected=\"selected\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>",
- opts
- end
-
- def test_time_zone_options_with_unselected_priority_zones
- zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
- opts = time_zone_options_for_select( "C", zones )
- assert_dom_equal "<option value=\"B\">B</option>\n" +
- "<option value=\"E\">E</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"C\" selected=\"selected\">C</option>\n" +
- "<option value=\"D\">D</option>",
- opts
- end
-
- def test_time_zone_options_returns_html_safe_string
- assert time_zone_options_for_select.html_safe?
- end
-
- def test_select
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest))
- )
- end
-
- def test_select_without_multiple
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"></select>",
- select(:post, :category, "", {}, :multiple => false)
- )
- end
-
- def test_select_with_grouped_collection_as_nested_array
- @post = Post.new
-
- countries_by_continent = [
- ["<Africa>", [["<South Africa>", "<sa>"], ["Somalia", "so"]]],
- ["Europe", [["Denmark", "dk"], ["Ireland", "ie"]]],
- ]
-
- assert_dom_equal(
- [
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
- %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
- %Q{<option value="ie">Ireland</option></optgroup></select>},
- ].join("\n"),
- select("post", "origin", countries_by_continent)
- )
- end
-
- def test_select_with_grouped_collection_as_hash
- @post = Post.new
-
- countries_by_continent = {
- "<Africa>" => [["<South Africa>", "<sa>"], ["Somalia", "so"]],
- "Europe" => [["Denmark", "dk"], ["Ireland", "ie"]],
- }
-
- assert_dom_equal(
- [
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
- %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
- %Q{<option value="ie">Ireland</option></optgroup></select>},
- ].join("\n"),
- select("post", "origin", countries_by_continent)
- )
- end
-
- def test_select_with_boolean_method
- @post = Post.new
- @post.allow_comments = false
- assert_dom_equal(
- "<select id=\"post_allow_comments\" name=\"post[allow_comments]\"><option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option></select>",
- select("post", "allow_comments", %w( true false ))
- )
- end
-
- def test_select_under_fields_for
- @post = Post.new
- @post.category = "<mus>"
-
- output_buffer = fields_for :post, @post do |f|
- concat f.select(:category, %w( abe <mus> hest))
- end
-
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- output_buffer
- )
- end
-
- def test_fields_for_with_record_inherited_from_hash
- map = Map.new
-
- output_buffer = fields_for :map, map do |f|
- concat f.select(:category, %w( abe <mus> hest))
- end
-
- assert_dom_equal(
- "<select id=\"map_category\" name=\"map[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- output_buffer
- )
- end
-
- def test_select_under_fields_for_with_index
- @post = Post.new
- @post.category = "<mus>"
-
- output_buffer = fields_for :post, @post, :index => 108 do |f|
- concat f.select(:category, %w( abe <mus> hest))
- end
-
- assert_dom_equal(
- "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- output_buffer
- )
- end
-
- def test_select_under_fields_for_with_auto_index
- @post = Post.new
- @post.category = "<mus>"
- def @post.to_param; 108; end
-
- output_buffer = fields_for "post[]", @post do |f|
- concat f.select(:category, %w( abe <mus> hest))
- end
-
- assert_dom_equal(
- "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- output_buffer
- )
- end
-
- def test_select_under_fields_for_with_string_and_given_prompt
- @post = Post.new
- options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe
-
- output_buffer = fields_for :post, @post do |f|
- concat f.select(:category, options, :prompt => 'The prompt')
- end
-
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n#{options}</select>",
- output_buffer
- )
- end
-
- def test_select_with_multiple_to_add_hidden_input
- output_buffer = select(:post, :category, "", {}, :multiple => true)
- assert_dom_equal(
- "<input type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
- output_buffer
- )
- end
-
- def test_select_with_multiple_and_without_hidden_input
- output_buffer = select(:post, :category, "", {:include_hidden => false}, :multiple => true)
- assert_dom_equal(
- "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
- output_buffer
- )
- end
-
- def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input
- output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true)
- assert_dom_equal(
- "<input disabled=\"disabled\"type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" disabled=\"disabled\" id=\"post_category\" name=\"post[category][]\"></select>",
- output_buffer
- )
- end
-
- def test_select_with_blank
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => true)
- )
- end
-
- def test_select_with_blank_as_string
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">None</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => 'None')
- )
- end
-
- def test_select_with_blank_as_string_escaped
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;None&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :include_blank => '<None>')
- )
- end
-
- def test_select_with_default_prompt
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true)
- )
- end
-
- def test_select_no_prompt_when_select_has_value
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true)
- )
- end
-
- def test_select_with_given_prompt
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => 'The prompt')
- )
- end
-
- def test_select_with_given_prompt_escaped
- @post = Post.new
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;The prompt&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => '<The prompt>')
- )
- end
-
- def test_select_with_prompt_and_blank
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest), :prompt => true, :include_blank => true)
- )
- end
-
- def test_empty
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>",
- select("post", "category", [], :prompt => true, :include_blank => true)
- )
- end
-
- def test_select_with_nil
- @post = Post.new
- @post.category = "othervalue"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"othervalue\" selected=\"selected\">othervalue</option></select>",
- select("post", "category", [nil, "othervalue"])
- )
- end
-
- def test_required_select
- assert_dom_equal(
- %(<select id="post_category" name="post[category]" required="required"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true)
- )
- end
-
- def test_required_select_with_include_blank_prompt
- assert_dom_equal(
- %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), { include_blank: "Select one" }, required: true)
- )
- end
-
- def test_required_select_with_prompt
- assert_dom_equal(
- %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), { prompt: "Select one" }, required: true)
- )
- end
-
- def test_required_select_display_size_equals_to_one
- assert_dom_equal(
- %(<select id="post_category" name="post[category]" required="required" size="1"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, size: 1)
- )
- end
-
- def test_required_select_with_display_size_bigger_than_one
- assert_dom_equal(
- %(<select id="post_category" name="post[category]" required="required" size="2"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, size: 2)
- )
- end
-
- def test_required_select_with_multiple_option
- assert_dom_equal(
- %(<input name="post[category][]" type="hidden" value=""/><select id="post_category" multiple="multiple" name="post[category][]" required="required"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
- select("post", "category", %w(abe mus hest), {}, required: true, multiple: true)
- )
- end
-
- def test_select_with_fixnum
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"1\">1</option></select>",
- select("post", "category", [1], :prompt => true, :include_blank => true)
- )
- end
-
- def test_list_of_lists
- @post = Post.new
- @post.category = ""
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"number\">Number</option>\n<option value=\"text\">Text</option>\n<option value=\"boolean\">Yes/No</option></select>",
- select("post", "category", [["Number", "number"], ["Text", "text"], ["Yes/No", "boolean"]], :prompt => true, :include_blank => true)
- )
- end
-
- def test_select_with_selected_value
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" selected=\"selected\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :selected => 'abe')
- )
- end
-
- def test_select_with_index_option
- @album = Album.new
- @album.id = 1
-
- expected = "<select id=\"album__genre\" name=\"album[][genre]\"><option value=\"rap\">rap</option>\n<option value=\"rock\">rock</option>\n<option value=\"country\">country</option></select>"
-
- assert_dom_equal(
- expected,
- select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
- )
- end
-
- def test_select_escapes_options
- assert_dom_equal(
- '<select id="post_title" name="post[title]">&lt;script&gt;alert(1)&lt;/script&gt;</select>',
- select('post', 'title', '<script>alert(1)</script>')
- )
- end
-
- def test_select_with_selected_nil
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :selected => nil)
- )
- end
-
- def test_select_with_disabled_value
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :disabled => 'hest')
- )
- end
-
- def test_select_with_disabled_array
- @post = Post.new
- @post.category = "<mus>"
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" disabled=\"disabled\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
- select("post", "category", %w( abe <mus> hest ), :disabled => ['hest', 'abe'])
- )
- end
-
- def test_select_with_range
- @post = Post.new
- @post.category = 0
- assert_dom_equal(
- "<select id=\"post_category\" name=\"post[category]\"><option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option></select>",
- select("post", "category", 1..3)
- )
- end
-
- def test_collection_select
- @post = Post.new
- @post.author_name = "Babe"
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name")
- )
- end
-
- def test_collection_select_under_fields_for
- @post = Post.new
- @post.author_name = "Babe"
-
- output_buffer = fields_for :post, @post do |f|
- concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
- end
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- output_buffer
- )
- end
-
- def test_collection_select_under_fields_for_with_index
- @post = Post.new
- @post.author_name = "Babe"
-
- output_buffer = fields_for :post, @post, :index => 815 do |f|
- concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
- end
-
- assert_dom_equal(
- "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- output_buffer
- )
- end
-
- def test_collection_select_under_fields_for_with_auto_index
- @post = Post.new
- @post.author_name = "Babe"
- def @post.to_param; 815; end
-
- output_buffer = fields_for "post[]", @post do |f|
- concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
- end
-
- assert_dom_equal(
- "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- output_buffer
- )
- end
-
- def test_collection_select_with_blank_and_style
- @post = Post.new
- @post.author_name = "Babe"
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
- )
- end
-
- def test_collection_select_with_blank_as_string_and_style
- @post = Post.new
- @post.author_name = "Babe"
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\">No Selection</option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => 'No Selection' }, "style" => "width: 200px")
- )
- end
-
- def test_collection_select_with_multiple_option_appends_array_brackets_and_hidden_input
- @post = Post.new
- @post.author_name = "Babe"
-
- expected = "<input type=\"hidden\" name=\"post[author_name][]\" value=\"\"/><select id=\"post_author_name\" name=\"post[author_name][]\" multiple=\"multiple\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>"
-
- # Should suffix default name with [].
- assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, :multiple => true)
-
- # Shouldn't suffix custom name with [].
- assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true)
- end
-
- def test_collection_select_with_blank_and_selected
- @post = Post.new
- @post.author_name = "Babe"
-
- assert_dom_equal(
- %{<select id="post_author_name" name="post[author_name]"><option value=""></option>\n<option value="&lt;Abe&gt;" selected="selected">&lt;Abe&gt;</option>\n<option value="Babe">Babe</option>\n<option value="Cabe">Cabe</option></select>},
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", {:include_blank => true, :selected => "<Abe>"})
- )
- end
-
- def test_collection_select_with_disabled
- @post = Post.new
- @post.author_name = "Babe"
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", "author_name", :disabled => 'Cabe')
- )
- end
-
- def test_collection_select_with_proc_for_value_method
- @post = Post.new
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>",
- collection_select("post", "author_name", dummy_posts, lambda { |p| p.author_name }, "title")
- )
- end
-
- def test_collection_select_with_proc_for_text_method
- @post = Post.new
-
- assert_dom_equal(
- "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>",
- collection_select("post", "author_name", dummy_posts, "author_name", lambda { |p| p.title })
- )
- end
-
- def test_time_zone_select
- @firm = Firm.new("D")
- html = time_zone_select( "firm", "time_zone" )
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_under_fields_for
- @firm = Firm.new("D")
-
- output_buffer = fields_for :firm, @firm do |f|
- concat f.time_zone_select(:time_zone)
- end
-
- assert_dom_equal(
- "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- output_buffer
- )
- end
-
- def test_time_zone_select_under_fields_for_with_index
- @firm = Firm.new("D")
-
- output_buffer = fields_for :firm, @firm, :index => 305 do |f|
- concat f.time_zone_select(:time_zone)
- end
-
- assert_dom_equal(
- "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- output_buffer
- )
- end
-
- def test_time_zone_select_under_fields_for_with_auto_index
- @firm = Firm.new("D")
- def @firm.to_param; 305; end
-
- output_buffer = fields_for "firm[]", @firm do |f|
- concat f.time_zone_select(:time_zone)
- end
-
- assert_dom_equal(
- "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- output_buffer
- )
- end
-
- def test_time_zone_select_with_blank
- @firm = Firm.new("D")
- html = time_zone_select("firm", "time_zone", nil, :include_blank => true)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\"></option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_with_blank_as_string
- @firm = Firm.new("D")
- html = time_zone_select("firm", "time_zone", nil, :include_blank => 'No Zone')
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"\">No Zone</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_with_style
- @firm = Firm.new("D")
- html = time_zone_select("firm", "time_zone", nil, {},
- "style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {},
- :style => "color: red")
- end
-
- def test_time_zone_select_with_blank_and_style
- @firm = Firm.new("D")
- html = time_zone_select("firm", "time_zone", nil,
- { :include_blank => true }, "style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"\"></option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
- { :include_blank => true }, :style => "color: red")
- end
-
- def test_time_zone_select_with_blank_as_string_and_style
- @firm = Firm.new("D")
- html = time_zone_select("firm", "time_zone", nil,
- { :include_blank => 'No Zone' }, "style" => "color: red")
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
- "<option value=\"\">No Zone</option>\n" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
- { :include_blank => 'No Zone' }, :style => "color: red")
- end
-
- def test_time_zone_select_with_priority_zones
- @firm = Firm.new("D")
- zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
- html = time_zone_select("firm", "time_zone", zones )
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_with_priority_zones_as_regexp
- @firm = Firm.new("D")
- @fake_timezones.each_with_index do |tz, i|
- tz.stubs(:=~).returns(i.zero? || i == 3)
- end
-
- html = time_zone_select("firm", "time_zone", /A|D/)
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>" +
- "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_with_default_time_zone_and_nil_value
- @firm = Firm.new()
- @firm.time_zone = nil
- html = time_zone_select( "firm", "time_zone", nil, :default => 'B' )
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\" selected=\"selected\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_time_zone_select_with_default_time_zone_and_value
- @firm = Firm.new('D')
- html = time_zone_select( "firm", "time_zone", nil, :default => 'B' )
- assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
- "<option value=\"A\">A</option>\n" +
- "<option value=\"B\">B</option>\n" +
- "<option value=\"C\">C</option>\n" +
- "<option value=\"D\" selected=\"selected\">D</option>\n" +
- "<option value=\"E\">E</option>" +
- "</select>",
- html
- end
-
- def test_options_for_select_with_element_attributes
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\" class=\"bold\">&lt;Denmark&gt;</option>\n<option value=\"USA\" onclick=\"alert(&#39;Hello World&#39;);\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>",
- options_for_select([ [ "<Denmark>", { :class => 'bold' } ], [ "USA", { :onclick => "alert('Hello World');" } ], [ "Sweden" ], "Germany" ])
- )
- end
-
- def test_options_for_select_with_data_element
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\" data-test=\"bold\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :data => { :test => 'bold' } } ] ])
- )
- end
-
- def test_options_for_select_with_data_element_with_special_characters
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\" data-test=\"&lt;bold&gt;\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :data => { :test => '<bold>' } } ] ])
- )
- end
-
- def test_options_for_select_with_element_attributes_and_selection
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\">Sweden</option>",
- options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], "USA")
- )
- end
-
- def test_options_for_select_with_element_attributes_and_selection_array
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
- options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], [ "USA", "Sweden" ])
- )
- end
-
- def test_options_for_select_with_special_characters
- assert_dom_equal(
- "<option value=\"&lt;Denmark&gt;\" onclick=\"alert(&quot;&lt;code&gt;&quot;)\">&lt;Denmark&gt;</option>",
- options_for_select([ [ "<Denmark>", { :onclick => %(alert("<code>")) } ] ])
- )
- end
-
- def test_option_html_attributes_with_no_array_element
- assert_equal({}, option_html_attributes('foo'))
- end
-
- def test_option_html_attributes_without_hash
- assert_equal({}, option_html_attributes([ 'foo', 'bar' ]))
- end
-
- def test_option_html_attributes_with_single_element_hash
- assert_equal(
- {:class => 'fancy'},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy' } ])
- )
- end
-
- def test_option_html_attributes_with_multiple_element_hash
- assert_equal(
- {:class => 'fancy', 'onclick' => "alert('Hello World');"},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy', 'onclick' => "alert('Hello World');" } ])
- )
- end
-
- def test_option_html_attributes_with_multiple_hashes
- assert_equal(
- {:class => 'fancy', 'onclick' => "alert('Hello World');"},
- option_html_attributes([ 'foo', 'bar', { :class => 'fancy' }, { 'onclick' => "alert('Hello World');" } ])
- )
- end
-
- def test_option_html_attributes_with_multiple_hashes_does_not_modify_them
- options1 = { class: 'fancy' }
- options2 = { onclick: "alert('Hello World');" }
- option_html_attributes([ 'foo', 'bar', options1, options2 ])
-
- assert_equal({ class: 'fancy' }, options1)
- assert_equal({ onclick: "alert('Hello World');" }, options2)
- end
-
- def test_grouped_collection_select
- @post = Post.new
- @post.origin = 'dk'
-
- assert_dom_equal(
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name)
- )
- end
-
- def test_grouped_collection_select_with_selected
- @post = Post.new
-
- assert_dom_equal(
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :selected => 'dk')
- )
- end
-
- def test_grouped_collection_select_with_disabled_value
- @post = Post.new
-
- assert_dom_equal(
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option disabled="disabled" value="dk">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :disabled => 'dk')
- )
- end
-
- def test_grouped_collection_select_under_fields_for
- @post = Post.new
- @post.origin = 'dk'
-
- output_buffer = fields_for :post, @post do |f|
- concat f.grouped_collection_select("origin", dummy_continents, :countries, :continent_name, :country_id, :country_name)
- end
-
- assert_dom_equal(
- %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
- output_buffer
- )
- end
-
- private
-
- def dummy_posts
- [ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
- Post.new("Babe went home", "Babe", "To a little house", "shh!"),
- Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ]
- end
-
- def dummy_continents
- [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]),
- Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")]) ]
- end
-end
diff --git a/actionpack/test/template/form_tag_helper_test.rb b/actionpack/test/template/form_tag_helper_test.rb
deleted file mode 100644
index 945dff2a29..0000000000
--- a/actionpack/test/template/form_tag_helper_test.rb
+++ /dev/null
@@ -1,642 +0,0 @@
-require 'abstract_unit'
-
-class FormTagHelperTest < ActionView::TestCase
- include RenderERBUtils
-
- tests ActionView::Helpers::FormTagHelper
-
- def setup
- super
- @controller = BasicController.new
- end
-
- def hidden_fields(options = {})
- method = options[:method]
-
- txt = %{<div style="margin:0;padding:0;display:inline">}
- txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
- if method && !%w(get post).include?(method.to_s)
- txt << %{<input name="_method" type="hidden" value="#{method}" />}
- end
- txt << %{</div>}
- end
-
- def form_text(action = "http://www.example.com", options = {})
- remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method)
-
- method = method.to_s == "get" ? "get" : "post"
-
- txt = %{<form accept-charset="UTF-8" action="#{action}"}
- txt << %{ enctype="multipart/form-data"} if enctype
- txt << %{ data-remote="true"} if remote
- txt << %{ class="#{html_class}"} if html_class
- txt << %{ id="#{id}"} if id
- txt << %{ method="#{method}">}
- end
-
- def whole_form(action = "http://www.example.com", options = {})
- out = form_text(action, options) + hidden_fields(options)
-
- if block_given?
- out << yield << "</form>"
- end
-
- out
- end
-
- def url_for(options)
- if options.is_a?(Hash)
- "http://www.example.com"
- else
- super
- end
- end
-
- VALID_HTML_ID = /^[A-Za-z][-_:.A-Za-z0-9]*$/ # see http://www.w3.org/TR/html4/types.html#type-name
-
- def test_check_box_tag
- actual = check_box_tag "admin"
- expected = %(<input id="admin" name="admin" type="checkbox" value="1" />)
- assert_dom_equal expected, actual
- end
-
- def test_check_box_tag_id_sanitized
- label_elem = root_elem(check_box_tag("project[2][admin]"))
- assert_match VALID_HTML_ID, label_elem['id']
- end
-
- def test_form_tag
- actual = form_tag
- expected = whole_form
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_multipart
- actual = form_tag({}, { 'multipart' => true })
- expected = whole_form("http://www.example.com", :enctype => true)
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_method_patch
- actual = form_tag({}, { :method => :patch })
- expected = whole_form("http://www.example.com", :method => :patch)
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_method_put
- actual = form_tag({}, { :method => :put })
- expected = whole_form("http://www.example.com", :method => :put)
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_method_delete
- actual = form_tag({}, { :method => :delete })
-
- expected = whole_form("http://www.example.com", :method => :delete)
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_remote
- actual = form_tag({}, :remote => true)
-
- expected = whole_form("http://www.example.com", :remote => true)
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_remote_false
- actual = form_tag({}, :remote => false)
-
- expected = whole_form
- assert_dom_equal expected, actual
- end
-
- def test_form_tag_with_block_in_erb
- output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>")
-
- expected = whole_form { "Hello world!" }
- assert_dom_equal expected, output_buffer
- end
-
- def test_form_tag_with_block_and_method_in_erb
- output_buffer = render_erb("<%= form_tag('http://www.example.com', :method => :put) do %>Hello world!<% end %>")
-
- expected = whole_form("http://www.example.com", :method => "put") do
- "Hello world!"
- end
-
- assert_dom_equal expected, output_buffer
- end
-
- def test_hidden_field_tag
- actual = hidden_field_tag "id", 3
- expected = %(<input id="id" name="id" type="hidden" value="3" />)
- assert_dom_equal expected, actual
- end
-
- def test_hidden_field_tag_id_sanitized
- input_elem = root_elem(hidden_field_tag("item[][title]"))
- assert_match VALID_HTML_ID, input_elem['id']
- end
-
- def test_file_field_tag
- assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" />", file_field_tag("picsplz")
- end
-
- def test_file_field_tag_with_options
- assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>", file_field_tag("picsplz", :class => "pix")
- end
-
- def test_password_field_tag
- actual = password_field_tag
- expected = %(<input id="password" name="password" type="password" />)
- assert_dom_equal expected, actual
- end
-
- def test_radio_button_tag
- actual = radio_button_tag "people", "david"
- expected = %(<input id="people_david" name="people" type="radio" value="david" />)
- assert_dom_equal expected, actual
-
- actual = radio_button_tag("num_people", 5)
- expected = %(<input id="num_people_5" name="num_people" type="radio" value="5" />)
- assert_dom_equal expected, actual
-
- actual = radio_button_tag("gender", "m") + radio_button_tag("gender", "f")
- expected = %(<input id="gender_m" name="gender" type="radio" value="m" /><input id="gender_f" name="gender" type="radio" value="f" />)
- assert_dom_equal expected, actual
-
- actual = radio_button_tag("opinion", "-1") + radio_button_tag("opinion", "1")
- expected = %(<input id="opinion_-1" name="opinion" type="radio" value="-1" /><input id="opinion_1" name="opinion" type="radio" value="1" />)
- assert_dom_equal expected, actual
-
- actual = radio_button_tag("person[gender]", "m")
- expected = %(<input id="person_gender_m" name="person[gender]" type="radio" value="m" />)
- assert_dom_equal expected, actual
-
- actual = radio_button_tag('ctrlname', 'apache2.2')
- expected = %(<input id="ctrlname_apache2.2" name="ctrlname" type="radio" value="apache2.2" />)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag
- actual = select_tag "people", "<option>david</option>".html_safe
- expected = %(<select id="people" name="people"><option>david</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_with_multiple
- actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, :multiple => :true
- expected = %(<select id="colors" multiple="multiple" name="colors"><option>Red</option><option>Blue</option><option>Green</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_disabled
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :disabled => :true
- expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_id_sanitized
- input_elem = root_elem(select_tag("project[1]people", "<option>david</option>"))
- assert_match VALID_HTML_ID, input_elem['id']
- end
-
- def test_select_tag_with_include_blank
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :include_blank => true
- expected = %(<select id="places" name="places"><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_with_prompt
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string"
- expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_escapes_prompt
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>"
- expected = %(<select id="places" name="places"><option value="">&lt;script&gt;alert(1337)&lt;/script&gt;</option><option>Home</option><option>Work</option><option>Pub</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_with_prompt_and_include_blank
- actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true
- expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_with_nil_option_tags_and_include_blank
- actual = select_tag "places", nil, :include_blank => true
- expected = %(<select id="places" name="places"><option value=""></option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_select_tag_with_nil_option_tags_and_prompt
- actual = select_tag "places", nil, :prompt => "string"
- expected = %(<select id="places" name="places"><option value="">string</option></select>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_size_string
- actual = text_area_tag "body", "hello world", "size" => "20x40"
- expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_size_symbol
- actual = text_area_tag "body", "hello world", :size => "20x40"
- expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_should_disregard_size_if_its_given_as_an_integer
- actual = text_area_tag "body", "hello world", :size => 20
- expected = %(<textarea id="body" name="body">\nhello world</textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_id_sanitized
- input_elem = root_elem(text_area_tag("item[][description]"))
- assert_match VALID_HTML_ID, input_elem['id']
- end
-
- def test_text_area_tag_escape_content
- actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40"
- expected = %(<textarea cols="20" id="body" name="body" rows="40">\n&lt;b&gt;hello world&lt;/b&gt;</textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_unescaped_content
- actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40", :escape => false
- expected = %(<textarea cols="20" id="body" name="body" rows="40">\n<b>hello world</b></textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_area_tag_unescaped_nil_content
- actual = text_area_tag "body", nil, :escape => false
- expected = %(<textarea id="body" name="body">\n</textarea>)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag
- actual = text_field_tag "title", "Hello!"
- expected = %(<input id="title" name="title" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_class_string
- actual = text_field_tag "title", "Hello!", "class" => "admin"
- expected = %(<input class="admin" id="title" name="title" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_size_symbol
- actual = text_field_tag "title", "Hello!", :size => 75
- expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_size_string
- actual = text_field_tag "title", "Hello!", "size" => "75"
- expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_maxlength_symbol
- actual = text_field_tag "title", "Hello!", :maxlength => 75
- expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_maxlength_string
- actual = text_field_tag "title", "Hello!", "maxlength" => "75"
- expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_disabled
- actual = text_field_tag "title", "Hello!", :disabled => :true
- expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_with_multiple_options
- actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80
- expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_text_field_tag_id_sanitized
- input_elem = root_elem(text_field_tag("item[][title]"))
- assert_match VALID_HTML_ID, input_elem['id']
- end
-
- def test_label_tag_without_text
- actual = label_tag "title"
- expected = %(<label for="title">Title</label>)
- assert_dom_equal expected, actual
- end
-
- def test_label_tag_with_symbol
- actual = label_tag :title
- expected = %(<label for="title">Title</label>)
- assert_dom_equal expected, actual
- end
-
- def test_label_tag_with_text
- actual = label_tag "title", "My Title"
- expected = %(<label for="title">My Title</label>)
- assert_dom_equal expected, actual
- end
-
- def test_label_tag_class_string
- actual = label_tag "title", "My Title", "class" => "small_label"
- expected = %(<label for="title" class="small_label">My Title</label>)
- assert_dom_equal expected, actual
- end
-
- def test_label_tag_id_sanitized
- label_elem = root_elem(label_tag("item[title]"))
- assert_match VALID_HTML_ID, label_elem['for']
- end
-
- def test_label_tag_with_block
- assert_dom_equal('<label>Blocked</label>', label_tag { "Blocked" })
- end
-
- def test_label_tag_with_block_and_argument
- output = label_tag("clock") { "Grandfather" }
- assert_dom_equal('<label for="clock">Grandfather</label>', output)
- end
-
- def test_label_tag_with_block_and_argument_and_options
- output = label_tag("clock", :id => "label_clock") { "Grandfather" }
- assert_dom_equal('<label for="clock" id="label_clock">Grandfather</label>', output)
- end
-
- def test_boolean_options
- assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes")
- assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil)
- assert_dom_equal %(<input type="checkbox" />), tag(:input, :type => "checkbox", :checked => false)
- assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => true)
- assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", "<option>david</option>".html_safe, :multiple => true)
- assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => nil)
- end
-
- def test_stringify_symbol_keys
- actual = text_field_tag "title", "Hello!", :id => "admin"
- expected = %(<input id="admin" name="title" type="text" value="Hello!" />)
- assert_dom_equal expected, actual
- end
-
- def test_submit_tag
- assert_dom_equal(
- %(<input name='commit' data-disable-with="Saving..." onclick="alert(&#39;hello!&#39;)" type="submit" value="Save" />),
- submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." })
- )
- end
-
- def test_submit_tag_with_no_onclick_options
- assert_dom_equal(
- %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />),
- submit_tag("Save", :data => { :disable_with => "Saving..." })
- )
- end
-
- def test_submit_tag_with_confirmation
- assert_dom_equal(
- %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />),
- submit_tag("Save", :data => { :confirm => "Are you sure?" })
- )
- end
-
- def test_submit_tag_with_deprecated_confirmation
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />),
- submit_tag("Save", :confirm => "Are you sure?")
- )
- end
- end
-
- def test_button_tag
- assert_dom_equal(
- %(<button name="button" type="submit">Button</button>),
- button_tag
- )
- end
-
- def test_button_tag_with_submit_type
- assert_dom_equal(
- %(<button name="button" type="submit">Save</button>),
- button_tag("Save", :type => "submit")
- )
- end
-
- def test_button_tag_with_button_type
- assert_dom_equal(
- %(<button name="button" type="button">Button</button>),
- button_tag("Button", :type => "button")
- )
- end
-
- def test_button_tag_with_reset_type
- assert_dom_equal(
- %(<button name="button" type="reset">Reset</button>),
- button_tag("Reset", :type => "reset")
- )
- end
-
- def test_button_tag_with_disabled_option
- assert_dom_equal(
- %(<button name="button" type="reset" disabled="disabled">Reset</button>),
- button_tag("Reset", :type => "reset", :disabled => true)
- )
- end
-
- def test_button_tag_escape_content
- assert_dom_equal(
- %(<button name="button" type="reset" disabled="disabled">&lt;b&gt;Reset&lt;/b&gt;</button>),
- button_tag("<b>Reset</b>", :type => "reset", :disabled => true)
- )
- end
-
- def test_button_tag_with_block
- assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { 'Content' })
- end
-
- def test_button_tag_with_block_and_options
- output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') }
- assert_dom_equal('<button name="temptation" type="button"><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>),
- button_tag("Save", :type => "submit", :data => { :confirm => "Are you sure?" })
- )
- end
-
- def test_button_tag_with_deprecated_confirmation
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %(<button name="button" type="submit" data-confirm="Are you sure?">Save</button>),
- button_tag("Save", :type => "submit", :confirm => "Are you sure?")
- )
- end
- end
-
- def test_image_submit_tag_with_confirmation
- assert_dom_equal(
- %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
- image_submit_tag("save.gif", :data => { :confirm => "Are you sure?" })
- )
- end
-
- def test_image_submit_tag_with_deprecated_confirmation
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %(<input type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
- image_submit_tag("save.gif", :confirm => "Are you sure?")
- )
- end
- end
-
-
- def test_color_field_tag
- expected = %{<input id="car" name="car" type="color" />}
- assert_dom_equal(expected, color_field_tag("car"))
- end
-
- def test_search_field_tag
- expected = %{<input id="query" name="query" type="search" />}
- assert_dom_equal(expected, search_field_tag("query"))
- end
-
- def test_telephone_field_tag
- expected = %{<input id="cell" name="cell" type="tel" />}
- assert_dom_equal(expected, telephone_field_tag("cell"))
- end
-
- def test_date_field_tag
- expected = %{<input id="cell" name="cell" type="date" />}
- assert_dom_equal(expected, date_field_tag("cell"))
- end
-
- def test_time_field_tag
- expected = %{<input id="cell" name="cell" type="time" />}
- assert_dom_equal(expected, time_field_tag("cell"))
- end
-
- def test_datetime_field_tag
- expected = %{<input id="appointment" name="appointment" type="datetime" />}
- assert_dom_equal(expected, datetime_field_tag("appointment"))
- end
-
- def test_datetime_local_field_tag
- expected = %{<input id="appointment" name="appointment" type="datetime-local" />}
- assert_dom_equal(expected, datetime_local_field_tag("appointment"))
- end
-
- def test_month_field_tag
- expected = %{<input id="birthday" name="birthday" type="month" />}
- assert_dom_equal(expected, month_field_tag("birthday"))
- end
-
- def test_week_field_tag
- expected = %{<input id="birthday" name="birthday" type="week" />}
- assert_dom_equal(expected, week_field_tag("birthday"))
- end
-
- def test_url_field_tag
- expected = %{<input id="homepage" name="homepage" type="url" />}
- assert_dom_equal(expected, url_field_tag("homepage"))
- end
-
- def test_email_field_tag
- expected = %{<input id="address" name="address" type="email" />}
- assert_dom_equal(expected, email_field_tag("address"))
- end
-
- def test_number_field_tag
- expected = %{<input name="quantity" max="9" id="quantity" type="number" min="1" />}
- assert_dom_equal(expected, number_field_tag("quantity", nil, :in => 1...10))
- end
-
- def test_range_input_tag
- expected = %{<input name="volume" step="0.1" max="11" id="volume" type="range" min="0" />}
- assert_dom_equal(expected, range_field_tag("volume", nil, :in => 0..11, :step => 0.1))
- end
-
- def test_field_set_tag_in_erb
- output_buffer = render_erb("<%= field_set_tag('Your details') do %>Hello world!<% end %>")
-
- expected = %(<fieldset><legend>Your details</legend>Hello world!</fieldset>)
- assert_dom_equal expected, output_buffer
-
- output_buffer = render_erb("<%= field_set_tag do %>Hello world!<% end %>")
-
- expected = %(<fieldset>Hello world!</fieldset>)
- assert_dom_equal expected, output_buffer
-
- output_buffer = render_erb("<%= field_set_tag('') do %>Hello world!<% end %>")
-
- expected = %(<fieldset>Hello world!</fieldset>)
- assert_dom_equal expected, output_buffer
-
- output_buffer = render_erb("<%= field_set_tag('', :class => 'format') do %>Hello world!<% end %>")
-
- expected = %(<fieldset class="format">Hello world!</fieldset>)
- assert_dom_equal expected, output_buffer
-
- output_buffer = render_erb("<%= field_set_tag %>")
-
- expected = %(<fieldset></fieldset>)
- assert_dom_equal expected, output_buffer
-
- output_buffer = render_erb("<%= field_set_tag('You legend!') %>")
-
- expected = %(<fieldset><legend>You legend!</legend></fieldset>)
- assert_dom_equal expected, output_buffer
- end
-
- def test_text_area_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
- text_area_tag "body", "hello world", options
- assert_equal options, { :option => "random_option" }
- end
-
- def test_submit_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
- submit_tag "submit value", options
- assert_equal options, { :option => "random_option" }
- end
-
- def test_button_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
- button_tag "button value", options
- assert_equal options, { :option => "random_option" }
- end
-
- def test_image_submit_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
- image_submit_tag "submit source", options
- assert_equal options, { :option => "random_option" }
- end
-
- def test_image_label_tag_options_symbolize_keys_side_effects
- options = { :option => "random_option" }
- label_tag "submit source", "title", options
- assert_equal options, { :option => "random_option" }
- end
-
- def protect_against_forgery?
- false
- end
-
- private
-
- def root_elem(rendered_content)
- HTML::Document.new(rendered_content).root.children[0]
- end
-end
diff --git a/actionpack/test/template/html-scanner/sanitizer_test.rb b/actionpack/test/template/html-scanner/sanitizer_test.rb
deleted file mode 100644
index d9b57776c9..0000000000
--- a/actionpack/test/template/html-scanner/sanitizer_test.rb
+++ /dev/null
@@ -1,315 +0,0 @@
-require 'abstract_unit'
-
-class SanitizerTest < ActionController::TestCase
- def setup
- @sanitizer = nil # used by assert_sanitizer
- end
-
- def test_strip_tags_with_quote
- sanitizer = HTML::FullSanitizer.new
- string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
-
- assert_equal ' hi', sanitizer.sanitize(string)
- end
-
- def test_strip_tags
- sanitizer = HTML::FullSanitizer.new
- assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))
- assert_equal("<<", sanitizer.sanitize("<<<bad html>"))
- assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me"))
- assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
- assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
- assert_equal("This is a test.", sanitizer.sanitize("This is a test."))
- assert_equal(
- %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize(
- %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
- assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.")
- assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.")
- assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...")
- [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) }
- assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) }
- end
-
- def test_strip_links
- sanitizer = HTML::LinkSanitizer.new
- assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me")
- assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
- assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
- assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
- assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>")
- assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
- assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
-
- assert_equal "<a<a", sanitizer.sanitize("<a<a")
- end
-
- def test_sanitize_form
- assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ''
- end
-
- def test_sanitize_plaintext
- raw = "<plaintext><span>foo</span></plaintext>"
- assert_sanitized raw, "<span>foo</span>"
- end
-
- def test_sanitize_script
- assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
- end
-
- def test_sanitize_js_handlers
- raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
- assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>}
- end
-
- def test_sanitize_javascript_href
- raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
- assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
- end
-
- def test_sanitize_image_src
- raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
- assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>}
- end
-
- HTML::WhiteListSanitizer.allowed_tags.each do |tag_name|
- define_method "test_should_allow_#{tag_name}_tag" do
- assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end)
- end
- end
-
- def test_should_allow_anchors
- assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
- end
-
- # RFC 3986, sec 4.2
- def test_allow_colons_in_path_component
- assert_sanitized("<a href=\"./this:that\">foo</a>")
- end
-
- %w(src width height alt).each do |img_attr|
- define_method "test_should_allow_image_#{img_attr}_attribute" do
- assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
- end
- end
-
- def test_should_handle_non_html
- assert_sanitized 'abc'
- end
-
- def test_should_handle_blank_text
- assert_sanitized nil
- assert_sanitized ''
- end
-
- def test_should_allow_custom_tags
- text = "<u>foo</u>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_only_custom_tags
- text = "<u>foo</u> with <i>bar</i>"
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u)))
- end
-
- def test_should_allow_custom_tags_with_attributes
- text = %(<blockquote cite="http://example.com/">foo</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text))
- end
-
- def test_should_allow_custom_tags_with_custom_attributes
- text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
- sanitizer = HTML::WhiteListSanitizer.new
- assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo']))
- end
-
- def test_should_raise_argument_error_if_tags_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :tags => 'foo')
- end
-
- assert_equal "You should pass :tags as an Enumerable", e.message
- end
-
- def test_should_raise_argument_error_if_attributes_is_not_enumerable
- sanitizer = HTML::WhiteListSanitizer.new
- e = assert_raise(ArgumentError) do
- sanitizer.sanitize('', :attributes => 'foo')
- end
-
- assert_equal "You should pass :attributes as an Enumerable", e.message
- end
-
- [%w(img src), %w(a href)].each do |(tag, attr)|
- define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
- assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
- end
- end
-
- def test_should_flag_bad_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
- assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_case
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good")
- end
- end
-
- def test_should_accept_good_protocols_ignoring_space
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good")
- end
- end
-
- def test_should_accept_good_protocols
- sanitizer = HTML::WhiteListSanitizer.new
- HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
- assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good")
- end
- end
-
- def test_should_reject_hex_codes_in_protocol
- assert_sanitized %(<a href="&#37;6A&#37;61&#37;76&#37;61&#37;73&#37;63&#37;72&#37;69&#37;70&#37;74&#37;3A&#37;61&#37;6C&#37;65&#37;72&#37;74&#37;28&#37;22&#37;58&#37;53&#37;53&#37;22&#37;29">1</a>), "<a>1</a>"
- assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
- end
-
- def test_should_block_script_tag
- assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
- end
-
- [%(<IMG SRC="javascript:alert('XSS');">),
- %(<IMG SRC=javascript:alert('XSS')>),
- %(<IMG SRC=JaVaScRiPt:alert('XSS')>),
- %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
- %(<IMG SRC=javascript:alert(&quot;XSS&quot;)>),
- %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
- %(<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>),
- %(<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>),
- %(<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>),
- %(<IMG SRC="jav\tascript:alert('XSS');">),
- %(<IMG SRC="jav&#x09;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0A;ascript:alert('XSS');">),
- %(<IMG SRC="jav&#x0D;ascript:alert('XSS');">),
- %(<IMG SRC=" &#14; javascript:alert('XSS');">),
- %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
- define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
- assert_sanitized img_hack, "<img>"
- end
- end
-
- def test_should_sanitize_tag_broken_up_by_null
- assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")"
- end
-
- def test_should_sanitize_invalid_script_tag
- assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
- end
-
- def test_should_sanitize_script_tag_with_multiple_open_brackets
- assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "&lt;"
- assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(&lt;a)
- end
-
- def test_should_sanitize_unclosed_script
- assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
- end
-
- def test_should_sanitize_half_open_scripts
- assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>"
- end
-
- def test_should_not_fall_for_ridiculous_hack
- img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
- assert_sanitized img_hack, "<img>"
- end
-
- def test_should_sanitize_attributes
- assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>)
- end
-
- def test_should_sanitize_illegal_style_properties
- raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
- expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_with_trailing_space
- raw = "display:block; "
- expected = "display: block;"
- assert_equal expected, sanitize_css(raw)
- end
-
- def test_should_sanitize_xul_style_attributes
- raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_invalid_tag_names
- assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
- end
-
- def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
- assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
- end
-
- def test_should_sanitize_invalid_tag_names_in_single_tags
- assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />")
- end
-
- def test_should_sanitize_img_dynsrc_lowsrc
- assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />")
- end
-
- def test_should_sanitize_div_background_image_unicode_encoded
- raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_div_style_expression
- raw = %(width: expression(alert('XSS'));)
- assert_equal '', sanitize_css(raw)
- end
-
- def test_should_sanitize_img_vbscript
- assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
- end
-
- def test_should_sanitize_cdata_section
- assert_sanitized "<![CDATA[<span>section</span>]]>", "&lt;![CDATA[&lt;span>section&lt;/span>]]>"
- end
-
- def test_should_sanitize_unterminated_cdata_section
- assert_sanitized "<![CDATA[<span>neverending...", "&lt;![CDATA[&lt;span>neverending...]]>"
- end
-
- def test_should_not_mangle_urls_with_ampersand
- assert_sanitized %{<a href=\"http://www.domain.com?var1=1&amp;var2=2\">my link</a>}
- end
-
- def test_should_sanitize_neverending_attribute
- assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
- end
-
-protected
- def assert_sanitized(input, expected = nil)
- @sanitizer ||= HTML::WhiteListSanitizer.new
- if input
- assert_dom_equal expected || input, @sanitizer.sanitize(input)
- else
- assert_nil @sanitizer.sanitize(input)
- end
- end
-
- def sanitize_css(input)
- (@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input)
- end
-end
diff --git a/actionpack/test/template/javascript_helper_test.rb b/actionpack/test/template/javascript_helper_test.rb
deleted file mode 100644
index 58784c26fa..0000000000
--- a/actionpack/test/template/javascript_helper_test.rb
+++ /dev/null
@@ -1,104 +0,0 @@
-require 'abstract_unit'
-
-class JavaScriptHelperTest < ActionView::TestCase
- tests ActionView::Helpers::JavaScriptHelper
-
- def _evaluate_assigns_and_ivars() end
-
- attr_accessor :formats, :output_buffer
-
- def update_details(details)
- @details = details
- yield if block_given?
- end
-
- def setup
- super
- ActiveSupport.escape_html_entities_in_json = true
- @template = self
- end
-
- def teardown
- ActiveSupport.escape_html_entities_in_json = false
- end
-
- def test_escape_javascript
- assert_equal '', escape_javascript(nil)
- assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
- assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) )
- assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags))
- assert_equal %(unicode &#x2028; newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding('UTF-8').encode!)
- assert_equal %(unicode &#x2029; newline), escape_javascript(%(unicode \342\200\251 newline).force_encoding('UTF-8').encode!)
-
- assert_equal %(dont <\\/close> tags), j(%(dont </close> tags))
- end
-
- def test_escape_javascript_with_safebuffer
- given = %('quoted' "double-quoted" new-line:\n </closed>)
- expect = %(\\'quoted\\' \\"double-quoted\\" new-line:\\n <\\/closed>)
- assert_equal expect, escape_javascript(given)
- assert_equal expect, escape_javascript(ActiveSupport::SafeBuffer.new(given))
- assert_instance_of String, escape_javascript(given)
- assert_instance_of ActiveSupport::SafeBuffer, escape_javascript(ActiveSupport::SafeBuffer.new(given))
- end
-
- def test_button_to_function
- assert_deprecated "button_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal %(<input type="button" onclick="alert(&#39;Hello world!&#39;);" value="Greeting" />),
- button_to_function("Greeting", "alert('Hello world!')")
- end
- end
-
- def test_button_to_function_with_onclick
- assert_deprecated "button_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal "<input onclick=\"alert(&#39;Goodbye World :(&#39;); alert(&#39;Hello world!&#39;);\" type=\"button\" value=\"Greeting\" />",
- button_to_function("Greeting", "alert('Hello world!')", :onclick => "alert('Goodbye World :(')")
- end
- end
-
- def test_button_to_function_without_function
- assert_deprecated "button_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal "<input onclick=\";\" type=\"button\" value=\"Greeting\" />",
- button_to_function("Greeting")
- end
- end
-
- def test_link_to_function
- assert_deprecated "link_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal %(<a href="#" onclick="alert(&#39;Hello world!&#39;); return false;">Greeting</a>),
- link_to_function("Greeting", "alert('Hello world!')")
- end
- end
-
- def test_link_to_function_with_existing_onclick
- assert_deprecated "link_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal %(<a href="#" onclick="confirm(&#39;Sanity!&#39;); alert(&#39;Hello world!&#39;); return false;">Greeting</a>),
- link_to_function("Greeting", "alert('Hello world!')", :onclick => "confirm('Sanity!')")
- end
- end
-
- def test_function_with_href
- assert_deprecated "link_to_function is deprecated and will be removed from Rails 4.1. Use Unobtrusive JavaScript instead." do
- assert_dom_equal %(<a href="http://example.com/" onclick="alert(&#39;Hello world!&#39;); return false;">Greeting</a>),
- link_to_function("Greeting", "alert('Hello world!')", :href => 'http://example.com/')
- end
- end
-
- def test_javascript_tag
- self.output_buffer = 'foo'
-
- assert_dom_equal "<script>\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
- javascript_tag("alert('hello')")
-
- assert_equal 'foo', output_buffer, 'javascript_tag without a block should not concat to output_buffer'
- end
-
- def test_javascript_tag_with_options
- assert_dom_equal "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
- javascript_tag("alert('hello')", :id => "the_js_tag")
- end
-
- def test_javascript_cdata_section
- assert_dom_equal "\n//<![CDATA[\nalert('hello')\n//]]>\n", javascript_cdata_section("alert('hello')")
- end
-end
diff --git a/actionpack/test/template/lookup_context_test.rb b/actionpack/test/template/lookup_context_test.rb
deleted file mode 100644
index 073bd14783..0000000000
--- a/actionpack/test/template/lookup_context_test.rb
+++ /dev/null
@@ -1,263 +0,0 @@
-require "abstract_unit"
-require "abstract_controller/rendering"
-
-class LookupContextTest < ActiveSupport::TestCase
- def setup
- @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {})
- ActionView::LookupContext::DetailsKey.clear
- end
-
- def teardown
- I18n.locale = :en
- end
-
- test "allows to override default_formats with ActionView::Base.default_formats" do
- begin
- formats = ActionView::Base.default_formats
- ActionView::Base.default_formats = [:foo, :bar]
-
- assert_equal [:foo, :bar], ActionView::LookupContext.new([]).default_formats
- ensure
- ActionView::Base.default_formats = formats
- end
- end
-
- test "process view paths on initialization" do
- assert_kind_of ActionView::PathSet, @lookup_context.view_paths
- end
-
- test "normalizes details on initialization" do
- assert_equal Mime::SET, @lookup_context.formats
- assert_equal :en, @lookup_context.locale
- end
-
- test "allows me to freeze and retrieve frozen formats" do
- @lookup_context.formats.freeze
- assert @lookup_context.formats.frozen?
- end
-
- test "provides getters and setters for formats" do
- @lookup_context.formats = [:html]
- assert_equal [:html], @lookup_context.formats
- end
-
- test "handles */* formats" do
- @lookup_context.formats = ["*/*"]
- assert_equal Mime::SET, @lookup_context.formats
- end
-
- test "handles explicitly defined */* formats fallback to :js" do
- @lookup_context.formats = [:js, Mime::ALL]
- assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats
- end
-
- test "adds :html fallback to :js formats" do
- @lookup_context.formats = [:js]
- assert_equal [:js, :html], @lookup_context.formats
- end
-
- test "provides getters and setters for locale" do
- @lookup_context.locale = :pt
- assert_equal :pt, @lookup_context.locale
- end
-
- test "changing lookup_context locale, changes I18n.locale" do
- @lookup_context.locale = :pt
- assert_equal :pt, I18n.locale
- end
-
- test "delegates changing the locale to the I18n configuration object if it contains a lookup_context object" do
- begin
- I18n.config = AbstractController::I18nProxy.new(I18n.config, @lookup_context)
- @lookup_context.locale = :pt
- assert_equal :pt, I18n.locale
- assert_equal :pt, @lookup_context.locale
- ensure
- I18n.config = I18n.config.original_config
- end
-
- assert_equal :pt, I18n.locale
- end
-
- test "find templates using the given view paths and configured details" do
- template = @lookup_context.find("hello_world", %w(test))
- assert_equal "Hello world!", template.source
-
- @lookup_context.locale = :da
- template = @lookup_context.find("hello_world", %w(test))
- assert_equal "Hey verden", 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]
- template = @lookup_context.find("hello", %w(test))
- assert_equal [:text], template.formats
- end
-
- test "adds fallbacks to view paths when required" do
- assert_equal 1, @lookup_context.view_paths.size
-
- @lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new(""))
- assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/"))
- end
- end
-
- test "add fallbacks just once in nested fallbacks calls" do
- @lookup_context.with_fallbacks do
- @lookup_context.with_fallbacks do
- assert_equal 3, @lookup_context.view_paths.size
- end
- end
- end
-
- test "generates a new details key for each details hash" do
- keys = []
- keys << @lookup_context.details_key
- assert_equal 1, keys.uniq.size
-
- @lookup_context.locale = :da
- keys << @lookup_context.details_key
- assert_equal 2, keys.uniq.size
-
- @lookup_context.locale = :en
- keys << @lookup_context.details_key
- assert_equal 2, keys.uniq.size
-
- @lookup_context.formats = [:html]
- keys << @lookup_context.details_key
- assert_equal 3, keys.uniq.size
-
- @lookup_context.formats = nil
- keys << @lookup_context.details_key
- assert_equal 3, keys.uniq.size
- end
-
- test "gives the key forward to the resolver, so it can be used as cache key" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now we are going to change the template, but it won't change the returned template
- # since we will hit the cache.
- @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar"
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # This time we will change the locale. The updated template should be picked since
- # lookup_context generated a new key after we changed the locale.
- @lookup_context.locale = :da
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
-
- # Now we will change back the locale and it will still pick the old template.
- # This is expected because lookup_context will reuse the previous key for :en locale.
- @lookup_context.locale = :en
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Finally, we can expire the cache. And the expected template will be used.
- @lookup_context.view_paths.first.clear_cache
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
- end
-
- test "can disable the cache on demand" do
- @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
- old_template = @lookup_context.find("foo", %w(test), true)
-
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal template, old_template
-
- assert @lookup_context.cache
- template = @lookup_context.disable_cache do
- assert !@lookup_context.cache
- @lookup_context.find("foo", %w(test), true)
- end
- assert @lookup_context.cache
-
- assert_not_equal template, old_template
- end
-
- test "responds to #prefixes" do
- assert_equal [], @lookup_context.prefixes
- @lookup_context.prefixes = ["foo"]
- assert_equal ["foo"], @lookup_context.prefixes
- end
-end
-
-class LookupContextWithFalseCaching < ActiveSupport::TestCase
- def setup
- @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
- ActionView::Resolver.stubs(:caching?).returns(false)
- @lookup_context = ActionView::LookupContext.new(@resolver, {})
- end
-
- test "templates are always found in the resolver but timestamp is checked before being compiled" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now we are going to change the template, but it won't change the returned template
- # since the timestamp is the same.
- @resolver.hash["test/_foo.erb"][0] = "Bar"
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- # Now update the timestamp.
- @resolver.hash["test/_foo.erb"][1] = Time.now.utc
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Bar", template.source
- end
-
- test "if no template was found in the second lookup, with no cache, raise error" do
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
-
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
- end
-
- test "if no template was cached in the first lookup, retrieval should work in the second call" do
- @resolver.hash.clear
- assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(test), true)
- end
-
- @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
- template = @lookup_context.find("foo", %w(test), true)
- assert_equal "Foo", template.source
- end
-end
-
-class TestMissingTemplate < ActiveSupport::TestCase
- def setup
- @lookup_context = ActionView::LookupContext.new("/Path/to/views", {})
- end
-
- test "if no template was found we get a helpful error message including the inheritance chain" do
- e = assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(parent child))
- end
- assert_match %r{Missing template parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
- end
-
- test "if no partial was found we get a helpful error message including the inheritance chain" do
- e = assert_raise ActionView::MissingTemplate do
- @lookup_context.find("foo", %w(parent child), true)
- end
- assert_match %r{Missing partial parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
- end
-
- test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do
- e = assert_raise ActionView::MissingTemplate do
- details = {:handlers=>[], :formats=>[], :locale=>[]}
- @lookup_context.view_paths.find("foo", "parent", true, details)
- end
- assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
- end
-
-end
diff --git a/actionpack/test/template/number_helper_test.rb b/actionpack/test/template/number_helper_test.rb
deleted file mode 100644
index d8fffe75ed..0000000000
--- a/actionpack/test/template/number_helper_test.rb
+++ /dev/null
@@ -1,399 +0,0 @@
-require 'abstract_unit'
-
-class NumberHelperTest < ActionView::TestCase
- tests ActionView::Helpers::NumberHelper
-
- def kilobytes(number)
- number * 1024
- end
-
- def megabytes(number)
- kilobytes(number) * 1024
- end
-
- def gigabytes(number)
- megabytes(number) * 1024
- end
-
- def terabytes(number)
- gigabytes(number) * 1024
- end
-
- def test_number_to_phone
- assert_equal("555-1234", number_to_phone(5551234))
- assert_equal("800-555-1212", number_to_phone(8005551212))
- assert_equal("(800) 555-1212", number_to_phone(8005551212, {:area_code => true}))
- assert_equal("", number_to_phone("", {:area_code => true}))
- assert_equal("800 555 1212", number_to_phone(8005551212, {:delimiter => " "}))
- assert_equal("(800) 555-1212 x 123", number_to_phone(8005551212, {:area_code => true, :extension => 123}))
- assert_equal("800-555-1212", number_to_phone(8005551212, :extension => " "))
- assert_equal("555.1212", number_to_phone(5551212, :delimiter => '.'))
- assert_equal("800-555-1212", number_to_phone("8005551212"))
- assert_equal("+1-800-555-1212", number_to_phone(8005551212, :country_code => 1))
- assert_equal("+18005551212", number_to_phone(8005551212, :country_code => 1, :delimiter => ''))
- assert_equal("22-555-1212", number_to_phone(225551212))
- assert_equal("+45-22-555-1212", number_to_phone(225551212, :country_code => 45))
- assert_equal '111&lt;script&gt;&lt;/script&gt;111&lt;script&gt;&lt;/script&gt;1111', number_to_phone(1111111111, :delimiter => "<script></script>")
- end
-
- def test_number_to_currency
- assert_equal("$1,234,567,890.50", number_to_currency(1234567890.50))
- assert_equal("$1,234,567,890.51", number_to_currency(1234567890.506))
- assert_equal("-$1,234,567,890.50", number_to_currency(-1234567890.50))
- assert_equal("-$ 1,234,567,890.50", number_to_currency(-1234567890.50, {:format => "%u %n"}))
- assert_equal("($1,234,567,890.50)", number_to_currency(-1234567890.50, {:negative_format => "(%u%n)"}))
- assert_equal("$1,234,567,892", number_to_currency(1234567891.50, {:precision => 0}))
- assert_equal("$1,234,567,890.5", number_to_currency(1234567890.50, {:precision => 1}))
- assert_equal("&pound;1234567890,50", number_to_currency(1234567890.50, {:unit => "&pound;", :separator => ",", :delimiter => ""}))
- assert_equal("$1,234,567,890.50", number_to_currency("1234567890.50"))
- assert_equal("1,234,567,890.50 K&#269;", number_to_currency("1234567890.50", {:unit => "K&#269;", :format => "%n %u"}))
- assert_equal("1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", {:unit => "K&#269;", :format => "%n %u", :negative_format => "%n - %u"}))
- assert_equal '$1&lt;script&gt;&lt;/script&gt;01', number_to_currency(1.01, :separator => "<script></script>")
- assert_equal '$1&lt;script&gt;&lt;/script&gt;000.00', number_to_currency(1000, :delimiter => "<script></script>")
- end
-
- def test_number_to_percentage
- assert_equal("100.000%", number_to_percentage(100))
- assert_equal("100%", number_to_percentage(100, {:precision => 0}))
- assert_equal("302.06%", number_to_percentage(302.0574, {:precision => 2}))
- assert_equal("100.000%", number_to_percentage("100"))
- assert_equal("1000.000%", number_to_percentage("1000"))
- assert_equal("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("1000.000 %", number_to_percentage(1000, :format => "%n %"))
- assert_equal '1&lt;script&gt;&lt;/script&gt;010%', number_to_percentage(1.01, :separator => "<script></script>")
- assert_equal '1&lt;script&gt;&lt;/script&gt;000.000%', number_to_percentage(1000, :delimiter => "<script></script>")
- end
-
- def test_number_with_delimiter
- assert_equal("12,345,678", number_with_delimiter(12345678))
- assert_equal("0", number_with_delimiter(0))
- assert_equal("123", number_with_delimiter(123))
- assert_equal("123,456", number_with_delimiter(123456))
- assert_equal("123,456.78", number_with_delimiter(123456.78))
- assert_equal("123,456.789", number_with_delimiter(123456.789))
- assert_equal("123,456.78901", number_with_delimiter(123456.78901))
- assert_equal("123,456,789.78901", number_with_delimiter(123456789.78901))
- assert_equal("0.78901", number_with_delimiter(0.78901))
- assert_equal("123,456.78", number_with_delimiter("123456.78"))
- end
-
- def test_number_with_delimiter_with_options_hash
- assert_equal '12 345 678', number_with_delimiter(12345678, :delimiter => ' ')
- assert_equal '12,345,678-05', number_with_delimiter(12345678.05, :separator => '-')
- assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :separator => ',', :delimiter => '.')
- assert_equal '12.345.678,05', number_with_delimiter(12345678.05, :delimiter => '.', :separator => ',')
- assert_equal '1&lt;script&gt;&lt;/script&gt;01', number_with_delimiter(1.01, :separator => "<script></script>")
- assert_equal '1&lt;script&gt;&lt;/script&gt;000', number_with_delimiter(1000, :delimiter => "<script></script>")
- end
-
- def test_number_with_precision
- assert_equal("-111.235", number_with_precision(-111.2346))
- assert_equal("111.235", number_with_precision(111.2346))
- assert_equal("31.83", number_with_precision(31.825, :precision => 2))
- assert_equal("111.23", number_with_precision(111.2346, :precision => 2))
- assert_equal("111.00", number_with_precision(111, :precision => 2))
- assert_equal("111.235", number_with_precision("111.2346"))
- assert_equal("31.83", number_with_precision("31.825", :precision => 2))
- assert_equal("3268", number_with_precision((32.6751 * 100.00), :precision => 0))
- assert_equal("112", number_with_precision(111.50, :precision => 0))
- assert_equal("1234567892", number_with_precision(1234567891.50, :precision => 0))
- assert_equal("0", number_with_precision(0, :precision => 0))
- assert_equal("0.00100", number_with_precision(0.001, :precision => 5))
- assert_equal("0.001", number_with_precision(0.00111, :precision => 3))
- assert_equal("10.00", number_with_precision(9.995, :precision => 2))
- assert_equal("11.00", number_with_precision(10.995, :precision => 2))
- assert_equal("0.00", number_with_precision(-0.001, :precision => 2))
- end
-
- def test_number_with_precision_with_custom_delimiter_and_separator
- assert_equal '31,83', number_with_precision(31.825, :precision => 2, :separator => ',')
- assert_equal '1.231,83', number_with_precision(1231.825, :precision => 2, :separator => ',', :delimiter => '.')
- assert_equal '1&lt;script&gt;&lt;/script&gt;010', number_with_precision(1.01, :separator => "<script></script>")
- assert_equal '1&lt;script&gt;&lt;/script&gt;000.000', number_with_precision(1000, :delimiter => "<script></script>")
- end
-
- def test_number_with_precision_with_significant_digits
- assert_equal "124000", number_with_precision(123987, :precision => 3, :significant => true)
- assert_equal "120000000", number_with_precision(123987876, :precision => 2, :significant => true )
- assert_equal "40000", number_with_precision("43523", :precision => 1, :significant => true )
- assert_equal "9775", number_with_precision(9775, :precision => 4, :significant => true )
- assert_equal "5.4", number_with_precision(5.3923, :precision => 2, :significant => true )
- assert_equal "5", number_with_precision(5.3923, :precision => 1, :significant => true )
- assert_equal "1", number_with_precision(1.232, :precision => 1, :significant => true )
- assert_equal "7", number_with_precision(7, :precision => 1, :significant => true )
- assert_equal "1", number_with_precision(1, :precision => 1, :significant => true )
- assert_equal "53", number_with_precision(52.7923, :precision => 2, :significant => true )
- assert_equal "9775.00", number_with_precision(9775, :precision => 6, :significant => true )
- assert_equal "5.392900", number_with_precision(5.3929, :precision => 7, :significant => true )
- assert_equal "0.0", number_with_precision(0, :precision => 2, :significant => true )
- assert_equal "0", number_with_precision(0, :precision => 1, :significant => true )
- assert_equal "0.0001", number_with_precision(0.0001, :precision => 1, :significant => true )
- assert_equal "0.000100", number_with_precision(0.0001, :precision => 3, :significant => true )
- assert_equal "0.0001", number_with_precision(0.0001111, :precision => 1, :significant => true )
- assert_equal "10.0", number_with_precision(9.995, :precision => 3, :significant => true)
- assert_equal "9.99", number_with_precision(9.994, :precision => 3, :significant => true)
- assert_equal "11.0", number_with_precision(10.995, :precision => 3, :significant => true)
- end
-
- def test_number_with_precision_with_strip_insignificant_zeros
- assert_equal "9775.43", number_with_precision(9775.43, :precision => 4, :strip_insignificant_zeros => true )
- assert_equal "9775.2", number_with_precision(9775.2, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
- assert_equal "0", number_with_precision(0, :precision => 6, :significant => true, :strip_insignificant_zeros => true )
- end
-
- def test_number_with_precision_with_significant_true_and_zero_precision
- # Zero precision with significant is a mistake (would always return zero),
- # so we treat it as if significant was false (increases backwards compatibility for number_to_human_size)
- assert_equal "124", number_with_precision(123.987, :precision => 0, :significant => true)
- assert_equal "12", number_with_precision(12, :precision => 0, :significant => true )
- assert_equal "12", number_with_precision("12.3", :precision => 0, :significant => true )
- end
-
- def test_number_to_human_size
- assert_equal '0 Bytes', number_to_human_size(0)
- assert_equal '1 Byte', number_to_human_size(1)
- assert_equal '3 Bytes', number_to_human_size(3.14159265)
- assert_equal '123 Bytes', number_to_human_size(123.0)
- assert_equal '123 Bytes', number_to_human_size(123)
- assert_equal '1.21 KB', number_to_human_size(1234)
- assert_equal '12.1 KB', number_to_human_size(12345)
- assert_equal '1.18 MB', number_to_human_size(1234567)
- assert_equal '1.15 GB', number_to_human_size(1234567890)
- assert_equal '1.12 TB', number_to_human_size(1234567890123)
- assert_equal '1030 TB', number_to_human_size(terabytes(1026))
- assert_equal '444 KB', number_to_human_size(kilobytes(444))
- assert_equal '1020 MB', number_to_human_size(megabytes(1023))
- assert_equal '3 TB', number_to_human_size(terabytes(3))
- assert_equal '1.2 MB', number_to_human_size(1234567, :precision => 2)
- assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4)
- assert_equal '123 Bytes', number_to_human_size('123')
- assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 2)
- assert_equal '1.01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4)
- assert_equal '10 KB', number_to_human_size(kilobytes(10.000), :precision => 4)
- assert_equal '1 Byte', number_to_human_size(1.1)
- assert_equal '10 Bytes', number_to_human_size(10)
- end
-
- def test_number_to_human_size_with_si_prefix
- assert_equal '3 Bytes', number_to_human_size(3.14159265, :prefix => :si)
- assert_equal '123 Bytes', number_to_human_size(123.0, :prefix => :si)
- assert_equal '123 Bytes', number_to_human_size(123, :prefix => :si)
- assert_equal '1.23 KB', number_to_human_size(1234, :prefix => :si)
- assert_equal '12.3 KB', number_to_human_size(12345, :prefix => :si)
- assert_equal '1.23 MB', number_to_human_size(1234567, :prefix => :si)
- assert_equal '1.23 GB', number_to_human_size(1234567890, :prefix => :si)
- assert_equal '1.23 TB', number_to_human_size(1234567890123, :prefix => :si)
- end
-
- def test_number_to_human_size_with_options_hash
- assert_equal '1.2 MB', number_to_human_size(1234567, :precision => 2)
- assert_equal '3 Bytes', number_to_human_size(3.14159265, :precision => 4)
- assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 2)
- assert_equal '1.01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4)
- assert_equal '10 KB', number_to_human_size(kilobytes(10.000), :precision => 4)
- assert_equal '1 TB', number_to_human_size(1234567890123, :precision => 1)
- assert_equal '500 MB', number_to_human_size(524288000, :precision=>3)
- assert_equal '10 MB', number_to_human_size(9961472, :precision=>0)
- assert_equal '40 KB', number_to_human_size(41010, :precision => 1)
- assert_equal '40 KB', number_to_human_size(41100, :precision => 2)
- assert_equal '1.0 KB', number_to_human_size(kilobytes(1.0123), :precision => 2, :strip_insignificant_zeros => false)
- assert_equal '1.012 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :significant => false)
- assert_equal '1 KB', number_to_human_size(kilobytes(1.0123), :precision => 0, :significant => true) #ignores significant it precision is 0
- assert_equal '9&lt;script&gt;&lt;/script&gt;86 KB', number_to_human_size(10100, :separator => "<script></script>")
- end
-
- def test_number_to_human_size_with_custom_delimiter_and_separator
- assert_equal '1,01 KB', number_to_human_size(kilobytes(1.0123), :precision => 3, :separator => ',')
- assert_equal '1,01 KB', number_to_human_size(kilobytes(1.0100), :precision => 4, :separator => ',')
- assert_equal '1.000,1 TB', number_to_human_size(terabytes(1000.1), :precision => 5, :delimiter => '.', :separator => ',')
- end
-
- def test_number_to_human
- assert_equal '-123', number_to_human(-123)
- assert_equal '-0.5', number_to_human(-0.5)
- assert_equal '0', number_to_human(0)
- assert_equal '0.5', number_to_human(0.5)
- assert_equal '123', number_to_human(123)
- assert_equal '1.23 Thousand', number_to_human(1234)
- assert_equal '12.3 Thousand', number_to_human(12345)
- assert_equal '1.23 Million', number_to_human(1234567)
- assert_equal '1.23 Billion', number_to_human(1234567890)
- assert_equal '1.23 Trillion', number_to_human(1234567890123)
- assert_equal '1.23 Quadrillion', number_to_human(1234567890123456)
- assert_equal '1230 Quadrillion', number_to_human(1234567890123456789)
- assert_equal '490 Thousand', number_to_human(489939, :precision => 2)
- assert_equal '489.9 Thousand', number_to_human(489939, :precision => 4)
- assert_equal '489 Thousand', number_to_human(489000, :precision => 4)
- assert_equal '489.0 Thousand', number_to_human(489000, :precision => 4, :strip_insignificant_zeros => false)
- assert_equal '1.2346 Million', number_to_human(1234567, :precision => 4, :significant => false)
- assert_equal '1,2 Million', number_to_human(1234567, :precision => 1, :significant => false, :separator => ',')
- assert_equal '1 Million', number_to_human(1234567, :precision => 0, :significant => true, :separator => ',') #significant forced to false
- end
-
- def test_number_to_human_with_custom_units
- #Only integers
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123 lt', number_to_human(123456, :units => volume)
- assert_equal '12 ml', number_to_human(12, :units => volume)
- assert_equal '1.23 m3', number_to_human(1234567, :units => volume)
-
- #Including fractionals
- distance = {:mili => "mm", :centi => "cm", :deci => "dm", :unit => "m", :ten => "dam", :hundred => "hm", :thousand => "km"}
- assert_equal '1.23 mm', number_to_human(0.00123, :units => distance)
- assert_equal '1.23 cm', number_to_human(0.0123, :units => distance)
- assert_equal '1.23 dm', number_to_human(0.123, :units => distance)
- assert_equal '1.23 m', number_to_human(1.23, :units => distance)
- assert_equal '1.23 dam', number_to_human(12.3, :units => distance)
- assert_equal '1.23 hm', number_to_human(123, :units => distance)
- assert_equal '1.23 km', number_to_human(1230, :units => distance)
- assert_equal '1.23 km', number_to_human(1230, :units => distance)
- assert_equal '1.23 km', number_to_human(1230, :units => distance)
- assert_equal '12.3 km', number_to_human(12300, :units => distance)
-
- #The quantifiers don't need to be a continuous sequence
- gangster = {:hundred => "hundred bucks", :million => "thousand quids"}
- assert_equal '1 hundred bucks', number_to_human(100, :units => gangster)
- assert_equal '25 hundred bucks', number_to_human(2500, :units => gangster)
- assert_equal '25 thousand quids', number_to_human(25000000, :units => gangster)
- assert_equal '12300 thousand quids', number_to_human(12345000000, :units => gangster)
-
- #Spaces are stripped from the resulting string
- assert_equal '4', number_to_human(4, :units => {:unit => "", :ten => 'tens '})
- assert_equal '4.5 tens', number_to_human(45, :units => {:unit => "", :ten => ' tens '})
-
- assert_equal '1&lt;script&gt;&lt;/script&gt;01', number_to_human(1.01, :separator => "<script></script>")
- 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_format
- assert_equal '123 times Thousand', number_to_human(123456, :format => "%n times %u")
- volume = {:unit => "ml", :thousand => "lt", :million => "m3"}
- assert_equal '123.lt', number_to_human(123456, :units => volume, :format => "%n.%u")
- end
-
- def test_number_helpers_should_return_nil_when_given_nil
- assert_nil number_to_phone(nil)
- assert_nil number_to_currency(nil)
- assert_nil number_to_percentage(nil)
- assert_nil number_with_delimiter(nil)
- assert_nil number_with_precision(nil)
- assert_nil number_to_human_size(nil)
- assert_nil number_to_human(nil)
- end
-
- def test_number_helpers_do_not_mutate_options_hash
- options = { 'raise' => true }
-
- number_to_phone(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_to_currency(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_to_percentage(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_with_delimiter(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_with_precision(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_to_human_size(1, options)
- assert_equal({ 'raise' => true }, options)
-
- number_to_human(1, options)
- assert_equal({ 'raise' => true }, options)
- end
-
- def test_number_helpers_should_return_non_numeric_param_unchanged
- assert_equal("+1-x x 123", number_to_phone("x", :country_code => 1, :extension => 123))
- assert_equal("x", number_to_phone("x"))
- assert_equal("$x.", number_to_currency("x."))
- assert_equal("$x", number_to_currency("x"))
- assert_equal("x%", number_to_percentage("x"))
- assert_equal("x", number_with_delimiter("x"))
- assert_equal("x.", number_with_precision("x."))
- assert_equal("x", number_with_precision("x"))
- assert_equal "x", number_to_human_size('x')
- assert_equal "x", number_to_human('x')
- end
-
- def test_number_helpers_outputs_are_html_safe
- assert number_to_human(1).html_safe?
- assert !number_to_human("<script></script>").html_safe?
- assert number_to_human("asdf".html_safe).html_safe?
- assert number_to_human("1".html_safe).html_safe?
-
- assert number_to_human_size(1).html_safe?
- assert number_to_human_size(1000000).html_safe?
- assert !number_to_human_size("<script></script>").html_safe?
- assert number_to_human_size("asdf".html_safe).html_safe?
- assert number_to_human_size("1".html_safe).html_safe?
-
- assert number_with_precision(1, :strip_insignificant_zeros => false).html_safe?
- assert number_with_precision(1, :strip_insignificant_zeros => true).html_safe?
- assert !number_with_precision("<script></script>").html_safe?
- assert number_with_precision("asdf".html_safe).html_safe?
- assert number_with_precision("1".html_safe).html_safe?
-
- assert number_to_currency(1).html_safe?
- assert !number_to_currency("<script></script>").html_safe?
- assert number_to_currency("asdf".html_safe).html_safe?
- assert number_to_currency("1".html_safe).html_safe?
-
- assert number_to_percentage(1).html_safe?
- assert !number_to_percentage("<script></script>").html_safe?
- assert number_to_percentage("asdf".html_safe).html_safe?
- assert number_to_percentage("1".html_safe).html_safe?
-
- assert number_to_phone(1).html_safe?
- assert_equal "&lt;script&gt;&lt;/script&gt;", number_to_phone("<script></script>")
- assert number_to_phone("<script></script>").html_safe?
- assert number_to_phone("asdf".html_safe).html_safe?
- assert number_to_phone("1".html_safe).html_safe?
-
- assert number_with_delimiter(1).html_safe?
- assert !number_with_delimiter("<script></script>").html_safe?
- assert number_with_delimiter("asdf".html_safe).html_safe?
- assert number_with_delimiter("1".html_safe).html_safe?
- end
-
- def test_number_helpers_should_raise_error_if_invalid_when_specified
- exception = assert_raise InvalidNumberError do
- number_to_human("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_to_human_size("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_with_precision("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_to_currency("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_to_percentage("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_with_delimiter("x", :raise => true)
- end
- assert_equal "x", exception.number
-
- exception = assert_raise InvalidNumberError do
- number_to_phone("x", :raise => true)
- end
- assert_equal "x", exception.number
- end
-end
diff --git a/actionpack/test/template/record_tag_helper_test.rb b/actionpack/test/template/record_tag_helper_test.rb
deleted file mode 100644
index a84034c02e..0000000000
--- a/actionpack/test/template/record_tag_helper_test.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-require 'abstract_unit'
-
-class RecordTagPost
- extend ActiveModel::Naming
- include ActiveModel::Conversion
- attr_accessor :id, :body
-
- def initialize
- @id = 45
- @body = "What a wonderful world!"
-
- yield self if block_given?
- end
-end
-
-class RecordTagHelperTest < ActionView::TestCase
- include RenderERBUtils
-
- tests ActionView::Helpers::RecordTagHelper
-
- def setup
- super
- @post = RecordTagPost.new
- end
-
- def test_content_tag_for
- expected = %(<li class="record_tag_post" id="record_tag_post_45"></li>)
- actual = content_tag_for(:li, @post) { }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_prefix
- expected = %(<ul class="archived_record_tag_post" id="archived_record_tag_post_45"></ul>)
- actual = content_tag_for(:ul, @post, :archived) { }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_with_extra_html_options
- expected = %(<tr class="record_tag_post special" id="record_tag_post_45" style='background-color: #f0f0f0'></tr>)
- actual = content_tag_for(:tr, @post, :class => "special", :style => "background-color: #f0f0f0") { }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_with_prefix_and_extra_html_options
- expected = %(<tr class="archived_record_tag_post special" id="archived_record_tag_post_45" style='background-color: #f0f0f0'></tr>)
- actual = content_tag_for(:tr, @post, :archived, :class => "special", :style => "background-color: #f0f0f0") { }
- assert_dom_equal expected, actual
- end
-
- def test_block_not_in_erb_multiple_calls
- expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
- actual = div_for(@post, :class => "special") { @post.body }
- assert_dom_equal expected, actual
- actual = div_for(@post, :class => "special") { @post.body }
- assert_dom_equal expected, actual
- end
-
- def test_block_works_with_content_tag_for_in_erb
- expected = %(<tr class="record_tag_post" id="record_tag_post_45">What a wonderful world!</tr>)
- actual = render_erb("<%= content_tag_for(:tr, @post) do %><%= @post.body %><% end %>")
- assert_dom_equal expected, actual
- end
-
- def test_div_for_in_erb
- expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
- actual = render_erb("<%= div_for(@post, :class => 'special') do %><%= @post.body %><% end %>")
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_collection
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- expected = %(<li class="record_tag_post" id="record_tag_post_101">Hello!</li>\n<li class="record_tag_post" id="record_tag_post_102">World!</li>)
- actual = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
- assert_dom_equal expected, actual
- end
-
- def test_div_for_collection
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- expected = %(<div class="record_tag_post" id="record_tag_post_101">Hello!</div>\n<div class="record_tag_post" id="record_tag_post_102">World!</div>)
- actual = div_for([post_1, post_2]) { |post| post.body }
- assert_dom_equal expected, actual
- end
-
- def test_content_tag_for_single_record_is_html_safe
- result = div_for(@post, :class => "special") { @post.body }
- assert result.html_safe?
- end
-
- def test_content_tag_for_collection_is_html_safe
- post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
- post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
- result = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
- assert result.html_safe?
- end
-
- def test_content_tag_for_does_not_change_options_hash
- options = { :class => "important" }
- content_tag_for(:li, @post, options) { }
- assert_equal({ :class => "important" }, options)
- end
-end
diff --git a/actionpack/test/template/render_test.rb b/actionpack/test/template/render_test.rb
deleted file mode 100644
index ddf5c6a1b3..0000000000
--- a/actionpack/test/template/render_test.rb
+++ /dev/null
@@ -1,533 +0,0 @@
-# encoding: utf-8
-require 'abstract_unit'
-require 'controller/fake_models'
-
-class TestController < ActionController::Base
-end
-
-module RenderTestCases
- def setup_view(paths)
- @assigns = { :secret => 'in the sauce' }
- @view = ActionView::Base.new(paths, @assigns)
- @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', {}
-
- # Ensure original are still the same since we are reindexing view paths
- assert_equal ORIGINAL_LOCALES, I18n.available_locales.map {|l| l.to_s }.sort
- end
-
- 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
- end
-
- def test_render_file
- assert_equal "Hello world!", @view.render(:file => "test/hello_world")
- end
-
- def test_render_file_not_using_full_path
- assert_equal "Hello world!", @view.render(:file => "test/hello_world")
- end
-
- def test_render_file_without_specific_extension
- assert_equal "Hello world!", @view.render(:file => "test/hello_world")
- end
-
- # Test if :formats, :locale etc. options are passed correctly to the resolvers.
- def test_render_file_with_format
- assert_match "<h1>No Comment</h1>", @view.render(:file => "comments/empty", :formats => [:html])
- assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => [:xml])
- assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => :xml)
- end
-
- def test_render_template_with_format
- assert_match "<h1>No Comment</h1>", @view.render(:template => "comments/empty", :formats => [:html])
- assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml])
- end
-
- def test_render_partial_implicitly_use_format_of_the_rendered_template
- @view.lookup_context.formats = [:json]
- assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html])
- end
-
- def test_render_partial_implicitly_use_format_of_the_rendered_partial
- @view.lookup_context.formats = [:html]
- assert_equal "Third level", @view.render(:template => "test/html_template")
- end
-
- def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names
- @view.lookup_context.formats = [:html]
- assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priorty")
- end
-
- def test_render_template_with_a_missing_partial_of_another_format
- @view.lookup_context.formats = [:html]
- assert_raise ActionView::Template::Error, "Missing partial /missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do
- @view.render(:template => "with_format", :formats => [:json])
- end
- end
-
- def test_render_file_with_locale
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => [:de])
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => :de)
- end
-
- def test_render_template_with_locale
- assert_equal "<h1>Kein Kommentar</h1>", @view.render(:template => "comments/empty", :locale => [:de])
- end
-
- def test_render_file_with_handlers
- assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => [:builder])
- assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => :builder)
- end
-
- def test_render_template_with_handlers
- assert_equal "<h1>No Comment</h1>\n", @view.render(:template => "comments/empty", :handlers => [:builder])
- end
-
- def test_render_raw_template_with_handlers
- assert_equal "<%= hello_world %>\n", @view.render(:template => "plain_text")
- end
-
- def test_render_raw_template_with_quotes
- assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters")
- end
-
- def test_render_ruby_template_with_handlers
- assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template")
- end
-
- def test_render_ruby_template_inline
- assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :ruby)
- end
-
- def test_render_file_with_localization_on_context_level
- old_locale, @view.locale = @view.locale, :da
- assert_equal "Hey verden", @view.render(:file => "test/hello_world")
- ensure
- @view.locale = old_locale
- end
-
- def test_render_file_with_dashed_locale
- old_locale, @view.locale = @view.locale, :"pt-BR"
- assert_equal "Ola mundo", @view.render(:file => "test/hello_world")
- ensure
- @view.locale = old_locale
- end
-
- def test_render_file_at_top_level
- assert_equal 'Elastica', @view.render(:file => '/shared')
- end
-
- def test_render_file_with_full_path
- template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world')
- assert_equal "Hello world!", @view.render(:file => template_path)
- end
-
- def test_render_file_with_instance_variables
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_ivar")
- end
-
- def test_render_file_with_locals
- locals = { :secret => 'in the sauce' }
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_locals", :locals => locals)
- end
-
- def test_render_file_not_using_full_path_with_dot_in_path
- assert_equal "The secret is in the sauce\n", @view.render(:file => "test/dot.directory/render_file_with_ivar")
- end
-
- def test_render_partial_from_default
- assert_equal "only partial", @view.render("test/partial_only")
- end
-
- def test_render_partial
- assert_equal "only partial", @view.render(:partial => "test/partial_only")
- end
-
- def test_render_partial_with_format
- assert_equal 'partial html', @view.render(:partial => 'test/partial')
- end
-
- def test_render_partial_with_selected_format
- assert_equal 'partial html', @view.render(:partial => 'test/partial', :formats => :html)
- assert_equal 'partial js', @view.render(:partial => 'test/partial', :formats => [:js])
- end
-
- def test_render_partial_at_top_level
- # file fixtures/_top_level_partial_only (not fixtures/test)
- assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only')
- end
-
- def test_render_partial_with_format_at_top_level
- # file fixtures/_top_level_partial.html (not fixtures/test, with format extension)
- assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial')
- end
-
- def test_render_partial_with_locals
- assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 })
- end
-
- def test_render_partial_with_locals_from_default
- assert_equal "only partial", @view.render("test/partial_only", :counter_counter => 5)
- end
-
- def test_render_partial_with_invalid_name
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") }
- assert_equal "The partial name (test/200) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores.", e.message
- end
-
- def test_render_partial_with_missing_filename
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/") }
- assert_equal "The partial name (test/) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores.", e.message
- end
-
- def test_render_partial_with_incompatible_object
- e = assert_raises(ArgumentError) { @view.render(:partial => nil) }
- assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message
- end
-
- def test_render_partial_with_hyphen
- e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in") }
- assert_equal "The partial name (test/a-in) is not a valid Ruby identifier; " +
- "make sure your partial name starts with a lowercase letter or underscore, " +
- "and is followed by any combination of letters, numbers and underscores.", e.message
- end
-
- def test_render_partial_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise") }
- assert_match %r!method.*doesnt_exist!, e.message
- assert_equal "", e.sub_template_message
- assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
- assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
- end
-
- def test_render_error_indentation
- e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") }
- error_lines = e.annoted_source_code.split("\n")
- assert_match %r!error\shere!, e.message
- assert_equal "11", e.line_number
- assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second
- assert_equal " 10: <p>Tenth paragraph</p>", error_lines.third
- end
-
- def test_render_sub_template_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:template => "test/sub_template_raise") }
- assert_match %r!method.*doesnt_exist!, e.message
- assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message
- assert_equal "1", e.line_number
- assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
- end
-
- def test_render_file_with_errors
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) }
- assert_match %r!method.*doesnt_exist!, e.message
- assert_equal "", e.sub_template_message
- assert_equal "1", e.line_number
- assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
- assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
- end
-
- def test_render_object
- assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david"))
- end
-
- def test_render_object_with_array
- assert_equal "[1, 2, 3]", @view.render(:partial => "test/object_inspector", :object => [1, 2, 3])
- end
-
- def test_render_partial_collection
- assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
- end
-
- def test_render_partial_collection_as_by_string
- assert_equal "david david davidmary mary mary",
- @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer')
- end
-
- def test_render_partial_collection_as_by_symbol
- assert_equal "david david davidmary mary mary",
- @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer)
- end
-
- def test_render_partial_collection_without_as
- assert_equal "local_inspector,local_inspector_counter",
- @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
- end
-
- def test_render_partial_with_empty_collection_should_return_nil
- assert_nil @view.render(:partial => "test/customer", :collection => [])
- end
-
- def test_render_partial_with_nil_collection_should_return_nil
- assert_nil @view.render(:partial => "test/customer", :collection => nil)
- end
-
- def test_render_partial_with_nil_values_in_collection
- assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ])
- end
-
- def test_render_partial_with_layout_using_collection_and_template
- assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
- end
-
- def test_render_partial_with_layout_using_collection_and_template_makes_current_item_available_in_layout
- assert_equal '<b class="amazon">Hello: Amazon</b><b class="yahoo">Hello: Yahoo</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
- end
-
- def test_render_partial_with_layout_using_collection_and_template_makes_current_item_counter_available_in_layout
- assert_equal '<b data-counter="0">Hello: Amazon</b><b data-counter="1">Hello: Yahoo</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object_counter', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
- end
-
- def test_render_partial_with_layout_using_object_and_template_makes_object_available_in_layout
- assert_equal '<b class="amazon">Hello: Amazon</b>',
- @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :object => Customer.new("Amazon"))
- end
-
- def test_render_partial_with_empty_array_should_return_nil
- assert_nil @view.render(:partial => [])
- end
-
- def test_render_partial_using_string
- assert_equal "Hello: Anonymous", @controller_view.render('customer')
- end
-
- def test_render_partial_with_locals_using_string
- assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david"))
- end
-
- def test_render_partial_using_object
- assert_equal "Hello: lifo",
- @controller_view.render(Customer.new("lifo"), :greeting => "Hello")
- end
-
- def test_render_partial_using_collection
- customers = [ Customer.new("Amazon"), Customer.new("Yahoo") ]
- assert_equal "Hello: AmazonHello: Yahoo",
- @controller_view.render(customers, :greeting => "Hello")
- end
-
- # TODO: The reason for this test is unclear, improve documentation
- def test_render_partial_and_fallback_to_layout
- assert_equal "Before (Josh)\n\nAfter", @view.render(:partial => "test/layout_for_partial", :locals => { :name => "Josh" })
- end
-
- # TODO: The reason for this test is unclear, improve documentation
- def test_render_missing_xml_partial_and_raise_missing_template
- @view.formats = [:xml]
- assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/layout_for_partial") }
- ensure
- @view.formats = nil
- end
-
- def test_render_layout_with_block_and_other_partial_inside
- render = @view.render(:layout => "test/layout_with_partial_and_yield") { "Yield!" }
- assert_equal "Before\npartial html\nYield!\nAfter\n", render
- end
-
- def test_render_inline
- assert_equal "Hello, World!", @view.render(:inline => "Hello, World!")
- end
-
- def test_render_inline_with_locals
- assert_equal "Hello, Josh!", @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" })
- end
-
- def test_render_fallbacks_to_erb_for_unknown_types
- assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :bar)
- end
-
- CustomHandler = lambda do |template|
- "@output_buffer = ''\n" +
- "@output_buffer << 'source: #{template.source.inspect}'\n"
- end
-
- 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)
- 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)
- 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)
- 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)
- end
-
- def test_render_ignores_templates_with_malformed_template_handlers
- ActiveSupport::Deprecation.silence do
- %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
- assert_raises(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") }
- end
- end
- end
-
- def test_render_with_layout
- assert_equal %(<title></title>\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield")
- end
-
- def test_render_with_layout_which_has_render_inline
- assert_equal %(welcome\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
- end
-
- def test_render_with_layout_which_renders_another_partial
- assert_equal %(partial html\nHello world!\n),
- @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
- end
-
- def test_render_layout_with_block_and_yield
- assert_equal %(Content from block!\n),
- @view.render(:layout => "layouts/yield_only") { "Content from block!" }
- end
-
- def test_render_layout_with_block_and_yield_with_params
- assert_equal %(Yield! Content from block!\n),
- @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" }
- end
-
- def test_render_layout_with_block_which_renders_another_partial_and_yields
- assert_equal %(partial html\nContent from block!\n),
- @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" }
- end
-
- def test_render_partial_and_layout_without_block_with_locals
- assert_equal %(Before (Foo!)\npartial html\nAfter),
- @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
- end
-
- def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial
- assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter),
- @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
- end
-
- def test_render_layout_with_a_nested_render_layout_call
- assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
- end
-
- def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial
- assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
- end
-
- def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content
- assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter),
- @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
- end
-
- def test_render_with_nested_layout
- assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
- @view.render(:file => "test/nested_layout", :layout => "layouts/yield")
- end
-
- def test_render_with_file_in_layout
- assert_equal %(\n<title>title</title>\n\n),
- @view.render(:file => "test/layout_render_file")
- end
-
- def test_render_layout_with_object
- assert_equal %(<title>David</title>),
- @view.render(:file => "test/layout_render_object")
- end
-
- 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)
- end
-
- def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call
- assert_raises(ArgumentError) { ActionView::Template.register_template_handler CustomHandler }
- end
-end
-
-class CachedViewRenderTest < ActiveSupport::TestCase
- include RenderTestCases
-
- # Ensure view path cache is primed
- def setup
- view_paths = ActionController::Base.view_paths
- assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
- setup_view(view_paths)
- end
-
- def teardown
- GC.start
- end
-end
-
-class LazyViewRenderTest < ActiveSupport::TestCase
- include RenderTestCases
-
- # Test the same thing as above, but make sure the view path
- # is not eager loaded
- def setup
- path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
- view_paths = ActionView::PathSet.new([path])
- assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first
- setup_view(view_paths)
- end
-
- def teardown
- GC.start
- end
-
- def test_render_utf8_template_with_magic_comment
- with_external_encoding Encoding::ASCII_8BIT do
- result = @view.render(:file => "test/utf8_magic", :formats => [:html], :layouts => "layouts/yield")
- assert_equal Encoding::UTF_8, result.encoding
- assert_equal "\nРуÑÑкий \nтекÑÑ‚\n\nUTF-8\nUTF-8\nUTF-8\n", result
- end
- end
-
- def test_render_utf8_template_with_default_external_encoding
- with_external_encoding Encoding::UTF_8 do
- result = @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield")
- assert_equal Encoding::UTF_8, result.encoding
- assert_equal "РуÑÑкий текÑÑ‚\n\nUTF-8\nUTF-8\nUTF-8\n", result
- end
- end
-
- def test_render_utf8_template_with_incompatible_external_encoding
- with_external_encoding Encoding::SHIFT_JIS do
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") }
- assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
- end
- end
-
- def test_render_utf8_template_with_partial_with_incompatible_encoding
- with_external_encoding Encoding::SHIFT_JIS do
- e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8_magic_with_bare_partial", :formats => [:html], :layouts => "layouts/yield") }
- assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
- end
- end
-
- def with_external_encoding(encoding)
- old = Encoding.default_external
- silence_warnings { Encoding.default_external = encoding }
- yield
- ensure
- silence_warnings { Encoding.default_external = old }
- end
-end
diff --git a/actionpack/test/template/resolver_patterns_test.rb b/actionpack/test/template/resolver_patterns_test.rb
deleted file mode 100644
index 97b1bad055..0000000000
--- a/actionpack/test/template/resolver_patterns_test.rb
+++ /dev/null
@@ -1,31 +0,0 @@
-require 'abstract_unit'
-
-class ResolverPatternsTest < ActiveSupport::TestCase
- def setup
- path = File.expand_path("../../fixtures/", __FILE__)
- pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}"
- @resolver = ActionView::FileSystemResolver.new(path, pattern)
- end
-
- def test_should_return_empty_list_for_unknown_path
- templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
- assert_equal [], templates, "expected an empty list of templates"
- end
-
- def test_should_return_template_for_declared_path
- templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
- assert_equal 1, templates.size, "expected one template"
- assert_equal "Hello custom patterns!", templates.first.source
- assert_equal "custom_pattern/path", templates.first.virtual_path
- assert_equal [:html], templates.first.formats
- end
-
- def test_should_return_all_templates_when_ambigous_pattern
- templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
- assert_equal 2, templates.size, "expected two templates"
- assert_equal "Another template!", templates[0].source
- assert_equal "custom_pattern/another", templates[0].virtual_path
- assert_equal "Hello custom patterns!", templates[1].source
- assert_equal "custom_pattern/another", templates[1].virtual_path
- end
-end
diff --git a/actionpack/test/template/spec_type_test.rb b/actionpack/test/template/spec_type_test.rb
deleted file mode 100644
index 08a7bdf81d..0000000000
--- a/actionpack/test/template/spec_type_test.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'abstract_unit'
-
-class ActionViewSpecTypeTest < ActiveSupport::TestCase
- def assert_view actual
- assert_equal ActionView::TestCase, actual
- end
-
- def refute_view actual
- refute_equal ActionView::TestCase, actual
- end
-
- def test_spec_type_resolves_for_matching_helper_strings
- assert_view MiniTest::Spec.spec_type("WidgetHelper")
- assert_view MiniTest::Spec.spec_type("WidgetHelperTest")
- assert_view MiniTest::Spec.spec_type("Widget Helper Test")
- # And is not case sensitive
- assert_view MiniTest::Spec.spec_type("widgethelper")
- assert_view MiniTest::Spec.spec_type("widgethelpertest")
- assert_view MiniTest::Spec.spec_type("widget helper test")
- end
-
- def test_spec_type_resolves_for_matching_view_strings
- assert_view MiniTest::Spec.spec_type("WidgetView")
- assert_view MiniTest::Spec.spec_type("WidgetViewTest")
- assert_view MiniTest::Spec.spec_type("Widget View Test")
- # And is not case sensitive
- assert_view MiniTest::Spec.spec_type("widgetview")
- assert_view MiniTest::Spec.spec_type("widgetviewtest")
- assert_view MiniTest::Spec.spec_type("widget view test")
- end
-
- def test_spec_type_wont_match_non_space_characters
- refute_view MiniTest::Spec.spec_type("Widget Helper\tTest")
- refute_view MiniTest::Spec.spec_type("Widget Helper\rTest")
- refute_view MiniTest::Spec.spec_type("Widget Helper\nTest")
- refute_view MiniTest::Spec.spec_type("Widget Helper\fTest")
- refute_view MiniTest::Spec.spec_type("Widget HelperXTest")
- end
-end
diff --git a/actionpack/test/template/streaming_render_test.rb b/actionpack/test/template/streaming_render_test.rb
deleted file mode 100644
index 520bf3a824..0000000000
--- a/actionpack/test/template/streaming_render_test.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-# encoding: utf-8
-require 'abstract_unit'
-require 'controller/fake_models'
-
-class TestController < ActionController::Base
-end
-
-class FiberedTest < ActiveSupport::TestCase
- def setup
- view_paths = ActionController::Base.view_paths
- @assigns = { :secret => 'in the sauce', :name => nil }
- @view = ActionView::Base.new(view_paths, @assigns)
- @controller_view = TestController.new.view_context
- end
-
- def render_body(options)
- @view.view_renderer.render_body(@view, options)
- end
-
- def buffered_render(options)
- body = render_body(options)
- string = ""
- body.each do |piece|
- string << piece
- end
- string
- end
-
- def test_streaming_works
- content = []
- body = render_body(:template => "test/hello_world", :layout => "layouts/yield")
-
- body.each do |piece|
- content << piece
- end
-
- assert_equal "<title>", content[0]
- assert_equal "", content[1]
- assert_equal "</title>\n", content[2]
- assert_equal "Hello world!", content[3]
- assert_equal "\n", content[4]
- end
-
- def test_render_file
- assert_equal "Hello world!", buffered_render(:file => "test/hello_world")
- end
-
- def test_render_file_with_locals
- locals = { :secret => 'in the sauce' }
- assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals", :locals => locals)
- end
-
- def test_render_partial
- assert_equal "only partial", buffered_render(:partial => "test/partial_only")
- end
-
- def test_render_inline
- assert_equal "Hello, World!", buffered_render(:inline => "Hello, World!")
- end
-
- def test_render_without_layout
- assert_equal "Hello world!", buffered_render(:template => "test/hello_world")
- end
-
- def test_render_with_layout
- assert_equal %(<title></title>\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield")
- end
-
- def test_render_with_layout_which_has_render_inline
- assert_equal %(welcome\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
- end
-
- def test_render_with_layout_which_renders_another_partial
- assert_equal %(partial html\nHello world!\n),
- buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
- end
-
- def test_render_with_nested_layout
- assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
- buffered_render(:template => "test/nested_layout", :layout => "layouts/yield")
- end
-
- def test_render_with_file_in_layout
- assert_equal %(\n<title>title</title>\n\n),
- buffered_render(:template => "test/layout_render_file")
- end
-
- def test_render_with_handler_without_streaming_support
- assert_match "<p>This is grand!</p>", buffered_render(:template => "test/hello")
- end
-
- def test_render_with_streaming_multiple_yields_provide_and_content_for
- assert_equal "Yes, \nthis works\n like a charm.",
- buffered_render(:template => "test/streaming", :layout => "layouts/streaming")
- end
-
- def test_render_with_streaming_with_fake_yields_and_streaming_buster
- assert_equal "This won't look\n good.",
- buffered_render(:template => "test/streaming_buster", :layout => "layouts/streaming")
- end
-
- def test_render_with_nested_streaming_multiple_yields_provide_and_content_for
- assert_equal "?Yes, \n\nthis works\n\n? like a charm.",
- buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming")
- end
-
-end
diff --git a/actionpack/test/template/tag_helper_test.rb b/actionpack/test/template/tag_helper_test.rb
deleted file mode 100644
index f0a7ce0bc9..0000000000
--- a/actionpack/test/template/tag_helper_test.rb
+++ /dev/null
@@ -1,125 +0,0 @@
-require 'abstract_unit'
-
-class TagHelperTest < ActionView::TestCase
- include RenderERBUtils
-
- tests ActionView::Helpers::TagHelper
-
- def test_tag
- assert_equal "<br />", tag("br")
- assert_equal "<br clear=\"left\" />", tag(:br, :clear => "left")
- assert_equal "<br>", tag("br", nil, true)
- end
-
- def test_tag_options
- str = tag("p", "class" => "show", :class => "elsewhere")
- assert_match(/class="show"/, str)
- assert_match(/class="elsewhere"/, str)
- end
-
- def test_tag_options_rejects_nil_option
- assert_equal "<p />", tag("p", :ignored => nil)
- end
-
- def test_tag_options_accepts_false_option
- assert_equal "<p value=\"false\" />", tag("p", :value => false)
- end
-
- def test_tag_options_accepts_blank_option
- assert_equal "<p included=\"\" />", tag("p", :included => '')
- end
-
- def test_tag_options_converts_boolean_option
- assert_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" />',
- tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true)
- end
-
- def test_content_tag
- assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create")
- assert content_tag("a", "Create", "href" => "create").html_safe?
- assert_equal content_tag("a", "Create", "href" => "create"),
- content_tag("a", "Create", :href => "create")
- assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
- content_tag(:p, '<script>evil_js</script>')
- assert_equal "<p><script>evil_js</script></p>",
- content_tag(:p, '<script>evil_js</script>', nil, false)
- end
-
- def test_content_tag_with_block_in_erb
- buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>")
- assert_dom_equal "<div>Hello world!</div>", buffer
- end
-
- def test_content_tag_with_block_and_options_in_erb
- buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>")
- assert_dom_equal %(<div class="green">Hello world!</div>), buffer
- end
-
- def test_content_tag_with_block_and_options_out_of_erb
- assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, :class => "green") { "Hello world!" }
- end
-
- def test_content_tag_with_block_and_options_outside_out_of_erb
- assert_equal content_tag("a", "Create", :href => "create"),
- content_tag("a", "href" => "create") { "Create" }
- end
-
- def test_content_tag_nested_in_content_tag_out_of_erb
- assert_equal content_tag("p", content_tag("b", "Hello")),
- content_tag("p") { content_tag("b", "Hello") },
- output_buffer
- end
-
- def test_content_tag_nested_in_content_tag_in_erb
- assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag")
- end
-
- def test_content_tag_with_escaped_array_class
- str = content_tag('p', "limelight", :class => ["song", "play>"])
- assert_equal "<p class=\"song play&gt;\">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
- end
-
- def test_cdata_section
- assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>")
- end
-
- def test_cdata_section_splitted
- assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world")
- assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again")
- end
-
- def test_escape_once
- assert_equal '1 &lt; 2 &amp; 3', escape_once('1 < 2 &amp; 3')
- end
-
- def test_tag_honors_html_safe_for_param_values
- ['1&amp;2', '1 &lt; 2', '&#8220;test&#8220;'].each do |escaped|
- assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe)
- end
- end
-
- def test_skip_invalid_escaped_attributes
- ['&1;', '&#1dfa3;', '& #123;'].each do |escaped|
- assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag('a', :href => escaped)
- end
- end
-
- def test_disable_escaping
- assert_equal '<a href="&amp;" />', tag('a', { :href => '&amp;' }, false, false)
- end
-
- def test_data_attributes
- ['data', :data].each { |data|
- assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string="hello" data-symbol="foo" />',
- tag('a', { data => { :a_float => 3.14, :a_big_decimal => BigDecimal.new("-123.456"), :a_number => 1, :string => 'hello', :symbol => :foo, :array => [1, 2, 3], :hash => { :key => 'value'} } })
- }
- end
-end
diff --git a/actionpack/test/template/template_test.rb b/actionpack/test/template/template_test.rb
deleted file mode 100644
index 86ba5f3b4d..0000000000
--- a/actionpack/test/template/template_test.rb
+++ /dev/null
@@ -1,192 +0,0 @@
-require "abstract_unit"
-require "logger"
-
-class TestERBTemplate < ActiveSupport::TestCase
- ERBHandler = ActionView::Template::Handlers::ERB.new
-
- class LookupContext
- def disable_cache
- yield
- end
-
- def find_template(*args)
- end
-
- attr_accessor :formats
- end
-
- class Context
- def initialize
- @output_buffer = "original"
- @virtual_path = nil
- end
-
- def hello
- "Hello"
- end
-
- def partial
- ActionView::Template.new(
- "<%= @virtual_path %>",
- "partial",
- ERBHandler,
- :virtual_path => "partial"
- )
- end
-
- def lookup_context
- @lookup_context ||= LookupContext.new
- end
-
- def logger
- ActiveSupport::Logger.new(STDERR)
- end
-
- def my_buffer
- @output_buffer
- end
- end
-
- def new_template(body = "<%= hello %>", details = {})
- ActionView::Template.new(body, "hello template", details.fetch(:handler) { ERBHandler }, {:virtual_path => "hello"}.merge!(details))
- end
-
- def render(locals = {})
- @template.render(@context, locals)
- end
-
- def setup
- @context = Context.new
- end
-
- def test_mime_type_is_deprecated
- template = new_template
- assert_deprecated 'Template#mime_type is deprecated and will be removed' do
- template.mime_type
- end
- end
-
- def test_basic_template
- @template = new_template
- assert_equal "Hello", render
- end
-
- def test_raw_template
- @template = new_template("<%= hello %>", :handler => ActionView::Template::Handlers::Raw.new)
- assert_equal "<%= hello %>", render
- end
-
- def test_template_loses_its_source_after_rendering
- @template = new_template
- render
- assert_nil @template.source
- end
-
- def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path
- @template = new_template("Hello", :virtual_path => nil)
- render
- assert_equal "Hello", @template.source
- end
-
- def test_locals
- @template = new_template("<%= my_local %>")
- @template.locals = [:my_local]
- assert_equal "I am a local", render(:my_local => "I am a local")
- end
-
- def test_restores_buffer
- @template = new_template
- assert_equal "Hello", render
- assert_equal "original", @context.my_buffer
- end
-
- def test_virtual_path
- @template = new_template("<%= @virtual_path %>" \
- "<%= partial.render(self, {}) %>" \
- "<%= @virtual_path %>")
- assert_equal "hellopartialhello", render
- end
-
- def test_refresh_with_templates
- @template = new_template("Hello", :virtual_path => "test/foo/bar")
- @template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template")
- assert_equal "template", @template.refresh(@context)
- end
-
- def test_refresh_with_partials
- @template = new_template("Hello", :virtual_path => "test/_foo")
- @template.locals = [:key]
- @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial")
- assert_equal "partial", @template.refresh(@context)
- end
-
- def test_refresh_raises_an_error_without_virtual_path
- @template = new_template("Hello", :virtual_path => nil)
- assert_raise RuntimeError do
- @template.refresh(@context)
- end
- end
-
- def test_resulting_string_is_utf8
- @template = new_template
- assert_equal Encoding::UTF_8, render.encoding
- end
-
- def test_no_magic_comment_word_with_utf_8
- @template = new_template("hello \u{fc}mlat")
- assert_equal Encoding::UTF_8, render.encoding
- assert_equal "hello \u{fc}mlat", render
- end
-
- # This test ensures that if the default_external
- # is set to something other than UTF-8, we don't
- # get any errors and get back a UTF-8 String.
- def test_default_external_works
- with_external_encoding "ISO-8859-1" do
- @template = new_template("hello \xFCmlat")
- assert_equal Encoding::UTF_8, render.encoding
- assert_equal "hello \u{fc}mlat", render
- end
- end
-
- def test_encoding_can_be_specified_with_magic_comment
- @template = new_template("# encoding: ISO-8859-1\nhello \xFCmlat")
- assert_equal Encoding::UTF_8, render.encoding
- assert_equal "\nhello \u{fc}mlat", render
- end
-
- # TODO: This is currently handled inside ERB. The case of explicitly
- # lying about encodings via the normal Rails API should be handled
- # inside Rails.
- def test_lying_with_magic_comment
- assert_raises(ActionView::Template::Error) do
- @template = new_template("# encoding: UTF-8\nhello \xFCmlat", :virtual_path => nil)
- render
- end
- end
-
- def test_encoding_can_be_specified_with_magic_comment_in_erb
- with_external_encoding Encoding::UTF_8 do
- @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", :virtual_path => nil)
- assert_equal Encoding::UTF_8, render.encoding
- assert_equal "hello \u{fc}mlat", render
- end
- end
-
- def test_error_when_template_isnt_valid_utf8
- assert_raises(ActionView::Template::Error, /\xFC/) do
- @template = new_template("hello \xFCmlat", :virtual_path => nil)
- render
- end
- end
-
- def with_external_encoding(encoding)
- old = Encoding.default_external
- Encoding::Converter.new old, encoding if old != encoding
- silence_warnings { Encoding.default_external = encoding }
- yield
- ensure
- silence_warnings { Encoding.default_external = old }
- end
-end
diff --git a/actionpack/test/template/test_case_test.rb b/actionpack/test/template/test_case_test.rb
deleted file mode 100644
index c7231d9cd5..0000000000
--- a/actionpack/test/template/test_case_test.rb
+++ /dev/null
@@ -1,346 +0,0 @@
-require 'abstract_unit'
-require 'controller/fake_controllers'
-
-module ActionView
-
- module ATestHelper
- end
-
- module AnotherTestHelper
- def from_another_helper
- 'Howdy!'
- end
- end
-
- module ASharedTestHelper
- def from_shared_helper
- 'Holla!'
- end
- end
-
- class TestCase
- helper ASharedTestHelper
-
- module SharedTests
- def self.included(test_case)
- test_case.class_eval do
- test "helpers defined on ActionView::TestCase are available" do
- assert test_case.ancestors.include?(ASharedTestHelper)
- assert_equal 'Holla!', from_shared_helper
- end
- end
- end
- end
- end
-
- class GeneralViewTest < ActionView::TestCase
- include SharedTests
- test_case = self
-
- test "memoizes the view" do
- assert_same view, view
- end
-
- test "exposes view as _view for backwards compatibility" do
- assert_same _view, view
- end
-
- test "retrieve non existing config values" do
- assert_equal nil, ActionView::Base.new.config.something_odd
- end
-
- test "works without testing a helper module" do
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
- end
-
- test "can render a layout with block" do
- assert_equal "Before (ChrisCruft)\n!\nAfter",
- render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"}
- end
-
- helper AnotherTestHelper
- test "additional helper classes can be specified as in a controller" do
- assert test_case.ancestors.include?(AnotherTestHelper)
- assert_equal 'Howdy!', from_another_helper
- end
-
- test "determine_default_helper_class returns nil if the test name constant resolves to a class" do
- assert_nil self.class.determine_default_helper_class("String")
- end
-
- test "delegates notice to request.flash[:notice]" do
- view.request.flash.expects(:[]).with(:notice)
- view.notice
- end
-
- test "delegates alert to request.flash[:alert]" do
- view.request.flash.expects(:[]).with(:alert)
- view.alert
- end
-
- test "uses controller lookup context" do
- assert_equal self.lookup_context, @controller.lookup_context
- end
- end
-
- class ClassMethodsTest < ActionView::TestCase
- include SharedTests
- test_case = self
-
- tests ATestHelper
- test "tests the specified helper module" do
- assert_equal ATestHelper, test_case.helper_class
- assert test_case.ancestors.include?(ATestHelper)
- end
-
- helper AnotherTestHelper
- test "additional helper classes can be specified as in a controller" do
- assert test_case.ancestors.include?(AnotherTestHelper)
- assert_equal 'Howdy!', from_another_helper
-
- test_case.helper_class.module_eval do
- def render_from_helper
- from_another_helper
- end
- end
- assert_equal 'Howdy!', render(:partial => 'test/from_helper')
- end
- end
-
- class HelperInclusionTest < ActionView::TestCase
- module RenderHelper
- def render_from_helper
- render :partial => 'customer', :collection => @customers
- end
- end
-
- helper RenderHelper
-
- test "helper class that is being tested is always included in view instance" do
- @controller.controller_path = 'test'
-
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper'))
- end
- end
-
- class ControllerHelperMethod < ActionView::TestCase
- module SomeHelper
- def some_method
- render :partial => 'test/from_helper'
- end
- end
-
- helper SomeHelper
-
- test "can call a helper method defined on the current controller from a helper" do
- @controller.singleton_class.class_eval <<-EOF, __FILE__, __LINE__ + 1
- def render_from_helper
- 'controller_helper_method'
- end
- EOF
- @controller.class.helper_method :render_from_helper
-
- assert_equal 'controller_helper_method', some_method
- end
- end
-
- class ViewAssignsTest < ActionView::TestCase
- test "view_assigns returns a Hash of user defined ivars" do
- @a = 'b'
- @c = 'd'
- assert_equal({:a => 'b', :c => 'd'}, view_assigns)
- end
-
- test "view_assigns excludes internal ivars" do
- INTERNAL_IVARS.each do |ivar|
- assert defined?(ivar), "expected #{ivar} to be defined"
- assert !view_assigns.keys.include?(ivar.to_s.sub('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns"
- end
- end
- end
-
- class HelperExposureTest < ActionView::TestCase
- helper(Module.new do
- def render_from_helper
- from_test_case
- end
- end)
- test "is able to make methods available to the view" do
- assert_equal 'Word!', render(:partial => 'test/from_helper')
- end
-
- def from_test_case; 'Word!'; end
- helper_method :from_test_case
- end
-
- class IgnoreProtectAgainstForgeryTest < ActionView::TestCase
- module HelperThatInvokesProtectAgainstForgery
- def help_me
- protect_against_forgery?
- end
- end
-
- helper HelperThatInvokesProtectAgainstForgery
-
- test "protect_from_forgery? in any helpers returns false" do
- assert !view.help_me
- end
-
- end
-
- class ATestHelperTest < ActionView::TestCase
- include SharedTests
- test_case = self
-
- test "inflects the name of the helper module to test from the test case class" do
- assert_equal ATestHelper, test_case.helper_class
- assert test_case.ancestors.include?(ATestHelper)
- end
-
- test "a configured test controller is available" do
- assert_kind_of ActionController::Base, controller
- assert_equal '', controller.controller_path
- end
-
- test "no additional helpers should shared across test cases" do
- assert !test_case.ancestors.include?(AnotherTestHelper)
- assert_raise(NoMethodError) { send :from_another_helper }
- end
-
- test "is able to use routes" do
- controller.request.assign_parameters(@routes, 'foo', 'index')
- assert_equal '/foo', url_for
- assert_equal '/bar', url_for(:controller => 'bar')
- end
-
- test "is able to use named routes" do
- with_routing do |set|
- set.draw { resources :contents }
- assert_equal 'http://test.host/contents/new', new_content_url
- assert_equal 'http://test.host/contents/1', content_url(:id => 1)
- end
- end
-
- test "is able to use mounted routes" do
- with_routing do |set|
- app = Class.new do
- def self.routes
- @routes ||= ActionDispatch::Routing::RouteSet.new
- end
-
- routes.draw { get "bar", :to => lambda {} }
-
- def self.call(*)
- end
- end
-
- set.draw { mount app => "/foo", :as => "foo_app" }
-
- assert_equal '/foo/bar', foo_app.bar_path
- end
- end
-
- test "named routes can be used from helper included in view" do
- with_routing do |set|
- set.draw { resources :contents }
- _helpers.module_eval do
- def render_from_helper
- new_content_url
- end
- end
-
- assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper')
- end
- end
-
- test "is able to render partials with local variables" do
- assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
- assert_equal 'Eloy', render(:partial => 'developers/developer',
- :locals => { :developer => stub(:name => 'Eloy') })
- end
-
- test "is able to render partials from templates and also use instance variables" do
- @controller.controller_path = "test"
-
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
- end
-
- test "is able to render partials from templates and also use instance variables after view has been referenced" do
- @controller.controller_path = "test"
-
- view
-
- @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
- assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
- end
-
- end
-
- class AssertionsTest < ActionView::TestCase
- def render_from_helper
- form_tag('/foo') do
- safe_concat render(:text => '<ul><li>foo</li></ul>')
- end
- end
- helper_method :render_from_helper
-
- test "uses the output_buffer for assert_select" do
- render(:partial => 'test/from_helper')
-
- assert_select 'form' do
- assert_select 'li', :text => 'foo'
- end
- end
- end
-
- class RenderTemplateTest < ActionView::TestCase
- test "supports specifying templates with a Regexp" do
- controller.controller_path = "fun"
- render(:template => "fun/games/hello_world")
- assert_template %r{\Afun/games/hello_world\Z}
- end
-
- test "supports specifying partials" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_template :partial => "_partial_for_use_in_layout"
- end
-
- test "supports specifying locals (passing)" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" }
- end
-
- test "supports specifying locals (failing)" do
- controller.controller_path = "test"
- render(:template => "test/calling_partial_with_layout")
- assert_raise ActiveSupport::TestCase::Assertion, /Somebody else.*David/m do
- assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" }
- end
- end
-
- test 'supports different locals on the same partial' do
- controller.controller_path = "test"
- render(:template => "test/render_two_partials")
- assert_template partial: '_partial', locals: { 'first' => '1' }
- assert_template partial: '_partial', locals: { 'second' => '2' }
- end
-
- end
-
- module AHelperWithInitialize
- def initialize(*)
- super
- @called_initialize = true
- end
- end
-
- class AHelperWithInitializeTest < ActionView::TestCase
- test "the helper's initialize was actually called" do
- assert @called_initialize
- end
- end
-end
diff --git a/actionpack/test/template/test_test.rb b/actionpack/test/template/test_test.rb
deleted file mode 100644
index e843a1deb4..0000000000
--- a/actionpack/test/template/test_test.rb
+++ /dev/null
@@ -1,136 +0,0 @@
-require 'abstract_unit'
-
-module PeopleHelper
- def title(text)
- content_tag(:h1, text)
- end
-
- def homepage_path
- people_path
- end
-
- def homepage_url
- people_url
- end
-
- def link_to_person(person)
- link_to person.name, person
- end
-end
-
-class PeopleHelperTest < ActionView::TestCase
- def test_title
- assert_equal "<h1>Ruby on Rails</h1>", title("Ruby on Rails")
- end
-
- def test_homepage_path
- with_test_route_set do
- assert_equal "/people", homepage_path
- end
- end
-
- def test_homepage_url
- with_test_route_set do
- assert_equal "http://test.host/people", homepage_url
- end
- end
-
- 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")
- assert_equal '<a href="/people/1">David</a>', link_to_person(person)
- end
- end
-
- private
- def with_test_route_set
- with_routing do |set|
- set.draw do
- get 'people', :to => 'people#index', :as => :people
- end
- yield
- end
- end
-end
-
-class CrazyHelperTest < ActionView::TestCase
- tests PeopleHelper
-
- def test_helper_class_can_be_set_manually_not_just_inferred
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-class CrazySymbolHelperTest < ActionView::TestCase
- tests :people
-
- def test_set_helper_class_using_symbol
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-class CrazyStringHelperTest < ActionView::TestCase
- tests 'people'
-
- def test_set_helper_class_using_string
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-describe PeopleHelper do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-describe PeopleHelper, :helper_class do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-describe PeopleHelper do
- describe "even while nested" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
- end
-end
-
-describe PeopleHelper, :helper_class do
- describe "even while nested" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
- end
-end
-
-describe "PeopleHelper" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-describe "PeopleHelperTest" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
-end
-
-describe "PeopleHelper" do
- describe "even while nested" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
- end
-end
-
-describe "PeopleHelperTest" do
- describe "even while nested" do
- it "resolves the right helper_class" do
- assert_equal PeopleHelper, self.class.helper_class
- end
- end
-end
diff --git a/actionpack/test/template/text_helper_test.rb b/actionpack/test/template/text_helper_test.rb
deleted file mode 100644
index 412d13bb2b..0000000000
--- a/actionpack/test/template/text_helper_test.rb
+++ /dev/null
@@ -1,467 +0,0 @@
-# encoding: utf-8
-require 'abstract_unit'
-
-class TextHelperTest < ActionView::TestCase
- tests ActionView::Helpers::TextHelper
-
- def setup
- super
- # This simulates the fact that instance variables are reset every time
- # a view is rendered. The cycle helper depends on this behavior.
- @_cycles = nil if (defined? @_cycles)
- end
-
- def test_concat
- self.output_buffer = 'foo'
- assert_equal 'foobar', concat('bar')
- assert_equal 'foobar', output_buffer
- end
-
- def test_simple_format_should_be_html_safe
- assert simple_format("<b> test with html tags </b>").html_safe?
- end
-
- def test_simple_format
- assert_equal "<p></p>", simple_format(nil)
-
- assert_equal "<p>crazy\n<br /> cross\n<br /> platform linebreaks</p>", simple_format("crazy\r\n cross\r platform linebreaks")
- assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!")
- assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline")
-
- text = "A\nB\nC\nD".freeze
- assert_equal "<p>A\n<br />B\n<br />C\n<br />D</p>", simple_format(text)
-
- text = "A\r\n \nB\n\n\r\n\t\nC\nD".freeze
- assert_equal "<p>A\n<br /> \n<br />B</p>\n\n<p>\t\n<br />C\n<br />D</p>", simple_format(text)
-
- assert_equal %q(<p class="test">This is a classy test</p>), simple_format("This is a classy test", :class => 'test')
- assert_equal %Q(<p class="test">para 1</p>\n\n<p class="test">para 2</p>), simple_format("para 1\n\npara 2", :class => 'test')
- end
-
- def test_simple_format_should_sanitize_input_when_sanitize_option_is_not_false
- assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>")
- end
-
- def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false
- assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false)
- end
-
- def test_simple_format_with_custom_wrapper
- assert_equal "<div></div>", simple_format(nil, {}, :wrapper_tag => "div")
- end
-
- def test_simple_format_with_custom_wrapper_and_multi_line_breaks
- assert_equal "<div>We want to put a wrapper...</div>\n\n<div>...right there.</div>", simple_format("We want to put a wrapper...\n\n...right there.", {}, :wrapper_tag => "div")
- end
-
- def test_simple_format_should_not_change_the_text_passed
- text = "<b>Ok</b><script>code!</script>"
- text_clone = text.dup
- simple_format(text)
- assert_equal text_clone, text
- end
-
- def test_simple_format_does_not_modify_the_html_options_hash
- options = { :class => "foobar"}
- passed_options = options.dup
- simple_format("some text", passed_options)
- assert_equal options, passed_options
- end
-
- def test_simple_format_does_not_modify_the_options_hash
- options = { :wrapper_tag => :div, :sanitize => false }
- passed_options = options.dup
- simple_format("some text", {}, passed_options)
- assert_equal options, passed_options
- end
-
- def test_truncate
- assert_equal "Hello World!", truncate("Hello World!", :length => 12)
- assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12)
- end
-
- def test_truncate_should_use_default_length_of_30
- str = "This is a string that will go longer then the default truncate length of 30"
- assert_equal str[0...27] + "...", truncate(str)
- end
-
- def test_truncate_with_options_hash
- assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]")
- assert_equal "Hello W...", truncate("Hello World!", :length => 10)
- assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10)
- assert_equal "Hello[...]", truncate("Hello Big World!", :omission => "[...]", :length => 13, :separator => ' ')
- assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 14, :separator => ' ')
- assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 15, :separator => ' ')
- end
-
- def test_truncate_multibyte
- assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_encoding('UTF-8'),
- truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding('UTF-8'), :length => 10)
- end
-
- def test_truncate_does_not_modify_the_options_hash
- options = { :length => 10 }
- passed_options = options.dup
- truncate("some text", passed_options)
- assert_equal options, passed_options
- end
-
- def test_truncate_with_link_options
- assert_equal "Here is a long test and ...<a href=\"#\">Continue</a>",
- truncate("Here is a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
- end
-
- def test_truncate_should_be_html_safe
- assert truncate("Hello World!", :length => 12).html_safe?
- end
-
- def test_truncate_should_escape_the_input
- assert_equal "Hello &lt;sc...", truncate("Hello <script>code!</script>World!!", :length => 12)
- end
-
- def test_truncate_should_not_escape_the_input_with_escape_false
- assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
- end
-
- def test_truncate_with_escape_false_should_be_html_safe
- truncated = truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
- assert truncated.html_safe?
- end
-
- def test_truncate_with_block_should_be_html_safe
- truncated = truncate("Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
- assert truncated.html_safe?
- end
-
- def test_truncate_with_block_should_escape_the_input
- assert_equal "&lt;script&gt;code!&lt;/script&gt;He...<a href=\"#\">Continue</a>",
- truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
- end
-
- def test_truncate_with_block_should_not_escape_the_input_with_escape_false
- assert_equal "<script>code!</script>He...<a href=\"#\">Continue</a>",
- truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
- end
-
- def test_truncate_with_block_with_escape_false_should_be_html_safe
- truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
- assert truncated.html_safe?
- end
-
- def test_truncate_with_block_should_escape_the_block
- assert_equal "Here is a long test and ...&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;",
- truncate("Here is a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" }
- end
-
- def test_highlight_should_be_html_safe
- assert highlight("This is a beautiful morning", "beautiful").html_safe?
- end
-
- def test_highlight
- assert_equal(
- "This is a <mark>beautiful</mark> morning",
- highlight("This is a beautiful morning", "beautiful")
- )
-
- assert_equal(
- "This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day",
- highlight("This is a beautiful morning, but also a beautiful day", "beautiful")
- )
-
- assert_equal(
- "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day",
- highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '<b>\1</b>')
- )
-
- assert_equal(
- "This text is not changed because we supplied an empty phrase",
- highlight("This text is not changed because we supplied an empty phrase", nil)
- )
-
- assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
- end
-
- def test_highlight_should_sanitize_input
- assert_equal(
- "This is a <mark>beautiful</mark> morning",
- highlight("This is a beautiful morning<script>code!</script>", "beautiful")
- )
- end
-
- def test_highlight_should_not_sanitize_if_sanitize_option_if_false
- assert_equal(
- "This is a <mark>beautiful</mark> morning<script>code!</script>",
- highlight("This is a beautiful morning<script>code!</script>", "beautiful", :sanitize => false)
- )
- end
-
- def test_highlight_with_regexp
- assert_equal(
- "This is a <mark>beautiful!</mark> morning",
- highlight("This is a beautiful! morning", "beautiful!")
- )
-
- assert_equal(
- "This is a <mark>beautiful! morning</mark>",
- highlight("This is a beautiful! morning", "beautiful! morning")
- )
-
- assert_equal(
- "This is a <mark>beautiful? morning</mark>",
- highlight("This is a beautiful? morning", "beautiful? morning")
- )
- 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
-
- def test_highlight_with_html
- assert_equal(
- "<p>This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>",
- highlight("<p>This is a beautiful morning, but also a beautiful day</p>", "beautiful")
- )
- assert_equal(
- "<p>This is a <em><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> day</p>",
- highlight("<p>This is a <em>beautiful</em> morning, but also a beautiful day</p>", "beautiful")
- )
- assert_equal(
- "<p>This is a <em class=\"error\"><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> <span class=\"last\">day</span></p>",
- highlight("<p>This is a <em class=\"error\">beautiful</em> morning, but also a beautiful <span class=\"last\">day</span></p>", "beautiful")
- )
- assert_equal(
- "<p class=\"beautiful\">This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>",
- highlight("<p class=\"beautiful\">This is a beautiful morning, but also a beautiful day</p>", "beautiful")
- )
- assert_equal(
- "<p>This is a <mark>beautiful</mark> <a href=\"http://example.com/beautiful#top?what=beautiful%20morning&amp;when=now+then\">morning</a>, but also a <mark>beautiful</mark> day</p>",
- highlight("<p>This is a beautiful <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a beautiful day</p>", "beautiful")
- )
- assert_equal(
- "<div>abc <b>div</b></div>",
- highlight("<div>abc div</div>", "div", :highlighter => '<b>\1</b>')
- )
- end
-
- def test_highlight_does_not_modify_the_options_hash
- options = { :highlighter => '<b>\1</b>', :sanitize => false }
- passed_options = options.dup
- highlight("<div>abc div</div>", "div", passed_options)
- assert_equal options, passed_options
- 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))
- assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", :radius => 5))
- assert_nil excerpt("This is a beautiful morning", "day")
- end
-
- def test_excerpt_should_not_be_html_safe
- assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
- end
-
- def test_excerpt_in_borderline_cases
- assert_equal("", excerpt("", "", :radius => 0))
- assert_equal("a", excerpt("a", "a", :radius => 0))
- assert_equal("...b...", excerpt("abc", "b", :radius => 0))
- assert_equal("abc", excerpt("abc", "b", :radius => 1))
- assert_equal("abc...", excerpt("abcd", "b", :radius => 1))
- assert_equal("...abc", excerpt("zabc", "b", :radius => 1))
- assert_equal("...abc...", excerpt("zabcd", "b", :radius => 1))
- assert_equal("zabcd", excerpt("zabcd", "b", :radius => 2))
-
- # excerpt strips the resulting string before ap-/prepending excerpt_string.
- # whether this behavior is meaningful when excerpt_string is not to be
- # appended is questionable.
- assert_equal("zabcd", excerpt(" zabcd ", "b", :radius => 4))
- 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(
- "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]",
- excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very",
- :omission => "[...]")
- )
- end
-
- def test_excerpt_with_utf8
- assert_equal("...\357\254\203ciency could not be...".force_encoding('UTF-8'), excerpt("That's why e\357\254\203ciency could not be helped".force_encoding('UTF-8'), 'could', :radius => 8))
- end
-
- def test_excerpt_does_not_modify_the_options_hash
- options = { :omission => "[...]",:radius => 5 }
- passed_options = options.dup
- excerpt("This is a beautiful morning", "beautiful", passed_options)
- assert_equal options, passed_options
- end
-
- def test_excerpt_with_separator
- options = { :separator => ' ', :radius => 1 }
- assert_equal('...a very beautiful...', excerpt('This is a very beautiful morning', 'very', options))
- assert_equal('This is...', excerpt('This is a very beautiful morning', 'this', options))
- assert_equal('...beautiful morning', excerpt('This is a very beautiful morning', 'morning', options))
-
- options = { :separator => "\n", :radius => 0 }
- assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", 'long', options))
-
- options = { :separator => "\n", :radius => 1 }
- assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options))
- end
-
- def test_word_wrap
- assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15))
- end
-
- def test_word_wrap_with_extra_newlines
- assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", :line_width => 15))
- end
-
- def test_word_wrap_does_not_modify_the_options_hash
- options = { :line_width => 15 }
- passed_options = options.dup
- word_wrap("some text", passed_options)
- assert_equal options, passed_options
- end
-
- def test_pluralization
- assert_equal("1 count", pluralize(1, "count"))
- assert_equal("2 counts", pluralize(2, "count"))
- assert_equal("1 count", pluralize('1', "count"))
- assert_equal("2 counts", pluralize('2', "count"))
- assert_equal("1,066 counts", pluralize('1,066', "count"))
- assert_equal("1.25 counts", pluralize('1.25', "count"))
- assert_equal("1.0 count", pluralize('1.0', "count"))
- assert_equal("1.00 count", pluralize('1.00', "count"))
- assert_equal("2 counters", pluralize(2, "count", "counters"))
- assert_equal("0 counters", pluralize(nil, "count", "counters"))
- assert_equal("2 people", pluralize(2, "person"))
- assert_equal("10 buffaloes", pluralize(10, "buffalo"))
- assert_equal("1 berry", pluralize(1, "berry"))
- assert_equal("12 berries", pluralize(12, "berry"))
- end
-
- def test_cycle_class
- value = Cycle.new("one", 2, "3")
- assert_equal("one", value.to_s)
- assert_equal("2", value.to_s)
- assert_equal("3", value.to_s)
- assert_equal("one", value.to_s)
- value.reset
- assert_equal("one", value.to_s)
- assert_equal("2", value.to_s)
- assert_equal("3", value.to_s)
- end
-
- def test_cycle_class_with_no_arguments
- assert_raise(ArgumentError) { Cycle.new }
- end
-
- def test_cycle
- assert_equal("one", cycle("one", 2, "3"))
- assert_equal("2", cycle("one", 2, "3"))
- assert_equal("3", cycle("one", 2, "3"))
- assert_equal("one", cycle("one", 2, "3"))
- assert_equal("2", cycle("one", 2, "3"))
- assert_equal("3", cycle("one", 2, "3"))
- end
-
- def test_cycle_with_no_arguments
- assert_raise(ArgumentError) { cycle }
- end
-
- def test_cycle_resets_with_new_values
- assert_equal("even", cycle("even", "odd"))
- assert_equal("odd", cycle("even", "odd"))
- assert_equal("even", cycle("even", "odd"))
- assert_equal("1", cycle(1, 2, 3))
- assert_equal("2", cycle(1, 2, 3))
- assert_equal("3", cycle(1, 2, 3))
- assert_equal("1", cycle(1, 2, 3))
- end
-
- def test_named_cycles
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
- assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("blue", cycle("red", "blue", :name => "colors"))
- assert_equal("3", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
- end
-
- def test_current_cycle_with_default_name
- cycle("even","odd")
- assert_equal "even", current_cycle
- cycle("even","odd")
- assert_equal "odd", current_cycle
- cycle("even","odd")
- assert_equal "even", current_cycle
- end
-
- def test_current_cycle_with_named_cycles
- cycle("red", "blue", :name => "colors")
- assert_equal "red", current_cycle("colors")
- cycle("red", "blue", :name => "colors")
- assert_equal "blue", current_cycle("colors")
- cycle("red", "blue", :name => "colors")
- assert_equal "red", current_cycle("colors")
- end
-
- def test_current_cycle_safe_call
- assert_nothing_raised { current_cycle }
- assert_nothing_raised { current_cycle("colors") }
- end
-
- def test_current_cycle_with_more_than_two_names
- cycle(1,2,3)
- assert_equal "1", current_cycle
- cycle(1,2,3)
- assert_equal "2", current_cycle
- cycle(1,2,3)
- assert_equal "3", current_cycle
- cycle(1,2,3)
- assert_equal "1", current_cycle
- end
-
- def test_default_named_cycle
- assert_equal("1", cycle(1, 2, 3))
- assert_equal("2", cycle(1, 2, 3, :name => "default"))
- assert_equal("3", cycle(1, 2, 3))
- end
-
- def test_reset_cycle
- assert_equal("1", cycle(1, 2, 3))
- assert_equal("2", cycle(1, 2, 3))
- reset_cycle
- assert_equal("1", cycle(1, 2, 3))
- end
-
- def test_reset_unknown_cycle
- reset_cycle("colors")
- end
-
- def test_recet_named_cycle
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
- reset_cycle("numbers")
- assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("blue", cycle("red", "blue", :name => "colors"))
- assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
- assert_equal("red", cycle("red", "blue", :name => "colors"))
- end
-
- def test_cycle_no_instance_variable_clashes
- @cycles = %w{Specialized Fuji Giant}
- assert_equal("red", cycle("red", "blue"))
- assert_equal("blue", cycle("red", "blue"))
- assert_equal("red", cycle("red", "blue"))
- assert_equal(%w{Specialized Fuji Giant}, @cycles)
- end
-end
diff --git a/actionpack/test/template/url_helper_test.rb b/actionpack/test/template/url_helper_test.rb
deleted file mode 100644
index bbfdf7f944..0000000000
--- a/actionpack/test/template/url_helper_test.rb
+++ /dev/null
@@ -1,864 +0,0 @@
-# encoding: utf-8
-require 'abstract_unit'
-require 'controller/fake_controllers'
-
-class UrlHelperTest < ActiveSupport::TestCase
-
- # In a few cases, the helper proxies to 'controller'
- # or request.
- #
- # In those cases, we'll set up a simple mock
- attr_accessor :controller, :request
-
- cattr_accessor :request_forgery
- self.request_forgery = false
-
- routes = ActionDispatch::Routing::RouteSet.new
- routes.draw do
- get "/" => "foo#bar"
- get "/other" => "foo#other"
- get "/article/:id" => "foo#article", :as => :article
- end
-
- include ActionView::Helpers::UrlHelper
- include routes.url_helpers
-
- include ActionView::Helpers::JavaScriptHelper
- include ActionDispatch::Assertions::DomAssertions
- include ActionView::Context
- include RenderERBUtils
-
- setup :_prepare_context
-
- def hash_for(options = {})
- { controller: "foo", action: "bar" }.merge!(options)
- end
- alias url_hash hash_for
-
- def test_url_for_does_not_escape_urls
- assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d))
- end
-
- def test_url_for_with_back
- referer = 'http://www.example.com/referer'
- @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer))
-
- assert_equal 'http://www.example.com/referer', url_for(:back)
- end
-
- def test_url_for_with_back_and_no_referer
- @controller = Struct.new(:request).new(Struct.new(:env).new({}))
- assert_equal 'javascript:history.back()', url_for(:back)
- end
-
- # TODO: missing test cases
- def test_button_to_with_straight_url
- assert_dom_equal %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /></div></form>}, button_to("Hello", "http://www.example.com")
- end
-
- def test_button_to_with_straight_url_and_request_forgery
- self.request_forgery = true
-
- assert_dom_equal(
- %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></div></form>},
- button_to("Hello", "http://www.example.com")
- )
- ensure
- self.request_forgery = false
- 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')
- 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>')
- 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")
- 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)
- 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")
- 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>},
- button_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
- )
- end
-
- def test_button_to_with_deprecated_confirm
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- 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>},
- button_to("Hello", "http://www.example.com", confirm: "Are you sure?")
- )
- end
- 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>},
- button_to("Hello", "http://www.example.com", data: { disable_with: "Greeting..." })
- )
- end
-
- def test_button_to_with_javascript_deprecated_disable_with
- assert_deprecated ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" do
- 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>},
- button_to("Hello", "http://www.example.com", disable_with: "Greeting...")
- )
- end
- 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>},
- 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>},
- button_to("Hello", "http://www.example.com", remote: true, data: { confirm: "Are you sure?" })
- )
- end
-
- def test_button_to_with_remote_and_javascript_with_deprecated_confirm
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- 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>},
- button_to("Hello", "http://www.example.com", remote: true, confirm: "Are you sure?")
- )
- end
- 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>},
- button_to("Hello", "http://www.example.com", remote: true, data: { disable_with: "Greeting..." })
- )
- end
-
- def test_button_to_with_remote_and_javascript_deprecated_disable_with
- assert_deprecated ":disable_with option is deprecated and will be removed from Rails 4.1. Use ':data => { :disable_with => \'Text\' }' instead" do
- 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>},
- button_to("Hello", "http://www.example.com", remote: true, disable_with: "Greeting...")
- )
- end
- 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>},
- 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>},
- 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>},
- 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>},
- 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>},
- 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>},
- button_to("http://www.example.com") { content_tag(:span, 'Hello') }
- )
- end
-
- def test_link_tag_with_straight_url
- assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com")
- end
-
- def test_link_tag_without_host_option
- assert_dom_equal(%{<a href="/">Test Link</a>}, link_to('Test Link', url_hash))
- end
-
- def test_link_tag_with_host_option
- hash = hash_for(host: "www.example.com")
- expected = %{<a href="http://www.example.com/">Test Link</a>}
- assert_dom_equal(expected, link_to('Test Link', hash))
- end
-
- def test_link_tag_with_query
- expected = %{<a href="http://www.example.com?q1=v1&amp;q2=v2">Hello</a>}
- assert_dom_equal expected, link_to("Hello", "http://www.example.com?q1=v1&q2=v2")
- end
-
- def test_link_tag_with_query_and_no_name
- expected = %{<a href="http://www.example.com?q1=v1&amp;q2=v2">http://www.example.com?q1=v1&amp;q2=v2</a>}
- assert_dom_equal expected, link_to(nil, "http://www.example.com?q1=v1&q2=v2")
- end
-
- def test_link_tag_with_back
- env = {"HTTP_REFERER" => "http://www.example.com/referer"}
- @controller = Struct.new(:request).new(Struct.new(:env).new(env))
- expected = %{<a href="#{env["HTTP_REFERER"]}">go back</a>}
- assert_dom_equal expected, link_to('go back', :back)
- end
-
- def test_link_tag_with_back_and_no_referer
- @controller = Struct.new(:request).new(Struct.new(:env).new({}))
- link = link_to('go back', :back)
- assert_dom_equal %{<a href="javascript:history.back()">go back</a>}, link
- end
-
- def test_link_tag_with_img
- link = link_to("<img src='/favicon.jpg' />".html_safe, "/")
- expected = %{<a href="/"><img src='/favicon.jpg' /></a>}
- assert_dom_equal expected, link
- end
-
- def test_link_with_nil_html_options
- link = link_to("Hello", url_hash, nil)
- assert_dom_equal %{<a href="/">Hello</a>}, link
- end
-
- def test_link_tag_with_custom_onclick
- link = link_to("Hello", "http://www.example.com", onclick: "alert('yay!')")
- expected = %{<a href="http://www.example.com" onclick="alert(&#39;yay!&#39;)">Hello</a>}
- assert_dom_equal expected, link
- end
-
- def test_link_tag_with_javascript_confirm
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="Are you sure?">Hello</a>},
- link_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
- )
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="You cant possibly be sure, can you?">Hello</a>},
- link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure, can you?" })
- )
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="You cant possibly be sure,\n can you?">Hello</a>},
- link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure,\n can you?" })
- )
- end
-
- def test_link_tag_with_deprecated_confirm
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="Are you sure?">Hello</a>},
- link_to("Hello", "http://www.example.com", confirm: "Are you sure?")
- )
- end
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="You cant possibly be sure, can you?">Hello</a>},
- link_to("Hello", "http://www.example.com", confirm: "You cant possibly be sure, can you?")
- )
- end
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %{<a href="http://www.example.com" data-confirm="You cant possibly be sure,\n can you?">Hello</a>},
- link_to("Hello", "http://www.example.com", confirm: "You cant possibly be sure,\n can you?")
- )
- end
- end
-
- def test_link_to_with_remote
- assert_dom_equal(
- %{<a href="http://www.example.com" data-remote="true">Hello</a>},
- link_to("Hello", "http://www.example.com", remote: true)
- )
- end
-
- def test_link_to_with_remote_false
- assert_dom_equal(
- %{<a href="http://www.example.com">Hello</a>},
- link_to("Hello", "http://www.example.com", remote: false)
- )
- end
-
- def test_link_to_with_symbolic_remote_in_non_html_options
- assert_dom_equal(
- %{<a href="/" data-remote="true">Hello</a>},
- link_to("Hello", hash_for(remote: true), {})
- )
- end
-
- def test_link_to_with_string_remote_in_non_html_options
- assert_dom_equal(
- %{<a href="/" data-remote="true">Hello</a>},
- link_to("Hello", hash_for('remote' => true), {})
- )
- end
-
- def test_link_tag_using_post_javascript
- assert_dom_equal(
- %{<a href="http://www.example.com" data-method="post" rel="nofollow">Hello</a>},
- link_to("Hello", "http://www.example.com", method: :post)
- )
- end
-
- def test_link_tag_using_delete_javascript
- assert_dom_equal(
- %{<a href="http://www.example.com" rel="nofollow" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete)
- )
- end
-
- def test_link_tag_using_delete_javascript_and_href
- assert_dom_equal(
- %{<a href="\#" rel="nofollow" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete, href: '#')
- )
- end
-
- def test_link_tag_using_post_javascript_and_rel
- assert_dom_equal(
- %{<a href="http://www.example.com" data-method="post" rel="example nofollow">Hello</a>},
- link_to("Hello", "http://www.example.com", method: :post, rel: 'example')
- )
- end
-
- def test_link_tag_using_post_javascript_and_confirm
- assert_dom_equal(
- %{<a href="http://www.example.com" data-method="post" rel="nofollow" data-confirm="Are you serious?">Hello</a>},
- link_to("Hello", "http://www.example.com", method: :post, data: { confirm: "Are you serious?" })
- )
- end
-
- def test_link_tag_using_post_javascript_and_with_deprecated_confirm
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %{<a href="http://www.example.com" data-method="post" rel="nofollow" data-confirm="Are you serious?">Hello</a>},
- link_to("Hello", "http://www.example.com", method: :post, confirm: "Are you serious?")
- )
- end
- end
-
- def test_link_tag_using_delete_javascript_and_href_and_confirm
- assert_dom_equal(
- %{<a href="\#" rel="nofollow" data-confirm="Are you serious?" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete, href: '#', data: { confirm: "Are you serious?" })
- )
- end
-
- def test_link_tag_using_delete_javascript_and_href_and_with_deprecated_confirm
- assert_deprecated ":confirm option is deprecated and will be removed from Rails 4.1. Use ':data => { :confirm => \'Text\' }' instead" do
- assert_dom_equal(
- %{<a href="\#" rel="nofollow" data-confirm="Are you serious?" data-method="delete">Destroy</a>},
- link_to("Destroy", "http://www.example.com", method: :delete, href: '#', confirm: "Are you serious?")
- )
- end
- end
-
- def test_link_tag_with_block
- assert_dom_equal %{<a href="/"><span>Example site</span></a>},
- link_to('/') { content_tag(:span, 'Example site') }
- end
-
- def test_link_tag_with_block_and_html_options
- assert_dom_equal %{<a class="special" href="/"><span>Example site</span></a>},
- link_to('/', class: "special") { content_tag(:span, 'Example site') }
- end
-
- def test_link_tag_using_block_in_erb
- out = render_erb %{<%= link_to('/') do %>Example site<% end %>}
- assert_equal '<a href="/">Example site</a>', out
- end
-
- def test_link_tag_with_html_safe_string
- assert_dom_equal(
- %{<a href="/article/Gerd_M%C3%BCller">Gerd Müller</a>},
- link_to("Gerd Müller", article_path("Gerd_Müller".html_safe))
- )
- end
-
- def test_link_tag_escapes_content
- assert_dom_equal %{<a href="/">Malicious &lt;script&gt;content&lt;/script&gt;</a>},
- link_to("Malicious <script>content</script>", "/")
- end
-
- def test_link_tag_does_not_escape_html_safe_content
- assert_dom_equal %{<a href="/">Malicious <script>content</script></a>},
- link_to("Malicious <script>content</script>".html_safe, "/")
- end
-
- def test_link_to_unless
- assert_equal "Showing", link_to_unless(true, "Showing", url_hash)
-
- assert_dom_equal %{<a href="/">Listing</a>},
- link_to_unless(false, "Listing", url_hash)
-
- assert_equal "Showing", link_to_unless(true, "Showing", url_hash)
-
- assert_equal "<strong>Showing</strong>",
- link_to_unless(true, "Showing", url_hash) { |name|
- "<strong>#{name}</strong>".html_safe
- }
-
- assert_equal "test",
- link_to_unless(true, "Showing", url_hash) {
- "test"
- }
- end
-
- def test_link_to_if
- assert_equal "Showing", link_to_if(false, "Showing", url_hash)
- assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash)
- assert_equal "Showing", link_to_if(false, "Showing", url_hash)
- end
-
- def request_for_url(url, opts = {})
- env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts)
- ActionDispatch::Request.new(env)
- end
-
- def test_current_page_with_simple_url
- @request = request_for_url("/")
- assert current_page?(url_hash)
- assert current_page?("http://www.example.com/")
- end
-
- def test_current_page_ignoring_params
- @request = request_for_url("/?order=desc&page=1")
-
- assert current_page?(url_hash)
- assert current_page?("http://www.example.com/")
- end
-
- def test_current_page_with_params_that_match
- @request = request_for_url("/?order=desc&page=1")
-
- assert current_page?(hash_for(order: "desc", page: "1"))
- assert current_page?("http://www.example.com/?order=desc&page=1")
- end
-
- def test_current_page_with_not_get_verb
- @request = request_for_url("/events", method: :post)
-
- assert !current_page?('/events')
- end
-
- def test_link_unless_current
- @request = request_for_url("/")
-
- assert_equal "Showing",
- link_to_unless_current("Showing", url_hash)
- assert_equal "Showing",
- link_to_unless_current("Showing", "http://www.example.com/")
-
- @request = request_for_url("/?order=desc")
-
- assert_equal "Showing",
- link_to_unless_current("Showing", url_hash)
- assert_equal "Showing",
- link_to_unless_current("Showing", "http://www.example.com/")
-
- @request = request_for_url("/?order=desc&page=1")
-
- assert_equal "Showing",
- link_to_unless_current("Showing", hash_for(order: 'desc', page: '1'))
- assert_equal "Showing",
- link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=1")
-
- @request = request_for_url("/?order=desc")
-
- assert_equal %{<a href="/?order=asc">Showing</a>},
- link_to_unless_current("Showing", hash_for(order: :asc))
- assert_equal %{<a href="http://www.example.com/?order=asc">Showing</a>},
- link_to_unless_current("Showing", "http://www.example.com/?order=asc")
-
- @request = request_for_url("/?order=desc")
- assert_equal %{<a href="/?order=desc&amp;page=2\">Showing</a>},
- link_to_unless_current("Showing", hash_for(order: "desc", page: 2))
- assert_equal %{<a href="http://www.example.com/?order=desc&amp;page=2">Showing</a>},
- link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=2")
-
- @request = request_for_url("/show")
-
- assert_equal %{<a href="/">Listing</a>},
- link_to_unless_current("Listing", url_hash)
- assert_equal %{<a href="http://www.example.com/">Listing</a>},
- link_to_unless_current("Listing", "http://www.example.com/")
- end
-
- def test_mail_to
- assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com")
- assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson")
- assert_dom_equal(
- %{<a class="admin" href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>},
- mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin")
- )
- assert_equal mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin"),
- mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin")
- end
-
- def test_mail_to_with_javascript
- snippet = mail_to("me@domain.com", "My email", encode: "javascript")
- assert_dom_equal "<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet
- end
-
- def test_mail_to_with_javascript_unicode
- snippet = mail_to("unicode@example.com", "únicode", encode: "javascript")
- assert_dom_equal "<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%75%6e%69%63%6f%64%65%40%65%78%61%6d%70%6c%65%2e%63%6f%6d%5c%22%3e%c3%ba%6e%69%63%6f%64%65%3c%5c%2f%61%3e%27%29%3b'))</script>", snippet
- end
-
- def test_mail_with_options
- assert_dom_equal(
- %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email">My email</a>},
- mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.")
- )
- end
-
- def test_mail_to_with_img
- assert_dom_equal %{<a href="mailto:feedback@example.com"><img src="/feedback.png" /></a>},
- mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe)
- end
-
- def test_mail_to_with_hex
- assert_dom_equal(
- %{<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>},
- mail_to("me@domain.com", "My email", encode: "hex")
- )
-
- assert_dom_equal(
- %{<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">&#109;&#101;&#64;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;</a>},
- mail_to("me@domain.com", nil, encode: "hex")
- )
- end
-
- def test_mail_to_with_replace_options
- assert_dom_equal(
- %{<a href="mailto:wolfgang@stufenlos.net">wolfgang(at)stufenlos(dot)net</a>},
- mail_to("wolfgang@stufenlos.net", nil, replace_at: "(at)", replace_dot: "(dot)")
- )
-
- assert_dom_equal(
- %{<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#46;&#99;&#111;&#109;</a>},
- mail_to("me@domain.com", nil, encode: "hex", replace_at: "(at)")
- )
-
- assert_dom_equal(
- %{<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">My email</a>},
- mail_to("me@domain.com", "My email", encode: "hex", replace_at: "(at)")
- )
-
- assert_dom_equal(
- %{<a href="&#109;&#97;&#105;&#108;&#116;&#111;&#58;%6d%65@%64%6f%6d%61%69%6e.%63%6f%6d">&#109;&#101;&#40;&#97;&#116;&#41;&#100;&#111;&#109;&#97;&#105;&#110;&#40;&#100;&#111;&#116;&#41;&#99;&#111;&#109;</a>},
- mail_to("me@domain.com", nil, encode: "hex", replace_at: "(at)", replace_dot: "(dot)")
- )
-
- assert_dom_equal(
- %{<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%4d%79%20%65%6d%61%69%6c%3c%5c%2f%61%3e%27%29%3b'))</script>},
- mail_to("me@domain.com", "My email", encode: "javascript", replace_at: "(at)", replace_dot: "(dot)")
- )
-
- assert_dom_equal(
- %{<script>eval(decodeURIComponent('%64%6f%63%75%6d%65%6e%74%2e%77%72%69%74%65%28%27%3c%61%20%68%72%65%66%3d%5c%22%6d%61%69%6c%74%6f%3a%6d%65%40%64%6f%6d%61%69%6e%2e%63%6f%6d%5c%22%3e%6d%65%28%61%74%29%64%6f%6d%61%69%6e%28%64%6f%74%29%63%6f%6d%3c%5c%2f%61%3e%27%29%3b'))</script>},
- mail_to("me@domain.com", nil, encode: "javascript", replace_at: "(at)", replace_dot: "(dot)")
- )
- end
-
- def test_mail_to_returns_html_safe_string
- assert mail_to("david@loudthinking.com").html_safe?
- assert mail_to("me@domain.com", "My email", encode: "javascript").html_safe?
- assert mail_to("me@domain.com", "My email", encode: "hex").html_safe?
- end
-
- def protect_against_forgery?
- self.request_forgery
- end
-
- def form_authenticity_token
- "secret"
- end
-
- def request_forgery_protection_token
- "form_token"
- end
-
- private
- def sort_query_string_params(uri)
- path, qs = uri.split('?')
- qs = qs.split('&amp;').sort.join('&amp;') if qs
- qs ? "#{path}?#{qs}" : path
- end
-end
-
-class UrlHelperControllerTest < ActionController::TestCase
- class UrlHelperController < ActionController::Base
- test_routes do
- get 'url_helper_controller_test/url_helper/show/:id',
- to: 'url_helper_controller_test/url_helper#show',
- as: :show
-
- get 'url_helper_controller_test/url_helper/profile/:name',
- to: 'url_helper_controller_test/url_helper#show',
- as: :profile
-
- get 'url_helper_controller_test/url_helper/show_named_route',
- to: 'url_helper_controller_test/url_helper#show_named_route',
- as: :show_named_route
-
- get "/:controller(/:action(/:id))"
-
- get 'url_helper_controller_test/url_helper/normalize_recall_params',
- to: UrlHelperController.action(:normalize_recall),
- as: :normalize_recall_params
-
- get '/url_helper_controller_test/url_helper/override_url_helper/default',
- to: 'url_helper_controller_test/url_helper#override_url_helper',
- as: :override_url_helper
- end
-
- def show
- if params[:name]
- render inline: 'ok'
- else
- redirect_to profile_path(params[:id])
- end
- end
-
- def show_url_for
- render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>"
- end
-
- def show_overriden_url_for
- render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>"
- end
-
- def show_named_route
- render inline: "<%= show_named_route_#{params[:kind]} %>"
- end
-
- def nil_url_for
- render inline: '<%= url_for(nil) %>'
- end
-
- def normalize_recall_params
- render inline: '<%= normalize_recall_params_path %>'
- end
-
- def recall_params_not_changed
- render inline: '<%= url_for(action: :show_url_for) %>'
- end
-
- def override_url_helper
- render inline: '<%= override_url_helper_path %>'
- end
-
- def override_url_helper_path
- '/url_helper_controller_test/url_helper/override_url_helper/override'
- end
- helper_method :override_url_helper_path
- end
-
- tests UrlHelperController
-
- def test_url_for_shows_only_path
- get :show_url_for
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
- end
-
- def test_overriden_url_for_shows_only_path
- get :show_overriden_url_for
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
- end
-
- def test_named_route_url_shows_host_and_path
- get :show_named_route, kind: 'url'
- assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route',
- @response.body
- end
-
- def test_named_route_path_shows_only_path
- get :show_named_route, kind: 'path'
- assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body
- end
-
- def test_url_for_nil_returns_current_path
- get :nil_url_for
- assert_equal '/url_helper_controller_test/url_helper/nil_url_for', @response.body
- end
-
- def test_named_route_should_show_host_and_path_using_controller_default_url_options
- class << @controller
- def default_url_options
- { host: 'testtwo.host' }
- end
- end
-
- get :show_named_route, kind: 'url'
- assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body
- end
-
- def test_recall_params_should_be_normalized
- get :normalize_recall_params
- assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body
- end
-
- def test_recall_params_should_not_be_changed
- get :recall_params_not_changed
- assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
- end
-
- def test_recall_params_should_normalize_id
- get :show, id: '123'
- assert_equal 302, @response.status
- assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location
-
- get :show, name: '123'
- assert_equal 'ok', @response.body
- end
-
- def test_url_helper_can_be_overriden
- get :override_url_helper
- assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body
- end
-end
-
-class TasksController < ActionController::Base
- test_routes do
- resources :tasks
- end
-
- def index
- render_default
- end
-
- def show
- render_default
- end
-
- protected
- def render_default
- render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" +
- "<%= link_to_unless_current('tasks', tasks_url) %>"
- end
-end
-
-class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase
- tests TasksController
-
- def test_link_to_unless_current_to_current
- get :index
- assert_equal "tasks\ntasks", @response.body
- end
-
- def test_link_to_unless_current_shows_link
- get :show, id: 1
- assert_equal %{<a href="/tasks">tasks</a>\n} +
- %{<a href="#{@request.protocol}#{@request.host_with_port}/tasks">tasks</a>},
- @response.body
- end
-end
-
-class Session
- extend ActiveModel::Naming
- include ActiveModel::Conversion
- attr_accessor :id, :workshop_id
-
- def initialize(id)
- @id = id
- end
-
- def persisted?
- id.present?
- end
-
- def to_s
- id.to_s
- end
-end
-
-class WorkshopsController < ActionController::Base
- test_routes do
- resources :workshops do
- resources :sessions
- end
- end
-
- def index
- @workshop = Workshop.new(nil)
- render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>"
- end
-
- def show
- @workshop = Workshop.new(params[:id])
- render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>"
- end
-end
-
-class SessionsController < ActionController::Base
- test_routes do
- resources :workshops do
- resources :sessions
- end
- end
-
- def index
- @workshop = Workshop.new(params[:workshop_id])
- @session = Session.new(nil)
- render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
- end
-
- def show
- @workshop = Workshop.new(params[:workshop_id])
- @session = Session.new(params[:id])
- render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
- end
-end
-
-class PolymorphicControllerTest < ActionController::TestCase
- def test_new_resource
- @controller = WorkshopsController.new
-
- get :index
- assert_equal %{/workshops\n<a href="/workshops">Workshop</a>}, @response.body
- end
-
- def test_existing_resource
- @controller = WorkshopsController.new
-
- get :show, id: 1
- assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body
- end
-
- def test_new_nested_resource
- @controller = SessionsController.new
-
- get :index, workshop_id: 1
- assert_equal %{/workshops/1/sessions\n<a href="/workshops/1/sessions">Session</a>}, @response.body
- end
-
- def test_existing_nested_resource
- @controller = SessionsController.new
-
- get :show, workshop_id: 1, id: 1
- assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body
- end
-end
diff --git a/actionpack/test/ts_isolated.rb b/actionpack/test/ts_isolated.rb
deleted file mode 100644
index c44c5d8968..0000000000
--- a/actionpack/test/ts_isolated.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'minitest/autorun'
-require 'rbconfig'
-require 'abstract_unit'
-
-class TestIsolated < ActiveSupport::TestCase
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
-
- Dir["#{File.dirname(__FILE__)}/{abstract,controller,dispatch,template}/**/*_test.rb"].each do |file|
- define_method("test #{file}") do
- command = "#{ruby} -Ilib:test #{file}"
- result = silence_stderr { `#{command}` }
- assert $?.to_i.zero?, "#{command}\n#{result}"
- end
- end
-end
diff --git a/actionview/CHANGELOG.md b/actionview/CHANGELOG.md
new file mode 100644
index 0000000000..4adf1dbd8f
--- /dev/null
+++ b/actionview/CHANGELOG.md
@@ -0,0 +1,210 @@
+* Ensure ActionView::Digestor.cache is correctly cleaned up when
+ combining recursive templates with ActionView::Resolver.caching = false
+
+ *wyaeld*
+
+* Fix `collection_check_boxes` generated hidden input to use the name attribute provided
+ in the options hash.
+
+ *Angel N. Sciortino*
+
+* Fix some edge cases for AV `select` helper with `:selected` option
+
+ *Bogdan Gusiev*
+
+* Ability to pass block to `select` helper
+
+ <%= 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 -%>
+
+ *Bogdan Gusiev*
+
+* Handle `:namespace` form option in collection labels
+
+ *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.
+
+ Before:
+
+ javascript_include_tag('templates.jst')
+ # => <script src="/javascripts/templates.jst.js"></script>
+
+ 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.
+
+ *Rafael Mendonça França*
+
+* Fix `text_area` to behave like `text_field` when `nil` is given as
+ value.
+
+ Before:
+
+ f.text_field :field, value: nil #=> <input value="">
+ f.text_area :field, value: nil #=> <textarea>value of field</textarea>
+
+ After:
+
+ f.text_area :field, value: nil #=> <textarea></textarea>
+
+ *Joel Cogen*
+
+* Element of the `grouped_options_for_select` can
+ optionally contain html attributes as the last element of the array.
+
+ grouped_options_for_select(
+ [["North America", [['United States','US'],"Canada"], data: { foo: 'bar' }]]
+ )
+
+ *Vasiliy Ermolovich*
+
+* Fix default rendered format problem when calling `render` without :content_type option.
+ It should return :html. Fix #11393.
+
+ *Gleb Mazovetskiy* *Oleg* *kennyj*
+
+* Fix `link_to` with block and url hashes.
+
+ 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.
+
+ Before:
+
+ 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\" />"
+
+ After:
+
+ image_tag("HTTP://google.com")
+ # => "<img alt=\"Google\" src=\"HTTP://google.com\" />"
+ image_tag("http://google.com")
+ # => "<img alt=\"Google\" src=\"http://google.com\" />"
+
+ *David Celis*
+
+* Element of the `collection_check_boxes` and `collection_radio_buttons` can
+ optionally contain html attributes as the last element of the array.
+
+ *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`).
+
+ *Alex Peattie*
+
+* Fix an issue where partials with a number in the filename weren't being digested for cache dependencies.
+
+ *Bryan Ricker*
+
+* First release, ActionView extracted from ActionPack
+
+ *Piotr Sarnacki*, *Åukasz StrzaÅ‚kowski*
+
+Please check [4-0-stable (ActionPack's CHANGELOG)](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for previous changes.
diff --git a/actionview/MIT-LICENSE b/actionview/MIT-LICENSE
new file mode 100644
index 0000000000..5c668d9624
--- /dev/null
+++ b/actionview/MIT-LICENSE
@@ -0,0 +1,21 @@
+Copyright (c) 2004-2013 David Heinemeier Hansson
+
+Permission is hereby granted, free of charge, to any person obtaining
+a copy of this software and associated documentation files (the
+"Software"), to deal in the Software without restriction, including
+without limitation the rights to use, copy, modify, merge, publish,
+distribute, sublicense, and/or sell copies of the Software, and to
+permit persons to whom the Software is furnished to do so, subject to
+the following conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+
diff --git a/actionview/README.rdoc b/actionview/README.rdoc
new file mode 100644
index 0000000000..35f805346c
--- /dev/null
+++ b/actionview/README.rdoc
@@ -0,0 +1,34 @@
+= Action View
+
+Action View is a framework for handling view template lookup and rendering, and provides
+view helpers that assist when building HTML forms, Atom feeds and more.
+Template formats that Action View handles are ERB (embedded Ruby, typically
+used to inline short Ruby snippets inside HTML), and XML Builder.
+
+== Download and installation
+
+The latest version of Action View can be installed with RubyGems:
+
+ % [sudo] gem install actionview
+
+Source code can be downloaded as part of the Rails project on GitHub
+
+* https://github.com/rails/rails/tree/master/actionview
+
+
+== License
+
+Action View is released under the MIT license:
+
+* http://www.opensource.org/licenses/MIT
+
+
+== Support
+
+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:
+
+* https://github.com/rails/rails/issues
diff --git a/actionview/RUNNING_UNIT_TESTS.rdoc b/actionview/RUNNING_UNIT_TESTS.rdoc
new file mode 100644
index 0000000000..104b3e288d
--- /dev/null
+++ b/actionview/RUNNING_UNIT_TESTS.rdoc
@@ -0,0 +1,27 @@
+== Running with Rake
+
+The easiest way to run the unit tests is through Rake. The default task runs
+the entire test suite for all classes. For more information, checkout the
+full array of rake tasks with "rake -T"
+
+Rake can be found at http://rake.rubyforge.org
+
+== Running by hand
+
+To run a single test suite
+
+ rake test TEST=path/to/test.rb
+
+which can be further narrowed down to one test:
+
+ rake test TEST=path/to/test.rb TESTOPTS="--name=test_something"
+
+== 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,
+these tests are skipped.
+
+Other tests are runnable from a fresh copy of actionview without any configuration.
+
diff --git a/actionview/Rakefile b/actionview/Rakefile
new file mode 100644
index 0000000000..d56fe9ea76
--- /dev/null
+++ b/actionview/Rakefile
@@ -0,0 +1,80 @@
+require 'rake/testtask'
+require 'rubygems/package_task'
+
+desc "Default Task"
+task :default => :test
+
+# Run the unit tests
+
+desc "Run all unit tests"
+task :test => ["test:template", "test:integration:action_pack", "test:integration:active_record"]
+
+namespace :test do
+ task :isolated do
+ Dir.glob("test/{actionpack,activerecord,template}/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
+ end or raise "Failures"
+ end
+
+ Rake::TestTask.new(:template) do |t|
+ t.libs << 'test'
+ t.test_files = Dir.glob('test/template/**/*_test.rb').sort
+ t.warning = true
+ t.verbose = true
+ end
+
+ namespace :integration do
+ desc 'ActiveRecord Integration Tests'
+ Rake::TestTask.new(:active_record) do |t|
+ t.libs << 'test'
+ t.test_files = Dir.glob("test/activerecord/*_test.rb")
+ t.warning = true
+ t.verbose = true
+ end
+
+ desc 'ActionPack Integration Tests'
+ Rake::TestTask.new(:action_pack) do |t|
+ t.libs << 'test'
+ t.test_files = Dir.glob("test/actionpack/**/*_test.rb")
+ t.warning = true
+ t.verbose = true
+ end
+ end
+end
+
+spec = eval(File.read('actionview.gemspec'))
+
+Gem::PackageTask.new(spec) do |p|
+ p.gem_spec = spec
+end
+
+desc "Release to rubygems"
+task :release => :package do
+ require 'rake/gemcutter'
+ Rake::Gemcutter::Tasks.new(spec).define
+ Rake::Task['gem:push'].invoke
+end
+
+task :lines do
+ lines, codelines, total_lines, total_codelines = 0, 0, 0, 0
+
+ FileList["lib/**/*.rb"].each do |file_name|
+ next if file_name =~ /vendor/
+ File.open(file_name, 'r') do |f|
+ while line = f.gets
+ lines += 1
+ next if line =~ /^\s*$/
+ next if line =~ /^\s*#/
+ codelines += 1
+ end
+ end
+ puts "L: #{sprintf("%4d", lines)}, LOC #{sprintf("%4d", codelines)} | #{file_name}"
+
+ total_lines += lines
+ total_codelines += codelines
+
+ lines, codelines = 0, 0
+ end
+
+ puts "Total: Lines #{total_lines}, LOC #{total_codelines}"
+end
diff --git a/actionview/actionview.gemspec b/actionview/actionview.gemspec
new file mode 100644
index 0000000000..cdac074973
--- /dev/null
+++ b/actionview/actionview.gemspec
@@ -0,0 +1,29 @@
+version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+
+Gem::Specification.new do |s|
+ s.platform = Gem::Platform::RUBY
+ s.name = 'actionview'
+ s.version = version
+ s.summary = 'Rendering framework putting the V in MVC (part of Rails).'
+ s.description = 'Simple, battle-tested conventions and helpers for building web pages.'
+
+ s.required_ruby_version = '>= 1.9.3'
+
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
+
+ s.files = Dir['CHANGELOG.md', 'README.rdoc', 'MIT-LICENSE', 'lib/**/*']
+ s.require_path = 'lib'
+ s.requirements << 'none'
+
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'activemodel', version
+
+ s.add_dependency 'builder', '~> 3.1.0'
+ s.add_dependency 'erubis', '~> 2.7.0'
+
+ s.add_development_dependency 'actionpack', version
+end
diff --git a/actionview/lib/action_view.rb b/actionview/lib/action_view.rb
new file mode 100644
index 0000000000..39c0c6c856
--- /dev/null
+++ b/actionview/lib/action_view.rb
@@ -0,0 +1,96 @@
+#--
+# Copyright (c) 2004-2013 David Heinemeier Hansson
+#
+# Permission is hereby granted, free of charge, to any person obtaining
+# a copy of this software and associated documentation files (the
+# "Software"), to deal in the Software without restriction, including
+# without limitation the rights to use, copy, modify, merge, publish,
+# distribute, sublicense, and/or sell copies of the Software, and to
+# permit persons to whom the Software is furnished to do so, subject to
+# the following conditions:
+#
+# The above copyright notice and this permission notice shall be
+# included in all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
+# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
+# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
+# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+#++
+
+require 'active_support'
+require 'active_support/rails'
+
+module ActionView
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :Base
+ autoload :Context
+ autoload :CompiledTemplates, "action_view/context"
+ autoload :Digestor
+ autoload :Helpers
+ autoload :LookupContext
+ autoload :Layouts
+ autoload :PathSet
+ autoload :RecordIdentifier
+ autoload :Rendering
+ autoload :RoutingUrlFor
+ autoload :Template
+ autoload :ViewPaths
+
+ autoload_under "renderer" do
+ autoload :Renderer
+ autoload :AbstractRenderer
+ autoload :PartialRenderer
+ autoload :TemplateRenderer
+ autoload :StreamingTemplateRenderer
+ end
+
+ autoload_at "action_view/template/resolver" do
+ autoload :Resolver
+ autoload :PathResolver
+ autoload :FileSystemResolver
+ autoload :OptimizedFileSystemResolver
+ autoload :FallbackFileSystemResolver
+ end
+
+ autoload_at "action_view/buffers" do
+ autoload :OutputBuffer
+ autoload :StreamingBuffer
+ end
+
+ autoload_at "action_view/flows" do
+ autoload :OutputFlow
+ autoload :StreamingFlow
+ end
+
+ autoload_at "action_view/template/error" do
+ autoload :MissingTemplate
+ autoload :ActionViewError
+ autoload :EncodingError
+ autoload :MissingRequestError
+ autoload :TemplateError
+ autoload :WrongEncodingError
+ end
+ end
+
+ autoload :TestCase
+
+ ENCODING_FLAG = '#.*coding[:=]\s*(\S+)[ \t]*'
+
+ def self.eager_load!
+ super
+ ActionView::Template.eager_load!
+ HTML.eager_load!
+ end
+end
+
+require 'active_support/core_ext/string/output_safety'
+
+ActiveSupport.on_load(:i18n) do
+ I18n.load_path << "#{File.dirname(__FILE__)}/action_view/locale/en.yml"
+end
diff --git a/actionview/lib/action_view/base.rb b/actionview/lib/action_view/base.rb
new file mode 100644
index 0000000000..caade8f43b
--- /dev/null
+++ b/actionview/lib/action_view/base.rb
@@ -0,0 +1,205 @@
+require 'active_support/core_ext/module/attr_internal'
+require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/ordered_options'
+require 'action_view/log_subscriber'
+require 'action_view/helpers'
+require 'action_view/context'
+require 'action_view/template'
+require 'action_view/lookup_context'
+
+module ActionView #:nodoc:
+ # = Action View Base
+ #
+ # Action View templates can be written in several ways. If the template file has a <tt>.erb</tt> extension then it uses a mixture of ERB
+ # (included in Ruby) and HTML. If the template file has a <tt>.builder</tt> extension then Jim Weirich's Builder::XmlMarkup library is used.
+ #
+ # == ERB
+ #
+ # You trigger ERB by using embeddings such as <% %>, <% -%>, and <%= %>. The <%= %> tag set is used when you want output. Consider the
+ # following loop for names:
+ #
+ # <b>Names of all the people</b>
+ # <% @people.each do |person| %>
+ # Name: <%= person.name %><br/>
+ # <% end %>
+ #
+ # The loop is setup in regular embedding tags <% %> and the name is written using the output embedding tag <%= %>. Note that this
+ # is not just a usage suggestion. Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
+ #
+ # <%# WRONG %>
+ # Hi, Mr. <% puts "Frodo" %>
+ #
+ # If you absolutely must write from within a function use +concat+.
+ #
+ # <%- and -%> suppress leading and trailing whitespace, including the trailing newline, and can be used interchangeably with <% and %>.
+ #
+ # === Using sub templates
+ #
+ # Using sub templates allows you to sidestep tedious replication and extract common display structures in shared templates. The
+ # classic example is the use of a header and footer (even though the Action Pack-way would be to use Layouts):
+ #
+ # <%= render "shared/header" %>
+ # Something really specific and terrific
+ # <%= render "shared/footer" %>
+ #
+ # As you see, we use the output embeddings for the render methods. The render call itself will just return a string holding the
+ # result of the rendering. The output embedding writes it to the current template.
+ #
+ # But you don't have to restrict yourself to static includes. Templates can share variables amongst themselves by using instance
+ # variables defined using the regular embedding tags. Like this:
+ #
+ # <% @page_title = "A Wonderful Hello" %>
+ # <%= render "shared/header" %>
+ #
+ # Now the header can pick up on the <tt>@page_title</tt> variable and use it for outputting a title tag:
+ #
+ # <title><%= @page_title %></title>
+ #
+ # === Passing local variables to sub templates
+ #
+ # You can pass local variables to sub templates by using a hash with the variable names as keys and the objects as values:
+ #
+ # <%= render "shared/header", { headline: "Welcome", person: person } %>
+ #
+ # These can now be accessed in <tt>shared/header</tt> with:
+ #
+ # 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,
+ # Rails will check the file's modification time and recompile it in development mode.
+ #
+ # == Builder
+ #
+ # Builder templates are a more programmatic alternative to ERB. They are especially useful for generating XML content. An XmlMarkup object
+ # named +xml+ is automatically made available to templates with a <tt>.builder</tt> extension.
+ #
+ # Here are some basic examples:
+ #
+ # xml.em("emphasized") # => <em>emphasized</em>
+ # xml.em { xml.b("emph & bold") } # => <em><b>emph &amp; bold</b></em>
+ # xml.a("A Link", "href" => "http://onestepback.org") # => <a href="http://onestepback.org">A Link</a>
+ # xml.target("name" => "compile", "option" => "fast") # => <target option="fast" name="compile"\>
+ # # NOTE: order of attributes is not specified.
+ #
+ # Any method with a block will be treated as an XML markup tag with nested markup in the block. For example, the following:
+ #
+ # xml.div do
+ # xml.h1(@person.name)
+ # xml.p(@person.bio)
+ # end
+ #
+ # would produce something like:
+ #
+ # <div>
+ # <h1>David Heinemeier Hansson</h1>
+ # <p>A product of Danish Design during the Winter of '79...</p>
+ # </div>
+ #
+ # A full-length RSS example actually used on Basecamp:
+ #
+ # xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
+ # xml.channel do
+ # xml.title(@feed_title)
+ # xml.link(@url)
+ # xml.description "Basecamp: Recent items"
+ # xml.language "en-us"
+ # xml.ttl "40"
+ #
+ # @recent_items.each do |item|
+ # xml.item do
+ # xml.title(item_title(item))
+ # xml.description(item_description(item)) if item_description(item)
+ # xml.pubDate(item_pubDate(item))
+ # xml.guid(@person.firm.account.url + @recent_items.url(item))
+ # xml.link(@person.firm.account.url + @recent_items.url(item))
+ #
+ # xml.tag!("dc:creator", item.author_name) if item_has_creator?(item)
+ # end
+ # end
+ # end
+ # end
+ #
+ # More builder documentation can be found at http://builder.rubyforge.org.
+ class Base
+ include Helpers, ::ERB::Util, Context
+
+ # Specify the proc used to decorate input tags that refer to attributes with errors.
+ cattr_accessor :field_error_proc
+ @@field_error_proc = Proc.new{ |html_tag, instance| "<div class=\"field_with_errors\">#{html_tag}</div>".html_safe }
+
+ # How to complete the streaming when an exception occurs.
+ # This is our best guess: first try to close the attribute, then the tag.
+ cattr_accessor :streaming_completion_on_exception
+ @@streaming_completion_on_exception = %("><script>window.location = "/500.html"</script></html>)
+
+ # Specify whether rendering within namespaced controllers should prefix
+ # the partial paths for ActiveModel objects with the namespace.
+ # (e.g., an Admin::PostsController would render @post using /admin/posts/_post.erb)
+ cattr_accessor :prefix_partial_path_with_controller_namespace
+ @@prefix_partial_path_with_controller_namespace = true
+
+ # Specify default_formats that can be rendered.
+ cattr_accessor :default_formats
+
+ class_attribute :_routes
+ class_attribute :logger
+
+ class << self
+ delegate :erb_trim_mode=, :to => 'ActionView::Template::Handlers::ERB'
+
+ def cache_template_loading
+ ActionView::Resolver.caching?
+ end
+
+ def cache_template_loading=(value)
+ ActionView::Resolver.caching = value
+ end
+
+ def xss_safe? #:nodoc:
+ true
+ end
+ end
+
+ attr_accessor :view_renderer
+ attr_internal :config, :assigns
+
+ delegate :lookup_context, :to => :view_renderer
+ delegate :formats, :formats=, :locale, :locale=, :view_paths, :view_paths=, :to => :lookup_context
+
+ def assign(new_assigns) # :nodoc:
+ @_assigns = new_assigns.each { |key, value| instance_variable_set("@#{key}", value) }
+ end
+
+ def initialize(context = nil, assigns = {}, controller = nil, formats = nil) #:nodoc:
+ @_config = ActiveSupport::InheritableOptions.new
+
+ if context.is_a?(ActionView::Renderer)
+ @view_renderer = context
+ else
+ lookup_context = context.is_a?(ActionView::LookupContext) ?
+ context : ActionView::LookupContext.new(context)
+ lookup_context.formats = formats if formats
+ lookup_context.prefixes = controller._prefixes if controller
+ @view_renderer = ActionView::Renderer.new(lookup_context)
+ end
+
+ assign(assigns)
+ assign_controller(controller)
+ _prepare_context
+ end
+
+ ActiveSupport.run_load_hooks(:action_view, self)
+ end
+end
diff --git a/actionview/lib/action_view/buffers.rb b/actionview/lib/action_view/buffers.rb
new file mode 100644
index 0000000000..361a0dccbe
--- /dev/null
+++ b/actionview/lib/action_view/buffers.rb
@@ -0,0 +1,49 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ class OutputBuffer < ActiveSupport::SafeBuffer #:nodoc:
+ def initialize(*)
+ super
+ encode!
+ end
+
+ def <<(value)
+ return self if value.nil?
+ super(value.to_s)
+ end
+ alias :append= :<<
+
+ def safe_concat(value)
+ return self if value.nil?
+ super(value.to_s)
+ end
+ alias :safe_append= :safe_concat
+ end
+
+ class StreamingBuffer #:nodoc:
+ def initialize(block)
+ @block = block
+ end
+
+ def <<(value)
+ value = value.to_s
+ value = ERB::Util.h(value) unless value.html_safe?
+ @block.call(value)
+ end
+ alias :concat :<<
+ alias :append= :<<
+
+ def safe_concat(value)
+ @block.call(value.to_s)
+ end
+ alias :safe_append= :safe_concat
+
+ def html_safe?
+ true
+ end
+
+ def html_safe
+ self
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/context.rb b/actionview/lib/action_view/context.rb
index ee263df484..ee263df484 100644
--- a/actionpack/lib/action_view/context.rb
+++ b/actionview/lib/action_view/context.rb
diff --git a/actionview/lib/action_view/dependency_tracker.rb b/actionview/lib/action_view/dependency_tracker.rb
new file mode 100644
index 0000000000..b2e8334077
--- /dev/null
+++ b/actionview/lib/action_view/dependency_tracker.rb
@@ -0,0 +1,93 @@
+require 'thread_safe'
+
+module ActionView
+ class DependencyTracker
+ @trackers = ThreadSafe::Cache.new
+
+ def self.find_dependencies(name, template)
+ tracker = @trackers[template.handler]
+
+ if tracker.present?
+ tracker.call(name, template)
+ else
+ []
+ end
+ end
+
+ def self.register_tracker(extension, tracker)
+ handler = Template.handler_for_extension(extension)
+ @trackers[handler] = tracker
+ end
+
+ def self.remove_tracker(handler)
+ @trackers.delete(handler)
+ end
+
+ class ERBTracker
+ EXPLICIT_DEPENDENCY = /# Template Dependency: (\S+)/
+
+ # Matches:
+ # render partial: "comments/comment", collection: commentable.comments
+ # render "comments/comments"
+ # render 'comments/comments'
+ # render('comments/comments')
+ #
+ # render(@topic) => render("topics/topic")
+ # render(topics) => render("topics/topic")
+ # render(message.topics) => render("topics/topic")
+ RENDER_DEPENDENCY = /
+ render\s* # render, followed by optional whitespace
+ \(? # start an optional parenthesis for the render call
+ (partial:|:partial\s+=>)?\s* # naming the partial, used with collection -- 1st capture
+ ([@a-z"'][@\w\/\."']+) # the template name itself -- 2nd capture
+ /x
+
+ def self.call(name, template)
+ new(name, template).dependencies
+ end
+
+ def initialize(name, template)
+ @name, @template = name, template
+ end
+
+ def dependencies
+ render_dependencies + explicit_dependencies
+ end
+
+ attr_reader :name, :template
+ private :name, :template
+
+ private
+
+ def source
+ template.source
+ end
+
+ def directory
+ name.split("/")[0..-2].join("/")
+ end
+
+ def render_dependencies
+ source.scan(RENDER_DEPENDENCY).
+ collect(&:second).uniq.
+
+ # render(@topic) => render("topics/topic")
+ # render(topics) => render("topics/topic")
+ # render(message.topics) => render("topics/topic")
+ collect { |name| name.sub(/\A@?([a-z_]+\.)*([a-z_]+)\z/) { "#{$2.pluralize}/#{$2.singularize}" } }.
+
+ # render("headline") => render("message/headline")
+ collect { |name| name.include?("/") ? name : "#{directory}/#{name}" }.
+
+ # replace quotes from string renders
+ collect { |name| name.gsub(/["']/, "") }
+ end
+
+ def explicit_dependencies
+ source.scan(EXPLICIT_DEPENDENCY).flatten.uniq
+ end
+ end
+
+ register_tracker :erb, ERBTracker
+ end
+end
diff --git a/actionview/lib/action_view/digestor.rb b/actionview/lib/action_view/digestor.rb
new file mode 100644
index 0000000000..5570e2a8dc
--- /dev/null
+++ b/actionview/lib/action_view/digestor.rb
@@ -0,0 +1,116 @@
+require 'thread_safe'
+require 'action_view/dependency_tracker'
+require 'monitor'
+
+module ActionView
+ class Digestor
+ cattr_reader(:cache)
+ @@cache = ThreadSafe::Cache.new
+ @@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('.')
+
+ # 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)
+ 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
+
+ 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
+ end
+
+ attr_reader :name, :format, :finder, :options
+
+ def initialize(name, format, finder, options={})
+ @name, @format, @finder, @options = name, format, finder, options
+ end
+
+ def digest
+ Digest::MD5.hexdigest("#{source}-#{dependency_digest}").tap do |digest|
+ logger.try :info, "Cache digest for #{name}.#{format}: #{digest}"
+ end
+ rescue ActionView::MissingTemplate
+ logger.try :error, "Couldn't find template for digesting: #{name}.#{format}"
+ ''
+ end
+
+ def dependencies
+ DependencyTracker.find_dependencies(name, template)
+ rescue ActionView::MissingTemplate
+ [] # File doesn't exist, so no dependencies
+ end
+
+ def nested_dependencies
+ dependencies.collect do |dependency|
+ dependencies = PartialDigestor.new(dependency, format, finder).nested_dependencies
+ dependencies.any? ? { dependency => dependencies } : dependency
+ end
+ end
+
+ private
+
+ def logger
+ ActionView::Base.logger
+ end
+
+ def logical_name
+ name.gsub(%r|/_|, "/")
+ end
+
+ def partial?
+ false
+ end
+
+ def template
+ @template ||= finder.find(logical_name, [], partial?, formats: [ format ])
+ end
+
+ def source
+ template.source
+ end
+
+ def dependency_digest
+ template_digests = dependencies.collect do |template_name|
+ Digestor.digest(template_name, format, finder, partial: true)
+ end
+
+ (template_digests + injected_dependencies).join("-")
+ end
+
+ def injected_dependencies
+ Array.wrap(options[:dependencies])
+ end
+ end
+
+ class PartialDigestor < Digestor # :nodoc:
+ def partial?
+ true
+ end
+ end
+end
diff --git a/actionview/lib/action_view/flows.rb b/actionview/lib/action_view/flows.rb
new file mode 100644
index 0000000000..ba24510e56
--- /dev/null
+++ b/actionview/lib/action_view/flows.rb
@@ -0,0 +1,76 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ class OutputFlow #:nodoc:
+ attr_reader :content
+
+ def initialize
+ @content = Hash.new { |h,k| h[k] = ActiveSupport::SafeBuffer.new }
+ end
+
+ # Called by _layout_for to read stored values.
+ def get(key)
+ @content[key]
+ end
+
+ # Called by each renderer object to set the layout contents.
+ def set(key, value)
+ @content[key] = value
+ end
+
+ # Called by content_for
+ def append(key, value)
+ @content[key] << value
+ end
+ alias_method :append!, :append
+
+ end
+
+ class StreamingFlow < OutputFlow #:nodoc:
+ def initialize(view, fiber)
+ @view = view
+ @parent = nil
+ @child = view.output_buffer
+ @content = view.view_flow.content
+ @fiber = fiber
+ @root = Fiber.current.object_id
+ end
+
+ # Try to get stored content. If the content
+ # is not available and we are inside the layout
+ # fiber, we set that we are waiting for the given
+ # key and yield.
+ def get(key)
+ return super if @content.key?(key)
+
+ if inside_fiber?
+ view = @view
+
+ begin
+ @waiting_for = key
+ view.output_buffer, @parent = @child, view.output_buffer
+ Fiber.yield
+ ensure
+ @waiting_for = nil
+ view.output_buffer, @child = @parent, view.output_buffer
+ end
+ end
+
+ super
+ end
+
+ # Appends the contents for the given key. This is called
+ # by provides and resumes back to the fiber if it is
+ # the key it is waiting for.
+ def append!(key, value)
+ super
+ @fiber.resume if @waiting_for == key
+ end
+
+ private
+
+ def inside_fiber?
+ Fiber.current.object_id != @root
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers.rb b/actionview/lib/action_view/helpers.rb
new file mode 100644
index 0000000000..8a78685ae1
--- /dev/null
+++ b/actionview/lib/action_view/helpers.rb
@@ -0,0 +1,58 @@
+require 'active_support/benchmarkable'
+
+module ActionView #:nodoc:
+ module Helpers #:nodoc:
+ extend ActiveSupport::Autoload
+
+ autoload :ActiveModelHelper
+ autoload :AssetTagHelper
+ autoload :AssetUrlHelper
+ autoload :AtomFeedHelper
+ autoload :CacheHelper
+ autoload :CaptureHelper
+ autoload :ControllerHelper
+ autoload :CsrfHelper
+ autoload :DateHelper
+ autoload :DebugHelper
+ autoload :FormHelper
+ autoload :FormOptionsHelper
+ autoload :FormTagHelper
+ autoload :JavaScriptHelper, "action_view/helpers/javascript_helper"
+ autoload :NumberHelper
+ autoload :OutputSafetyHelper
+ autoload :RecordTagHelper
+ autoload :RenderingHelper
+ autoload :SanitizeHelper
+ autoload :TagHelper
+ autoload :TextHelper
+ autoload :TranslationHelper
+ autoload :UrlHelper
+
+ extend ActiveSupport::Concern
+
+ include ActiveSupport::Benchmarkable
+ include ActiveModelHelper
+ include AssetTagHelper
+ include AssetUrlHelper
+ include AtomFeedHelper
+ include CacheHelper
+ include CaptureHelper
+ include ControllerHelper
+ include CsrfHelper
+ include DateHelper
+ include DebugHelper
+ include FormHelper
+ include FormOptionsHelper
+ include FormTagHelper
+ include JavaScriptHelper
+ include NumberHelper
+ include OutputSafetyHelper
+ include RecordTagHelper
+ include RenderingHelper
+ include SanitizeHelper
+ include TagHelper
+ include TextHelper
+ include TranslationHelper
+ include UrlHelper
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/active_model_helper.rb b/actionview/lib/action_view/helpers/active_model_helper.rb
index 901f433c70..901f433c70 100644
--- a/actionpack/lib/action_view/helpers/active_model_helper.rb
+++ b/actionview/lib/action_view/helpers/active_model_helper.rb
diff --git a/actionview/lib/action_view/helpers/asset_tag_helper.rb b/actionview/lib/action_view/helpers/asset_tag_helper.rb
new file mode 100644
index 0000000000..e2430140c2
--- /dev/null
+++ b/actionview/lib/action_view/helpers/asset_tag_helper.rb
@@ -0,0 +1,319 @@
+require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/hash/keys'
+require 'action_view/helpers/asset_url_helper'
+require 'action_view/helpers/tag_helper'
+
+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
+ # the assets exist before linking to them:
+ #
+ # image_tag("rails.png")
+ # # => <img alt="Rails" src="/assets/rails.png" />
+ # stylesheet_link_tag("application")
+ # # => <link href="/assets/application.css?body=1" media="screen" rel="stylesheet" />
+ module AssetTagHelper
+ extend ActiveSupport::Concern
+
+ include AssetUrlHelper
+ include TagHelper
+
+ # Returns an HTML script tag for each of the +sources+ provided.
+ #
+ # Sources may be paths to JavaScript files. Relative paths are assumed to be relative
+ # to <tt>assets/javascripts</tt>, full paths are assumed to be relative to the document
+ # root. Relative paths are idiomatic, use absolute paths only when needed.
+ #
+ # When passing paths, the ".js" extension is optional. If you do not want ".js"
+ # appended to the path <tt>extname: false</tt> can be set on the options.
+ #
+ # You can modify the HTML attributes of the script tag by passing a hash as the
+ # last argument.
+ #
+ # When the Asset Pipeline is enabled, you can pass the name of your manifest as
+ # source, and include other JavaScript or CoffeeScript files inside the manifest.
+ #
+ # javascript_include_tag "xmlhr"
+ # # => <script src="/assets/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag "template.jst", extname: false
+ # # => <script src="/assets/template.jst?1284139606"></script>
+ #
+ # javascript_include_tag "xmlhr.js"
+ # # => <script src="/assets/xmlhr.js?1284139606"></script>
+ #
+ # javascript_include_tag "common.javascript", "/elsewhere/cools"
+ # # => <script src="/assets/common.javascript?1284139606"></script>
+ # # <script src="/elsewhere/cools.js?1423139606"></script>
+ #
+ # javascript_include_tag "http://www.example.com/xmlhr"
+ # # => <script src="http://www.example.com/xmlhr"></script>
+ #
+ # javascript_include_tag "http://www.example.com/xmlhr.js"
+ # # => <script src="http://www.example.com/xmlhr.js"></script>
+ def javascript_include_tag(*sources)
+ options = sources.extract_options!.stringify_keys
+ path_options = options.extract!('protocol', 'extname').symbolize_keys
+ sources.uniq.map { |source|
+ tag_options = {
+ "src" => path_to_javascript(source, path_options)
+ }.merge!(options)
+ content_tag(:script, "", tag_options)
+ }.join("\n").html_safe
+ end
+
+ # Returns a stylesheet link tag for the sources specified as arguments. If
+ # you don't specify an extension, <tt>.css</tt> will be appended automatically.
+ # You can modify the link attributes by passing a hash as the last argument.
+ # For historical reasons, the 'media' attribute will always be present and defaults
+ # to "screen", so you must explicitly set it to "all" for the stylesheet(s) to
+ # apply to all media types.
+ #
+ # stylesheet_link_tag "style"
+ # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
+ #
+ # stylesheet_link_tag "style.css"
+ # # => <link href="/assets/style.css" media="screen" rel="stylesheet" />
+ #
+ # stylesheet_link_tag "http://www.example.com/style.css"
+ # # => <link href="http://www.example.com/style.css" media="screen" rel="stylesheet" />
+ #
+ # stylesheet_link_tag "style", media: "all"
+ # # => <link href="/assets/style.css" media="all" rel="stylesheet" />
+ #
+ # stylesheet_link_tag "style", media: "print"
+ # # => <link href="/assets/style.css" media="print" rel="stylesheet" />
+ #
+ # stylesheet_link_tag "random.styles", "/css/stylish"
+ # # => <link href="/assets/random.styles" media="screen" rel="stylesheet" />
+ # # <link href="/css/stylish.css" media="screen" rel="stylesheet" />
+ def stylesheet_link_tag(*sources)
+ options = sources.extract_options!.stringify_keys
+ path_options = options.extract!('protocol').symbolize_keys
+
+ sources.uniq.map { |source|
+ tag_options = {
+ "rel" => "stylesheet",
+ "media" => "screen",
+ "href" => path_to_stylesheet(source, path_options)
+ }.merge!(options)
+ tag(:link, tag_options)
+ }.join("\n").html_safe
+ end
+
+ # Returns a link tag that browsers and news readers can use to auto-detect
+ # an RSS or Atom feed. The +type+ can either be <tt>:rss</tt> (default) or
+ # <tt>:atom</tt>. Control the link options in url_for format using the
+ # +url_options+. You can modify the LINK tag itself in +tag_options+.
+ #
+ # ==== Options
+ #
+ # * <tt>:rel</tt> - Specify the relation of this link, defaults to "alternate"
+ # * <tt>:type</tt> - Override the auto-generated mime type
+ # * <tt>:title</tt> - Specify the title of the link, defaults to the +type+
+ #
+ # ==== Examples
+ #
+ # auto_discovery_link_tag
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/action" />
+ # auto_discovery_link_tag(:atom)
+ # # => <link rel="alternate" type="application/atom+xml" title="ATOM" href="http://www.currenthost.com/controller/action" />
+ # auto_discovery_link_tag(:rss, {action: "feed"})
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/controller/feed" />
+ # auto_discovery_link_tag(:rss, {action: "feed"}, {title: "My RSS"})
+ # # => <link rel="alternate" type="application/rss+xml" title="My RSS" href="http://www.currenthost.com/controller/feed" />
+ # auto_discovery_link_tag(:rss, {controller: "news", action: "feed"})
+ # # => <link rel="alternate" type="application/rss+xml" title="RSS" href="http://www.currenthost.com/news/feed" />
+ # auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "Example RSS"})
+ # # => <link rel="alternate" type="application/rss+xml" title="Example RSS" href="http://www.example.com/feed" />
+ def auto_discovery_link_tag(type = :rss, url_options = {}, tag_options = {})
+ if !(type == :rss || type == :atom) && tag_options[:type].blank?
+ raise ArgumentError.new("You should pass :type tag_option key explicitly, because you have passed #{type} type other than :rss or :atom.")
+ end
+
+ tag(
+ "link",
+ "rel" => tag_options[:rel] || "alternate",
+ "type" => tag_options[:type] || Mime::Type.lookup_by_extension(type.to_s).to_s,
+ "title" => tag_options[:title] || type.to_s.upcase,
+ "href" => url_options.is_a?(Hash) ? url_for(url_options.merge(:only_path => false)) : url_options
+ )
+ 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".
+ #
+ # ==== Options
+ #
+ # * <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'
+ #
+ # ==== Examples
+ #
+ # favicon_link_tag '/myicon.ico'
+ # # => <link href="/assets/myicon.ico" rel="shortcut icon" type="image/vnd.microsoft.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.
+ # The following call would generate such a tag:
+ #
+ # favicon_link_tag '/mb-icon.png', rel: 'apple-touch-icon', type: 'image/png'
+ # # => <link href="/assets/mb-icon.png" rel="apple-touch-icon" type="image/png" />
+ def favicon_link_tag(source='favicon.ico', options={})
+ tag('link', {
+ :rel => 'shortcut icon',
+ :type => 'image/vnd.microsoft.icon',
+ :href => path_to_image(source)
+ }.merge!(options.symbolize_keys))
+ end
+
+ # Returns an HTML image tag for the +source+. The +source+ can be a full
+ # path or a file.
+ #
+ # ==== Options
+ #
+ # You can add HTML attributes using the +options+. The +options+ supports
+ # three additional keys for convenience and conformance:
+ #
+ # * <tt>:alt</tt> - If no alt text is given, the file name part of the
+ # +source+ is used (capitalized and without the extension)
+ # * <tt>:size</tt> - Supplied as "{Width}x{Height}" or "{Number}", so "30x45" becomes
+ # width="30" and height="45", and "50" becomes width="50" and height="50".
+ # <tt>:size</tt> will be ignored if the value is not in the correct format.
+ #
+ # ==== Examples
+ #
+ # image_tag("icon")
+ # # => <img alt="Icon" src="/assets/icon" />
+ # image_tag("icon.png")
+ # # => <img alt="Icon" src="/assets/icon.png" />
+ # image_tag("icon.png", size: "16x10", alt: "Edit Entry")
+ # # => <img src="/assets/icon.png" width="16" height="10" alt="Edit Entry" />
+ # image_tag("/icons/icon.gif", size: "16")
+ # # => <img src="/icons/icon.gif" width="16" height="16" alt="Icon" />
+ # image_tag("/icons/icon.gif", height: '32', width: '32')
+ # # => <img alt="Icon" height="32" src="/icons/icon.gif" width="32" />
+ # image_tag("/icons/icon.gif", class: "menu_icon")
+ # # => <img alt="Icon" class="menu_icon" src="/icons/icon.gif" />
+ def image_tag(source, options={})
+ options = options.symbolize_keys
+
+ src = options[:src] = path_to_image(source)
+
+ unless src =~ /^(?:cid|data):/ || src.blank?
+ options[:alt] = options.fetch(:alt){ image_alt(src) }
+ end
+
+ if size = options.delete(:size)
+ options[:width], options[:height] = size.split("x") if size =~ %r{\A\d+x\d+\z}
+ options[:width] = options[:height] = size if size =~ %r{\A\d+\z}
+ end
+
+ tag("img", options)
+ end
+
+ # Returns a string suitable for an html image tag alt attribute.
+ # The +src+ argument is meant to be an image file path.
+ # The method removes the basename of the file path and the digest,
+ # if any. It also removes hyphens and underscores from file names and
+ # replaces them with spaces, returning a space-separated, titleized
+ # string.
+ #
+ # ==== Examples
+ #
+ # image_alt('rails.png')
+ # # => Rails
+ #
+ # image_alt('hyphenated-file-name.png')
+ # # => Hyphenated file name
+ #
+ # image_alt('underscored_file_name.png')
+ # # => Underscored file name
+ def image_alt(src)
+ File.basename(src, '.*').sub(/-[[:xdigit:]]{32}\z/, '').tr('-_', ' ').capitalize
+ end
+
+ # Returns an html video tag for the +sources+. If +sources+ is a string,
+ # a single video tag will be returned. If +sources+ is an array, a video
+ # tag with nested source tags for each source will be returned. The
+ # +sources+ can be full paths or files that exists in your public videos
+ # directory.
+ #
+ # ==== Options
+ # You can add HTML attributes using the +options+. The +options+ supports
+ # two additional keys for convenience and conformance:
+ #
+ # * <tt>:poster</tt> - Set an image (like a screenshot) to be shown
+ # before the video loads. The path is calculated like the +src+ of +image_tag+.
+ # * <tt>:size</tt> - Supplied as "{Width}x{Height}", so "30x45" becomes
+ # width="30" and height="45". <tt>:size</tt> will be ignored if the
+ # value is not in the correct format.
+ #
+ # ==== Examples
+ #
+ # video_tag("trailer")
+ # # => <video src="/videos/trailer" />
+ # video_tag("trailer.ogg")
+ # # => <video src="/videos/trailer.ogg" />
+ # video_tag("trailer.ogg", controls: true, autobuffer: true)
+ # # => <video autobuffer="autobuffer" controls="controls" src="/videos/trailer.ogg" />
+ # video_tag("trailer.m4v", size: "16x10", poster: "screenshot.png")
+ # # => <video src="/videos/trailer.m4v" width="16" height="10" poster="/assets/screenshot.png" />
+ # video_tag("/trailers/hd.avi", size: "16x16")
+ # # => <video src="/trailers/hd.avi" width="16" height="16" />
+ # video_tag("/trailers/hd.avi", height: '32', width: '32')
+ # # => <video height="32" src="/trailers/hd.avi" width="32" />
+ # video_tag("trailer.ogg", "trailer.flv")
+ # # => <video><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></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"], size: "160x120")
+ # # => <video height="120" width="160"><source src="/videos/trailer.ogg" /><source src="/videos/trailer.flv" /></video>
+ def video_tag(*sources)
+ multiple_sources_tag('video', sources) do |options|
+ options[:poster] = path_to_image(options[:poster]) if options[:poster]
+
+ if size = options.delete(:size)
+ options[:width], options[:height] = size.split("x") if size =~ %r{^\d+x\d+$}
+ end
+ end
+ end
+
+ # Returns an HTML audio tag for the +source+.
+ # The +source+ can be full path or file that exists in
+ # your public audios directory.
+ #
+ # audio_tag("sound")
+ # # => <audio src="/audios/sound" />
+ # audio_tag("sound.wav")
+ # # => <audio src="/audios/sound.wav" />
+ # audio_tag("sound.wav", autoplay: true, controls: true)
+ # # => <audio autoplay="autoplay" controls="controls" src="/audios/sound.wav" />
+ # audio_tag("sound.wav", "sound.mid")
+ # # => <audio><source src="/audios/sound.wav" /><source src="/audios/sound.mid" /></audio>
+ def audio_tag(*sources)
+ multiple_sources_tag('audio', sources)
+ end
+
+ private
+ def multiple_sources_tag(type, sources)
+ options = sources.extract_options!.symbolize_keys
+ sources.flatten!
+
+ yield options if block_given?
+
+ if sources.size > 1
+ content_tag(type, options) do
+ safe_join sources.map { |source| tag("source", :src => send("path_to_#{type}", source)) }
+ end
+ else
+ options[:src] = send("path_to_#{type}", sources.first)
+ content_tag(type, nil, options)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/asset_url_helper.rb b/actionview/lib/action_view/helpers/asset_url_helper.rb
new file mode 100644
index 0000000000..c830ab23e3
--- /dev/null
+++ b/actionview/lib/action_view/helpers/asset_url_helper.rb
@@ -0,0 +1,355 @@
+require 'zlib'
+
+module ActionView
+ # = Action View Asset URL Helpers
+ module Helpers
+ # This module provides methods for generating asset paths and
+ # urls.
+ #
+ # image_path("rails.png")
+ # # => "/assets/rails.png"
+ #
+ # image_url("rails.png")
+ # # => "http://www.example.com/assets/rails.png"
+ #
+ # === Using asset hosts
+ #
+ # By default, Rails links to these assets on the current host in the public
+ # folder, but you can direct Rails to link to assets from a dedicated asset
+ # server by setting <tt>ActionController::Base.asset_host</tt> in the application
+ # configuration, typically in <tt>config/environments/production.rb</tt>.
+ # For example, you'd define <tt>assets.example.com</tt> to be your asset
+ # host this way, inside the <tt>configure</tt> block of your environment-specific
+ # configuration files or <tt>config/application.rb</tt>:
+ #
+ # config.action_controller.asset_host = "assets.example.com"
+ #
+ # Helpers take that into account:
+ #
+ # image_tag("rails.png")
+ # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
+ # stylesheet_link_tag("application")
+ # # => <link href="http://assets.example.com/assets/application.css" media="screen" rel="stylesheet" />
+ #
+ # Browsers typically open at most two simultaneous connections to a single
+ # host, which means your assets often have to wait for other assets to finish
+ # downloading. You can alleviate this by using a <tt>%d</tt> wildcard in the
+ # +asset_host+. For example, "assets%d.example.com". If that wildcard is
+ # present Rails distributes asset requests among the corresponding four hosts
+ # "assets0.example.com", ..., "assets3.example.com". With this trick browsers
+ # will open eight simultaneous connections rather than two.
+ #
+ # image_tag("rails.png")
+ # # => <img alt="Rails" src="http://assets0.example.com/assets/rails.png" />
+ # stylesheet_link_tag("application")
+ # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
+ #
+ # To do this, you can either setup four actual hosts, or you can use wildcard
+ # DNS to CNAME the wildcard to a single asset host. You can read more about
+ # setting up your DNS CNAME records from your ISP.
+ #
+ # Note: This is purely a browser performance optimization and is not meant
+ # for server load balancing. See http://www.die.net/musings/page_load_time/
+ # for background.
+ #
+ # Alternatively, you can exert more control over the asset host by setting
+ # +asset_host+ to a proc like this:
+ #
+ # ActionController::Base.asset_host = Proc.new { |source|
+ # "http://assets#{Digest::MD5.hexdigest(source).to_i(16) % 2 + 1}.example.com"
+ # }
+ # image_tag("rails.png")
+ # # => <img alt="Rails" src="http://assets1.example.com/assets/rails.png" />
+ # stylesheet_link_tag("application")
+ # # => <link href="http://assets2.example.com/assets/application.css" media="screen" rel="stylesheet" />
+ #
+ # The example above generates "http://assets1.example.com" and
+ # "http://assets2.example.com". This option is useful for example if
+ # you need fewer/more than four hosts, custom host names, etc.
+ #
+ # As you see the proc takes a +source+ parameter. That's a string with the
+ # absolute path of the asset, for example "/assets/rails.png".
+ #
+ # ActionController::Base.asset_host = Proc.new { |source|
+ # if source.ends_with?('.css')
+ # "http://stylesheets.example.com"
+ # else
+ # "http://assets.example.com"
+ # end
+ # }
+ # image_tag("rails.png")
+ # # => <img alt="Rails" src="http://assets.example.com/assets/rails.png" />
+ # stylesheet_link_tag("application")
+ # # => <link href="http://stylesheets.example.com/assets/application.css" media="screen" rel="stylesheet" />
+ #
+ # Alternatively you may ask for a second parameter +request+. That one is
+ # particularly useful for serving assets from an SSL-protected page. The
+ # example proc below disables asset hosting for HTTPS connections, while
+ # 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.
+ #
+ # config.action_controller.asset_host = Proc.new { |source, request|
+ # if request.ssl?
+ # "#{request.protocol}#{request.host_with_port}"
+ # else
+ # "#{request.protocol}assets.example.com"
+ # end
+ # }
+ #
+ # You can also implement a custom asset host object that responds to +call+
+ # and takes either one or two parameters just like the proc.
+ #
+ # config.action_controller.asset_host = AssetHostingWithMinimumSsl.new(
+ # "http://asset%d.example.com", "https://asset1.example.com"
+ # )
+ #
+ module AssetUrlHelper
+ URI_REGEXP = %r{^[-a-z]+://|^(?:cid|data):|^//}i
+
+ # Computes the path to asset in public directory. If :type
+ # options is set, a file extension will be appended and scoped
+ # to the corresponding public directory.
+ #
+ # 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 "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?
+ return source if source =~ URI_REGEXP
+
+ tail, source = source[/([\?#].+)$/], source.sub(/([\?#].+)$/, '')
+
+ if extname = compute_asset_extname(source, options)
+ source = "#{source}#{extname}"
+ end
+
+ if source[0] != ?/
+ source = compute_asset_path(source, options)
+ end
+
+ 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}/")
+ end
+
+ if host = compute_asset_host(source, options)
+ source = "#{host}#{source}"
+ end
+
+ "#{source}#{tail}"
+ end
+ alias_method :path_to_asset, :asset_path # aliased to avoid conflicts with an asset_path named route
+
+ # Computes the full URL to an asset in the public directory. This
+ # will use +asset_path+ internally, so most of their behaviors
+ # will be the same.
+ def asset_url(source, options = {})
+ path_to_asset(source, options.merge(:protocol => :request))
+ end
+ alias_method :url_to_asset, :asset_url # aliased to avoid conflicts with an asset_url named route
+
+ ASSET_EXTENSIONS = {
+ javascript: '.js',
+ stylesheet: '.css'
+ }
+
+ # Compute extname to append to asset path. Returns nil if
+ # nothing should be added.
+ def compute_asset_extname(source, options = {})
+ return if options[:extname] == false
+ extname = options[:extname] || ASSET_EXTENSIONS[options[:type]]
+ extname if extname && File.extname(source) != extname
+ end
+
+ # Maps asset types to public directory.
+ ASSET_PUBLIC_DIRECTORIES = {
+ audio: '/audios',
+ font: '/fonts',
+ image: '/images',
+ javascript: '/javascripts',
+ stylesheet: '/stylesheets',
+ video: '/videos'
+ }
+
+ # Computes asset path to public directory. Plugins and
+ # extensions can override this method to point to custom assets
+ # or generate digested paths or query strings.
+ def compute_asset_path(source, options = {})
+ dir = ASSET_PUBLIC_DIRECTORIES[options[:type]] || ""
+ File.join(dir, source)
+ end
+
+ # Pick an asset host for this source. Returns +nil+ if no host is set,
+ # the host if no wildcard is set, the host interpolated with the
+ # numbers 0-3 if it contains <tt>%d</tt> (the number is the source hash mod 4),
+ # or the value returned from invoking call on an object responding to call
+ # (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
+
+ if host.respond_to?(:call)
+ arity = host.respond_to?(:arity) ? host.arity : host.method(:call).arity
+ args = [source]
+ args << request if request && (arity > 1 || arity < 0)
+ host = host.call(*args)
+ elsif host =~ /%d/
+ host = host % (Zlib.crc32(source) % 4)
+ end
+
+ return unless host
+
+ if host =~ URI_REGEXP
+ host
+ else
+ protocol = options[:protocol] || config.default_asset_host_protocol || (request ? :request : :relative)
+ case protocol
+ when :relative
+ "//#{host}"
+ when :request
+ "#{request.protocol}#{host}"
+ else
+ "#{protocol}://#{host}"
+ end
+ end
+ end
+
+ # 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.
+ #
+ # javascript_path "xmlhr" # => /javascripts/xmlhr.js
+ # javascript_path "dir/xmlhr.js" # => /javascripts/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
+ def javascript_path(source, options = {})
+ path_to_asset(source, {type: :javascript}.merge!(options))
+ 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.
+ # 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))
+ end
+ 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).
+ # 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 "/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
+ def stylesheet_path(source, options = {})
+ path_to_asset(source, {type: :stylesheet}.merge!(options))
+ end
+ alias_method :path_to_stylesheet, :stylesheet_path # aliased to avoid conflicts with a stylesheet_path named route
+
+ # Computes the full URL to a stylesheet asset in the public stylesheets directory.
+ # This will use +stylesheet_path+ internally, so most of their behaviors will be the same.
+ def stylesheet_url(source, options = {})
+ url_to_asset(source, {type: :stylesheet}.merge!(options))
+ end
+ alias_method :url_to_stylesheet, :stylesheet_url # aliased to avoid conflicts with a stylesheet_url named route
+
+ # Computes the path to an image asset.
+ # Full paths from the document root will be passed through.
+ # Used internally by +image_tag+ to build the image path:
+ #
+ # image_path("edit") # => "/assets/edit"
+ # image_path("edit.png") # => "/assets/edit.png"
+ # image_path("icons/edit.png") # => "/assets/icons/edit.png"
+ # image_path("/icons/edit.png") # => "/icons/edit.png"
+ # image_path("http://www.example.com/img/edit.png") # => "http://www.example.com/img/edit.png"
+ #
+ # If you have images as application resources this method may conflict with their named routes.
+ # The alias +path_to_image+ is provided to avoid that. Rails uses the alias internally, and
+ # plugin authors are encouraged to do so.
+ def image_path(source, options = {})
+ path_to_asset(source, {type: :image}.merge!(options))
+ end
+ alias_method :path_to_image, :image_path # aliased to avoid conflicts with an image_path named route
+
+ # Computes the full URL to an image asset.
+ # This will use +image_path+ internally, so most of their behaviors will be the same.
+ def image_url(source, options = {})
+ url_to_asset(source, {type: :image}.merge!(options))
+ end
+ alias_method :url_to_image, :image_url # aliased to avoid conflicts with an image_url named route
+
+ # Computes the path to a video asset in the public videos directory.
+ # Full paths from the document root will be passed through.
+ # Used internally by +video_tag+ to build the video path.
+ #
+ # video_path("hd") # => /videos/hd
+ # video_path("hd.avi") # => /videos/hd.avi
+ # video_path("trailers/hd.avi") # => /videos/trailers/hd.avi
+ # video_path("/trailers/hd.avi") # => /trailers/hd.avi
+ # video_path("http://www.example.com/vid/hd.avi") # => http://www.example.com/vid/hd.avi
+ def video_path(source, options = {})
+ path_to_asset(source, {type: :video}.merge!(options))
+ end
+ alias_method :path_to_video, :video_path # aliased to avoid conflicts with a video_path named route
+
+ # Computes the full URL to a video asset in the public videos directory.
+ # This will use +video_path+ internally, so most of their behaviors will be the same.
+ def video_url(source, options = {})
+ url_to_asset(source, {type: :video}.merge!(options))
+ end
+ alias_method :url_to_video, :video_url # aliased to avoid conflicts with an video_url named route
+
+ # Computes the path to an audio asset in the public audios directory.
+ # Full paths from the document root will be passed through.
+ # Used internally by +audio_tag+ to build the audio path.
+ #
+ # audio_path("horse") # => /audios/horse
+ # audio_path("horse.wav") # => /audios/horse.wav
+ # audio_path("sounds/horse.wav") # => /audios/sounds/horse.wav
+ # audio_path("/sounds/horse.wav") # => /sounds/horse.wav
+ # audio_path("http://www.example.com/sounds/horse.wav") # => http://www.example.com/sounds/horse.wav
+ def audio_path(source, options = {})
+ path_to_asset(source, {type: :audio}.merge!(options))
+ end
+ alias_method :path_to_audio, :audio_path # aliased to avoid conflicts with an audio_path named route
+
+ # Computes the full URL to an audio asset in the public audios directory.
+ # This will use +audio_path+ internally, so most of their behaviors will be the same.
+ def audio_url(source, options = {})
+ url_to_asset(source, {type: :audio}.merge!(options))
+ end
+ alias_method :url_to_audio, :audio_url # aliased to avoid conflicts with an audio_url named route
+
+ # 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("/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 = {})
+ path_to_asset(source, {type: :font}.merge!(options))
+ end
+ alias_method :path_to_font, :font_path # aliased to avoid conflicts with an font_path named route
+
+ # Computes the full URL to a font asset.
+ # This will use +font_path+ internally, so most of their behaviors will be the same.
+ def font_url(source, options = {})
+ url_to_asset(source, {type: :font}.merge!(options))
+ end
+ alias_method :url_to_font, :font_url # aliased to avoid conflicts with an font_url named route
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/atom_feed_helper.rb b/actionview/lib/action_view/helpers/atom_feed_helper.rb
new file mode 100644
index 0000000000..42b1dd8933
--- /dev/null
+++ b/actionview/lib/action_view/helpers/atom_feed_helper.rb
@@ -0,0 +1,203 @@
+require 'set'
+
+module ActionView
+ # = Action View Atom Feed Helpers
+ module Helpers
+ module AtomFeedHelper
+ # Adds easy defaults to writing Atom feeds with the Builder template engine (this does not work on ERB or any other
+ # template languages).
+ #
+ # Full usage example:
+ #
+ # config/routes.rb:
+ # Basecamp::Application.routes.draw do
+ # resources :posts
+ # root to: "posts#index"
+ # end
+ #
+ # app/controllers/posts_controller.rb:
+ # class PostsController < ApplicationController::Base
+ # # GET /posts.html
+ # # GET /posts.atom
+ # def index
+ # @posts = Post.all
+ #
+ # respond_to do |format|
+ # format.html
+ # format.atom
+ # end
+ # end
+ # end
+ #
+ # app/views/posts/index.atom.builder:
+ # atom_feed do |feed|
+ # feed.title("My great blog!")
+ # feed.updated(@posts[0].created_at) if @posts.length > 0
+ #
+ # @posts.each do |post|
+ # feed.entry(post) do |entry|
+ # entry.title(post.title)
+ # entry.content(post.body, type: 'html')
+ #
+ # entry.author do |author|
+ # author.name("DHH")
+ # end
+ # end
+ # end
+ # end
+ #
+ # The options for atom_feed are:
+ #
+ # * <tt>:language</tt>: Defaults to "en-US".
+ # * <tt>:root_url</tt>: The HTML alternative that this feed is doubling for. Defaults to / on the current host.
+ # * <tt>:url</tt>: The URL for this feed. Defaults to the current URL.
+ # * <tt>:id</tt>: The id for this feed. Defaults to "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}"
+ # * <tt>:schema_date</tt>: The date at which the tag scheme for the feed was first used. A good default is the year you
+ # created the feed. See http://feedvalidator.org/docs/error/InvalidTAG.html for more information. If not specified,
+ # 2005 is used (as an "I don't care" value).
+ # * <tt>:instruct</tt>: Hash of XML processing instructions in the form {target => {attribute => value, }} or {target => [{attribute => value, }, ]}
+ #
+ # Other namespaces can be added to the root element:
+ #
+ # app/views/posts/index.atom.builder:
+ # atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
+ # 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
+ # feed.title("My great blog!")
+ # feed.updated((@posts.first.created_at))
+ # feed.tag!(openSearch:totalResults, 10)
+ #
+ # @posts.each do |post|
+ # feed.entry(post) do |entry|
+ # entry.title(post.title)
+ # entry.content(post.body, type: 'html')
+ # entry.tag!('app:edited', Time.now)
+ #
+ # entry.author do |author|
+ # author.name("DHH")
+ # end
+ # end
+ # end
+ # end
+ #
+ # The Atom spec defines five elements (content rights title subtitle
+ # summary) which may directly contain xhtml content if type: 'xhtml'
+ # is specified as an attribute. If so, this helper will take care of
+ # the enclosing div and xhtml namespace declaration. Example usage:
+ #
+ # entry.summary type: 'xhtml' do |xhtml|
+ # xhtml.p pluralize(order.line_items.count, "line item")
+ # xhtml.p "Shipped to #{order.address}"
+ # xhtml.p "Paid by #{order.pay_type}"
+ # end
+ #
+ #
+ # <tt>atom_feed</tt> yields an +AtomFeedBuilder+ instance. Nested elements yield
+ # an +AtomBuilder+ instance.
+ def atom_feed(options = {}, &block)
+ if options[:schema_date]
+ options[:schema_date] = options[:schema_date].strftime("%Y-%m-%d") if options[:schema_date].respond_to?(:strftime)
+ else
+ options[:schema_date] = "2005" # The Atom spec copyright date
+ end
+
+ xml = options.delete(:xml) || eval("xml", block.binding)
+ xml.instruct!
+ if options[:instruct]
+ options[:instruct].each do |target,attrs|
+ if attrs.respond_to?(:keys)
+ xml.instruct!(target, attrs)
+ elsif attrs.respond_to?(:each)
+ attrs.each { |attr_group| xml.instruct!(target, attr_group) }
+ end
+ end
+ end
+
+ feed_opts = {"xml:lang" => options[:language] || "en-US", "xmlns" => 'http://www.w3.org/2005/Atom'}
+ feed_opts.merge!(options).reject!{|k,v| !k.to_s.match(/^xml/)}
+
+ xml.feed(feed_opts) do
+ xml.id(options[:id] || "tag:#{request.host},#{options[:schema_date]}:#{request.fullpath.split(".")[0]}")
+ xml.link(:rel => 'alternate', :type => 'text/html', :href => options[:root_url] || (request.protocol + request.host_with_port))
+ xml.link(:rel => 'self', :type => 'application/atom+xml', :href => options[:url] || request.url)
+
+ yield AtomFeedBuilder.new(xml, self, options)
+ end
+ end
+
+ class AtomBuilder #:nodoc:
+ XHTML_TAG_NAMES = %w(content rights title subtitle summary).to_set
+
+ def initialize(xml)
+ @xml = xml
+ end
+
+ private
+ # Delegate to xml builder, first wrapping the element in a xhtml
+ # namespaced div element if the method and arguments indicate
+ # that an xhtml_block? is desired.
+ def method_missing(method, *arguments, &block)
+ if xhtml_block?(method, arguments)
+ @xml.__send__(method, *arguments) do
+ @xml.div(:xmlns => 'http://www.w3.org/1999/xhtml') do |xhtml|
+ block.call(xhtml)
+ end
+ end
+ else
+ @xml.__send__(method, *arguments, &block)
+ end
+ end
+
+ # True if the method name matches one of the five elements defined
+ # in the Atom spec as potentially containing XHTML content and
+ # if type: 'xhtml' is, in fact, specified.
+ def xhtml_block?(method, arguments)
+ if XHTML_TAG_NAMES.include?(method.to_s)
+ last = arguments.last
+ last.is_a?(Hash) && last[:type].to_s == 'xhtml'
+ end
+ end
+ end
+
+ class AtomFeedBuilder < AtomBuilder #:nodoc:
+ def initialize(xml, view, feed_options = {})
+ @xml, @view, @feed_options = xml, view, feed_options
+ end
+
+ # Accepts a Date or Time object and inserts it in the proper format. If nil is passed, current time in UTC is used.
+ def updated(date_or_time = nil)
+ @xml.updated((date_or_time || Time.now.utc).xmlschema)
+ end
+
+ # Creates an entry tag for a specific record and prefills the id using class and id.
+ #
+ # Options:
+ #
+ # * <tt>:published</tt>: Time first published. Defaults to the created_at attribute on the record if one such exists.
+ # * <tt>:updated</tt>: Time of update. Defaults to the updated_at attribute on the record if one such exists.
+ # * <tt>:url</tt>: The URL for this entry. Defaults to the polymorphic_url for the record.
+ # * <tt>:id</tt>: The ID for this entry. Defaults to "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}"
+ # * <tt>:type</tt>: The TYPE for this entry. Defaults to "text/html".
+ def entry(record, options = {})
+ @xml.entry do
+ @xml.id(options[:id] || "tag:#{@view.request.host},#{@feed_options[:schema_date]}:#{record.class}/#{record.id}")
+
+ if options[:published] || (record.respond_to?(:created_at) && record.created_at)
+ @xml.published((options[:published] || record.created_at).xmlschema)
+ end
+
+ if options[:updated] || (record.respond_to?(:updated_at) && record.updated_at)
+ @xml.updated((options[:updated] || record.updated_at).xmlschema)
+ end
+
+ type = options.fetch(:type, 'text/html')
+
+ @xml.link(:rel => 'alternate', :type => type, :href => options[:url] || @view.polymorphic_url(record))
+
+ yield AtomBuilder.new(@xml)
+ end
+ end
+ end
+
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/cache_helper.rb b/actionview/lib/action_view/helpers/cache_helper.rb
new file mode 100644
index 0000000000..b3af1d4da4
--- /dev/null
+++ b/actionview/lib/action_view/helpers/cache_helper.rb
@@ -0,0 +1,200 @@
+module ActionView
+ # = Action View Cache Helper
+ module Helpers
+ module CacheHelper
+ # This helper exposes a method for caching fragments of a view
+ # rather than an entire action or page. This technique is useful
+ # caching pieces like menus, lists of new topics, static HTML
+ # fragments, and so on. This method takes a block that contains
+ # the content you wish to cache.
+ #
+ # The best way to use this is by doing key-based cache expiration
+ # on top of a cache store like Memcached that'll automatically
+ # kick out old entries. For more on key-based expiration, see:
+ # http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works
+ #
+ # When using this method, you list the cache dependency as the name of the cache, like so:
+ #
+ # <% cache project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ #
+ # This approach will assume that when a new topic is added, you'll touch
+ # the project. The cache key generated from this call will be something like:
+ #
+ # views/projects/123-20120806214154/7a1156131a6928cb0026877f8b749ac9
+ # ^class ^id ^updated_at ^template tree digest
+ #
+ # The cache is thus automatically bumped whenever the project updated_at is touched.
+ #
+ # If your template cache depends on multiple sources (try to avoid this to keep things simple),
+ # you can name all these dependencies as part of an array:
+ #
+ # <% cache [ project, current_user ] do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ #
+ # This will include both records as part of the cache key and updating either of them will
+ # expire the cache.
+ #
+ # ==== Template digest
+ #
+ # The template digest that's added to the cache key is computed by taking an md5 of the
+ # contents of the entire template file. This ensures that your caches will automatically
+ # expire when you change the template file.
+ #
+ # Note that the md5 is taken of the entire template file, not just what's within the
+ # cache do/end call. So it's possible that changing something outside of that call will
+ # still expire the cache.
+ #
+ # Additionally, the digestor will automatically look through your template file for
+ # explicit and implicit dependencies, and include those as part of the digest.
+ #
+ # The digestor can be bypassed by passing skip_digest: true as an option to the cache call:
+ #
+ # <% cache project, skip_digest: true do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ #
+ # ==== Implicit dependencies
+ #
+ # Most template dependencies can be derived from calls to render in the template itself.
+ # Here are some examples of render calls that Cache Digests knows how to decode:
+ #
+ # render partial: "comments/comment", collection: commentable.comments
+ # render "comments/comments"
+ # render 'comments/comments'
+ # render('comments/comments')
+ #
+ # render "header" => render("comments/header")
+ #
+ # render(@topic) => render("topics/topic")
+ # render(topics) => render("topics/topic")
+ # render(message.topics) => render("topics/topic")
+ #
+ # It's not possible to derive all render calls like that, though. Here are a few examples of things that can't be derived:
+ #
+ # render group_of_attachments
+ # render @project.documents.where(published: true).order('created_at')
+ #
+ # You will have to rewrite those to the explicit form:
+ #
+ # render partial: 'attachments/attachment', collection: group_of_attachments
+ # render partial: 'documents/document', collection: @project.documents.where(published: true).order('created_at')
+ #
+ # === Explicit dependencies
+ #
+ # Some times you'll have template dependencies that can't be derived at all. This is typically
+ # the case when you have template rendering that happens in helpers. Here's an example:
+ #
+ # <%= render_sortable_todolists @project.todolists %>
+ #
+ # You'll need to use a special comment format to call those out:
+ #
+ # <%# Template Dependency: todolists/todolist %>
+ # <%= render_sortable_todolists @project.todolists %>
+ #
+ # The pattern used to match these is /# Template Dependency: ([^ ]+)/, so it's important that you type it out just so.
+ # You can only declare one template dependency per line.
+ #
+ # === External dependencies
+ #
+ # If you use a helper method, for example, inside of a cached block and you then update that helper,
+ # you'll have to bump the cache as well. It doesn't really matter how you do it, but the md5 of the template file
+ # must change. One recommendation is to simply be explicit in a comment, like:
+ #
+ # <%# Helper Dependency Updated: May 6, 2012 at 6pm %>
+ # <%= some_helper_method(person) %>
+ #
+ # Now all you'll have to do is change that timestamp when the helper method changes.
+ def cache(name = {}, options = nil, &block)
+ if controller.perform_caching
+ safe_concat(fragment_for(cache_fragment_name(name, options), options, &block))
+ else
+ yield
+ end
+
+ nil
+ end
+
+ # Cache fragments of a view if +condition+ is true
+ #
+ # <%= cache_if admin?, project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ def cache_if(condition, name = {}, options = nil, &block)
+ if condition
+ cache(name, options, &block)
+ else
+ yield
+ end
+
+ nil
+ end
+
+ # Cache fragments of a view unless +condition+ is true
+ #
+ # <%= cache_unless admin?, project do %>
+ # <b>All the topics on this project</b>
+ # <%= render project.topics %>
+ # <% end %>
+ def cache_unless(condition, name = {}, options = nil, &block)
+ cache_if !condition, name, options, &block
+ end
+
+ # This helper returns the name of a cache key for a given fragment cache
+ # call. By supplying skip_digest: true to cache, the digestion of cache
+ # fragments can be manually bypassed. This is useful when cache fragments
+ # cannot be manually expired unless you know the exact key which is the
+ # case when using memcached.
+ def cache_fragment_name(name = {}, options = nil)
+ skip_digest = options && options[:skip_digest]
+
+ if skip_digest
+ name
+ else
+ fragment_name_with_digest(name)
+ end
+ end
+
+ private
+
+ 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)
+ ]
+ else
+ name
+ end
+ end
+
+ # TODO: Create an object that has caching read/write on it
+ def fragment_for(name = {}, options = nil, &block) #:nodoc:
+ read_fragment_for(name, options) || write_fragment_for(name, options, &block)
+ end
+
+ def read_fragment_for(name, options) #:nodoc:
+ controller.read_fragment(name, options)
+ end
+
+ def write_fragment_for(name, options) #:nodoc:
+ # VIEW TODO: Make #capture usable outside of ERB
+ # This dance is needed because Builder can't use capture
+ pos = output_buffer.length
+ yield
+ output_safe = output_buffer.html_safe?
+ fragment = output_buffer.slice!(pos..-1)
+ if output_safe
+ self.output_buffer = output_buffer.class.new(output_buffer)
+ end
+ controller.write_fragment(name, fragment, options)
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/capture_helper.rb b/actionview/lib/action_view/helpers/capture_helper.rb
new file mode 100644
index 0000000000..5afe435459
--- /dev/null
+++ b/actionview/lib/action_view/helpers/capture_helper.rb
@@ -0,0 +1,216 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ # = Action View Capture Helper
+ module Helpers
+ # CaptureHelper exposes methods to let you extract generated markup which
+ # can be used in other parts of a template or layout file.
+ #
+ # It provides a method to capture blocks into variables through capture and
+ # a way to capture a block of markup for use in a layout through content_for.
+ module CaptureHelper
+ # The capture method allows you to extract part of a template into a
+ # variable. You can then use this variable anywhere in your templates or layout.
+ #
+ # The capture method can be used in ERB templates...
+ #
+ # <% @greeting = capture do %>
+ # Welcome to my shiny new web page! The date and time is
+ # <%= Time.now %>
+ # <% end %>
+ #
+ # ...and Builder (RXML) templates.
+ #
+ # @timestamp = capture do
+ # "The current timestamp is #{Time.now}."
+ # end
+ #
+ # You can then use that variable anywhere else. For example:
+ #
+ # <html>
+ # <head><title><%= @greeting %></title></head>
+ # <body>
+ # <b><%= @greeting %></b>
+ # </body></html>
+ #
+ def capture(*args)
+ value = nil
+ buffer = with_output_buffer { value = yield(*args) }
+ if string = buffer.presence || value and string.is_a?(String)
+ ERB::Util.html_escape string
+ end
+ end
+
+ # Calling content_for stores a block of markup in an identifier for later use.
+ # In order to access this stored content in other templates, helper modules
+ # or the layout, you would pass the identifier as an argument to <tt>content_for</tt>.
+ #
+ # Note: <tt>yield</tt> can still be used to retrieve the stored content, but calling
+ # <tt>yield</tt> doesn't work in helper modules, while <tt>content_for</tt> does.
+ #
+ # <% content_for :not_authorized do %>
+ # alert('You are not authorized to do that!')
+ # <% end %>
+ #
+ # You can then use <tt>content_for :not_authorized</tt> anywhere in your templates.
+ #
+ # <%= content_for :not_authorized if current_user.nil? %>
+ #
+ # This is equivalent to:
+ #
+ # <%= yield :not_authorized if current_user.nil? %>
+ #
+ # <tt>content_for</tt>, however, can also be used in helper modules.
+ #
+ # module StorageHelper
+ # def stored_content
+ # content_for(:storage) || "Your storage is empty"
+ # end
+ # end
+ #
+ # This helper works just like normal helpers.
+ #
+ # <%= stored_content %>
+ #
+ # You can also use the <tt>yield</tt> syntax alongside an existing call to
+ # <tt>yield</tt> in a layout. For example:
+ #
+ # <%# This is the layout %>
+ # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ # <head>
+ # <title>My Website</title>
+ # <%= yield :script %>
+ # </head>
+ # <body>
+ # <%= yield %>
+ # </body>
+ # </html>
+ #
+ # And now, we'll create a view that has a <tt>content_for</tt> call that
+ # creates the <tt>script</tt> identifier.
+ #
+ # <%# This is our view %>
+ # Please login!
+ #
+ # <% content_for :script do %>
+ # <script>alert('You are not authorized to view this page!')</script>
+ # <% end %>
+ #
+ # Then, in another view, you could to do something like this:
+ #
+ # <%= link_to 'Logout', action: 'logout', remote: true %>
+ #
+ # <% content_for :script do %>
+ # <%= javascript_include_tag :defaults %>
+ # <% end %>
+ #
+ # That will place +script+ tags for your default set of JavaScript files on the page;
+ # this technique is useful if you'll only be using these scripts in a few views.
+ #
+ # Note that content_for concatenates (default) the blocks it is given for a particular
+ # identifier in order. For example:
+ #
+ # <% content_for :navigation do %>
+ # <li><%= link_to 'Home', action: 'index' %></li>
+ # <% end %>
+ #
+ # And in other place:
+ #
+ # <% content_for :navigation do %>
+ # <li><%= link_to 'Login', action: 'login' %></li>
+ # <% end %>
+ #
+ # Then, in another template or layout, this code would render both links in order:
+ #
+ # <ul><%= content_for :navigation %></ul>
+ #
+ # If the flush parameter is true content_for replaces the blocks it is given. For example:
+ #
+ # <% content_for :navigation do %>
+ # <li><%= link_to 'Home', action: 'index' %></li>
+ # <% end %>
+ #
+ # <%# Add some other content, or use a different template: %>
+ #
+ # <% content_for :navigation, flush: true do %>
+ # <li><%= link_to 'Login', action: 'login' %></li>
+ # <% end %>
+ #
+ # Then, in another template or layout, this code would render only the last link:
+ #
+ # <ul><%= content_for :navigation %></ul>
+ #
+ # Lastly, simple content can be passed as a parameter:
+ #
+ # <% content_for :script, javascript_include_tag(:defaults) %>
+ #
+ # WARNING: content_for is ignored in caches. So you shouldn't use it for elements that will be fragment cached.
+ def content_for(name, content = nil, options = {}, &block)
+ if content || block_given?
+ if block_given?
+ options = content if content
+ content = capture(&block)
+ end
+ if content
+ options[:flush] ? @view_flow.set(name, content) : @view_flow.append(name, content)
+ end
+ nil
+ else
+ @view_flow.get(name).presence
+ end
+ end
+
+ # The same as +content_for+ but when used with streaming flushes
+ # straight back to the layout. In other words, if you want to
+ # concatenate several times to the same buffer when rendering a given
+ # template, you should use +content_for+, if not, use +provide+ to tell
+ # the layout to stop looking for more contents.
+ def provide(name, content = nil, &block)
+ content = capture(&block) if block_given?
+ result = @view_flow.append!(name, content) if content
+ result unless content
+ end
+
+ # content_for? checks whether any content has been captured yet using `content_for`.
+ # Useful to render parts of your layout differently based on what is in your views.
+ #
+ # <%# This is the layout %>
+ # <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
+ # <head>
+ # <title>My Website</title>
+ # <%= yield :script %>
+ # </head>
+ # <body class="<%= content_for?(:right_col) ? 'two-column' : 'one-column' %>">
+ # <%= yield %>
+ # <%= yield :right_col %>
+ # </body>
+ # </html>
+ def content_for?(name)
+ @view_flow.get(name).present?
+ end
+
+ # Use an alternate output buffer for the duration of the block.
+ # Defaults to a new empty string.
+ def with_output_buffer(buf = nil) #:nodoc:
+ unless buf
+ buf = ActionView::OutputBuffer.new
+ buf.force_encoding(output_buffer.encoding) if output_buffer
+ end
+ self.output_buffer, old_buffer = buf, output_buffer
+ yield
+ output_buffer
+ 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/actionpack/lib/action_view/helpers/controller_helper.rb b/actionview/lib/action_view/helpers/controller_helper.rb
index 74ef25f7c1..74ef25f7c1 100644
--- a/actionpack/lib/action_view/helpers/controller_helper.rb
+++ b/actionview/lib/action_view/helpers/controller_helper.rb
diff --git a/actionpack/lib/action_view/helpers/csrf_helper.rb b/actionview/lib/action_view/helpers/csrf_helper.rb
index eeb0ed94b9..eeb0ed94b9 100644
--- a/actionpack/lib/action_view/helpers/csrf_helper.rb
+++ b/actionview/lib/action_view/helpers/csrf_helper.rb
diff --git a/actionview/lib/action_view/helpers/date_helper.rb b/actionview/lib/action_view/helpers/date_helper.rb
new file mode 100644
index 0000000000..d7e8e99200
--- /dev/null
+++ b/actionview/lib/action_view/helpers/date_helper.rb
@@ -0,0 +1,1075 @@
+require 'date'
+require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/date/conversions'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/object/with_options'
+
+module ActionView
+ module Helpers
+ # = Action View Date Helpers
+ #
+ # The Date Helper primarily creates select/option tags for different kinds of dates and times or date and time
+ # elements. All of the select-type methods share a number of common options that are as follows:
+ #
+ # * <tt>:prefix</tt> - overwrites the default prefix of "date" used for the select names. So specifying "birthday"
+ # would give \birthday[month] instead of \date[month] if passed to the <tt>select_month</tt> method.
+ # * <tt>:include_blank</tt> - set to true if it should be possible to set an empty date.
+ # * <tt>:discard_type</tt> - set to true if you want to discard the type part of the select name. If set to true,
+ # the <tt>select_month</tt> method would use simply "date" (which can be overwritten using <tt>:prefix</tt>) instead
+ # of \date[month].
+ module DateHelper
+ # 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:
+ #
+ # 0 <-> 29 secs # => less than a minute
+ # 30 secs <-> 1 min, 29 secs # => 1 minute
+ # 1 min, 30 secs <-> 44 mins, 29 secs # => [2..44] minutes
+ # 44 mins, 30 secs <-> 89 mins, 29 secs # => about 1 hour
+ # 89 mins, 30 secs <-> 23 hrs, 59 mins, 29 secs # => about [2..24] hours
+ # 23 hrs, 59 mins, 30 secs <-> 41 hrs, 59 mins, 29 secs # => 1 day
+ # 41 hrs, 59 mins, 30 secs <-> 29 days, 23 hrs, 59 mins, 29 secs # => [2..29] days
+ # 29 days, 23 hrs, 59 mins, 30 secs <-> 44 days, 23 hrs, 59 mins, 29 secs # => about 1 month
+ # 44 days, 23 hrs, 59 mins, 30 secs <-> 59 days, 23 hrs, 59 mins, 29 secs # => about 2 months
+ # 59 days, 23 hrs, 59 mins, 30 secs <-> 1 yr minus 1 sec # => [2..12] months
+ # 1 yr <-> 1 yr, 3 months # => about 1 year
+ # 1 yr, 3 months <-> 1 yr, 9 months # => over 1 year
+ # 1 yr, 9 months <-> 2 yr minus 1 sec # => almost 2 years
+ # 2 yrs <-> max time or date # => (same rules as 1 yr)
+ #
+ # With <tt>include_seconds: true</tt> and the difference < 1 minute 29 seconds:
+ # 0-4 secs # => less than 5 seconds
+ # 5-9 secs # => less than 10 seconds
+ # 10-19 secs # => less than 20 seconds
+ # 20-39 secs # => half a minute
+ # 40-59 secs # => less than a minute
+ # 60-89 secs # => 1 minute
+ #
+ # from_time = Time.now
+ # distance_of_time_in_words(from_time, from_time + 50.minutes) # => about 1 hour
+ # distance_of_time_in_words(from_time, 50.minutes.from_now) # => about 1 hour
+ # distance_of_time_in_words(from_time, from_time + 15.seconds) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time + 15.seconds, include_seconds: true) # => less than 20 seconds
+ # distance_of_time_in_words(from_time, 3.years.from_now) # => about 3 years
+ # distance_of_time_in_words(from_time, from_time + 60.hours) # => 3 days
+ # distance_of_time_in_words(from_time, from_time + 45.seconds, include_seconds: true) # => less than a minute
+ # distance_of_time_in_words(from_time, from_time - 45.seconds, include_seconds: true) # => less than a minute
+ # distance_of_time_in_words(from_time, 76.seconds.from_now) # => 1 minute
+ # distance_of_time_in_words(from_time, from_time + 1.year + 3.days) # => about 1 year
+ # distance_of_time_in_words(from_time, from_time + 3.years + 6.months) # => over 3 years
+ # distance_of_time_in_words(from_time, from_time + 4.years + 9.days + 30.minutes + 5.seconds) # => about 4 years
+ #
+ # to_time = Time.now + 6.years + 19.days
+ # distance_of_time_in_words(from_time, to_time, include_seconds: true) # => about 6 years
+ # distance_of_time_in_words(to_time, from_time, include_seconds: true) # => about 6 years
+ # distance_of_time_in_words(Time.now, Time.now) # => less than a minute
+ def distance_of_time_in_words(from_time, to_time = 0, options = {})
+ options = {
+ scope: :'datetime.distance_in_words'
+ }.merge!(options)
+
+ from_time = from_time.to_time if from_time.respond_to?(:to_time)
+ to_time = to_time.to_time if to_time.respond_to?(:to_time)
+ from_time, to_time = to_time, from_time if from_time > to_time
+ distance_in_minutes = ((to_time - from_time)/60.0).round
+ distance_in_seconds = (to_time - from_time).round
+
+ I18n.with_options :locale => options[:locale], :scope => options[:scope] do |locale|
+ case distance_in_minutes
+ when 0..1
+ return distance_in_minutes == 0 ?
+ locale.t(:less_than_x_minutes, :count => 1) :
+ locale.t(:x_minutes, :count => distance_in_minutes) unless options[:include_seconds]
+
+ case distance_in_seconds
+ when 0..4 then locale.t :less_than_x_seconds, :count => 5
+ when 5..9 then locale.t :less_than_x_seconds, :count => 10
+ when 10..19 then locale.t :less_than_x_seconds, :count => 20
+ when 20..39 then locale.t :half_a_minute
+ when 40..59 then locale.t :less_than_x_minutes, :count => 1
+ else locale.t :x_minutes, :count => 1
+ end
+
+ when 2...45 then locale.t :x_minutes, :count => distance_in_minutes
+ when 45...90 then locale.t :about_x_hours, :count => 1
+ # 90 mins up to 24 hours
+ when 90...1440 then locale.t :about_x_hours, :count => (distance_in_minutes.to_f / 60.0).round
+ # 24 hours up to 42 hours
+ when 1440...2520 then locale.t :x_days, :count => 1
+ # 42 hours up to 30 days
+ when 2520...43200 then locale.t :x_days, :count => (distance_in_minutes.to_f / 1440.0).round
+ # 30 days up to 60 days
+ when 43200...86400 then locale.t :about_x_months, :count => (distance_in_minutes.to_f / 43200.0).round
+ # 60 days up to 365 days
+ when 86400...525600 then locale.t :x_months, :count => (distance_in_minutes.to_f / 43200.0).round
+ else
+ if from_time.acts_like?(:time) && to_time.acts_like?(:time)
+ fyear = from_time.year
+ fyear += 1 if from_time.month >= 3
+ tyear = to_time.year
+ tyear -= 1 if to_time.month < 3
+ leap_years = (fyear > tyear) ? 0 : (fyear..tyear).count{|x| Date.leap?(x)}
+ minute_offset_for_leap_year = leap_years * 1440
+ # Discount the leap year days when calculating year distance.
+ # e.g. if there are 20 leap year days between 2 dates having the same day
+ # and month then the based on 365 days calculation
+ # the distance in years will come out to over 80 years when in written
+ # english it would read better as about 80 years.
+ minutes_with_offset = distance_in_minutes - minute_offset_for_leap_year
+ else
+ minutes_with_offset = distance_in_minutes
+ end
+ remainder = (minutes_with_offset % 525600)
+ distance_in_years = (minutes_with_offset.div 525600)
+ if remainder < 131400
+ locale.t(:about_x_years, :count => distance_in_years)
+ elsif remainder < 394200
+ locale.t(:over_x_years, :count => distance_in_years)
+ else
+ locale.t(:almost_x_years, :count => distance_in_years + 1)
+ end
+ end
+ end
+ end
+
+ # Like <tt>distance_of_time_in_words</tt>, but where <tt>to_time</tt> is fixed to <tt>Time.now</tt>.
+ #
+ # time_ago_in_words(3.minutes.from_now) # => 3 minutes
+ # time_ago_in_words(3.minutes.ago) # => 3 minutes
+ # time_ago_in_words(Time.now - 15.hours) # => about 15 hours
+ # time_ago_in_words(Time.now) # => less than a minute
+ # time_ago_in_words(Time.now, include_seconds: true) # => less than 5 seconds
+ #
+ # from_time = Time.now - 3.days - 14.minutes - 25.seconds
+ # time_ago_in_words(from_time) # => 3 days
+ #
+ # from_time = (3.days + 14.minutes + 25.seconds).ago
+ # time_ago_in_words(from_time) # => 3 days
+ #
+ # 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)
+ end
+
+ alias_method :distance_of_time_in_words_to_now, :time_ago_in_words
+
+ # Returns a set of select tags (one for year, month, and day) pre-selected for accessing a specified date-based
+ # attribute (identified by +method+) on an object assigned to the template (identified by +object+).
+ #
+ # ==== Options
+ # * <tt>:use_month_numbers</tt> - Set to true if you want to use month numbers rather than month names (e.g.
+ # "2" instead of "February").
+ # * <tt>:use_two_digit_numbers</tt> - Set to true if you want to display two digit month and day numbers (e.g.
+ # "02" instead of "February" and "08" instead of "8").
+ # * <tt>:use_short_month</tt> - Set to true if you want to use abbreviated month names instead of full
+ # month names (e.g. "Feb" instead of "February").
+ # * <tt>:add_month_numbers</tt> - Set to true if you want to use both month numbers and month names (e.g.
+ # "2 - February" instead of "February").
+ # * <tt>:use_month_names</tt> - Set to an array with 12 month names if you want to customize month names.
+ # Note: You can also use Rails' i18n functionality for this.
+ # * <tt>:date_separator</tt> - Specifies a string to separate the date fields. Default is "" (i.e. nothing).
+ # * <tt>:start_year</tt> - Set the start year for the year select. Default is <tt>Time.now.year - 5</tt>.
+ # * <tt>:end_year</tt> - Set the end year for the year select. Default is <tt>Time.now.year + 5</tt>.
+ # * <tt>:discard_day</tt> - Set to true if you don't want to show a day select. This includes the day
+ # as a hidden field instead of showing a select field. Also note that this implicitly sets the day to be the
+ # first of the given month in order to not create invalid dates like 31 February.
+ # * <tt>:discard_month</tt> - Set to true if you don't want to show a month select. This includes the month
+ # as a hidden field instead of showing a select field. Also note that this implicitly sets :discard_day to true.
+ # * <tt>:discard_year</tt> - Set to true if you don't want to show a year select. This includes the year
+ # as a hidden field instead of showing a select field.
+ # * <tt>:order</tt> - Set to an array containing <tt>:day</tt>, <tt>:month</tt> and <tt>:year</tt> to
+ # customize the order in which the select fields are shown. If you leave out any of the symbols, the respective
+ # select will not be shown (like when you set <tt>discard_xxx: true</tt>. Defaults to the order defined in
+ # the respective locale (e.g. [:year, :month, :day] in the en locale that ships with Rails).
+ # * <tt>:include_blank</tt> - Include a blank option in every select field so it's possible to set empty
+ # dates.
+ # * <tt>:default</tt> - Set a default date if the affected date isn't set or is nil.
+ # * <tt>:selected</tt> - Set a date that overrides the actual value.
+ # * <tt>:disabled</tt> - Set to true if you want show the select fields as disabled.
+ # * <tt>:prompt</tt> - Set to true (for a generic prompt), a prompt string or a hash of prompt strings
+ # for <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>, <tt>:minute</tt> and <tt>:second</tt>.
+ # Setting this option prepends a select option with a generic prompt (Day, Month, Year, Hour, Minute, Seconds)
+ # or the given prompt string.
+ # * <tt>:with_css_classes</tt> - Set to true if you want assign different styles for 'select' tags. This option
+ # automatically set classes 'year', 'month', 'day', 'hour', 'minute' and 'second' for your 'select' tags.
+ #
+ # If anything is passed in the +html_options+ hash it will be applied to every select tag in the set.
+ #
+ # NOTE: Discarded selects will default to 1. So if no month select is available, January will be assumed.
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute.
+ # date_select("article", "written_on")
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
+ # # with the year in the year drop down box starting at 1995.
+ # date_select("article", "written_on", start_year: 1995)
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
+ # # with the year in the year drop down box starting at 1995, numbers used for months instead of words,
+ # # and without a day select box.
+ # date_select("article", "written_on", start_year: 1995, use_month_numbers: true,
+ # discard_day: true, include_blank: true)
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute,
+ # # with two digit numbers used for months and days.
+ # date_select("article", "written_on", use_two_digit_numbers: true)
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
+ # # with the fields ordered as day, month, year rather than month, day, year.
+ # date_select("article", "written_on", order: [:day, :month, :year])
+ #
+ # # Generates a date select that when POSTed is stored in the user variable, in the birthday attribute
+ # # lacking a year field.
+ # date_select("user", "birthday", order: [:month, :day])
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
+ # # which is initially set to the date 3 days from the current date
+ # date_select("article", "written_on", default: 3.days.from_now)
+ #
+ # # Generates a date select that when POSTed is stored in the article variable, in the written_on attribute
+ # # which is set in the form with todays date, regardless of the value in the Active Record object.
+ # date_select("article", "written_on", selected: Date.today)
+ #
+ # # Generates a date select that when POSTed is stored in the credit_card variable, in the bill_due attribute
+ # # that will have a default day of 20.
+ # date_select("credit_card", "bill_due", default: { day: 20 })
+ #
+ # # Generates a date select with custom prompts.
+ # date_select("article", "written_on", prompt: { day: 'Select day', month: 'Select month', year: 'Select year' })
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ #
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
+ # all month choices are valid.
+ def date_select(object_name, method, options = {}, html_options = {})
+ Tags::DateSelect.new(object_name, method, self, options, html_options).render
+ end
+
+ # Returns a set of select tags (one for hour, minute and optionally second) pre-selected for accessing a
+ # specified time-based attribute (identified by +method+) on an object assigned to the template (identified by
+ # +object+). You can include the seconds with <tt>:include_seconds</tt>. You can get hours in the AM/PM format
+ # with <tt>:ampm</tt> option.
+ #
+ # This method will also generate 3 input hidden tags, for the actual year, month and day unless the option
+ # <tt>:ignore_date</tt> is set to +true+. If you set the <tt>:ignore_date</tt> to +true+, you must have a
+ # +date_select+ on the same method within the form otherwise an exception will be raised.
+ #
+ # If anything is passed in the html_options hash it will be applied to every select tag in the set.
+ #
+ # # Creates a time select tag that, when POSTed, will be stored in the article variable in the sunrise attribute.
+ # time_select("article", "sunrise")
+ #
+ # # Creates a time select tag with a seconds field that, when POSTed, will be stored in the article variables in
+ # # the sunrise attribute.
+ # time_select("article", "start_time", include_seconds: true)
+ #
+ # # You can set the <tt>:minute_step</tt> to 15 which will give you: 00, 15, 30 and 45.
+ # time_select 'game', 'game_time', {minute_step: 15}
+ #
+ # # Creates a time select tag with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
+ # time_select("article", "written_on", prompt: {hour: 'Choose hour', minute: 'Choose minute', second: 'Choose seconds'})
+ # time_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # time_select("article", "written_on", prompt: true) # generic prompts for all
+ #
+ # # You can set :ampm option to true which will show the hours as: 12 PM, 01 AM .. 11 PM.
+ # time_select 'game', 'game_time', {ampm: true}
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ #
+ # Note: If the day is not included as an option but the month is, the day will be set to the 1st to ensure that
+ # all month choices are valid.
+ def time_select(object_name, method, options = {}, html_options = {})
+ Tags::TimeSelect.new(object_name, method, self, options, html_options).render
+ end
+
+ # Returns a set of select tags (one for year, month, day, hour, and minute) pre-selected for accessing a
+ # specified datetime-based attribute (identified by +method+) on an object assigned to the template (identified
+ # by +object+).
+ #
+ # If anything is passed in the html_options hash it will be applied to every select tag in the set.
+ #
+ # # Generates a datetime select that, when POSTed, will be stored in the article variable in the written_on
+ # # attribute.
+ # datetime_select("article", "written_on")
+ #
+ # # Generates a datetime select with a year select that starts at 1995 that, when POSTed, will be stored in the
+ # # article variable in the written_on attribute.
+ # datetime_select("article", "written_on", start_year: 1995)
+ #
+ # # Generates a datetime select with a default value of 3 days from the current time that, when POSTed, will
+ # # be stored in the trip variable in the departing attribute.
+ # datetime_select("trip", "departing", default: 3.days.from_now)
+ #
+ # # Generate a datetime select with hours in the AM/PM format
+ # datetime_select("article", "written_on", ampm: true)
+ #
+ # # Generates a datetime select that discards the type that, when POSTed, will be stored in the article variable
+ # # as the written_on attribute.
+ # datetime_select("article", "written_on", discard_type: true)
+ #
+ # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
+ # datetime_select("article", "written_on", prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
+ # datetime_select("article", "written_on", prompt: {hour: true}) # generic prompt for hours
+ # datetime_select("article", "written_on", prompt: true) # generic prompts for all
+ #
+ # The selects are prepared for multi-parameter assignment to an Active Record object.
+ def datetime_select(object_name, method, options = {}, html_options = {})
+ Tags::DatetimeSelect.new(object_name, method, self, options, html_options).render
+ end
+
+ # Returns a set of html select-tags (one for year, month, day, hour, minute, and second) pre-selected with the
+ # +datetime+. It's also possible to explicitly set the order of the tags using the <tt>:order</tt> option with
+ # an array of symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order. If you do not
+ # supply a Symbol, it will be appended onto the <tt>:order</tt> passed in. You can also add
+ # <tt>:date_separator</tt>, <tt>:datetime_separator</tt> and <tt>:time_separator</tt> keys to the +options+ to
+ # control visual display of the elements.
+ #
+ # If anything is passed in the html_options hash it will be applied to every select tag in the set.
+ #
+ # my_date_time = Time.now + 4.days
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today).
+ # select_datetime(my_date_time)
+ #
+ # # Generates a datetime select that defaults to today (no specified datetime)
+ # select_datetime()
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with the fields ordered year, month, day rather than month, day, year.
+ # select_datetime(my_date_time, order: [:year, :month, :day])
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with a '/' between each date field.
+ # select_datetime(my_date_time, date_separator: '/')
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # with a date fields separated by '/', time fields separated by '' and the date and time fields
+ # # separated by a comma (',').
+ # select_datetime(my_date_time, date_separator: '/', time_separator: '', datetime_separator: ',')
+ #
+ # # Generates a datetime select that discards the type of the field and defaults to the datetime in
+ # # my_date_time (four days after today)
+ # select_datetime(my_date_time, discard_type: true)
+ #
+ # # Generate a datetime field with hours in the AM/PM format
+ # select_datetime(my_date_time, ampm: true)
+ #
+ # # Generates a datetime select that defaults to the datetime in my_date_time (four days after today)
+ # # prefixed with 'payday' rather than 'date'
+ # select_datetime(my_date_time, prefix: 'payday')
+ #
+ # # Generates a datetime select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
+ # select_datetime(my_date_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
+ # select_datetime(my_date_time, prompt: {hour: true}) # generic prompt for hours
+ # select_datetime(my_date_time, prompt: true) # generic prompts for all
+ def select_datetime(datetime = Time.current, options = {}, html_options = {})
+ DateTimeSelector.new(datetime, options, html_options).select_datetime
+ end
+
+ # Returns a set of html select-tags (one for year, month, and day) pre-selected with the +date+.
+ # It's possible to explicitly set the order of the tags using the <tt>:order</tt> option with an array of
+ # symbols <tt>:year</tt>, <tt>:month</tt> and <tt>:day</tt> in the desired order.
+ # If the array passed to the <tt>:order</tt> option does not contain all the three symbols, all tags will be hidden.
+ #
+ # If anything is passed in the html_options hash it will be applied to every select tag in the set.
+ #
+ # my_date = Time.now + 6.days
+ #
+ # # Generates a date select that defaults to the date in my_date (six days after today).
+ # select_date(my_date)
+ #
+ # # Generates a date select that defaults to today (no specified date).
+ # select_date()
+ #
+ # # Generates a date select that defaults to the date in my_date (six days after today)
+ # # with the fields ordered year, month, day rather than month, day, year.
+ # select_date(my_date, order: [:year, :month, :day])
+ #
+ # # Generates a date select that discards the type of the field and defaults to the date in
+ # # my_date (six days after today).
+ # select_date(my_date, discard_type: true)
+ #
+ # # Generates a date select that defaults to the date in my_date,
+ # # which has fields separated by '/'.
+ # select_date(my_date, date_separator: '/')
+ #
+ # # Generates a date select that defaults to the datetime in my_date (six days after today)
+ # # prefixed with 'payday' rather than 'date'.
+ # select_date(my_date, prefix: 'payday')
+ #
+ # # Generates a date select with a custom prompt. Use <tt>prompt: true</tt> for generic prompts.
+ # select_date(my_date, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
+ # select_date(my_date, prompt: {hour: true}) # generic prompt for hours
+ # select_date(my_date, prompt: true) # generic prompts for all
+ def select_date(date = Date.current, options = {}, html_options = {})
+ DateTimeSelector.new(date, options, html_options).select_date
+ end
+
+ # Returns a set of html select-tags (one for hour and minute).
+ # You can set <tt>:time_separator</tt> key to format the output, and
+ # the <tt>:include_seconds</tt> option to include an input for seconds.
+ #
+ # If anything is passed in the html_options hash it will be applied to every select tag in the set.
+ #
+ # my_time = Time.now + 5.days + 7.hours + 3.minutes + 14.seconds
+ #
+ # # Generates a time select that defaults to the time in my_time.
+ # select_time(my_time)
+ #
+ # # Generates a time select that defaults to the current time (no specified time).
+ # select_time()
+ #
+ # # Generates a time select that defaults to the time in my_time,
+ # # which has fields separated by ':'.
+ # select_time(my_time, time_separator: ':')
+ #
+ # # Generates a time select that defaults to the time in my_time,
+ # # that also includes an input for seconds.
+ # select_time(my_time, include_seconds: true)
+ #
+ # # Generates a time select that defaults to the time in my_time, that has fields
+ # # separated by ':' and includes an input for seconds.
+ # select_time(my_time, time_separator: ':', include_seconds: true)
+ #
+ # # Generate a time select field with hours in the AM/PM format
+ # select_time(my_time, ampm: true)
+ #
+ # # Generates a time select field with hours that range from 2 to 14
+ # select_time(my_time, start_hour: 2, end_hour: 14)
+ #
+ # # Generates a time select with a custom prompt. Use <tt>:prompt</tt> to true for generic prompts.
+ # select_time(my_time, prompt: {day: 'Choose day', month: 'Choose month', year: 'Choose year'})
+ # select_time(my_time, prompt: {hour: true}) # generic prompt for hours
+ # select_time(my_time, prompt: true) # generic prompts for all
+ def select_time(datetime = Time.current, options = {}, html_options = {})
+ DateTimeSelector.new(datetime, options, html_options).select_time
+ end
+
+ # Returns a select tag with options for each of the seconds 0 through 59 with the current second selected.
+ # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
+ # Override the field name using the <tt>:field_name</tt> option, 'second' by default.
+ #
+ # my_time = Time.now + 16.minutes
+ #
+ # # Generates a select field for seconds that defaults to the seconds for the time in my_time.
+ # select_second(my_time)
+ #
+ # # Generates a select field for seconds that defaults to the number given.
+ # select_second(33)
+ #
+ # # Generates a select field for seconds that defaults to the seconds for the time in my_time
+ # # that is named 'interval' rather than 'second'.
+ # select_second(my_time, field_name: 'interval')
+ #
+ # # Generates a select field for seconds with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_second(14, prompt: 'Choose seconds')
+ def select_second(datetime, options = {}, html_options = {})
+ DateTimeSelector.new(datetime, options, html_options).select_second
+ end
+
+ # Returns a select tag with options for each of the minutes 0 through 59 with the current minute selected.
+ # Also can return a select tag with options by <tt>minute_step</tt> from 0 through 59 with the 00 minute
+ # selected. The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
+ # Override the field name using the <tt>:field_name</tt> option, 'minute' by default.
+ #
+ # my_time = Time.now + 6.hours
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time.
+ # select_minute(my_time)
+ #
+ # # Generates a select field for minutes that defaults to the number given.
+ # select_minute(14)
+ #
+ # # Generates a select field for minutes that defaults to the minutes for the time in my_time
+ # # that is named 'moment' rather than 'minute'.
+ # select_minute(my_time, field_name: 'moment')
+ #
+ # # Generates a select field for minutes with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_minute(14, prompt: 'Choose minutes')
+ def select_minute(datetime, options = {}, html_options = {})
+ DateTimeSelector.new(datetime, options, html_options).select_minute
+ end
+
+ # Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
+ # The <tt>datetime</tt> can be either a +Time+ or +DateTime+ object or an integer.
+ # Override the field name using the <tt>:field_name</tt> option, 'hour' by default.
+ #
+ # my_time = Time.now + 6.hours
+ #
+ # # Generates a select field for hours that defaults to the hour for the time in my_time.
+ # select_hour(my_time)
+ #
+ # # Generates a select field for hours that defaults to the number given.
+ # select_hour(13)
+ #
+ # # Generates a select field for hours that defaults to the hour for the time in my_time
+ # # that is named 'stride' rather than 'hour'.
+ # select_hour(my_time, field_name: 'stride')
+ #
+ # # Generates a select field for hours with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_hour(13, prompt: 'Choose hour')
+ #
+ # # Generate a select field for hours in the AM/PM format
+ # select_hour(my_time, ampm: true)
+ #
+ # # Generates a select field that includes options for hours from 2 to 14.
+ # select_hour(my_time, start_hour: 2, end_hour: 14)
+ def select_hour(datetime, options = {}, html_options = {})
+ DateTimeSelector.new(datetime, options, html_options).select_hour
+ end
+
+ # Returns a select tag with options for each of the days 1 through 31 with the current day selected.
+ # The <tt>date</tt> can also be substituted for a day number.
+ # If you want to display days with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
+ # Override the field name using the <tt>:field_name</tt> option, 'day' by default.
+ #
+ # my_date = Time.now + 2.days
+ #
+ # # Generates a select field for days that defaults to the day for the date in my_date.
+ # select_day(my_date)
+ #
+ # # Generates a select field for days that defaults to the number given.
+ # select_day(5)
+ #
+ # # Generates a select field for days that defaults to the number given, but displays it with two digits.
+ # select_day(5, use_two_digit_numbers: true)
+ #
+ # # Generates a select field for days that defaults to the day for the date in my_date
+ # # that is named 'due' rather than 'day'.
+ # select_day(my_date, field_name: 'due')
+ #
+ # # Generates a select field for days with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_day(5, prompt: 'Choose day')
+ def select_day(date, options = {}, html_options = {})
+ DateTimeSelector.new(date, options, html_options).select_day
+ end
+
+ # Returns a select tag with options for each of the months January through December with the current month
+ # selected. The month names are presented as keys (what's shown to the user) and the month numbers (1-12) are
+ # used as values (what's submitted to the server). It's also possible to use month numbers for the presentation
+ # instead of names -- set the <tt>:use_month_numbers</tt> key in +options+ to true for this to happen. If you
+ # want both numbers and names, set the <tt>:add_month_numbers</tt> key in +options+ to true. If you would prefer
+ # to show month names as abbreviations, set the <tt>:use_short_month</tt> key in +options+ to true. If you want
+ # to use your own month names, set the <tt>:use_month_names</tt> key in +options+ to an array of 12 month names.
+ # If you want to display months with a leading zero set the <tt>:use_two_digit_numbers</tt> key in +options+ to true.
+ # Override the field name using the <tt>:field_name</tt> option, 'month' by default.
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "January", "March".
+ # select_month(Date.today)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # is named "start" rather than "month".
+ # select_month(Date.today, field_name: 'start')
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "1", "3".
+ # select_month(Date.today, use_month_numbers: true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "1 - January", "3 - March".
+ # select_month(Date.today, add_month_numbers: true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "Jan", "Mar".
+ # select_month(Date.today, use_short_month: true)
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys like "Januar", "Marts."
+ # select_month(Date.today, use_month_names: %w(Januar Februar Marts ...))
+ #
+ # # Generates a select field for months that defaults to the current month that
+ # # will use keys with two digit numbers like "01", "03".
+ # select_month(Date.today, use_two_digit_numbers: true)
+ #
+ # # Generates a select field for months with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_month(14, prompt: 'Choose month')
+ def select_month(date, options = {}, html_options = {})
+ DateTimeSelector.new(date, options, html_options).select_month
+ end
+
+ # Returns a select tag with options for each of the five years on each side of the current, which is selected.
+ # The five year radius can be changed using the <tt>:start_year</tt> and <tt>:end_year</tt> keys in the
+ # +options+. Both ascending and descending year lists are supported by making <tt>:start_year</tt> less than or
+ # greater than <tt>:end_year</tt>. The <tt>date</tt> can also be substituted for a year given as a number.
+ # Override the field name using the <tt>:field_name</tt> option, 'year' by default.
+ #
+ # # Generates a select field for years that defaults to the current year that
+ # # has ascending year values.
+ # select_year(Date.today, start_year: 1992, end_year: 2007)
+ #
+ # # Generates a select field for years that defaults to the current year that
+ # # is named 'birth' rather than 'year'.
+ # select_year(Date.today, field_name: 'birth')
+ #
+ # # Generates a select field for years that defaults to the current year that
+ # # has descending year values.
+ # select_year(Date.today, start_year: 2005, end_year: 1900)
+ #
+ # # Generates a select field for years that defaults to the year 2006 that
+ # # has ascending year values.
+ # select_year(2006, start_year: 2000, end_year: 2010)
+ #
+ # # Generates a select field for years with a custom prompt. Use <tt>prompt: true</tt> for a
+ # # generic prompt.
+ # select_year(14, prompt: 'Choose year')
+ def select_year(date, options = {}, html_options = {})
+ DateTimeSelector.new(date, options, html_options).select_year
+ end
+
+ # Returns an html time tag for the given date or time.
+ #
+ # time_tag Date.today # =>
+ # <time datetime="2010-11-04">November 04, 2010</time>
+ # time_tag Time.now # =>
+ # <time datetime="2010-11-04T17:55:45+01:00">November 04, 2010 17:55</time>
+ # time_tag Date.yesterday, 'Yesterday' # =>
+ # <time datetime="2010-11-03">Yesterday</time>
+ # time_tag Date.today, pubdate: true # =>
+ # <time datetime="2010-11-04" pubdate="pubdate">November 04, 2010</time>
+ # time_tag Date.today, datetime: Date.today.strftime('%G-W%V') # =>
+ # <time datetime="2010-W44">November 04, 2010</time>
+ #
+ # <%= time_tag Time.now do %>
+ # <span>Right now</span>
+ # <% end %>
+ # # => <time datetime="2010-11-04T17:55:45+01:00"><span>Right now</span></time>
+ def time_tag(date_or_time, *args, &block)
+ options = args.extract_options!
+ format = options.delete(:format) || :long
+ content = args.first || I18n.l(date_or_time, :format => format)
+ datetime = date_or_time.acts_like?(:time) ? date_or_time.xmlschema : date_or_time.iso8601
+
+ content_tag(:time, content, options.reverse_merge(:datetime => datetime), &block)
+ end
+ end
+
+ class DateTimeSelector #:nodoc:
+ include ActionView::Helpers::TagHelper
+
+ DEFAULT_PREFIX = 'date'.freeze
+ POSITION = {
+ :year => 1, :month => 2, :day => 3, :hour => 4, :minute => 5, :second => 6
+ }.freeze
+
+ AMPM_TRANSLATION = Hash[
+ [[0, "12 AM"], [1, "01 AM"], [2, "02 AM"], [3, "03 AM"],
+ [4, "04 AM"], [5, "05 AM"], [6, "06 AM"], [7, "07 AM"],
+ [8, "08 AM"], [9, "09 AM"], [10, "10 AM"], [11, "11 AM"],
+ [12, "12 PM"], [13, "01 PM"], [14, "02 PM"], [15, "03 PM"],
+ [16, "04 PM"], [17, "05 PM"], [18, "06 PM"], [19, "07 PM"],
+ [20, "08 PM"], [21, "09 PM"], [22, "10 PM"], [23, "11 PM"]]
+ ].freeze
+
+ def initialize(datetime, options = {}, html_options = {})
+ @options = options.dup
+ @html_options = html_options.dup
+ @datetime = datetime
+ @options[:datetime_separator] ||= ' &mdash; '
+ @options[:time_separator] ||= ' : '
+ end
+
+ def select_datetime
+ order = date_order.dup
+ order -= [:hour, :minute, :second]
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+ @options[:discard_minute] ||= true if @options[:discard_hour]
+ @options[:discard_second] ||= true unless @options[:include_seconds] && !@options[:discard_minute]
+
+ set_day_if_discarded
+
+ if @options[:tag] && @options[:ignore_date]
+ select_time
+ else
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+ order += [:hour, :minute, :second] unless @options[:discard_hour]
+
+ build_selects_from_types(order)
+ end
+ end
+
+ def select_date
+ order = date_order.dup
+
+ @options[:discard_hour] = true
+ @options[:discard_minute] = true
+ @options[:discard_second] = true
+
+ @options[:discard_year] ||= true unless order.include?(:year)
+ @options[:discard_month] ||= true unless order.include?(:month)
+ @options[:discard_day] ||= true if @options[:discard_month] || !order.include?(:day)
+
+ set_day_if_discarded
+
+ [:day, :month, :year].each { |o| order.unshift(o) unless order.include?(o) }
+
+ build_selects_from_types(order)
+ end
+
+ def select_time
+ order = []
+
+ @options[:discard_month] = true
+ @options[:discard_year] = true
+ @options[:discard_day] = true
+ @options[:discard_second] ||= true unless @options[:include_seconds]
+
+ order += [:year, :month, :day] unless @options[:ignore_date]
+
+ order += [:hour, :minute]
+ order << :second if @options[:include_seconds]
+
+ build_selects_from_types(order)
+ end
+
+ def select_second
+ if @options[:use_hidden] || @options[:discard_second]
+ build_hidden(:second, sec) if @options[:include_seconds]
+ else
+ build_options_and_select(:second, sec)
+ end
+ end
+
+ def select_minute
+ if @options[:use_hidden] || @options[:discard_minute]
+ build_hidden(:minute, min)
+ else
+ build_options_and_select(:minute, min, :step => @options[:minute_step])
+ end
+ end
+
+ def select_hour
+ if @options[:use_hidden] || @options[:discard_hour]
+ build_hidden(:hour, hour)
+ else
+ options = {}
+ options[:ampm] = @options[:ampm] || false
+ options[:start] = @options[:start_hour] || 0
+ options[:end] = @options[:end_hour] || 23
+ build_options_and_select(:hour, hour, options)
+ end
+ end
+
+ def select_day
+ if @options[:use_hidden] || @options[:discard_day]
+ build_hidden(:day, day || 1)
+ else
+ build_options_and_select(:day, day, :start => 1, :end => 31, :leading_zeros => false, :use_two_digit_numbers => @options[:use_two_digit_numbers])
+ end
+ end
+
+ def select_month
+ if @options[:use_hidden] || @options[:discard_month]
+ build_hidden(:month, month || 1)
+ else
+ month_options = []
+ 1.upto(12) do |month_number|
+ options = { :value => month_number }
+ options[:selected] = "selected" if month == month_number
+ month_options << content_tag(:option, month_name(month_number), options) + "\n"
+ end
+ build_select(:month, month_options.join)
+ end
+ end
+
+ def select_year
+ if !@datetime || @datetime == 0
+ val = '1'
+ middle_year = Date.today.year
+ else
+ val = middle_year = year
+ end
+
+ if @options[:use_hidden] || @options[:discard_year]
+ build_hidden(:year, val)
+ else
+ options = {}
+ options[:start] = @options[:start_year] || middle_year - 5
+ options[:end] = @options[:end_year] || middle_year + 5
+ options[:step] = options[:start] < options[:end] ? 1 : -1
+ options[:leading_zeros] = false
+ options[:max_years_allowed] = @options[:max_years_allowed] || 1000
+
+ if (options[:end] - options[:start]).abs > options[:max_years_allowed]
+ raise ArgumentError, "There are too many years options to be built. Are you sure you haven't mistyped something? You can provide the :max_years_allowed parameter."
+ end
+
+ build_options_and_select(:year, val, options)
+ end
+ end
+
+ private
+ %w( sec min hour day month year ).each do |method|
+ define_method(method) do
+ @datetime.kind_of?(Numeric) ? @datetime : @datetime.send(method) if @datetime
+ end
+ end
+
+ # If the day is hidden, the day should be set to the 1st so all month and year choices are
+ # valid. Otherwise, February 31st or February 29th, 2011 can be selected, which are invalid.
+ def set_day_if_discarded
+ if @datetime && @options[:discard_day]
+ @datetime = @datetime.change(:day => 1)
+ end
+ end
+
+ # Returns translated month names, but also ensures that a custom month
+ # name array has a leading nil element.
+ def month_names
+ @month_names ||= begin
+ month_names = @options[:use_month_names] || translated_month_names
+ month_names.unshift(nil) if month_names.size < 13
+ month_names
+ end
+ end
+
+ # Returns translated month names.
+ # => [nil, "January", "February", "March",
+ # "April", "May", "June", "July",
+ # "August", "September", "October",
+ # "November", "December"]
+ #
+ # If <tt>:use_short_month</tt> option is set
+ # => [nil, "Jan", "Feb", "Mar", "Apr", "May", "Jun",
+ # "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"]
+ def translated_month_names
+ key = @options[:use_short_month] ? :'date.abbr_month_names' : :'date.month_names'
+ I18n.translate(key, :locale => @options[:locale])
+ end
+
+ # Lookup month name for number.
+ # month_name(1) => "January"
+ #
+ # If <tt>:use_month_numbers</tt> option is passed
+ # month_name(1) => 1
+ #
+ # If <tt>:use_two_month_numbers</tt> option is passed
+ # month_name(1) => '01'
+ #
+ # If <tt>:add_month_numbers</tt> option is passed
+ # month_name(1) => "1 - January"
+ def month_name(number)
+ if @options[:use_month_numbers]
+ number
+ elsif @options[:use_two_digit_numbers]
+ sprintf "%02d", number
+ elsif @options[:add_month_numbers]
+ "#{number} - #{month_names[number]}"
+ else
+ month_names[number]
+ end
+ end
+
+ def date_order
+ @date_order ||= @options[:order] || translated_date_order
+ end
+
+ def translated_date_order
+ date_order = I18n.translate(:'date.order', :locale => @options[:locale], :default => [])
+ date_order = date_order.map { |element| element.to_sym }
+
+ forbidden_elements = date_order - [:year, :month, :day]
+ if forbidden_elements.any?
+ raise StandardError,
+ "#{@options[:locale]}.date.order only accepts :year, :month and :day"
+ end
+
+ date_order
+ end
+
+ # Build full select tag from date type and options.
+ def build_options_and_select(type, selected, options = {})
+ build_select(type, build_options(selected, options))
+ end
+
+ # Build select option html from date value and options.
+ # build_options(15, start: 1, end: 31)
+ # => "<option value="1">1</option>
+ # <option value="2">2</option>
+ # <option value="3">3</option>..."
+ #
+ # If <tt>use_two_digit_numbers: true</tt> option is passed
+ # build_options(15, start: 1, end: 31, use_two_digit_numbers: true)
+ # => "<option value="1">01</option>
+ # <option value="2">02</option>
+ # <option value="3">03</option>..."
+ #
+ # If <tt>:step</tt> options is passed
+ # build_options(15, start: 1, end: 31, step: 2)
+ # => "<option value="1">1</option>
+ # <option value="3">3</option>
+ # <option value="5">5</option>..."
+ def build_options(selected, options = {})
+ options = {
+ leading_zeros: true, ampm: false, use_two_digit_numbers: false
+ }.merge!(options)
+
+ start = options.delete(:start) || 0
+ stop = options.delete(:end) || 59
+ step = options.delete(:step) || 1
+ leading_zeros = options.delete(:leading_zeros)
+
+ select_options = []
+ start.step(stop, step) do |i|
+ value = leading_zeros ? sprintf("%02d", i) : i
+ tag_options = { :value => value }
+ tag_options[:selected] = "selected" if selected == i
+ text = options[:use_two_digit_numbers] ? sprintf("%02d", i) : value
+ text = options[:ampm] ? AMPM_TRANSLATION[i] : text
+ select_options << content_tag(:option, text, tag_options)
+ end
+
+ (select_options.join("\n") + "\n").html_safe
+ end
+
+ # Builds select tag from date type and html select options.
+ # build_select(:month, "<option value="1">January</option>...")
+ # => "<select id="post_written_on_2i" name="post[written_on(2i)]">
+ # <option value="1">January</option>...
+ # </select>"
+ def build_select(type, select_options_as_html)
+ select_options = {
+ :id => input_id_from_type(type),
+ :name => input_name_from_type(type)
+ }.merge!(@html_options)
+ select_options[:disabled] = 'disabled' if @options[:disabled]
+ select_options[:class] = type if @options[:with_css_classes]
+
+ select_html = "\n"
+ select_html << content_tag(:option, '', :value => '') + "\n" if @options[:include_blank]
+ select_html << prompt_option_tag(type, @options[:prompt]) + "\n" if @options[:prompt]
+ select_html << select_options_as_html
+
+ (content_tag(:select, select_html.html_safe, select_options) + "\n").html_safe
+ end
+
+ # Builds a prompt option tag with supplied options or from default options.
+ # prompt_option_tag(:month, prompt: 'Select month')
+ # => "<option value="">Select month</option>"
+ def prompt_option_tag(type, options)
+ prompt = case options
+ when Hash
+ default_options = {:year => false, :month => false, :day => false, :hour => false, :minute => false, :second => false}
+ default_options.merge!(options)[type.to_sym]
+ when String
+ options
+ else
+ I18n.translate(:"datetime.prompts.#{type}", :locale => @options[:locale])
+ end
+
+ prompt ? content_tag(:option, prompt, :value => '') : ''
+ end
+
+ # Builds hidden input tag for date part and value.
+ # build_hidden(:year, 2008)
+ # => "<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="2008" />"
+ def build_hidden(type, value)
+ select_options = {
+ :type => "hidden",
+ :id => input_id_from_type(type),
+ :name => input_name_from_type(type),
+ :value => value
+ }.merge!(@html_options.slice(:disabled))
+ select_options[:disabled] = 'disabled' if @options[:disabled]
+
+ tag(:input, select_options) + "\n".html_safe
+ end
+
+ # Returns the name attribute for the input tag.
+ # => post[written_on(1i)]
+ def input_name_from_type(type)
+ prefix = @options[:prefix] || ActionView::Helpers::DateTimeSelector::DEFAULT_PREFIX
+ prefix += "[#{@options[:index]}]" if @options.has_key?(:index)
+
+ field_name = @options[:field_name] || type
+ if @options[:include_position]
+ field_name += "(#{ActionView::Helpers::DateTimeSelector::POSITION[type]}i)"
+ end
+
+ @options[:discard_type] ? prefix : "#{prefix}[#{field_name}]"
+ end
+
+ # Returns the id attribute for the input tag.
+ # => "post_written_on_1i"
+ def input_id_from_type(type)
+ id = input_name_from_type(type).gsub(/([\[\(])|(\]\[)/, '_').gsub(/[\]\)]/, '')
+ id = @options[:namespace] + '_' + id if @options[:namespace]
+
+ id
+ end
+
+ # Given an ordering of datetime components, create the selection HTML
+ # and join them with their appropriate separators.
+ def build_selects_from_types(order)
+ select = ''
+ first_visible = order.find { |type| !@options[:"discard_#{type}"] }
+ order.reverse.each do |type|
+ separator = separator(type) unless type == first_visible # don't add before first visible field
+ select.insert(0, separator.to_s + send("select_#{type}").to_s)
+ end
+ select.html_safe
+ end
+
+ # Returns the separator for a given datetime component.
+ def separator(type)
+ return "" if @options[:use_hidden]
+
+ case type
+ when :year, :month, :day
+ @options[:"discard_#{type}"] ? "" : @options[:date_separator]
+ when :hour
+ (@options[:discard_year] && @options[:discard_day]) ? "" : @options[:datetime_separator]
+ when :minute, :second
+ @options[:"discard_#{type}"] ? "" : @options[:time_separator]
+ end
+ end
+ end
+
+ class FormBuilder
+ # Wraps ActionView::Helpers::DateHelper#date_select for form builders:
+ #
+ # <%= form_for @person do |f| %>
+ # <%= f.date_select :birth_date %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def date_select(method, options = {}, html_options = {})
+ @template.date_select(@object_name, method, objectify_options(options), html_options)
+ end
+
+ # Wraps ActionView::Helpers::DateHelper#time_select for form builders:
+ #
+ # <%= form_for @race do |f| %>
+ # <%= f.time_select :average_lap %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def time_select(method, options = {}, html_options = {})
+ @template.time_select(@object_name, method, objectify_options(options), html_options)
+ end
+
+ # Wraps ActionView::Helpers::DateHelper#datetime_select for form builders:
+ #
+ # <%= form_for @person do |f| %>
+ # <%= f.time_select :last_request_at %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def datetime_select(method, options = {}, html_options = {})
+ @template.datetime_select(@object_name, method, objectify_options(options), html_options)
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/debug_helper.rb b/actionview/lib/action_view/helpers/debug_helper.rb
new file mode 100644
index 0000000000..c29c1b1eea
--- /dev/null
+++ b/actionview/lib/action_view/helpers/debug_helper.rb
@@ -0,0 +1,39 @@
+module ActionView
+ # = Action View Debug Helper
+ #
+ # Provides a set of methods for making it easier to debug Rails objects.
+ module Helpers
+ module DebugHelper
+
+ include TagHelper
+
+ # Returns a YAML representation of +object+ wrapped with <pre> and </pre>.
+ # If the object cannot be converted to YAML using +to_yaml+, +inspect+ will be called instead.
+ # Useful for inspecting an object at the time of rendering.
+ #
+ # @user = User.new({ username: 'testing', password: 'xyz', age: 42}) %>
+ # debug(@user)
+ # # =>
+ # <pre class='debug_dump'>--- !ruby/object:User
+ # attributes:
+ # &nbsp; updated_at:
+ # &nbsp; username: testing
+ #
+ # &nbsp; age: 42
+ # &nbsp; password: xyz
+ # &nbsp; created_at:
+ # attributes_cache: {}
+ #
+ # new_record: true
+ # </pre>
+ def debug(object)
+ Marshal::dump(object)
+ object = ERB::Util.html_escape(object.to_yaml).gsub(" ", "&nbsp; ").html_safe
+ 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
+ content_tag(:code, object.inspect, :class => "debug_dump")
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/form_helper.rb b/actionview/lib/action_view/helpers/form_helper.rb
new file mode 100644
index 0000000000..38d969ed0c
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_helper.rb
@@ -0,0 +1,1877 @@
+require 'cgi'
+require 'action_view/helpers/date_helper'
+require 'action_view/helpers/tag_helper'
+require 'action_view/helpers/form_tag_helper'
+require 'action_view/helpers/active_model_helper'
+require 'action_view/helpers/tags'
+require 'action_view/model_naming'
+require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/string/inflections'
+
+module ActionView
+ # = Action View Form Helpers
+ module Helpers
+ # Form helpers are designed to make working with resources much easier
+ # compared to using vanilla HTML.
+ #
+ # Typically, a form designed to create or update a resource reflects the
+ # identity of the resource in several ways: (i) the url that the form is
+ # sent to (the form element's +action+ attribute) should result in a request
+ # being routed to the appropriate controller action (with the appropriate <tt>:id</tt>
+ # parameter in the case of an existing resource), (ii) input fields should
+ # be named in such a way that in the controller their values appear in the
+ # appropriate places within the +params+ hash, and (iii) for an existing record,
+ # when the form is initially displayed, input fields corresponding to attributes
+ # of the resource should show the current values of those attributes.
+ #
+ # In Rails, this is usually achieved by creating the form using +form_for+ and
+ # a number of related helper methods. +form_for+ generates an appropriate <tt>form</tt>
+ # tag and yields a form builder object that knows the model the form is about.
+ # Input fields are created by calling methods defined on the form builder, which
+ # means they are able to generate the appropriate names and default values
+ # corresponding to the model attributes, as well as convenient IDs, etc.
+ # Conventions in the generated field names allow controllers to receive form data
+ # nicely structured in +params+ with no effort on your side.
+ #
+ # For example, to create a new person you typically set up a new instance of
+ # +Person+ in the <tt>PeopleController#new</tt> action, <tt>@person</tt>, and
+ # in the view template pass that object to +form_for+:
+ #
+ # <%= form_for @person do |f| %>
+ # <%= f.label :first_name %>:
+ # <%= f.text_field :first_name %><br />
+ #
+ # <%= f.label :last_name %>:
+ # <%= f.text_field :last_name %><br />
+ #
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # The HTML generated for this would be (modulus formatting):
+ #
+ # <form action="/people" class="new_person" id="new_person" method="post">
+ # <div style="margin:0;padding:0;display:inline">
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
+ # </div>
+ # <label for="person_first_name">First name</label>:
+ # <input id="person_first_name" name="person[first_name]" type="text" /><br />
+ #
+ # <label for="person_last_name">Last name</label>:
+ # <input id="person_last_name" name="person[last_name]" type="text" /><br />
+ #
+ # <input name="commit" type="submit" value="Create Person" />
+ # </form>
+ #
+ # As you see, the HTML reflects knowledge about the resource in several spots,
+ # like the path the form should be submitted to, or the names of the input fields.
+ #
+ # In particular, thanks to the conventions followed in the generated field names, the
+ # controller gets a nested hash <tt>params[:person]</tt> with the person attributes
+ # set in the form. That hash is ready to be passed to <tt>Person.create</tt>:
+ #
+ # if @person = Person.create(params[:person])
+ # # success
+ # else
+ # # error handling
+ # end
+ #
+ # Interestingly, the exact same view code in the previous example can be used to edit
+ # a person. If <tt>@person</tt> is an existing record with name "John Smith" and ID 256,
+ # the code above as is would yield instead:
+ #
+ # <form action="/people/256" class="edit_person" id="edit_person_256" method="post">
+ # <div style="margin:0;padding:0;display:inline">
+ # <input name="_method" type="hidden" value="patch" />
+ # <input name="authenticity_token" type="hidden" value="NrOp5bsjoLRuK8IW5+dQEYjKGUJDe7TQoZVvq95Wteg=" />
+ # </div>
+ # <label for="person_first_name">First name</label>:
+ # <input id="person_first_name" name="person[first_name]" type="text" value="John" /><br />
+ #
+ # <label for="person_last_name">Last name</label>:
+ # <input id="person_last_name" name="person[last_name]" type="text" value="Smith" /><br />
+ #
+ # <input name="commit" type="submit" value="Update Person" />
+ # </form>
+ #
+ # Note that the endpoint, default values, and submit button label are tailored for <tt>@person</tt>.
+ # That works that way because the involved helpers know whether the resource is a new record or not,
+ # and generate HTML accordingly.
+ #
+ # The controller would receive the form data again in <tt>params[:person]</tt>, ready to be
+ # passed to <tt>Person#update</tt>:
+ #
+ # if @person.update(params[:person])
+ # # success
+ # else
+ # # error handling
+ # end
+ #
+ # That's how you typically work with resources.
+ module FormHelper
+ extend ActiveSupport::Concern
+
+ include FormTagHelper
+ include UrlHelper
+ include ModelNaming
+
+ # Creates a form that allows the user to create or update the attributes
+ # of a specific model object.
+ #
+ # The method can be used in several slightly different ways, depending on
+ # how much you wish to rely on Rails to infer automatically from the model
+ # how the form should be constructed. For a generic model object, a form
+ # can be created by passing +form_for+ a string or symbol representing
+ # the object we are concerned with:
+ #
+ # <%= form_for :person do |f| %>
+ # First name: <%= f.text_field :first_name %><br />
+ # Last name : <%= f.text_field :last_name %><br />
+ # Biography : <%= f.text_area :biography %><br />
+ # Admin? : <%= f.check_box :admin %><br />
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # The variable +f+ yielded to the block is a FormBuilder object that
+ # incorporates the knowledge about the model object represented by
+ # <tt>:person</tt> passed to +form_for+. Methods defined on the FormBuilder
+ # are used to generate fields bound to this model. Thus, for example,
+ #
+ # <%= f.text_field :first_name %>
+ #
+ # will get expanded to
+ #
+ # <%= text_field :person, :first_name %>
+ # which results in an html <tt><input></tt> tag whose +name+ attribute is
+ # <tt>person[first_name]</tt>. This means that when the form is submitted,
+ # the value entered by the user will be available in the controller as
+ # <tt>params[:person][:first_name]</tt>.
+ #
+ # For fields generated in this way using the FormBuilder,
+ # if <tt>:person</tt> also happens to be the name of an instance variable
+ # <tt>@person</tt>, the default value of the field shown when the form is
+ # initially displayed (e.g. in the situation where you are editing an
+ # existing record) will be the value of the corresponding attribute of
+ # <tt>@person</tt>.
+ #
+ # The rightmost argument to +form_for+ is an
+ # optional hash of options -
+ #
+ # * <tt>:url</tt> - The URL the form is to be submitted to. This may be
+ # represented in the same way as values passed to +url_for+ or +link_to+.
+ # So for example you may use a named route directly. When the model is
+ # represented by a string or symbol, as in the example above, if the
+ # <tt>:url</tt> option is not specified, by default the form will be
+ # sent back to the current url (We will describe below an alternative
+ # resource-oriented usage of +form_for+ in which the URL does not need
+ # to be specified explicitly).
+ # * <tt>:namespace</tt> - A namespace 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.
+ # * <tt>:html</tt> - Optional HTML attributes for the form tag.
+ #
+ # Also note that +form_for+ doesn't create an exclusive scope. It's still
+ # possible to use both the stand-alone FormHelper methods and methods
+ # from FormTagHelper. For example:
+ #
+ # <%= form_for :person do |f| %>
+ # First name: <%= f.text_field :first_name %>
+ # Last name : <%= f.text_field :last_name %>
+ # Biography : <%= text_area :person, :biography %>
+ # Admin? : <%= check_box_tag "person[admin]", "1", @person.company.admin? %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # This also works for the methods in FormOptionHelper and DateHelper that
+ # are designed to work with an object as base, like
+ # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ #
+ # === #form_for with a model object
+ #
+ # In the examples above, the object to be created or edited was
+ # represented by a symbol passed to +form_for+, and we noted that
+ # a string can also be used equivalently. It is also possible, however,
+ # to pass a model object itself to +form_for+. For example, if <tt>@post</tt>
+ # is an existing record you wish to edit, you can create the form using
+ #
+ # <%= form_for @post do |f| %>
+ # ...
+ # <% end %>
+ #
+ # This behaves in almost the same way as outlined previously, with a
+ # couple of small exceptions. First, the prefix used to name the input
+ # elements within the form (hence the key that denotes them in the +params+
+ # hash) is actually derived from the object's _class_, e.g. <tt>params[:post]</tt>
+ # if the object's class is +Post+. However, this can be overwritten using
+ # the <tt>:as</tt> option, e.g. -
+ #
+ # <%= form_for(@person, as: :client) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # would result in <tt>params[:client]</tt>.
+ #
+ # Secondly, the field values shown when the form is initially displayed
+ # are taken from the attributes of the object passed to +form_for+,
+ # regardless of whether the object is an instance
+ # variable. So, for example, if we had a _local_ variable +post+
+ # representing an existing record,
+ #
+ # <%= form_for post do |f| %>
+ # ...
+ # <% end %>
+ #
+ # would produce a form with fields whose initial state reflect the current
+ # values of the attributes of +post+.
+ #
+ # === Resource-oriented style
+ #
+ # In the examples just shown, although not indicated explicitly, we still
+ # need to use the <tt>:url</tt> option in order to specify where the
+ # form is going to be sent. However, further simplification is possible
+ # if the record passed to +form_for+ is a _resource_, i.e. it corresponds
+ # to a set of RESTful routes, e.g. defined using the +resources+ method
+ # in <tt>config/routes.rb</tt>. In this case Rails will simply infer the
+ # appropriate URL from the record itself. For example,
+ #
+ # <%= form_for @post do |f| %>
+ # ...
+ # <% end %>
+ #
+ # is then equivalent to something like:
+ #
+ # <%= form_for @post, as: :post, url: post_path(@post), method: :patch, html: { class: "edit_post", id: "edit_post_45" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # And for a new record
+ #
+ # <%= form_for(Post.new) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # is equivalent to something like:
+ #
+ # <%= form_for @post, as: :post, url: posts_path, html: { class: "new_post", id: "new_post" } do |f| %>
+ # ...
+ # <% end %>
+ #
+ # However you can still overwrite individual conventions, such as:
+ #
+ # <%= form_for(@post, url: super_posts_path) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # You can also set the answer format, like this:
+ #
+ # <%= form_for(@post, format: :json) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # For namespaced routes, like +admin_post_url+:
+ #
+ # <%= form_for([:admin, @post]) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # If your resource has associations defined, for example, you want to add comments
+ # to the document given that the routes are set correctly:
+ #
+ # <%= form_for([@document, @comment]) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # Where <tt>@document = Document.find(params[:id])</tt> and
+ # <tt>@comment = Comment.new</tt>.
+ #
+ # === Setting the method
+ #
+ # You can force the form to use the full array of HTTP verbs by setting
+ #
+ # method: (:get|:post|:patch|:put|:delete)
+ #
+ # in the options hash. If the verb is not GET or POST, which are natively
+ # supported by HTML forms, the form will be set to POST and a hidden input
+ # called _method will carry the intended verb for the server to interpret.
+ #
+ # === Unobtrusive JavaScript
+ #
+ # Specifying:
+ #
+ # remote: true
+ #
+ # in the options hash creates a form that will allow the unobtrusive JavaScript drivers to modify its
+ # behavior. The expected default behavior is an XMLHttpRequest in the background instead of the regular
+ # POST arrangement, but ultimately the behavior is the choice of the JavaScript driver implementor.
+ # Even though it's using JavaScript to serialize the form elements, the form submission will work just like
+ # a regular submission as viewed by the receiving side (all elements available in <tt>params</tt>).
+ #
+ # Example:
+ #
+ # <%= form_for(@post, remote: true) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # The HTML generated for this would be:
+ #
+ # <form action='http://www.example.com' method='post' data-remote='true'>
+ # <div style='margin:0;padding:0;display:inline'>
+ # <input name='_method' type='hidden' value='patch' />
+ # </div>
+ # ...
+ # </form>
+ #
+ # === Setting HTML options
+ #
+ # You can set data attributes directly by passing in a data hash, but all other HTML options must be wrapped in
+ # the HTML key. Example:
+ #
+ # <%= form_for(@post, data: { behavior: "autosave" }, html: { name: "go" }) do |f| %>
+ # ...
+ # <% end %>
+ #
+ # The HTML generated for this would be:
+ #
+ # <form action='http://www.example.com' method='post' data-behavior='autosave' name='go'>
+ # <div style='margin:0;padding:0;display:inline'>
+ # <input name='_method' type='hidden' value='patch' />
+ # </div>
+ # ...
+ # </form>
+ #
+ # === Removing hidden model id's
+ #
+ # The form_for method automatically includes the model id as a hidden field in the form.
+ # This is used to maintain the correlation between the form data and its associated model.
+ # Some ORM systems do not use IDs on nested models so in this case you want to be able
+ # to disable the hidden id.
+ #
+ # In the following example the Post model has many Comments stored within it in a NoSQL database,
+ # thus there is no primary key for comments.
+ #
+ # Example:
+ #
+ # <%= form_for(@post) do |f| %>
+ # <%= f.fields_for(:comments, include_id: false) do |cf| %>
+ # ...
+ # <% end %>
+ # <% end %>
+ #
+ # === Customized form builders
+ #
+ # You can also build forms using a customized FormBuilder class. Subclass
+ # FormBuilder and override or define some more helpers, then use your
+ # custom builder. For example, let's say you made a helper to
+ # automatically add labels to form inputs.
+ #
+ # <%= form_for @person, url: { action: "create" }, builder: LabellingFormBuilder do |f| %>
+ # <%= f.text_field :first_name %>
+ # <%= f.text_field :last_name %>
+ # <%= f.text_area :biography %>
+ # <%= f.check_box :admin %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # In this case, if you use this:
+ #
+ # <%= render f %>
+ #
+ # The rendered template is <tt>people/_labelling_form</tt> and the local
+ # variable referencing the form builder is called
+ # <tt>labelling_form</tt>.
+ #
+ # The custom FormBuilder class is automatically merged with the options
+ # of a nested fields_for call, unless it's explicitly set.
+ #
+ # In many cases you will want to wrap the above in another helper, so you
+ # could do something like the following:
+ #
+ # def labelled_form_for(record_or_name_or_array, *args, &block)
+ # options = args.extract_options!
+ # form_for(record_or_name_or_array, *(args << options.merge(builder: LabellingFormBuilder)), &block)
+ # end
+ #
+ # If you don't need to attach a form to a model instance, then check out
+ # FormTagHelper#form_tag.
+ #
+ # === Form to external resources
+ #
+ # When you build forms to external resources sometimes you need to set an authenticity token or just render a form
+ # without it, for example when you submit data to a payment gateway number and types of fields could be limited.
+ #
+ # To set an authenticity token you need to pass an <tt>:authenticity_token</tt> parameter
+ #
+ # <%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f|
+ # ...
+ # <% end %>
+ #
+ # If you don't want to an authenticity token field be rendered at all just pass <tt>false</tt>:
+ #
+ # <%= form_for @invoice, url: external_url, authenticity_token: false do |f|
+ # ...
+ # <% end %>
+ def form_for(record, options = {}, &block)
+ raise ArgumentError, "Missing block" unless block_given?
+ html_options = options[:html] ||= {}
+
+ case record
+ when String, Symbol
+ object_name = record
+ object = nil
+ else
+ object = record.is_a?(Array) ? record.last : record
+ raise ArgumentError, "First argument in form cannot contain nil or be empty" unless object
+ object_name = options[:as] || model_name_from_record_or_class(object).param_key
+ apply_form_for_options!(record, object, options)
+ end
+
+ html_options[:data] = options.delete(:data) if options.has_key?(:data)
+ html_options[:remote] = options.delete(:remote) if options.has_key?(:remote)
+ html_options[:method] = options.delete(:method) if options.has_key?(:method)
+ html_options[:authenticity_token] = options.delete(:authenticity_token)
+
+ builder = instantiate_builder(object_name, object, options)
+ output = capture(builder, &block)
+ html_options[:multipart] ||= builder.multipart?
+
+ form_tag(options[:url] || {}, html_options) { output }
+ end
+
+ def apply_form_for_options!(record, object, options) #:nodoc:
+ object = convert_to_model(object)
+
+ as = options[:as]
+ namespace = options[:namespace]
+ action, method = object.respond_to?(:persisted?) && object.persisted? ? [:edit, :patch] : [:new, :post]
+ options[:html].reverse_merge!(
+ class: as ? "#{action}_#{as}" : dom_class(object, action),
+ id: (as ? [namespace, action, as] : [namespace, dom_id(object, action)]).compact.join("_").presence,
+ method: method
+ )
+
+ options[:url] ||= polymorphic_path(record, format: options.delete(:format))
+ end
+ private :apply_form_for_options!
+
+ # Creates a scope around a specific model object like form_for, but
+ # 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,
+ # 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
+ # generate fields associated with the model object. Fields may reflect
+ # a model object in two ways - how they are named (hence how submitted
+ # values appear within the +params+ hash in the controller) and what
+ # default values are shown when the form the fields appear in is first
+ # displayed. In order for both of these features to be specified independently,
+ # both an object name (represented by either a symbol or string) and the
+ # object itself can be passed to the method separately -
+ #
+ # <%= form_for @person do |person_form| %>
+ # First name: <%= person_form.text_field :first_name %>
+ # Last name : <%= person_form.text_field :last_name %>
+ #
+ # <%= fields_for :permission, @person.permission do |permission_fields| %>
+ # Admin? : <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # In this case, the checkbox field will be represented by an HTML +input+
+ # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
+ # value will appear in the controller as <tt>params[:permission][:admin]</tt>.
+ # If <tt>@person.permission</tt> is an existing record with an attribute
+ # +admin+, the initial state of the checkbox when first displayed will
+ # reflect the value of <tt>@person.permission.admin</tt>.
+ #
+ # Often this can be simplified by passing just the name of the model
+ # object to +fields_for+ -
+ #
+ # <%= fields_for :permission do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # ...in which case, if <tt>:permission</tt> also happens to be the name of an
+ # instance variable <tt>@permission</tt>, the initial state of the input
+ # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
+ #
+ # Alternatively, you can pass just the model object itself (if the first
+ # argument isn't a string or symbol +fields_for+ will realize that the
+ # name has been omitted) -
+ #
+ # <%= fields_for @person.permission do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # and +fields_for+ will derive the required name of the field from the
+ # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
+ # of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
+ #
+ # Note: This also works for the methods in FormOptionHelper and
+ # DateHelper that are designed to work with an object as base, like
+ # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ #
+ # === Nested Attributes Examples
+ #
+ # When the object belonging to the current scope has a nested attribute
+ # writer for a certain attribute, fields_for will yield a new scope
+ # for that attribute. This allows you to create forms that set or change
+ # the attributes of a parent object and its associations in one go.
+ #
+ # Nested attribute writers are normal setter methods named after an
+ # association. The most common way of defining these writers is either
+ # with +accepts_nested_attributes_for+ in a model definition or by
+ # defining a method with the proper name. For example: the attribute
+ # writer for the association <tt>:address</tt> is called
+ # <tt>address_attributes=</tt>.
+ #
+ # Whether a one-to-one or one-to-many style form builder will be yielded
+ # depends on whether the normal reader method returns a _single_ object
+ # or an _array_ of objects.
+ #
+ # ==== One-to-one
+ #
+ # Consider a Person class which returns a _single_ Address from the
+ # <tt>address</tt> reader method and responds to the
+ # <tt>address_attributes=</tt> writer method:
+ #
+ # class Person
+ # def address
+ # @address
+ # end
+ #
+ # def address_attributes=(attributes)
+ # # Process the attributes hash
+ # end
+ # end
+ #
+ # This model can now be used with a nested fields_for, like so:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :address do |address_fields| %>
+ # Street : <%= address_fields.text_field :street %>
+ # Zip code: <%= address_fields.text_field :zip_code %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # When address is already an association on a Person you can use
+ # +accepts_nested_attributes_for+ to define the writer method for you:
+ #
+ # class Person < ActiveRecord::Base
+ # has_one :address
+ # accepts_nested_attributes_for :address
+ # end
+ #
+ # If you want to destroy the associated model through the form, you have
+ # to enable it first using the <tt>:allow_destroy</tt> option for
+ # +accepts_nested_attributes_for+:
+ #
+ # class Person < ActiveRecord::Base
+ # has_one :address
+ # accepts_nested_attributes_for :address, allow_destroy: true
+ # end
+ #
+ # Now, when you use a form element with the <tt>_destroy</tt> parameter,
+ # with a value that evaluates to +true+, you will destroy the associated
+ # model (eg. 1, '1', true, or 'true'):
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :address do |address_fields| %>
+ # ...
+ # Delete: <%= address_fields.check_box :_destroy %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # ==== One-to-many
+ #
+ # Consider a Person class which returns an _array_ of Project instances
+ # from the <tt>projects</tt> reader method and responds to the
+ # <tt>projects_attributes=</tt> writer method:
+ #
+ # class Person
+ # def projects
+ # [@project1, @project2]
+ # end
+ #
+ # def projects_attributes=(attributes)
+ # # Process the attributes hash
+ # end
+ # end
+ #
+ # Note that the <tt>projects_attributes=</tt> writer method is in fact
+ # required for fields_for to correctly identify <tt>:projects</tt> as a
+ # collection, and the correct indices to be set in the form markup.
+ #
+ # When projects is already an association on Person you can use
+ # +accepts_nested_attributes_for+ to define the writer method for you:
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :projects
+ # accepts_nested_attributes_for :projects
+ # end
+ #
+ # This model can now be used with a nested fields_for. The block given to
+ # the nested fields_for call will be repeated for each instance in the
+ # collection:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # <% if project_fields.object.active? %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # It's also possible to specify the instance to be used:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <% @person.projects.each do |project| %>
+ # <% if project.active? %>
+ # <%= person_form.fields_for :projects, project do |project_fields| %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # <% end %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # Or a collection to be used:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # If you want to destroy any of the associated models through the
+ # form, you have to enable it first using the <tt>:allow_destroy</tt>
+ # option for +accepts_nested_attributes_for+:
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :projects
+ # accepts_nested_attributes_for :projects, allow_destroy: true
+ # end
+ #
+ # This will allow you to specify which models to destroy in the
+ # attributes hash by adding a form element for the <tt>_destroy</tt>
+ # parameter with a value that evaluates to +true+
+ # (eg. 1, '1', true, or 'true'):
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # Delete: <%= project_fields.check_box :_destroy %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # When a collection is used you might want to know the index of each
+ # object into the array. For this purpose, the <tt>index</tt> method
+ # is available in the FormBuilder object.
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # Project #<%= project_fields.index %>
+ # ...
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # Note that fields_for will automatically generate a hidden field
+ # to store the ID of the record. There are circumstances where this
+ # hidden field is not needed and you can pass <tt>include_id: false</tt>
+ # to prevent fields_for from rendering it automatically.
+ def fields_for(record_name, record_object = nil, options = {}, &block)
+ builder = instantiate_builder(record_name, record_object, options)
+ capture(builder, &block)
+ end
+
+ # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
+ # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
+ # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
+ # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
+ # target labels for radio_button tags (where the value is used in the ID of the input tag).
+ #
+ # ==== Examples
+ # label(:post, :title)
+ # # => <label for="post_title">Title</label>
+ #
+ # You can localize your labels based on model and attribute names.
+ # For example you can define the following in your locale (e.g. en.yml)
+ #
+ # helpers:
+ # label:
+ # post:
+ # body: "Write your entire text here"
+ #
+ # Which then will result in
+ #
+ # label(:post, :body)
+ # # => <label for="post_body">Write your entire text here</label>
+ #
+ # Localization can also be based purely on the translation of the attribute-name
+ # (if you are using ActiveRecord):
+ #
+ # activerecord:
+ # attributes:
+ # post:
+ # cost: "Total cost"
+ #
+ # label(:post, :cost)
+ # # => <label for="post_cost">Total cost</label>
+ #
+ # label(:post, :title, "A short title")
+ # # => <label for="post_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(:post, :privacy, "Public Post", value: "public")
+ # # => <label for="post_privacy_public">Public Post</label>
+ #
+ # label(:post, :terms) do
+ # 'Accept <a href="/terms">Terms</a>.'.html_safe
+ # end
+ 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
+
+ # Returns an input tag of the "text" type tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # text_field(:post, :title, size: 20)
+ # # => <input type="text" id="post_title" name="post[title]" size="20" value="#{@post.title}" />
+ #
+ # text_field(:post, :title, class: "create_input")
+ # # => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" class="create_input" />
+ #
+ # text_field(:session, :user, onchange: "if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }")
+ # # => <input type="text" id="session_user" name="session[user]" value="#{@session.user}" onchange="if ($('#session_user').val() === 'admin') { alert('Your login can not be admin!'); }"/>
+ #
+ # text_field(:snippet, :code, size: 20, class: 'code_input')
+ # # => <input type="text" id="snippet_code" name="snippet[code]" size="20" value="#{@snippet.code}" class="code_input" />
+ def text_field(object_name, method, options = {})
+ Tags::TextField.new(object_name, method, self, options).render
+ end
+
+ # Returns an input tag of the "password" type tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown. For security reasons this field is blank by default; pass in a value via +options+ if this is not desired.
+ #
+ # ==== Examples
+ # password_field(:login, :pass, size: 20)
+ # # => <input type="password" id="login_pass" name="login[pass]" size="20" />
+ #
+ # password_field(:account, :secret, class: "form_input", value: @account.secret)
+ # # => <input type="password" id="account_secret" name="account[secret]" value="#{@account.secret}" class="form_input" />
+ #
+ # password_field(:user, :password, onchange: "if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }")
+ # # => <input type="password" id="user_password" name="user[password]" onchange="if ($('#user_password').val().length > 30) { alert('Your password needs to be shorter!'); }"/>
+ #
+ # password_field(:account, :pin, size: 20, class: 'form_input')
+ # # => <input type="password" id="account_pin" name="account[pin]" size="20" class="form_input" />
+ def password_field(object_name, method, options = {})
+ Tags::PasswordField.new(object_name, method, self, options).render
+ end
+
+ # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # hidden_field(:signup, :pass_confirm)
+ # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
+ #
+ # hidden_field(:post, :tag_list)
+ # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
+ #
+ # hidden_field(:user, :token)
+ # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+ def hidden_field(object_name, method, options = {})
+ Tags::HiddenField.new(object_name, method, self, options).render
+ end
+
+ # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
+ # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
+ #
+ # ==== Examples
+ # file_field(:user, :avatar)
+ # # => <input type="file" id="user_avatar" name="user[avatar]" />
+ #
+ # file_field(:post, :image, :multiple => true)
+ # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ #
+ # file_field(:post, :attached, accept: 'text/html')
+ # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
+ #
+ # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
+ # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
+ #
+ # file_field(:attachment, :file, class: 'file_input')
+ # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
+ def file_field(object_name, method, options = {})
+ Tags::FileField.new(object_name, method, self, options).render
+ end
+
+ # Returns a textarea opening and closing tag set tailored for accessing a specified attribute (identified by +method+)
+ # on an object assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+.
+ #
+ # ==== Examples
+ # text_area(:post, :body, cols: 20, rows: 40)
+ # # => <textarea cols="20" rows="40" id="post_body" name="post[body]">
+ # # #{@post.body}
+ # # </textarea>
+ #
+ # text_area(:comment, :text, size: "20x30")
+ # # => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
+ # # #{@comment.text}
+ # # </textarea>
+ #
+ # text_area(:application, :notes, cols: 40, rows: 15, class: 'app_input')
+ # # => <textarea cols="40" rows="15" id="application_notes" name="application[notes]" class="app_input">
+ # # #{@application.notes}
+ # # </textarea>
+ #
+ # text_area(:entry, :body, size: "20x20", disabled: 'disabled')
+ # # => <textarea cols="20" rows="20" id="entry_body" name="entry[body]" disabled="disabled">
+ # # #{@entry.body}
+ # # </textarea>
+ def text_area(object_name, method, options = {})
+ Tags::TextArea.new(object_name, method, self, options).render
+ end
+
+ # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
+ # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
+ # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
+ # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says unchecked check boxes are not successful, and
+ # thus web browsers do not send them. Unfortunately this introduces a gotcha:
+ # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
+ # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
+ # any mass-assignment idiom like
+ #
+ # @invoice.update(params[:invoice])
+ #
+ # wouldn't update the flag.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # the very check box. The hidden field has the same name and its
+ # attributes mimic an unchecked check box.
+ #
+ # This way, the client either sends only the hidden field (representing
+ # the check box is unchecked), or both fields. Since the HTML specification
+ # says key/value pairs have to be sent in the same order they appear in the
+ # form, and parameters extraction gets the last occurrence of any repeated
+ # key in the query string, that works for ordinary forms.
+ #
+ # Unfortunately that workaround does not work when the check box goes
+ # within an array-like parameter, as in
+ #
+ # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
+ # <%= form.check_box :paid %>
+ # ...
+ # <% end %>
+ #
+ # because parameter name repetition is precisely what Rails seeks to distinguish
+ # the elements of the array. For each item with a checked check box you
+ # get an extra ghost item with only that attribute, assigned to "0".
+ #
+ # In that case it is preferable to either use +check_box_tag+ or to use
+ # hashes instead of arrays.
+ #
+ # # Let's say that @post.validated? is 1:
+ # check_box("post", "validated")
+ # # => <input name="post[validated]" type="hidden" value="0" />
+ # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
+ #
+ # # Let's say that @puppy.gooddog is "no":
+ # check_box("puppy", "gooddog", {}, "yes", "no")
+ # # => <input name="puppy[gooddog]" type="hidden" value="no" />
+ # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
+ #
+ # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
+ # # => <input name="eula[accepted]" type="hidden" value="no" />
+ # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
+ def check_box(object_name, method, options = {}, checked_value = "1", unchecked_value = "0")
+ Tags::CheckBox.new(object_name, method, self, checked_value, unchecked_value, options).render
+ end
+
+ # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
+ # radio button will be checked.
+ #
+ # To force the radio button to be checked pass <tt>checked: true</tt> in the
+ # +options+ hash. You may pass HTML options there as well.
+ #
+ # # 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" />
+ #
+ # radio_button("user", "receive_newsletter", "yes")
+ # radio_button("user", "receive_newsletter", "no")
+ # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
+ # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
+ def radio_button(object_name, method, tag_value, options = {})
+ Tags::RadioButton.new(object_name, method, self, tag_value, options).render
+ end
+
+ # Returns a text_field of type "color".
+ #
+ # color_field("car", "color")
+ # # => <input id="car_color" name="car[color]" type="color" value="#000000" />
+ def color_field(object_name, method, options = {})
+ Tags::ColorField.new(object_name, method, self, options).render
+ end
+
+ # Returns an input of type "search" for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object_name+). Inputs of type "search" may be styled differently by
+ # some browsers.
+ #
+ # search_field(:user, :name)
+ # # => <input id="user_name" name="user[name]" type="search" />
+ # search_field(:user, :name, autosave: false)
+ # # => <input autosave="false" id="user_name" name="user[name]" type="search" />
+ # search_field(:user, :name, results: 3)
+ # # => <input id="user_name" name="user[name]" results="3" type="search" />
+ # # Assume request.host returns "www.example.com"
+ # search_field(:user, :name, autosave: true)
+ # # => <input autosave="com.example.www" id="user_name" name="user[name]" results="10" type="search" />
+ # search_field(:user, :name, onsearch: true)
+ # # => <input id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
+ # search_field(:user, :name, autosave: false, onsearch: true)
+ # # => <input autosave="false" id="user_name" incremental="true" name="user[name]" onsearch="true" type="search" />
+ # search_field(:user, :name, autosave: true, onsearch: true)
+ # # => <input autosave="com.example.www" id="user_name" incremental="true" name="user[name]" onsearch="true" results="10" type="search" />
+ def search_field(object_name, method, options = {})
+ Tags::SearchField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "tel".
+ #
+ # telephone_field("user", "phone")
+ # # => <input id="user_phone" name="user[phone]" type="tel" />
+ #
+ def telephone_field(object_name, method, options = {})
+ Tags::TelField.new(object_name, method, self, options).render
+ end
+ # aliases telephone_field
+ alias phone_field telephone_field
+
+ # Returns a text_field of type "date".
+ #
+ # date_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" />
+ #
+ # The default value is generated by trying to call "to_date"
+ # on the object's value, which makes it behave as expected for instances
+ # of DateTime and ActiveSupport::TimeWithZone. You can still override that
+ # by passing the "value" option explicitly, e.g.
+ #
+ # @user.born_on = Date.new(1984, 1, 27)
+ # date_field("user", "born_on", value: "1984-05-12")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-05-12" />
+ #
+ def date_field(object_name, method, options = {})
+ Tags::DateField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "time".
+ #
+ # The default value is generated by trying to call +strftime+ with "%T.%L"
+ # on the objects's value. It is still possible to override that
+ # by passing the "value" option.
+ #
+ # === Options
+ # * Accepts same options as time_field_tag
+ #
+ # === Example
+ # time_field("task", "started_at")
+ # # => <input id="task_started_at" name="task[started_at]" type="time" />
+ #
+ def time_field(object_name, method, options = {})
+ Tags::TimeField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "datetime".
+ #
+ # datetime_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" />
+ #
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T.%L%z"
+ # on the object's value, which makes it behave as expected for instances
+ # of DateTime and ActiveSupport::TimeWithZone.
+ #
+ # @user.born_on = Date.new(1984, 1, 12)
+ # datetime_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime" value="1984-01-12T00:00:00.000+0000" />
+ #
+ def datetime_field(object_name, method, options = {})
+ Tags::DatetimeField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "datetime-local".
+ #
+ # datetime_local_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" />
+ #
+ # The default value is generated by trying to call +strftime+ with "%Y-%m-%dT%T"
+ # on the object's value, which makes it behave as expected for instances
+ # of DateTime and ActiveSupport::TimeWithZone.
+ #
+ # @user.born_on = Date.new(1984, 1, 12)
+ # datetime_local_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="datetime-local" value="1984-01-12T00:00:00" />
+ #
+ def datetime_local_field(object_name, method, options = {})
+ Tags::DatetimeLocalField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "month".
+ #
+ # month_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="month" />
+ #
+ # The default value is generated by trying to call +strftime+ with "%Y-%m"
+ # on the object's value, which makes it behave as expected for instances
+ # of DateTime and ActiveSupport::TimeWithZone.
+ #
+ # @user.born_on = Date.new(1984, 1, 27)
+ # month_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-01" />
+ #
+ def month_field(object_name, method, options = {})
+ Tags::MonthField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "week".
+ #
+ # week_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="week" />
+ #
+ # The default value is generated by trying to call +strftime+ with "%Y-W%W"
+ # on the object's value, which makes it behave as expected for instances
+ # of DateTime and ActiveSupport::TimeWithZone.
+ #
+ # @user.born_on = Date.new(1984, 5, 12)
+ # week_field("user", "born_on")
+ # # => <input id="user_born_on" name="user[born_on]" type="date" value="1984-W19" />
+ #
+ def week_field(object_name, method, options = {})
+ Tags::WeekField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "url".
+ #
+ # url_field("user", "homepage")
+ # # => <input id="user_homepage" name="user[homepage]" type="url" />
+ #
+ def url_field(object_name, method, options = {})
+ Tags::UrlField.new(object_name, method, self, options).render
+ end
+
+ # Returns a text_field of type "email".
+ #
+ # email_field("user", "address")
+ # # => <input id="user_address" name="user[address]" type="email" />
+ #
+ def email_field(object_name, method, options = {})
+ Tags::EmailField.new(object_name, method, self, options).render
+ end
+
+ # Returns an input tag of type "number".
+ #
+ # ==== Options
+ # * Accepts same options as number_field_tag
+ def number_field(object_name, method, options = {})
+ Tags::NumberField.new(object_name, method, self, options).render
+ end
+
+ # Returns an input tag of type "range".
+ #
+ # ==== Options
+ # * Accepts same options as range_field_tag
+ def range_field(object_name, method, options = {})
+ Tags::RangeField.new(object_name, method, self, options).render
+ end
+
+ private
+
+ def instantiate_builder(record_name, record_object, options)
+ case record_name
+ when String, Symbol
+ object = record_object
+ object_name = record_name
+ else
+ object = record_name
+ object_name = model_name_from_record_or_class(object).param_key
+ end
+
+ builder = options[:builder] || default_form_builder
+ builder.new(object_name, object, self, options)
+ end
+
+ def default_form_builder
+ builder = ActionView::Base.default_form_builder
+ builder.respond_to?(:constantize) ? builder.constantize : builder
+ end
+ end
+
+ # A +FormBuilder+ object is associated with a particular model object and
+ # allows you to generate fields associated with the model object. The
+ # +FormBuilder+ object is yielded when using +form_for+ or +fields_for+.
+ # For example:
+ #
+ # <%= form_for @person do |person_form| %>
+ # Name: <%= person_form.text_field :name %>
+ # Admin: <%= person_form.check_box :admin %>
+ # <% end %>
+ #
+ # In the above block, the a +FormBuilder+ object is yielded as the
+ # +person_form+ variable. This allows you to generate the +text_field+
+ # and +check_box+ fields by specifying their eponymous methods, which
+ # modify the underlying template and associates the +@person+ model object
+ # with the form.
+ #
+ # The +FormBuilder+ object can be thought of as serving as a proxy for the
+ # methods in the +FormHelper+ module. This class, however, allows you to
+ # call methods with the model object you are building the form for.
+ #
+ # You can create your own custom FormBuilder templates by subclassing this
+ # class. For example:
+ #
+ # class MyFormBuilder < ActionView::Helpers::FormBuilder
+ # def div_radio_button(method, tag_value, options = {})
+ # @template.content_tag(:div,
+ # @template.radio_button(
+ # @object_name, method, tag_value, objectify_options(options)
+ # )
+ # )
+ # end
+ #
+ # The above code creates a new method +div_radio_button+ which wraps a div
+ # around the a new radio button. Note that when options are passed in, you
+ # must called +objectify_options+ in order for the model object to get
+ # correctly passed to the method. If +objectify_options+ is not called,
+ # then the newly created helper will not be linked back to the model.
+ #
+ # The +div_radio_button+ code from above can now be used as follows:
+ #
+ # <%= form_for @person, :builder => MyFormBuilder do |f| %>
+ # I am a child: <%= f.div_radio_button(:admin, "child") %>
+ # I am an adult: <%= f.div_radio_button(:admin, "adult") %>
+ # <% end -%>
+ #
+ # The standard set of helper methods for form building are located in the
+ # +field_helpers+ class attribute.
+ class FormBuilder
+ include ModelNaming
+
+ # The methods which wrap a form helper call.
+ class_attribute :field_helpers
+ self.field_helpers = [:fields_for, :label, :text_field, :password_field,
+ :hidden_field, :file_field, :text_area, :check_box,
+ :radio_button, :color_field, :search_field,
+ :telephone_field, :phone_field, :date_field,
+ :time_field, :datetime_field, :datetime_local_field,
+ :month_field, :week_field, :url_field, :email_field,
+ :number_field, :range_field]
+
+ attr_accessor :object_name, :object, :options
+
+ attr_reader :multipart, :index
+ alias :multipart? :multipart
+
+ def multipart=(multipart)
+ @multipart = multipart
+
+ if parent_builder = @options[:parent_builder]
+ parent_builder.multipart = multipart
+ end
+ end
+
+ def self._to_partial_path
+ @_to_partial_path ||= name.demodulize.underscore.sub!(/_builder$/, '')
+ end
+
+ def to_partial_path
+ self.class._to_partial_path
+ end
+
+ def to_model
+ self
+ end
+
+ def initialize(object_name, object, template, options)
+ @nested_child_index = {}
+ @object_name, @object, @template, @options = object_name, object, template, options
+ @default_options = @options ? @options.slice(:index, :namespace) : {}
+ if @object_name.to_s.match(/\[\]$/)
+ if object ||= @template.instance_variable_get("@#{Regexp.last_match.pre_match}") and object.respond_to?(:to_param)
+ @auto_index = object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
+ end
+ @multipart = nil
+ @index = options[:index] || options[:child_index]
+ end
+
+ (field_helpers - [:label, :check_box, :radio_button, :fields_for, :hidden_field, :file_field]).each do |selector|
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def #{selector}(method, options = {}) # def text_field(method, options = {})
+ @template.send( # @template.send(
+ #{selector.inspect}, # "text_field",
+ @object_name, # @object_name,
+ method, # method,
+ objectify_options(options)) # objectify_options(options))
+ end # end
+ RUBY_EVAL
+ end
+
+ # Creates a scope around a specific model object like form_for, but
+ # 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,
+ # 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
+ # generate fields associated with the model object. Fields may reflect
+ # a model object in two ways - how they are named (hence how submitted
+ # values appear within the +params+ hash in the controller) and what
+ # default values are shown when the form the fields appear in is first
+ # displayed. In order for both of these features to be specified independently,
+ # both an object name (represented by either a symbol or string) and the
+ # object itself can be passed to the method separately -
+ #
+ # <%= form_for @person do |person_form| %>
+ # First name: <%= person_form.text_field :first_name %>
+ # Last name : <%= person_form.text_field :last_name %>
+ #
+ # <%= fields_for :permission, @person.permission do |permission_fields| %>
+ # Admin? : <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # <%= person_form.submit %>
+ # <% end %>
+ #
+ # In this case, the checkbox field will be represented by an HTML +input+
+ # tag with the +name+ attribute <tt>permission[admin]</tt>, and the submitted
+ # value will appear in the controller as <tt>params[:permission][:admin]</tt>.
+ # If <tt>@person.permission</tt> is an existing record with an attribute
+ # +admin+, the initial state of the checkbox when first displayed will
+ # reflect the value of <tt>@person.permission.admin</tt>.
+ #
+ # Often this can be simplified by passing just the name of the model
+ # object to +fields_for+ -
+ #
+ # <%= fields_for :permission do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # ...in which case, if <tt>:permission</tt> also happens to be the name of an
+ # instance variable <tt>@permission</tt>, the initial state of the input
+ # field will reflect the value of that variable's attribute <tt>@permission.admin</tt>.
+ #
+ # Alternatively, you can pass just the model object itself (if the first
+ # argument isn't a string or symbol +fields_for+ will realize that the
+ # name has been omitted) -
+ #
+ # <%= fields_for @person.permission do |permission_fields| %>
+ # Admin?: <%= permission_fields.check_box :admin %>
+ # <% end %>
+ #
+ # and +fields_for+ will derive the required name of the field from the
+ # _class_ of the model object, e.g. if <tt>@person.permission</tt>, is
+ # of class +Permission+, the field will still be named <tt>permission[admin]</tt>.
+ #
+ # Note: This also works for the methods in FormOptionHelper and
+ # DateHelper that are designed to work with an object as base, like
+ # FormOptionHelper#collection_select and DateHelper#datetime_select.
+ #
+ # === Nested Attributes Examples
+ #
+ # When the object belonging to the current scope has a nested attribute
+ # writer for a certain attribute, fields_for will yield a new scope
+ # for that attribute. This allows you to create forms that set or change
+ # the attributes of a parent object and its associations in one go.
+ #
+ # Nested attribute writers are normal setter methods named after an
+ # association. The most common way of defining these writers is either
+ # with +accepts_nested_attributes_for+ in a model definition or by
+ # defining a method with the proper name. For example: the attribute
+ # writer for the association <tt>:address</tt> is called
+ # <tt>address_attributes=</tt>.
+ #
+ # Whether a one-to-one or one-to-many style form builder will be yielded
+ # depends on whether the normal reader method returns a _single_ object
+ # or an _array_ of objects.
+ #
+ # ==== One-to-one
+ #
+ # Consider a Person class which returns a _single_ Address from the
+ # <tt>address</tt> reader method and responds to the
+ # <tt>address_attributes=</tt> writer method:
+ #
+ # class Person
+ # def address
+ # @address
+ # end
+ #
+ # def address_attributes=(attributes)
+ # # Process the attributes hash
+ # end
+ # end
+ #
+ # This model can now be used with a nested fields_for, like so:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :address do |address_fields| %>
+ # Street : <%= address_fields.text_field :street %>
+ # Zip code: <%= address_fields.text_field :zip_code %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # When address is already an association on a Person you can use
+ # +accepts_nested_attributes_for+ to define the writer method for you:
+ #
+ # class Person < ActiveRecord::Base
+ # has_one :address
+ # accepts_nested_attributes_for :address
+ # end
+ #
+ # If you want to destroy the associated model through the form, you have
+ # to enable it first using the <tt>:allow_destroy</tt> option for
+ # +accepts_nested_attributes_for+:
+ #
+ # class Person < ActiveRecord::Base
+ # has_one :address
+ # accepts_nested_attributes_for :address, allow_destroy: true
+ # end
+ #
+ # Now, when you use a form element with the <tt>_destroy</tt> parameter,
+ # with a value that evaluates to +true+, you will destroy the associated
+ # model (eg. 1, '1', true, or 'true'):
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :address do |address_fields| %>
+ # ...
+ # Delete: <%= address_fields.check_box :_destroy %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # ==== One-to-many
+ #
+ # Consider a Person class which returns an _array_ of Project instances
+ # from the <tt>projects</tt> reader method and responds to the
+ # <tt>projects_attributes=</tt> writer method:
+ #
+ # class Person
+ # def projects
+ # [@project1, @project2]
+ # end
+ #
+ # def projects_attributes=(attributes)
+ # # Process the attributes hash
+ # end
+ # end
+ #
+ # Note that the <tt>projects_attributes=</tt> writer method is in fact
+ # required for fields_for to correctly identify <tt>:projects</tt> as a
+ # collection, and the correct indices to be set in the form markup.
+ #
+ # When projects is already an association on Person you can use
+ # +accepts_nested_attributes_for+ to define the writer method for you:
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :projects
+ # accepts_nested_attributes_for :projects
+ # end
+ #
+ # This model can now be used with a nested fields_for. The block given to
+ # the nested fields_for call will be repeated for each instance in the
+ # collection:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # <% if project_fields.object.active? %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # It's also possible to specify the instance to be used:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <% @person.projects.each do |project| %>
+ # <% if project.active? %>
+ # <%= person_form.fields_for :projects, project do |project_fields| %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # <% end %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # Or a collection to be used:
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects, @active_projects do |project_fields| %>
+ # Name: <%= project_fields.text_field :name %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # If you want to destroy any of the associated models through the
+ # form, you have to enable it first using the <tt>:allow_destroy</tt>
+ # option for +accepts_nested_attributes_for+:
+ #
+ # class Person < ActiveRecord::Base
+ # has_many :projects
+ # accepts_nested_attributes_for :projects, allow_destroy: true
+ # end
+ #
+ # This will allow you to specify which models to destroy in the
+ # attributes hash by adding a form element for the <tt>_destroy</tt>
+ # parameter with a value that evaluates to +true+
+ # (eg. 1, '1', true, or 'true'):
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # Delete: <%= project_fields.check_box :_destroy %>
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # When a collection is used you might want to know the index of each
+ # object into the array. For this purpose, the <tt>index</tt> method
+ # is available in the FormBuilder object.
+ #
+ # <%= form_for @person do |person_form| %>
+ # ...
+ # <%= person_form.fields_for :projects do |project_fields| %>
+ # Project #<%= project_fields.index %>
+ # ...
+ # <% end %>
+ # ...
+ # <% end %>
+ #
+ # Note that fields_for will automatically generate a hidden field
+ # to store the ID of the record. There are circumstances where this
+ # hidden field is not needed and you can pass <tt>include_id: false</tt>
+ # to prevent fields_for from rendering it automatically.
+ def fields_for(record_name, record_object = nil, fields_options = {}, &block)
+ fields_options, record_object = record_object, nil if record_object.is_a?(Hash) && record_object.extractable_options?
+ fields_options[:builder] ||= options[:builder]
+ fields_options[:namespace] = options[:namespace]
+ fields_options[:parent_builder] = self
+
+ case record_name
+ when String, Symbol
+ if nested_attributes_association?(record_name)
+ return fields_for_with_nested_attributes(record_name, record_object, fields_options, block)
+ end
+ else
+ record_object = record_name.is_a?(Array) ? record_name.last : record_name
+ record_name = model_name_from_record_or_class(record_object).param_key
+ end
+
+ index = if options.has_key?(:index)
+ options[:index]
+ elsif defined?(@auto_index)
+ self.object_name = @object_name.to_s.sub(/\[\]$/,"")
+ @auto_index
+ end
+
+ record_name = index ? "#{object_name}[#{index}][#{record_name}]" : "#{object_name}[#{record_name}]"
+ fields_options[:child_index] = index
+
+ @template.fields_for(record_name, record_object, fields_options, &block)
+ end
+
+ # Returns a label tag tailored for labelling an input field for a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). The text of label will default to the attribute name unless a translation
+ # is found in the current I18n locale (through helpers.label.<modelname>.<attribute>) or you specify it explicitly.
+ # Additional options on the label tag can be passed as a hash with +options+. These options will be tagged
+ # onto the HTML as an HTML element attribute as in the example shown, except for the <tt>:value</tt> option, which is designed to
+ # target labels for radio_button tags (where the value is used in the ID of the input tag).
+ #
+ # ==== Examples
+ # label(:post, :title)
+ # # => <label for="post_title">Title</label>
+ #
+ # You can localize your labels based on model and attribute names.
+ # For example you can define the following in your locale (e.g. en.yml)
+ #
+ # helpers:
+ # label:
+ # post:
+ # body: "Write your entire text here"
+ #
+ # Which then will result in
+ #
+ # label(:post, :body)
+ # # => <label for="post_body">Write your entire text here</label>
+ #
+ # Localization can also be based purely on the translation of the attribute-name
+ # (if you are using ActiveRecord):
+ #
+ # activerecord:
+ # attributes:
+ # post:
+ # cost: "Total cost"
+ #
+ # label(:post, :cost)
+ # # => <label for="post_cost">Total cost</label>
+ #
+ # label(:post, :title, "A short title")
+ # # => <label for="post_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(:post, :privacy, "Public Post", value: "public")
+ # # => <label for="post_privacy_public">Public Post</label>
+ #
+ # label(:post, :terms) do
+ # 'Accept <a href="/terms">Terms</a>.'.html_safe
+ # end
+ def label(method, text = nil, options = {}, &block)
+ @template.label(@object_name, method, text, objectify_options(options), &block)
+ end
+
+ # Returns a checkbox tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). This object must be an instance object (@object) and not a local object.
+ # It's intended that +method+ returns an integer and if that integer is above zero, then the checkbox is checked.
+ # Additional options on the input tag can be passed as a hash with +options+. The +checked_value+ defaults to 1
+ # while the default +unchecked_value+ is set to 0 which is convenient for boolean values.
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says unchecked check boxes are not successful, and
+ # thus web browsers do not send them. Unfortunately this introduces a gotcha:
+ # if an +Invoice+ model has a +paid+ flag, and in the form that edits a paid
+ # invoice the user unchecks its check box, no +paid+ parameter is sent. So,
+ # any mass-assignment idiom like
+ #
+ # @invoice.update(params[:invoice])
+ #
+ # wouldn't update the flag.
+ #
+ # To prevent this the helper generates an auxiliary hidden field before
+ # the very check box. The hidden field has the same name and its
+ # attributes mimic an unchecked check box.
+ #
+ # This way, the client either sends only the hidden field (representing
+ # the check box is unchecked), or both fields. Since the HTML specification
+ # says key/value pairs have to be sent in the same order they appear in the
+ # form, and parameters extraction gets the last occurrence of any repeated
+ # key in the query string, that works for ordinary forms.
+ #
+ # Unfortunately that workaround does not work when the check box goes
+ # within an array-like parameter, as in
+ #
+ # <%= fields_for "project[invoice_attributes][]", invoice, index: nil do |form| %>
+ # <%= form.check_box :paid %>
+ # ...
+ # <% end %>
+ #
+ # because parameter name repetition is precisely what Rails seeks to distinguish
+ # the elements of the array. For each item with a checked check box you
+ # get an extra ghost item with only that attribute, assigned to "0".
+ #
+ # In that case it is preferable to either use +check_box_tag+ or to use
+ # hashes instead of arrays.
+ #
+ # # Let's say that @post.validated? is 1:
+ # check_box("post", "validated")
+ # # => <input name="post[validated]" type="hidden" value="0" />
+ # # <input checked="checked" type="checkbox" id="post_validated" name="post[validated]" value="1" />
+ #
+ # # Let's say that @puppy.gooddog is "no":
+ # check_box("puppy", "gooddog", {}, "yes", "no")
+ # # => <input name="puppy[gooddog]" type="hidden" value="no" />
+ # # <input type="checkbox" id="puppy_gooddog" name="puppy[gooddog]" value="yes" />
+ #
+ # check_box("eula", "accepted", { class: 'eula_check' }, "yes", "no")
+ # # => <input name="eula[accepted]" type="hidden" value="no" />
+ # # <input type="checkbox" class="eula_check" id="eula_accepted" name="eula[accepted]" value="yes" />
+ def check_box(method, options = {}, checked_value = "1", unchecked_value = "0")
+ @template.check_box(@object_name, method, objectify_options(options), checked_value, unchecked_value)
+ end
+
+ # Returns a radio button tag for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). If the current value of +method+ is +tag_value+ the
+ # radio button will be checked.
+ #
+ # To force the radio button to be checked pass <tt>checked: true</tt> in the
+ # +options+ hash. You may pass HTML options there as well.
+ #
+ # # 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" />
+ #
+ # radio_button("user", "receive_newsletter", "yes")
+ # radio_button("user", "receive_newsletter", "no")
+ # # => <input type="radio" id="user_receive_newsletter_yes" name="user[receive_newsletter]" value="yes" />
+ # # <input type="radio" id="user_receive_newsletter_no" name="user[receive_newsletter]" value="no" checked="checked" />
+ def radio_button(method, tag_value, options = {})
+ @template.radio_button(@object_name, method, tag_value, objectify_options(options))
+ end
+
+ # Returns a hidden input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # ==== Examples
+ # hidden_field(:signup, :pass_confirm)
+ # # => <input type="hidden" id="signup_pass_confirm" name="signup[pass_confirm]" value="#{@signup.pass_confirm}" />
+ #
+ # hidden_field(:post, :tag_list)
+ # # => <input type="hidden" id="post_tag_list" name="post[tag_list]" value="#{@post.tag_list}" />
+ #
+ # hidden_field(:user, :token)
+ # # => <input type="hidden" id="user_token" name="user[token]" value="#{@user.token}" />
+ #
+ def hidden_field(method, options = {})
+ @emitted_hidden_id = true if method == :id
+ @template.hidden_field(@object_name, method, objectify_options(options))
+ end
+
+ # Returns a file upload input tag tailored for accessing a specified attribute (identified by +method+) on an object
+ # assigned to the template (identified by +object+). Additional options on the input tag can be passed as a
+ # hash with +options+. These options will be tagged onto the HTML as an HTML element attribute as in the example
+ # shown.
+ #
+ # Using this method inside a +form_for+ block will set the enclosing form's encoding to <tt>multipart/form-data</tt>.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
+ # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
+ #
+ # ==== Examples
+ # file_field(:user, :avatar)
+ # # => <input type="file" id="user_avatar" name="user[avatar]" />
+ #
+ # file_field(:post, :image, :multiple => true)
+ # # => <input type="file" id="post_image" name="post[image]" multiple="true" />
+ #
+ # file_field(:post, :attached, accept: 'text/html')
+ # # => <input accept="text/html" type="file" id="post_attached" name="post[attached]" />
+ #
+ # file_field(:post, :image, accept: 'image/png,image/gif,image/jpeg')
+ # # => <input type="file" id="post_image" name="post[image]" accept="image/png,image/gif,image/jpeg" />
+ #
+ # file_field(:attachment, :file, class: 'file_input')
+ # # => <input type="file" id="attachment_file" name="attachment[file]" class="file_input" />
+ def file_field(method, options = {})
+ self.multipart = true
+ @template.file_field(@object_name, method, objectify_options(options))
+ end
+
+ # Add the submit button for the given form. When no value is given, it checks
+ # if the object is a new resource or not to create the proper label:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # In the example above, if @post is a new record, it will use "Create Post" as
+ # submit button label, otherwise, it uses "Update Post".
+ #
+ # Those labels can be customized using I18n, under the helpers.submit key and accept
+ # the %{model} as translation interpolation:
+ #
+ # en:
+ # helpers:
+ # submit:
+ # create: "Create a %{model}"
+ # update: "Confirm changes to %{model}"
+ #
+ # It also searches for a key specific for the given object:
+ #
+ # en:
+ # helpers:
+ # submit:
+ # post:
+ # create: "Add %{model}"
+ #
+ def submit(value=nil, options={})
+ value, options = nil, value if value.is_a?(Hash)
+ value ||= submit_default_value
+ @template.submit_tag(value, options)
+ end
+
+ # Add the submit button for the given form. When no value is given, it checks
+ # if the object is a new resource or not to create the proper label:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.button %>
+ # <% end %>
+ #
+ # In the example above, if @post is a new record, it will use "Create Post" as
+ # button label, otherwise, it uses "Update Post".
+ #
+ # Those labels can be customized using I18n, under the helpers.submit key
+ # (the same as submit helper) and accept the %{model} as translation interpolation:
+ #
+ # en:
+ # helpers:
+ # submit:
+ # create: "Create a %{model}"
+ # update: "Confirm changes to %{model}"
+ #
+ # It also searches for a key specific for the given object:
+ #
+ # en:
+ # helpers:
+ # submit:
+ # post:
+ # create: "Add %{model}"
+ #
+ # ==== Examples
+ # button("Create a post")
+ # # => <button name='button' type='submit'>Create post</button>
+ #
+ # button do
+ # content_tag(:strong, 'Ask me!')
+ # end
+ # # => <button name='button' type='submit'>
+ # # <strong>Ask me!</strong>
+ # # </button>
+ #
+ def button(value = nil, options = {}, &block)
+ value, options = nil, value if value.is_a?(Hash)
+ value ||= submit_default_value
+ @template.button_tag(value, options, &block)
+ end
+
+ def emitted_hidden_id?
+ @emitted_hidden_id ||= nil
+ end
+
+ private
+ def objectify_options(options)
+ @default_options.merge(options.merge(object: @object))
+ end
+
+ def submit_default_value
+ object = convert_to_model(@object)
+ key = object ? (object.persisted? ? :update : :create) : :submit
+
+ model = if object.class.respond_to?(:model_name)
+ object.class.model_name.human
+ else
+ @object_name.to_s.humanize
+ end
+
+ defaults = []
+ defaults << :"helpers.submit.#{object_name}.#{key}"
+ defaults << :"helpers.submit.#{key}"
+ defaults << "#{key.to_s.humanize} #{model}"
+
+ I18n.t(defaults.shift, model: model, default: defaults)
+ end
+
+ def nested_attributes_association?(association_name)
+ @object.respond_to?("#{association_name}_attributes=")
+ end
+
+ def fields_for_with_nested_attributes(association_name, association, options, block)
+ name = "#{object_name}[#{association_name}_attributes]"
+ association = convert_to_model(association)
+
+ if association.respond_to?(:persisted?)
+ association = [association] if @object.send(association_name).respond_to?(:to_ary)
+ elsif !association.respond_to?(:to_ary)
+ association = @object.send(association_name)
+ end
+
+ if association.respond_to?(:to_ary)
+ explicit_child_index = options[:child_index]
+ output = ActiveSupport::SafeBuffer.new
+ association.each do |child|
+ options[:child_index] = nested_child_index(name) unless explicit_child_index
+ output << fields_for_nested_model("#{name}[#{options[:child_index]}]", child, options, block)
+ end
+ output
+ elsif association
+ fields_for_nested_model(name, association, options, block)
+ end
+ end
+
+ def fields_for_nested_model(name, object, fields_options, block)
+ object = convert_to_model(object)
+ emit_hidden_id = object.persisted? && fields_options.fetch(:include_id) {
+ options.fetch(:include_id, true)
+ }
+
+ @template.fields_for(name, object, fields_options) do |f|
+ output = @template.capture(f, &block)
+ output.concat f.hidden_field(:id) if output && emit_hidden_id && !f.emitted_hidden_id?
+ output
+ end
+ end
+
+ def nested_child_index(name)
+ @nested_child_index[name] ||= -1
+ @nested_child_index[name] += 1
+ end
+ end
+ end
+
+ ActiveSupport.on_load(:action_view) do
+ cattr_accessor(:default_form_builder) { ::ActionView::Helpers::FormBuilder }
+ end
+end
diff --git a/actionview/lib/action_view/helpers/form_options_helper.rb b/actionview/lib/action_view/helpers/form_options_helper.rb
new file mode 100644
index 0000000000..4347983bad
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_options_helper.rb
@@ -0,0 +1,843 @@
+require 'cgi'
+require 'erb'
+require 'action_view/helpers/form_helper'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/array/extract_options'
+require 'active_support/core_ext/array/wrap'
+
+module ActionView
+ # = Action View Form Option Helpers
+ module Helpers
+ # Provides a number of methods for turning different kinds of containers into a set of option tags.
+ #
+ # The <tt>collection_select</tt>, <tt>select</tt> and <tt>time_zone_select</tt> methods take an <tt>options</tt> parameter, a hash:
+ #
+ # * <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})
+ #
+ # could become:
+ #
+ # <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.
+ #
+ # Example with @post.person_id => 2:
+ #
+ # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: 'None'})
+ #
+ # 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>
+ #
+ # * <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'})
+ #
+ # 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>
+ #
+ # 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 })
+ #
+ # becomes:
+ #
+ # <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'})
+ #
+ # could become:
+ #
+ # <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.
+ #
+ # 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>
+ #
+ module FormOptionsHelper
+ # ERB::Util can mask some helpers like textilize. Make sure to include them.
+ include TextHelper
+
+ # Create a select tag and a series of contained option tags for the provided object and method.
+ # The option currently held by the object will be selected, provided that the object is available.
+ #
+ # There are two possible formats for the +choices+ parameter, corresponding to other helpers' output:
+ #
+ # * A flat collection (see +options_for_select+).
+ #
+ # * A nested collection (see +grouped_options_for_select+).
+ #
+ # For example:
+ #
+ # select("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, { include_blank: true })
+ #
+ # would become:
+ #
+ # <select name="post[person_id]">
+ # <option value=""></option>
+ # <option value="1" selected="selected">David</option>
+ # <option value="2">Sam</option>
+ # <option value="3">Tobias</option>
+ # </select>
+ #
+ # assuming the associated person has ID 1.
+ #
+ # This can be used to provide a default set of options in the standard way: before rendering the create form, a
+ # new model instance is assigned the default options and bound to @model_name. Usually this model is not saved
+ # to the database. Instead, a second model object is created when the create request is received.
+ # This allows the user to submit a form page more than once with the expected results of creating multiple records.
+ # In addition, this allows a single partial to be used to generate form inputs for both edit and create forms.
+ #
+ # By default, <tt>post.person_id</tt> is the selected option. Specify <tt>selected: value</tt> to use a different selection
+ # or <tt>selected: nil</tt> to leave all options unselected. Similarly, you can specify values to be disabled in the option
+ # tags by specifying the <tt>:disabled</tt> option. This can either be a single value or an array of values to be disabled.
+ #
+ # A block can be passed to +select+ to customize how the options tags will be rendered. This
+ # is useful when the options tag has complex attributes.
+ #
+ # select(report, "campaign_ids") do
+ # available_campaigns.each do |c|
+ # content_tag(:option, c.name, value: c.id, data: { tags: c.tags.to_json })
+ # end
+ # end
+ #
+ # ==== Gotcha
+ #
+ # The HTML specification says when +multiple+ parameter passed to select and all options got deselected
+ # web browsers do not send any value to server. Unfortunately this introduces a gotcha:
+ # if an +User+ model has many +roles+ and have +role_ids+ accessor, and in the form that edits roles of the user
+ # the user deselects all roles from +role_ids+ multiple select box, no +role_ids+ parameter is sent. So,
+ # any mass-assignment idiom like
+ #
+ # @user.update(params[:user])
+ #
+ # wouldn't update roles.
+ #
+ # 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.
+ #
+ # In case if you don't want the helper to generate this hidden field you can specify
+ # <tt>include_hidden: false</tt> option.
+ #
+ def select(object, method, choices = nil, options = {}, html_options = {}, &block)
+ Tags::Select.new(object, method, self, choices, options, html_options, &block).render
+ end
+
+ # Returns <tt><select></tt> and <tt><option></tt> tags for the collection of existing return values of
+ # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
+ # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
+ # or <tt>:include_blank</tt> in the +options+ hash.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are methods to be called on each member
+ # of +collection+. The return values are used as the +value+ attribute and contents of each
+ # <tt><option></tt> tag, respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
+ #
+ # Example object structure for use with this method:
+ #
+ # class Post < ActiveRecord::Base
+ # belongs_to :author
+ # end
+ #
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
+ #
+ # collection_select(:post, :author_id, Author.all, :id, :name_with_initial, prompt: true)
+ #
+ # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
+ # <select name="post[author_id]">
+ # <option value="">Please select</option>
+ # <option value="1" selected="selected">D. Heinemeier Hansson</option>
+ # <option value="2">D. Thomas</option>
+ # <option value="3">M. Clark</option>
+ # </select>
+ def collection_select(object, method, collection, value_method, text_method, options = {}, html_options = {})
+ Tags::CollectionSelect.new(object, method, self, collection, value_method, text_method, options, html_options).render
+ end
+
+ # Returns <tt><select></tt>, <tt><optgroup></tt> and <tt><option></tt> tags for the collection of existing return values of
+ # +method+ for +object+'s class. The value returned from calling +method+ on the instance +object+ will
+ # be selected. If calling +method+ returns +nil+, no selection is made without including <tt>:prompt</tt>
+ # or <tt>:include_blank</tt> in the +options+ hash.
+ #
+ # Parameters:
+ # * +object+ - The instance of the class to be used for the select tag
+ # * +method+ - The attribute of +object+ corresponding to the select tag
+ # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
+ # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
+ # array of child objects representing the <tt><option></tt> tags.
+ # * +group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
+ # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
+ # * +option_key_method+ - The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
+ # * +option_value_method+ - The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
+ #
+ # Example object structure for use with this method:
+ #
+ # class Continent < ActiveRecord::Base
+ # has_many :countries
+ # # attribs: id, name
+ # end
+ #
+ # class Country < ActiveRecord::Base
+ # belongs_to :continent
+ # # attribs: id, name, continent_id
+ # end
+ #
+ # class City < ActiveRecord::Base
+ # belongs_to :country
+ # # attribs: id, name, country_id
+ # end
+ #
+ # Sample usage:
+ #
+ # grouped_collection_select(:city, :country_id, @continents, :countries, :name, :id, :name)
+ #
+ # Possible output:
+ #
+ # <select name="city[country_id]">
+ # <optgroup label="Africa">
+ # <option value="1">South Africa</option>
+ # <option value="3">Somalia</option>
+ # </optgroup>
+ # <optgroup label="Europe">
+ # <option value="7" selected="selected">Denmark</option>
+ # <option value="2">Ireland</option>
+ # </optgroup>
+ # </select>
+ #
+ def grouped_collection_select(object, method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
+ Tags::GroupedCollectionSelect.new(object, method, self, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options).render
+ end
+
+ # Return select and option tags for the given object and method, using
+ # #time_zone_options_for_select to generate the list of option tags.
+ #
+ # In addition to the <tt>:include_blank</tt> option documented above,
+ # this method also supports a <tt>:model</tt> option, which defaults
+ # to ActiveSupport::TimeZone. This may be used by users to specify a
+ # different time zone model object. (See +time_zone_options_for_select+
+ # for more information.)
+ #
+ # You can also supply an array of ActiveSupport::TimeZone objects
+ # as +priority_zones+, so that they will be listed above the rest of the
+ # (long) list. (You can use ActiveSupport::TimeZone.us_zones as a convenience
+ # for obtaining a list of the US time zones, or a Regexp to select the zones
+ # of your choice)
+ #
+ # Finally, this method supports a <tt>:default</tt> option, which selects
+ # a default ActiveSupport::TimeZone if the object's time zone is +nil+.
+ #
+ # time_zone_select( "user", "time_zone", nil, include_blank: true)
+ #
+ # time_zone_select( "user", "time_zone", nil, default: "Pacific Time (US & Canada)" )
+ #
+ # time_zone_select( "user", 'time_zone', ActiveSupport::TimeZone.us_zones, default: "Pacific Time (US & Canada)")
+ #
+ # time_zone_select( "user", 'time_zone', [ ActiveSupport::TimeZone['Alaska'], ActiveSupport::TimeZone['Hawaii'] ])
+ #
+ # time_zone_select( "user", 'time_zone', /Australia/)
+ #
+ # time_zone_select( "user", "time_zone", ActiveSupport::TimeZone.all.sort, model: ActiveSupport::TimeZone)
+ def time_zone_select(object, method, priority_zones = nil, options = {}, html_options = {})
+ Tags::TimeZoneSelect.new(object, method, self, priority_zones, options, html_options).render
+ end
+
+ # Accepts a container (hash, array, enumerable, your type) and returns a string of option tags. Given a container
+ # where the elements respond to first and last (such as a two-element array), the "lasts" serve as option values and
+ # the "firsts" as option text. Hashes are turned into this form automatically, so the keys become "firsts" and values
+ # become lasts. If +selected+ is specified, the matching "last" or element will get the selected option-tag. +selected+
+ # may also be an array of values to be selected when using a multiple select.
+ #
+ # options_for_select([["Dollar", "$"], ["Kroner", "DKK"]])
+ # # => <option value="$">Dollar</option>
+ # # => <option value="DKK">Kroner</option>
+ #
+ # options_for_select([ "VISA", "MasterCard" ], "MasterCard")
+ # # => <option>VISA</option>
+ # # => <option selected="selected">MasterCard</option>
+ #
+ # options_for_select({ "Basic" => "$20", "Plus" => "$40" }, "$40")
+ # # => <option value="$20">Basic</option>
+ # # => <option value="$40" selected="selected">Plus</option>
+ #
+ # options_for_select([ "VISA", "MasterCard", "Discover" ], ["VISA", "Discover"])
+ # # => <option selected="selected">VISA</option>
+ # # => <option>MasterCard</option>
+ # # => <option selected="selected">Discover</option>
+ #
+ # You can optionally provide html attributes as the last element of the array.
+ #
+ # options_for_select([ "Denmark", ["USA", {class: 'bold'}], "Sweden" ], ["USA", "Sweden"])
+ # # => <option value="Denmark">Denmark</option>
+ # # => <option value="USA" class="bold" selected="selected">USA</option>
+ # # => <option value="Sweden" selected="selected">Sweden</option>
+ #
+ # options_for_select([["Dollar", "$", {class: "bold"}], ["Kroner", "DKK", {onclick: "alert('HI');"}]])
+ # # => <option value="$" class="bold">Dollar</option>
+ # # => <option value="DKK" onclick="alert('HI');">Kroner</option>
+ #
+ # If you wish to specify disabled option tags, set +selected+ to be a hash, with <tt>:disabled</tt> being either a value
+ # or array of values to be disabled. In this case, you can use <tt>:selected</tt> to specify selected option tags.
+ #
+ # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: "Super Platinum")
+ # # => <option value="Free">Free</option>
+ # # => <option value="Basic">Basic</option>
+ # # => <option value="Advanced">Advanced</option>
+ # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
+ #
+ # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], disabled: ["Advanced", "Super Platinum"])
+ # # => <option value="Free">Free</option>
+ # # => <option value="Basic">Basic</option>
+ # # => <option value="Advanced" disabled="disabled">Advanced</option>
+ # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
+ #
+ # options_for_select(["Free", "Basic", "Advanced", "Super Platinum"], selected: "Free", disabled: "Super Platinum")
+ # # => <option value="Free" selected="selected">Free</option>
+ # # => <option value="Basic">Basic</option>
+ # # => <option value="Advanced">Advanced</option>
+ # # => <option value="Super Platinum" disabled="disabled">Super Platinum</option>
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in a regular HTML select tag.
+ def options_for_select(container, selected = nil)
+ return container if String === container
+
+ selected, disabled = extract_selected_and_disabled(selected).map do |r|
+ Array(r).map { |item| item.to_s }
+ end
+
+ container.map do |element|
+ 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[:value] = value
+
+ content_tag_string(:option, text, html_attributes)
+ end.join("\n").html_safe
+ end
+
+ # Returns a string of option tags that have been compiled by iterating over the +collection+ and assigning
+ # the result of a call to the +value_method+ as the option value and the +text_method+ as the option text.
+ #
+ # options_from_collection_for_select(@people, 'id', 'name')
+ # # => <option value="#{person.id}">#{person.name}</option>
+ #
+ # This is more often than not used inside a #select_tag like this example:
+ #
+ # select_tag 'person', options_from_collection_for_select(@people, 'id', 'name')
+ #
+ # If +selected+ is specified as a value or array of values, the element(s) returning a match on +value_method+
+ # will be selected option tag(s).
+ #
+ # If +selected+ is specified as a Proc, those members of the collection that return true for the anonymous
+ # function are the selected values.
+ #
+ # +selected+ can also be a hash, specifying both <tt>:selected</tt> and/or <tt>:disabled</tt> values as required.
+ #
+ # Be sure to specify the same class as the +value_method+ when specifying selected or disabled options.
+ # Failure to do this will produce undesired results. Example:
+ # options_from_collection_for_select(@people, 'id', 'name', '1')
+ # Will not select a person with the id of 1 because 1 (an Integer) is not the same as '1' (a string)
+ # options_from_collection_for_select(@people, 'id', 'name', 1)
+ # should produce the desired results.
+ def options_from_collection_for_select(collection, value_method, text_method, selected = nil)
+ options = collection.map do |element|
+ [value_for_collection(element, text_method), value_for_collection(element, value_method), option_html_attributes(element)]
+ end
+ selected, disabled = extract_selected_and_disabled(selected)
+ select_deselect = {
+ selected: extract_values_from_collection(collection, value_method, selected),
+ disabled: extract_values_from_collection(collection, value_method, disabled)
+ }
+
+ options_for_select(options, select_deselect)
+ end
+
+ # Returns a string of <tt><option></tt> tags, like <tt>options_from_collection_for_select</tt>, but
+ # groups them by <tt><optgroup></tt> tags based on the object relationships of the arguments.
+ #
+ # Parameters:
+ # * +collection+ - An array of objects representing the <tt><optgroup></tt> tags.
+ # * +group_method+ - The name of a method which, when called on a member of +collection+, returns an
+ # array of child objects representing the <tt><option></tt> tags.
+ # * group_label_method+ - The name of a method which, when called on a member of +collection+, returns a
+ # string to be used as the +label+ attribute for its <tt><optgroup></tt> tag.
+ # * +option_key_method+ - The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the +value+ attribute for its <tt><option></tt> tag.
+ # * +option_value_method+ - The name of a method which, when called on a child object of a member of
+ # +collection+, returns a value to be used as the contents of its <tt><option></tt> tag.
+ # * +selected_key+ - A value equal to the +value+ attribute for one of the <tt><option></tt> tags,
+ # which will have the +selected+ attribute set. Corresponds to the return value of one of the calls
+ # to +option_key_method+. If +nil+, no selection is made. Can also be a hash if disabled values are
+ # to be specified.
+ #
+ # Example object structure for use with this method:
+ #
+ # class Continent < ActiveRecord::Base
+ # has_many :countries
+ # # attribs: id, name
+ # end
+ #
+ # class Country < ActiveRecord::Base
+ # belongs_to :continent
+ # # attribs: id, name, continent_id
+ # end
+ #
+ # Sample usage:
+ # option_groups_from_collection_for_select(@continents, :countries, :name, :id, :name, 3)
+ #
+ # Possible output:
+ # <optgroup label="Africa">
+ # <option value="1">Egypt</option>
+ # <option value="4">Rwanda</option>
+ # ...
+ # </optgroup>
+ # <optgroup label="Asia">
+ # <option value="3" selected="selected">China</option>
+ # <option value="12">India</option>
+ # <option value="5">Japan</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.
+ def option_groups_from_collection_for_select(collection, group_method, group_label_method, option_key_method, option_value_method, selected_key = nil)
+ collection.map do |group|
+ option_tags = options_from_collection_for_select(
+ group.send(group_method), option_key_method, option_value_method, selected_key)
+
+ content_tag(:optgroup, option_tags, label: group.send(group_label_method))
+ end.join.html_safe
+ 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.
+ #
+ # grouped_options = [
+ # ['North America',
+ # [['United States','US'],'Canada']],
+ # ['Europe',
+ # ['Denmark','Germany','France']]
+ # ]
+ # grouped_options_for_select(grouped_options)
+ #
+ # grouped_options = {
+ # 'North America' => [['United States','US'], 'Canada'],
+ # 'Europe' => ['Denmark','Germany','France']
+ # }
+ # grouped_options_for_select(grouped_options)
+ #
+ # Possible output:
+ # <optgroup label="North America">
+ # <option value="US">United States</option>
+ # <option value="Canada">Canada</option>
+ # </optgroup>
+ # <optgroup label="Europe">
+ # <option value="Denmark">Denmark</option>
+ # <option value="Germany">Germany</option>
+ # <option value="France">France</option>
+ # </optgroup>
+ #
+ # 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.
+ def grouped_options_for_select(grouped_options, selected_key = nil, options = {})
+ prompt = options[:prompt]
+ divider = options[:divider]
+
+ body = "".html_safe
+
+ if prompt
+ body.safe_concat content_tag(:option, prompt_text(prompt), value: "")
+ end
+
+ grouped_options.each do |container|
+ html_attributes = option_html_attributes(container)
+
+ if divider
+ label = divider
+ else
+ label, container = container
+ end
+
+ html_attributes = { label: label }.merge!(html_attributes)
+ body.safe_concat content_tag(:optgroup, options_for_select(container, selected_key), html_attributes)
+ end
+
+ body
+ end
+
+ # Returns a string of option tags for pretty much any time zone in the
+ # world. Supply a ActiveSupport::TimeZone name as +selected+ to have it
+ # marked as the selected option tag. You can also supply an array of
+ # ActiveSupport::TimeZone objects as +priority_zones+, so that they will
+ # be listed above the rest of the (long) list. (You can use
+ # ActiveSupport::TimeZone.us_zones as a convenience for obtaining a list
+ # of the US time zones, or a Regexp to select the zones of your choice)
+ #
+ # The +selected+ parameter must be either +nil+, or a string that names
+ # a ActiveSupport::TimeZone.
+ #
+ # By default, +model+ is the ActiveSupport::TimeZone constant (which can
+ # be obtained in Active Record as a value object). The only requirement
+ # is that the +model+ parameter be an object that responds to +all+, and
+ # returns an array of objects that represent time zones.
+ #
+ # NOTE: Only the option tags are returned, you have to wrap this call in
+ # a regular HTML select tag.
+ def time_zone_options_for_select(selected = nil, priority_zones = nil, model = ::ActiveSupport::TimeZone)
+ zone_options = "".html_safe
+
+ zones = model.all
+ convert_zones = lambda { |list| list.map { |z| [ z.to_s, z.name ] } }
+
+ if priority_zones
+ if priority_zones.is_a?(Regexp)
+ priority_zones = zones.select { |z| z =~ priority_zones }
+ end
+
+ zone_options.safe_concat options_for_select(convert_zones[priority_zones], selected)
+ zone_options.safe_concat content_tag(:option, '-------------', value: '', disabled: true)
+ zone_options.safe_concat "\n"
+
+ zones = zones - priority_zones
+ end
+
+ zone_options.safe_concat options_for_select(convert_zones[zones], selected)
+ end
+
+ # Returns radio button tags for the collection of existing return values
+ # of +method+ for +object+'s class. The value returned from calling
+ # +method+ on the instance +object+ will be selected. If calling +method+
+ # returns +nil+, no selection is made.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
+ # methods to be called on each member of +collection+. The return values
+ # are used as the +value+ attribute and contents of each radio button tag,
+ # respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
+ #
+ # Example object structure for use with this method:
+ # class Post < ActiveRecord::Base
+ # belongs_to :author
+ # end
+ # class Author < ActiveRecord::Base
+ # has_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial)
+ #
+ # If <tt>@post.author_id</tt> is already <tt>1</tt>, this would return:
+ # <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>
+ #
+ # It is also possible to customize the way the elements will be shown by
+ # giving a block to the method:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label { b.radio_button }
+ # end
+ #
+ # The argument passed to the block is a special kind of builder for this
+ # collection, which has the ability to generate the label and radio button
+ # for the current item in the collection, with proper text and value.
+ # Using it, you can change the label and radio button display order or
+ # even use the label as wrapper, as in the example above.
+ #
+ # The builder methods <tt>label</tt> and <tt>radio_button</tt> also accept
+ # extra html options:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label(class: "radio_button") { b.radio_button(class: "radio_button") }
+ # end
+ #
+ # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
+ # <tt>value</tt>, which are the current item being rendered, its text and value methods,
+ # respectively. You can use them like this:
+ # collection_radio_buttons(:post, :author_id, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:"data-value" => b.value) { b.radio_button + b.text }
+ # end
+ def collection_radio_buttons(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ Tags::CollectionRadioButtons.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
+ end
+
+ # Returns check box tags for the collection of existing return values of
+ # +method+ for +object+'s class. The value returned from calling +method+
+ # on the instance +object+ will be selected. If calling +method+ returns
+ # +nil+, no selection is made.
+ #
+ # The <tt>:value_method</tt> and <tt>:text_method</tt> parameters are
+ # methods to be called on each member of +collection+. The return values
+ # are used as the +value+ attribute and contents of each check box tag,
+ # respectively. They can also be any object that responds to +call+, such
+ # as a +proc+, that will be called for each member of the +collection+ to
+ # retrieve the value/text.
+ #
+ # Example object structure for use with this method:
+ # class Post < ActiveRecord::Base
+ # has_and_belongs_to_many :authors
+ # end
+ # class Author < ActiveRecord::Base
+ # has_and_belongs_to_many :posts
+ # def name_with_initial
+ # "#{first_name.first}. #{last_name}"
+ # end
+ # end
+ #
+ # Sample usage (selecting the associated Author for an instance of Post, <tt>@post</tt>):
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial)
+ #
+ # If <tt>@post.author_ids</tt> is already <tt>[1]</tt>, this would return:
+ # <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="" />
+ #
+ # It is also possible to customize the way the elements will be shown by
+ # giving a block to the method:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label { b.check_box }
+ # end
+ #
+ # The argument passed to the block is a special kind of builder for this
+ # collection, which has the ability to generate the label and check box
+ # for the current item in the collection, with proper text and value.
+ # Using it, you can change the label and check box display order or even
+ # use the label as wrapper, as in the example above.
+ #
+ # The builder methods <tt>label</tt> and <tt>check_box</tt> also accept
+ # extra html options:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label(class: "check_box") { b.check_box(class: "check_box") }
+ # end
+ #
+ # There are also three special methods available: <tt>object</tt>, <tt>text</tt> and
+ # <tt>value</tt>, which are the current item being rendered, its text and value methods,
+ # respectively. You can use them like this:
+ # collection_check_boxes(:post, :author_ids, Author.all, :id, :name_with_initial) do |b|
+ # b.label(:"data-value" => b.value) { b.check_box + b.text }
+ # end
+ def collection_check_boxes(object, method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ Tags::CollectionCheckBoxes.new(object, method, self, collection, value_method, text_method, options, html_options).render(&block)
+ end
+
+ private
+ def option_html_attributes(element)
+ if Array === element
+ element.select { |e| Hash === e }.reduce({}, :merge!)
+ else
+ {}
+ end
+ end
+
+ def option_text_and_value(option)
+ # Options are [text, value] pairs or strings used for both.
+ if !option.is_a?(String) && option.respond_to?(:first) && option.respond_to?(:last)
+ option = option.reject { |e| Hash === e } if Array === option
+ [option.first, option.last]
+ else
+ [option, option]
+ end
+ end
+
+ def option_value_selected?(value, selected)
+ Array(selected).include? value
+ end
+
+ def extract_selected_and_disabled(selected)
+ if selected.is_a?(Proc)
+ [selected, nil]
+ else
+ selected = Array.wrap(selected)
+ options = selected.extract_options!.symbolize_keys
+ selected_items = options.fetch(:selected, selected)
+ [selected_items, options[:disabled]]
+ end
+ end
+
+ def extract_values_from_collection(collection, value_method, selected)
+ if selected.is_a?(Proc)
+ collection.map do |element|
+ element.send(value_method) if selected.call(element)
+ end.compact
+ else
+ selected
+ end
+ end
+
+ def value_for_collection(item, value)
+ value.respond_to?(:call) ? value.call(item) : item.send(value)
+ end
+
+ def prompt_text(prompt)
+ prompt.kind_of?(String) ? prompt : I18n.translate('helpers.select.prompt', default: 'Please select')
+ end
+ end
+
+ class FormBuilder
+ # Wraps ActionView::Helpers::FormOptionsHelper#select for form builders:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.select :person_id, Person.all.collect { |p| [ p.name, p.id ] }, include_blank: true %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def select(method, choices = nil, options = {}, html_options = {}, &block)
+ @template.select(@object_name, method, choices, objectify_options(options), @default_options.merge(html_options), &block)
+ end
+
+ # Wraps ActionView::Helpers::FormOptionsHelper#collection_select for form builders:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.collection_select :person_id, Author.all, :id, :name_with_initial, prompt: true %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def collection_select(method, collection, value_method, text_method, options = {}, html_options = {})
+ @template.collection_select(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options))
+ end
+
+ # Wraps ActionView::Helpers::FormOptionsHelper#grouped_collection_select for form builders:
+ #
+ # <%= form_for @city do |f| %>
+ # <%= f.grouped_collection_select :country_id, @continents, :countries, :name, :id, :name %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def grouped_collection_select(method, collection, group_method, group_label_method, option_key_method, option_value_method, options = {}, html_options = {})
+ @template.grouped_collection_select(@object_name, method, collection, group_method, group_label_method, option_key_method, option_value_method, objectify_options(options), @default_options.merge(html_options))
+ end
+
+ # Wraps ActionView::Helpers::FormOptionsHelper#time_zone_select for form builders:
+ #
+ # <%= form_for @user do |f| %>
+ # <%= f.time_zone_select :time_zone, nil, include_blank: true %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def time_zone_select(method, priority_zones = nil, options = {}, html_options = {})
+ @template.time_zone_select(@object_name, method, priority_zones, objectify_options(options), @default_options.merge(html_options))
+ end
+
+ # Wraps ActionView::Helpers::FormOptionsHelper#collection_check_boxes for form builders:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.collection_check_boxes :author_ids, Author.all, :id, :name_with_initial %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def collection_check_boxes(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ @template.collection_check_boxes(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
+ end
+
+ # Wraps ActionView::Helpers::FormOptionsHelper#collection_radio_buttons for form builders:
+ #
+ # <%= form_for @post do |f| %>
+ # <%= f.collection_radio_buttons :author_id, Author.all, :id, :name_with_initial %>
+ # <%= f.submit %>
+ # <% end %>
+ #
+ # Please refer to the documentation of the base helper for details.
+ def collection_radio_buttons(method, collection, value_method, text_method, options = {}, html_options = {}, &block)
+ @template.collection_radio_buttons(@object_name, method, collection, value_method, text_method, objectify_options(options), @default_options.merge(html_options), &block)
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/form_tag_helper.rb b/actionview/lib/action_view/helpers/form_tag_helper.rb
new file mode 100644
index 0000000000..142c27ace0
--- /dev/null
+++ b/actionview/lib/action_view/helpers/form_tag_helper.rb
@@ -0,0 +1,746 @@
+require 'cgi'
+require 'action_view/helpers/tag_helper'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/core_ext/module/attribute_accessors'
+
+module ActionView
+ # = Action View Form Tag Helpers
+ module Helpers
+ # Provides a number of methods for creating form tags that don't rely on an Active Record object assigned to the template like
+ # FormHelper does. Instead, you provide the names and values manually.
+ #
+ # NOTE: The HTML options <tt>disabled</tt>, <tt>readonly</tt>, and <tt>multiple</tt> can all be treated as booleans. So specifying
+ # <tt>disabled: true</tt> will give <tt>disabled="disabled"</tt>.
+ module FormTagHelper
+ extend ActiveSupport::Concern
+
+ include UrlHelper
+ include TextHelper
+
+ mattr_accessor :embed_authenticity_token_in_remote_forms
+ self.embed_authenticity_token_in_remote_forms = false
+
+ # Starts a form tag that points the action to an url configured with <tt>url_for_options</tt> just like
+ # ActionController::Base#url_for. The method for the form defaults to POST.
+ #
+ # ==== Options
+ # * <tt>:multipart</tt> - If set to true, the enctype is set to "multipart/form-data".
+ # * <tt>:method</tt> - The method to use when submitting the form, usually either "get" or "post".
+ # If "patch", "put", "delete", or another verb is used, a hidden input with name <tt>_method</tt>
+ # is added to simulate the verb over post.
+ # * <tt>:authenticity_token</tt> - Authenticity token to use in the form. Use only if you need to
+ # pass custom authenticity token string, or to not add authenticity_token field at all
+ # (by passing <tt>false</tt>). Remote forms may omit the embedded authenticity token
+ # by setting <tt>config.action_view.embed_authenticity_token_in_remote_forms = false</tt>.
+ # This is helpful when you're fragment-caching the form. Remote forms get the
+ # authenticity token from the <tt>meta</tt> tag, so embedding is unnecessary unless you
+ # support browsers without JavaScript.
+ # * A list of parameters to feed to the URL the form will be posted to.
+ # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
+ # submit behavior. By default this behavior is an ajax submit.
+ # * <tt>:enforce_utf8</tt> - If set to false, a hidden input with name utf8 is not output.
+ #
+ # ==== Examples
+ # form_tag('/posts')
+ # # => <form action="/posts" method="post">
+ #
+ # form_tag('/posts/1', method: :put)
+ # # => <form action="/posts/1" method="post"> ... <input name="_method" type="hidden" value="put" /> ...
+ #
+ # form_tag('/upload', multipart: true)
+ # # => <form action="/upload" method="post" enctype="multipart/form-data">
+ #
+ # <%= form_tag('/posts') do -%>
+ # <div><%= submit_tag 'Save' %></div>
+ # <% end -%>
+ # # => <form action="/posts" method="post"><div><input type="submit" name="commit" value="Save" /></div></form>
+ #
+ # <%= form_tag('/posts', remote: true) %>
+ # # => <form action="/posts" method="post" data-remote="true">
+ #
+ # form_tag('http://far.away.com/form', authenticity_token: false)
+ # # form without authenticity token
+ #
+ # form_tag('http://far.away.com/form', authenticity_token: "cf50faa3fe97702ca1ae")
+ # # form with custom authenticity token
+ #
+ 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)
+ else
+ form_tag_html(html_options)
+ end
+ end
+
+ # Creates a dropdown selection box, or if the <tt>:multiple</tt> option is set to true, a multiple
+ # choice selection box.
+ #
+ # Helpers::FormOptions can be used to create common select boxes such as countries, time zones, or
+ # associated records. <tt>option_tags</tt> is a string containing the option tags for the select box.
+ #
+ # ==== 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
+ # * 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", "<option>David</option>".html_safe
+ # # => <select id="people" name="people"><option>David</option></select>
+ #
+ # select_tag "count", "<option>1</option><option>2</option><option>3</option><option>4</option>".html_safe
+ # # => <select id="count" name="count"><option>1</option><option>2</option>
+ # # <option>3</option><option>4</option></select>
+ #
+ # select_tag "colors", "<option>Red</option><option>Green</option><option>Blue</option>".html_safe, multiple: true
+ # # => <select id="colors" multiple="multiple" name="colors[]"><option>Red</option>
+ # # <option>Green</option><option>Blue</option></select>
+ #
+ # select_tag "locations", "<option>Home</option><option selected='selected'>Work</option><option>Out</option>".html_safe
+ # # => <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>
+ # # <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"), prompt: "Select something"
+ # # => <select id="people" name="people"><option value="">Select something</option><option value="1">David</option></select>
+ #
+ # select_tag "destination", "<option>NYC</option><option>Paris</option><option>Rome</option>".html_safe, disabled: true
+ # # => <select disabled="disabled" id="destination" name="destination"><option>NYC</option>
+ # # <option>Paris</option><option>Rome</option></select>
+ #
+ # select_tag "credit_card", options_for_select([ "VISA", "MasterCard" ], "MasterCard")
+ # # => <select id="credit_card" name="credit_card"><option>VISA</option>
+ # # <option selected="selected">MasterCard</option></select>
+ def select_tag(name, option_tags = nil, options = {})
+ option_tags ||= ""
+ html_name = (options[:multiple] == true && !name.to_s.ends_with?("[]")) ? "#{name}[]" : name
+
+ if options.delete(:include_blank)
+ option_tags = content_tag(:option, '', :value => '').safe_concat(option_tags)
+ end
+
+ if prompt = options.delete(:prompt)
+ option_tags = content_tag(:option, prompt, :value => '').safe_concat(option_tags)
+ end
+
+ content_tag :select, option_tags, { "name" => html_name, "id" => sanitize_to_id(name) }.update(options.stringify_keys)
+ end
+
+ # Creates a standard text field; use these text fields to input smaller chunks of text like a username
+ # or a search query.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:size</tt> - The number of visible characters that will fit in the input.
+ # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
+ # * <tt>:placeholder</tt> - The text contained in the field by default which is removed when the field receives focus.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # text_field_tag 'name'
+ # # => <input id="name" name="name" type="text" />
+ #
+ # text_field_tag 'query', 'Enter your search query here'
+ # # => <input id="query" name="query" type="text" value="Enter your search query here" />
+ #
+ # text_field_tag 'search', nil, placeholder: 'Enter search term...'
+ # # => <input id="search" name="search" placeholder="Enter search term..." type="text" />
+ #
+ # text_field_tag 'request', nil, class: 'special_input'
+ # # => <input class="special_input" id="request" name="request" type="text" />
+ #
+ # text_field_tag 'address', '', size: 75
+ # # => <input id="address" name="address" size="75" type="text" value="" />
+ #
+ # text_field_tag 'zip', nil, maxlength: 5
+ # # => <input id="zip" maxlength="5" name="zip" type="text" />
+ #
+ # text_field_tag 'payment_amount', '$0.00', disabled: true
+ # # => <input disabled="disabled" id="payment_amount" name="payment_amount" type="text" value="$0.00" />
+ #
+ # text_field_tag 'ip', '0.0.0.0', maxlength: 15, size: 20, class: "ip-input"
+ # # => <input class="ip-input" id="ip" maxlength="15" name="ip" size="20" type="text" value="0.0.0.0" />
+ def text_field_tag(name, value = nil, options = {})
+ tag :input, { "type" => "text", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
+ end
+
+ # Creates a label element. Accepts a block.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # label_tag 'name'
+ # # => <label for="name">Name</label>
+ #
+ # label_tag 'name', 'Your name'
+ # # => <label for="name">Your name</label>
+ #
+ # label_tag 'name', nil, class: 'small_label'
+ # # => <label for="name" class="small_label">Name</label>
+ def label_tag(name = nil, content_or_options = nil, options = nil, &block)
+ if block_given? && content_or_options.is_a?(Hash)
+ options = content_or_options = content_or_options.stringify_keys
+ else
+ options ||= {}
+ options = options.stringify_keys
+ end
+ options["for"] = sanitize_to_id(name) unless name.blank? || options.has_key?("for")
+ content_tag :label, content_or_options || name.to_s.humanize, options, &block
+ end
+
+ # Creates a hidden form input field used to transmit data that would be lost due to HTTP's statelessness or
+ # data that should be hidden from the user.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # hidden_field_tag 'tags_list'
+ # # => <input id="tags_list" name="tags_list" type="hidden" />
+ #
+ # hidden_field_tag 'token', 'VUBJKB23UIVI1UU1VOBVI@'
+ # # => <input id="token" name="token" type="hidden" value="VUBJKB23UIVI1UU1VOBVI@" />
+ #
+ # hidden_field_tag 'collected_input', '', onchange: "alert('Input collected!')"
+ # # => <input id="collected_input" name="collected_input" onchange="alert('Input collected!')"
+ # # type="hidden" value="" />
+ def hidden_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "hidden"))
+ end
+
+ # Creates a file upload field. If you are using file uploads then you will also need
+ # to set the multipart option for the form tag:
+ #
+ # <%= form_tag '/upload', multipart: true do %>
+ # <label for="file">File to Upload</label> <%= file_field_tag "file" %>
+ # <%= submit_tag %>
+ # <% end %>
+ #
+ # The specified URL will then be passed a File object containing the selected file, or if the field
+ # was left blank, a StringIO object.
+ #
+ # ==== Options
+ # * Creates standard HTML attributes for the tag.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:multiple</tt> - If set to true, *in most updated browsers* the user will be allowed to select multiple files.
+ # * <tt>:accept</tt> - If set to one or multiple mime-types, the user will be suggested a filter when choosing a file. You still need to set up model validations.
+ #
+ # ==== Examples
+ # file_field_tag 'attachment'
+ # # => <input id="attachment" name="attachment" type="file" />
+ #
+ # file_field_tag 'avatar', class: 'profile_input'
+ # # => <input class="profile_input" id="avatar" name="avatar" type="file" />
+ #
+ # file_field_tag 'picture', disabled: true
+ # # => <input disabled="disabled" id="picture" name="picture" type="file" />
+ #
+ # file_field_tag 'resume', value: '~/resume.doc'
+ # # => <input id="resume" name="resume" type="file" value="~/resume.doc" />
+ #
+ # file_field_tag 'user_pic', accept: 'image/png,image/gif,image/jpeg'
+ # # => <input accept="image/png,image/gif,image/jpeg" id="user_pic" name="user_pic" type="file" />
+ #
+ # file_field_tag 'file', accept: 'text/html', class: 'upload', value: 'index.html'
+ # # => <input accept="text/html" class="upload" id="file" name="file" type="file" value="index.html" />
+ def file_field_tag(name, options = {})
+ text_field_tag(name, nil, options.update("type" => "file"))
+ end
+
+ # Creates a password field, a masked text field that will hide the users input behind a mask character.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:size</tt> - The number of visible characters that will fit in the input.
+ # * <tt>:maxlength</tt> - The maximum number of characters that the browser will allow the user to enter.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # password_field_tag 'pass'
+ # # => <input id="pass" name="pass" type="password" />
+ #
+ # password_field_tag 'secret', 'Your secret here'
+ # # => <input id="secret" name="secret" type="password" value="Your secret here" />
+ #
+ # password_field_tag 'masked', nil, class: 'masked_input_field'
+ # # => <input class="masked_input_field" id="masked" name="masked" type="password" />
+ #
+ # password_field_tag 'token', '', size: 15
+ # # => <input id="token" name="token" size="15" type="password" value="" />
+ #
+ # password_field_tag 'key', nil, maxlength: 16
+ # # => <input id="key" maxlength="16" name="key" type="password" />
+ #
+ # password_field_tag 'confirm_pass', nil, disabled: true
+ # # => <input disabled="disabled" id="confirm_pass" name="confirm_pass" type="password" />
+ #
+ # password_field_tag 'pin', '1234', maxlength: 4, size: 6, class: "pin_input"
+ # # => <input class="pin_input" id="pin" maxlength="4" name="pin" size="6" type="password" value="1234" />
+ def password_field_tag(name = "password", value = nil, options = {})
+ text_field_tag(name, value, options.update("type" => "password"))
+ end
+
+ # Creates a text input area; use a textarea for longer text inputs such as blog posts or descriptions.
+ #
+ # ==== Options
+ # * <tt>:size</tt> - A string specifying the dimensions (columns by rows) of the textarea (e.g., "25x10").
+ # * <tt>:rows</tt> - Specify the number of rows in the textarea
+ # * <tt>:cols</tt> - Specify the number of columns in the textarea
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * <tt>:escape</tt> - By default, the contents of the text input are HTML escaped.
+ # If you need unescaped contents, set this to false.
+ # * Any other key creates standard HTML attributes for the tag.
+ #
+ # ==== Examples
+ # text_area_tag 'post'
+ # # => <textarea id="post" name="post"></textarea>
+ #
+ # text_area_tag 'bio', @user.bio
+ # # => <textarea id="bio" name="bio">This is my biography.</textarea>
+ #
+ # text_area_tag 'body', nil, rows: 10, cols: 25
+ # # => <textarea cols="25" id="body" name="body" rows="10"></textarea>
+ #
+ # text_area_tag 'body', nil, size: "25x10"
+ # # => <textarea name="body" id="body" cols="25" rows="10"></textarea>
+ #
+ # text_area_tag 'description', "Description goes here.", disabled: true
+ # # => <textarea disabled="disabled" id="description" name="description">Description goes here.</textarea>
+ #
+ # text_area_tag 'comment', nil, class: 'comment_input'
+ # # => <textarea class="comment_input" id="comment" name="comment"></textarea>
+ def text_area_tag(name, content = nil, options = {})
+ options = options.stringify_keys
+
+ if size = options.delete("size")
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
+ end
+
+ escape = options.delete("escape") { true }
+ content = ERB::Util.html_escape(content) if escape
+
+ content_tag :textarea, content.to_s.html_safe, { "name" => name, "id" => sanitize_to_id(name) }.update(options)
+ end
+
+ # Creates a check box form input tag.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # check_box_tag 'accept'
+ # # => <input id="accept" name="accept" type="checkbox" value="1" />
+ #
+ # check_box_tag 'rock', 'rock music'
+ # # => <input id="rock" name="rock" type="checkbox" value="rock music" />
+ #
+ # check_box_tag 'receive_email', 'yes', true
+ # # => <input checked="checked" id="receive_email" name="receive_email" type="checkbox" value="yes" />
+ #
+ # check_box_tag 'tos', 'yes', false, class: 'accept_tos'
+ # # => <input class="accept_tos" id="tos" name="tos" type="checkbox" value="yes" />
+ #
+ # check_box_tag 'eula', 'accepted', false, disabled: true
+ # # => <input disabled="disabled" id="eula" name="eula" type="checkbox" value="accepted" />
+ def check_box_tag(name, value = "1", checked = false, options = {})
+ html_options = { "type" => "checkbox", "name" => name, "id" => sanitize_to_id(name), "value" => value }.update(options.stringify_keys)
+ html_options["checked"] = "checked" if checked
+ tag :input, html_options
+ end
+
+ # Creates a radio button; use groups of radio buttons named the same to allow users to
+ # select from a group of options.
+ #
+ # ==== Options
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Examples
+ # radio_button_tag 'gender', 'male'
+ # # => <input id="gender_male" name="gender" type="radio" value="male" />
+ #
+ # radio_button_tag 'receive_updates', 'no', true
+ # # => <input checked="checked" id="receive_updates_no" name="receive_updates" type="radio" value="no" />
+ #
+ # radio_button_tag 'time_slot', "3:00 p.m.", false, disabled: true
+ # # => <input disabled="disabled" id="time_slot_300_pm" name="time_slot" type="radio" value="3:00 p.m." />
+ #
+ # radio_button_tag 'color', "green", true, class: "color_input"
+ # # => <input checked="checked" class="color_input" id="color_green" name="color" type="radio" value="green" />
+ def radio_button_tag(name, value, checked = false, options = {})
+ html_options = { "type" => "radio", "name" => name, "id" => "#{sanitize_to_id(name)}_#{sanitize_to_id(value)}", "value" => value }.update(options.stringify_keys)
+ html_options["checked"] = "checked" if checked
+ tag :input, html_options
+ end
+
+ # Creates a submit button with the text <tt>value</tt> as the caption.
+ #
+ # ==== Options
+ # * <tt>:data</tt> - This option can be used to add custom data attributes.
+ # * <tt>:disabled</tt> - If true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Data attributes
+ #
+ # * <tt>confirm: 'question?'</tt> - If present the unobtrusive JavaScript
+ # drivers will provide a prompt with the question specified. If the user accepts,
+ # the form is processed normally, otherwise no action is taken.
+ # * <tt>:disable_with</tt> - Value of this parameter will be used as the value for a
+ # disabled version of the submit button when the form is submitted. This feature is
+ # provided by the unobtrusive JavaScript driver.
+ #
+ # ==== Examples
+ # submit_tag
+ # # => <input name="commit" type="submit" value="Save changes" />
+ #
+ # submit_tag "Edit this article"
+ # # => <input name="commit" type="submit" value="Edit this article" />
+ #
+ # submit_tag "Save edits", disabled: true
+ # # => <input disabled="disabled" name="commit" type="submit" value="Save edits" />
+ #
+ # submit_tag "Complete sale", data: { disable_with: "Please wait..." }
+ # # => <input name="commit" data-disable-with="Please wait..." type="submit" value="Complete sale" />
+ #
+ # submit_tag nil, class: "form_submit"
+ # # => <input class="form_submit" name="commit" type="submit" />
+ #
+ # submit_tag "Edit", class: "edit_button"
+ # # => <input class="edit_button" name="commit" type="submit" value="Edit" />
+ #
+ # submit_tag "Save", data: { confirm: "Are you sure?" }
+ # # => <input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />
+ #
+ def submit_tag(value = "Save changes", options = {})
+ options = options.stringify_keys
+
+ tag :input, { "type" => "submit", "name" => "commit", "value" => value }.update(options)
+ end
+
+ # Creates a button element that defines a <tt>submit</tt> button,
+ # <tt>reset</tt>button or a generic button which can be used in
+ # JavaScript, for example. You can use the button tag as a regular
+ # submit tag but it isn't supported in legacy browsers. However,
+ # the button tag allows richer labels such as images and emphasis,
+ # so this helper will also accept a block.
+ #
+ # ==== Options
+ # * <tt>:data</tt> - This option can be used to add custom data attributes.
+ # * <tt>:disabled</tt> - If true, the user will not be able to
+ # use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Data attributes
+ #
+ # * <tt>confirm: 'question?'</tt> - If present, the
+ # unobtrusive JavaScript drivers will provide a prompt with
+ # the question specified. If the user accepts, the form is
+ # processed normally, otherwise no action is taken.
+ # * <tt>:disable_with</tt> - Value of this parameter will be
+ # used as the value for a disabled version of the submit
+ # button when the form is submitted. This feature is provided
+ # by the unobtrusive JavaScript driver.
+ #
+ # ==== Examples
+ # button_tag
+ # # => <button name="button" type="submit">Button</button>
+ #
+ # button_tag(type: 'button') do
+ # content_tag(:strong, 'Ask me!')
+ # end
+ # # => <button name="button" type="button">
+ # # <strong>Ask me!</strong>
+ # # </button>
+ #
+ # 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
+
+ options.reverse_merge! 'name' => 'button', 'type' => 'submit'
+
+ content_tag :button, content_or_options || 'Button', options, &block
+ end
+
+ # Displays an image which when clicked will submit the form.
+ #
+ # <tt>source</tt> is passed to AssetTagHelper#path_to_image
+ #
+ # ==== Options
+ # * <tt>:data</tt> - This option can be used to add custom data attributes.
+ # * <tt>:disabled</tt> - If set to true, the user will not be able to use this input.
+ # * Any other key creates standard HTML options for the tag.
+ #
+ # ==== Data attributes
+ #
+ # * <tt>confirm: 'question?'</tt> - This will add a JavaScript confirm
+ # prompt with the question specified. If the user accepts, the form is
+ # processed normally, otherwise no action is taken.
+ #
+ # ==== Examples
+ # image_submit_tag("login.png")
+ # # => <input alt="Login" src="/images/login.png" type="image" />
+ #
+ # image_submit_tag("purchase.png", disabled: true)
+ # # => <input alt="Purchase" disabled="disabled" src="/images/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" />
+ #
+ # 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" />
+ #
+ # image_submit_tag("save.png", data: { confirm: "Are you sure?" })
+ # # => <input alt="Save" src="/images/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)
+ end
+
+ # Creates a field set for grouping HTML form elements.
+ #
+ # <tt>legend</tt> will become the fieldset's title (optional as per W3C).
+ # <tt>options</tt> accept the same values as tag.
+ #
+ # ==== Examples
+ # <%= field_set_tag do %>
+ # <p><%= text_field_tag 'name' %></p>
+ # <% end %>
+ # # => <fieldset><p><input id="name" name="name" type="text" /></p></fieldset>
+ #
+ # <%= field_set_tag 'Your details' do %>
+ # <p><%= text_field_tag 'name' %></p>
+ # <% end %>
+ # # => <fieldset><legend>Your details</legend><p><input id="name" name="name" type="text" /></p></fieldset>
+ #
+ # <%= field_set_tag nil, class: 'format' do %>
+ # <p><%= text_field_tag 'name' %></p>
+ # <% end %>
+ # # => <fieldset class="format"><p><input id="name" name="name" type="text" /></p></fieldset>
+ def field_set_tag(legend = nil, options = nil, &block)
+ output = tag(:fieldset, options, true)
+ output.safe_concat(content_tag(:legend, legend)) unless legend.blank?
+ output.concat(capture(&block)) if block_given?
+ output.safe_concat("</fieldset>")
+ end
+
+ # Creates a text field of type "color".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def color_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "color"))
+ end
+
+ # Creates a text field of type "search".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def search_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "search"))
+ end
+
+ # Creates a text field of type "tel".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def telephone_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "tel"))
+ end
+ alias phone_field_tag telephone_field_tag
+
+ # Creates a text field of type "date".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def date_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "date"))
+ end
+
+ # Creates a text field of type "time".
+ #
+ # === Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ def time_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "time"))
+ end
+
+ # Creates a text field of type "datetime".
+ #
+ # === Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ def datetime_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "datetime"))
+ end
+
+ # Creates a text field of type "datetime-local".
+ #
+ # === Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ def datetime_local_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "datetime-local"))
+ end
+
+ # Creates a text field of type "month".
+ #
+ # === Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ def month_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "month"))
+ end
+
+ # Creates a text field of type "week".
+ #
+ # === Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ def week_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "week"))
+ end
+
+ # Creates a text field of type "url".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def url_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "url"))
+ end
+
+ # Creates a text field of type "email".
+ #
+ # ==== Options
+ # * Accepts the same options as text_field_tag.
+ def email_field_tag(name, value = nil, options = {})
+ text_field_tag(name, value, options.stringify_keys.update("type" => "email"))
+ end
+
+ # Creates a number field.
+ #
+ # ==== Options
+ # * <tt>:min</tt> - The minimum acceptable value.
+ # * <tt>:max</tt> - The maximum acceptable value.
+ # * <tt>:in</tt> - A range specifying the <tt>:min</tt> and
+ # <tt>:max</tt> values.
+ # * <tt>:step</tt> - The acceptable value granularity.
+ # * Otherwise accepts the same options as text_field_tag.
+ #
+ # ==== Examples
+ # number_field_tag 'quantity', nil, in: 1...10
+ # # => <input id="quantity" name="quantity" min="1" max="9" type="number" />
+ def number_field_tag(name, value = nil, options = {})
+ options = options.stringify_keys
+ options["type"] ||= "number"
+ if range = options.delete("in") || options.delete("within")
+ options.update("min" => range.min, "max" => range.max)
+ end
+ text_field_tag(name, value, options)
+ end
+
+ # Creates a range form element.
+ #
+ # ==== Options
+ # * Accepts the same options as number_field_tag.
+ def range_field_tag(name, value = nil, options = {})
+ number_field_tag(name, value, options.stringify_keys.update("type" => "range"))
+ end
+
+ # 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)
+ end
+
+ private
+ def html_options_for_form(url_for_options, options)
+ options.stringify_keys.tap do |html_options|
+ html_options["enctype"] = "multipart/form-data" if html_options.delete("multipart")
+ # The following URL is unescaped, this is just a hash of options, and it is the
+ # responsibility of the caller to escape all the values.
+ html_options["action"] = url_for(url_for_options)
+ html_options["accept-charset"] = "UTF-8"
+
+ html_options["data-remote"] = true if html_options.delete("remote")
+
+ if html_options["data-remote"] &&
+ !embed_authenticity_token_in_remote_forms &&
+ html_options["authenticity_token"].blank?
+ # The authenticity token is taken from the meta tag in this case
+ html_options["authenticity_token"] = false
+ elsif html_options["authenticity_token"] == true
+ # Include the default authenticity_token, which is only generated when its set to nil,
+ # but we needed the true value to override the default of no authenticity_token on data-remote.
+ html_options["authenticity_token"] = nil
+ end
+ end
+ end
+
+ def extra_tags_for_form(html_options)
+ authenticity_token = html_options.delete("authenticity_token")
+ method = html_options.delete("method").to_s
+
+ method_tag = case method
+ when /^get$/i # must be case-insensitive, but can't use downcase as might be nil
+ html_options["method"] = "get"
+ ''
+ when /^post$/i, "", nil
+ html_options["method"] = "post"
+ token_tag(authenticity_token)
+ else
+ html_options["method"] = "post"
+ 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 => 'margin:0;padding:0;display:inline')
+ end
+
+ def form_tag_html(html_options)
+ extra_tags = extra_tags_for_form(html_options)
+ tag(:form, html_options, true) + extra_tags
+ end
+
+ def form_tag_in_block(html_options, &block)
+ content = capture(&block)
+ output = form_tag_html(html_options)
+ output << content
+ output.safe_concat("</form>")
+ end
+
+ # see http://www.w3.org/TR/html4/types.html#type-name
+ def sanitize_to_id(name)
+ name.to_s.delete(']').gsub(/[^-a-zA-Z0-9:.]/, "_")
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/javascript_helper.rb b/actionview/lib/action_view/helpers/javascript_helper.rb
new file mode 100644
index 0000000000..e475d5b018
--- /dev/null
+++ b/actionview/lib/action_view/helpers/javascript_helper.rb
@@ -0,0 +1,75 @@
+require 'action_view/helpers/tag_helper'
+
+module ActionView
+ module Helpers
+ module JavaScriptHelper
+ JS_ESCAPE_MAP = {
+ '\\' => '\\\\',
+ '</' => '<\/',
+ "\r\n" => '\n',
+ "\n" => '\n',
+ "\r" => '\n',
+ '"' => '\\"',
+ "'" => "\\'"
+ }
+
+ JS_ESCAPE_MAP["\342\200\250".force_encoding(Encoding::UTF_8).encode!] = '&#x2028;'
+ JS_ESCAPE_MAP["\342\200\251".force_encoding(Encoding::UTF_8).encode!] = '&#x2029;'
+
+ # Escapes carriage returns and single and double quotes for JavaScript segments.
+ #
+ # Also available through the alias j(). This is particularly helpful in JavaScript
+ # responses, like:
+ #
+ # $('some_element').replaceWith('<%=j render 'some/element_template' %>');
+ def escape_javascript(javascript)
+ if javascript
+ result = javascript.gsub(/(\\|<\/|\r\n|\342\200\250|\342\200\251|[\n\r"'])/u) {|match| JS_ESCAPE_MAP[match] }
+ javascript.html_safe? ? result.html_safe : result
+ else
+ ''
+ end
+ end
+
+ alias_method :j, :escape_javascript
+
+ # Returns a JavaScript tag with the +content+ inside. Example:
+ # javascript_tag "alert('All is good')"
+ #
+ # Returns:
+ # <script>
+ # //<![CDATA[
+ # alert('All is good')
+ # //]]>
+ # </script>
+ #
+ # +html_options+ may be a hash of attributes for the <tt>\<script></tt>
+ # tag.
+ #
+ # javascript_tag "alert('All is good')", defer: 'defer'
+ # # => <script defer="defer">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.
+ #
+ # <%= javascript_tag defer: 'defer' do -%>
+ # alert('All is good')
+ # <% end -%>
+ def javascript_tag(content_or_options_with_block = nil, html_options = {}, &block)
+ content =
+ if block_given?
+ html_options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
+ capture(&block)
+ else
+ content_or_options_with_block
+ end
+
+ content_tag(:script, javascript_cdata_section(content), html_options)
+ end
+
+ def javascript_cdata_section(content) #:nodoc:
+ "\n//#{cdata_section("\n#{content}\n//")}\n".html_safe
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/number_helper.rb b/actionview/lib/action_view/helpers/number_helper.rb
new file mode 100644
index 0000000000..fda7038a5d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/number_helper.rb
@@ -0,0 +1,441 @@
+# encoding: utf-8
+
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/string/output_safety'
+require 'active_support/number_helper'
+
+module ActionView
+ # = Action View Number Helpers
+ module Helpers #:nodoc:
+
+ # Provides methods for converting numbers into formatted strings.
+ # Methods are provided for phone numbers, currency, percentage,
+ # precision, positional notation, file size and pretty printing.
+ #
+ # Most methods expect a +number+ argument, and will return it
+ # unchanged if can't be converted into a valid number.
+ module NumberHelper
+
+ # Raised when argument +number+ param given to the helpers is invalid and
+ # the option :raise is set to +true+.
+ class InvalidNumberError < StandardError
+ attr_accessor :number
+ def initialize(number)
+ @number = number
+ end
+ end
+
+ # Formats a +number+ into a US phone number (e.g., (555)
+ # 123-9876). You can customize the format in the +options+ hash.
+ #
+ # ==== Options
+ #
+ # * <tt>:area_code</tt> - Adds parentheses around the area code.
+ # * <tt>:delimiter</tt> - Specifies the delimiter to use
+ # (defaults to "-").
+ # * <tt>:extension</tt> - Specifies an extension to add to the
+ # end of the generated number.
+ # * <tt>:country_code</tt> - Sets the country code for the phone
+ # number.
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_to_phone(5551234) # => 555-1234
+ # number_to_phone("5551234") # => 555-1234
+ # number_to_phone(1235551234) # => 123-555-1234
+ # number_to_phone(1235551234, area_code: true) # => (123) 555-1234
+ # number_to_phone(1235551234, delimiter: " ") # => 123 555 1234
+ # number_to_phone(1235551234, area_code: true, extension: 555) # => (123) 555-1234 x 555
+ # number_to_phone(1235551234, country_code: 1) # => +1-123-555-1234
+ # number_to_phone("123a456") # => 123a456
+ # number_to_phone("1234a567", raise: true) # => InvalidNumberError
+ #
+ # number_to_phone(1235551234, country_code: 1, extension: 1343, delimiter: ".")
+ # # => +1.123.555.1234 x 1343
+ def number_to_phone(number, options = {})
+ return unless number
+ options = options.symbolize_keys
+
+ parse_float(number, true) if options.delete(:raise)
+ ERB::Util.html_escape(ActiveSupport::NumberHelper.number_to_phone(number, options))
+ end
+
+ # Formats a +number+ into a currency string (e.g., $13.65). You
+ # can customize the format in the +options+ hash.
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the level of precision (defaults
+ # to 2).
+ # * <tt>:unit</tt> - Sets the denomination of the currency
+ # (defaults to "$").
+ # * <tt>:separator</tt> - Sets the separator between the units
+ # (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to ",").
+ # * <tt>:format</tt> - Sets the format for non-negative numbers
+ # (defaults to "%u%n"). Fields are <tt>%u</tt> for the
+ # currency, and <tt>%n</tt> for the number.
+ # * <tt>:negative_format</tt> - Sets the format for negative
+ # numbers (defaults to prepending an hyphen to the formatted
+ # number given by <tt>:format</tt>). Accepts the same fields
+ # than <tt>:format</tt>, except <tt>%n</tt> is here the
+ # absolute value of the number.
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_to_currency(1234567890.50) # => $1,234,567,890.50
+ # number_to_currency(1234567890.506) # => $1,234,567,890.51
+ # number_to_currency(1234567890.506, precision: 3) # => $1,234,567,890.506
+ # number_to_currency(1234567890.506, locale: :fr) # => 1 234 567 890,51 €
+ # number_to_currency("123a456") # => $123a456
+ #
+ # number_to_currency("123a456", raise: true) # => InvalidNumberError
+ #
+ # number_to_currency(-1234567890.50, negative_format: "(%u%n)")
+ # # => ($1,234,567,890.50)
+ # number_to_currency(1234567890.50, unit: "&pound;", separator: ",", delimiter: "")
+ # # => &pound;1234567890,50
+ # number_to_currency(1234567890.50, unit: "&pound;", separator: ",", delimiter: "", format: "%n %u")
+ # # => 1234567890,50 &pound;
+ def number_to_currency(number, options = {})
+ return unless number
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_currency(number, options)
+ }
+ end
+
+ # Formats a +number+ as a percentage string (e.g., 65%). You can
+ # customize the format in the +options+ hash.
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the precision of the number
+ # (defaults to 3).
+ # * <tt>:significant</tt> - If +true+, precision will be the #
+ # of significant_digits. If +false+, the # of fractional
+ # digits (defaults to +false+).
+ # * <tt>:separator</tt> - Sets the separator between the
+ # fractional and integer digits (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to "").
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +false+).
+ # * <tt>:format</tt> - Specifies the format of the percentage
+ # string The number field is <tt>%n</tt> (defaults to "%n%").
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage("98") # => 98.000%
+ # number_to_percentage(100, precision: 0) # => 100%
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
+ # number_to_percentage(1000, locale: :fr) # => 1 000,000%
+ # number_to_percentage("98a") # => 98a%
+ # number_to_percentage(100, format: "%n %") # => 100 %
+ #
+ # number_to_percentage("98a", raise: true) # => InvalidNumberError
+ def number_to_percentage(number, options = {})
+ return unless number
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_percentage(number, options)
+ }
+ end
+
+ # Formats a +number+ with grouped thousands using +delimiter+
+ # (e.g., 12,324). You can customize the format in the +options+
+ # hash.
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to ",").
+ # * <tt>:separator</tt> - Sets the separator between the
+ # fractional and integer digits (defaults to ".").
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_with_delimiter(12345678) # => 12,345,678
+ # number_with_delimiter("123456") # => 123,456
+ # number_with_delimiter(12345678.05) # => 12,345,678.05
+ # number_with_delimiter(12345678, delimiter: ".") # => 12.345.678
+ # number_with_delimiter(12345678, delimiter: ",") # => 12,345,678
+ # number_with_delimiter(12345678.05, separator: " ") # => 12,345,678 05
+ # number_with_delimiter(12345678.05, locale: :fr) # => 12 345 678,05
+ # number_with_delimiter("112a") # => 112a
+ # number_with_delimiter(98765432.98, delimiter: " ", separator: ",")
+ # # => 98 765 432,98
+ #
+ # number_with_delimiter("112a", raise: true) # => raise InvalidNumberError
+ def number_with_delimiter(number, options = {})
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_delimited(number, options)
+ }
+ end
+
+ # Formats a +number+ with the specified level of
+ # <tt>:precision</tt> (e.g., 112.32 has a precision of 2 if
+ # +:significant+ is +false+, and 5 if +:significant+ is +true+).
+ # You can customize the format in the +options+ hash.
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the precision of the number
+ # (defaults to 3).
+ # * <tt>:significant</tt> - If +true+, precision will be the #
+ # of significant_digits. If +false+, the # of fractional
+ # digits (defaults to +false+).
+ # * <tt>:separator</tt> - Sets the separator between the
+ # fractional and integer digits (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to "").
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +false+).
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_with_precision(111.2345) # => 111.235
+ # number_with_precision(111.2345, precision: 2) # => 111.23
+ # number_with_precision(13, precision: 5) # => 13.00000
+ # number_with_precision(389.32314, precision: 0) # => 389
+ # number_with_precision(111.2345, significant: true) # => 111
+ # number_with_precision(111.2345, precision: 1, significant: true) # => 100
+ # number_with_precision(13, precision: 5, significant: true) # => 13.000
+ # number_with_precision(111.234, locale: :fr) # => 111,234
+ #
+ # number_with_precision(13, precision: 5, significant: true, strip_insignificant_zeros: true)
+ # # => 13
+ #
+ # number_with_precision(389.32314, precision: 4, significant: true) # => 389.3
+ # number_with_precision(1111.2345, precision: 2, separator: ',', delimiter: '.')
+ # # => 1.111,23
+ def number_with_precision(number, options = {})
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_rounded(number, options)
+ }
+ end
+
+ # Formats the bytes in +number+ into a more understandable
+ # representation (e.g., giving it 1500 yields 1.5 KB). This
+ # method is useful for reporting file sizes to users. You can
+ # customize the format in the +options+ hash.
+ #
+ # See <tt>number_to_human</tt> if you want to pretty-print a
+ # generic number.
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the precision of the number
+ # (defaults to 3).
+ # * <tt>:significant</tt> - If +true+, precision will be the #
+ # of significant_digits. If +false+, the # of fractional
+ # digits (defaults to +true+)
+ # * <tt>:separator</tt> - Sets the separator between the
+ # fractional and integer digits (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to "").
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +true+)
+ # * <tt>:prefix</tt> - If +:si+ formats the number using the SI
+ # prefix (defaults to :binary)
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_to_human_size(123) # => 123 Bytes
+ # number_to_human_size(1234) # => 1.21 KB
+ # number_to_human_size(12345) # => 12.1 KB
+ # number_to_human_size(1234567) # => 1.18 MB
+ # number_to_human_size(1234567890) # => 1.15 GB
+ # number_to_human_size(1234567890123) # => 1.12 TB
+ # 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"
+ def number_to_human_size(number, options = {})
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_human_size(number, options)
+ }
+ end
+
+ # Pretty prints (formats and approximates) a number in a way it
+ # is more readable by humans (eg.: 1200000000 becomes "1.2
+ # Billion"). This is useful for numbers that can get very large
+ # (and too hard to read).
+ #
+ # See <tt>number_to_human_size</tt> if you want to print a file
+ # size.
+ #
+ # You can also define you own unit-quantifier names if you want
+ # to use other decimal units (eg.: 1500 becomes "1.5
+ # kilometers", 0.150 becomes "150 milliliters", etc). You may
+ # define a wide range of unit quantifiers, even fractional ones
+ # (centi, deci, mili, etc).
+ #
+ # ==== Options
+ #
+ # * <tt>:locale</tt> - Sets the locale to be used for formatting
+ # (defaults to current locale).
+ # * <tt>:precision</tt> - Sets the precision of the number
+ # (defaults to 3).
+ # * <tt>:significant</tt> - If +true+, precision will be the #
+ # of significant_digits. If +false+, the # of fractional
+ # digits (defaults to +true+)
+ # * <tt>:separator</tt> - Sets the separator between the
+ # fractional and integer digits (defaults to ".").
+ # * <tt>:delimiter</tt> - Sets the thousands delimiter (defaults
+ # to "").
+ # * <tt>:strip_insignificant_zeros</tt> - If +true+ removes
+ # insignificant zeros after the decimal separator (defaults to
+ # +true+)
+ # * <tt>:units</tt> - A Hash of unit quantifier names. Or a
+ # string containing an i18n scope where to find this hash. It
+ # might have the following keys:
+ # * *integers*: <tt>:unit</tt>, <tt>:ten</tt>,
+ # *<tt>:hundred</tt>, <tt>:thousand</tt>, <tt>:million</tt>,
+ # *<tt>:billion</tt>, <tt>:trillion</tt>,
+ # *<tt>:quadrillion</tt>
+ # * *fractionals*: <tt>:deci</tt>, <tt>:centi</tt>,
+ # *<tt>:mili</tt>, <tt>:micro</tt>, <tt>:nano</tt>,
+ # *<tt>:pico</tt>, <tt>:femto</tt>
+ # * <tt>:format</tt> - Sets the format of the output string
+ # (defaults to "%n %u"). The field types are:
+ # * %u - The quantifier (ex.: 'thousand')
+ # * %n - The number
+ # * <tt>:raise</tt> - If true, raises +InvalidNumberError+ when
+ # the argument is invalid.
+ #
+ # ==== Examples
+ #
+ # number_to_human(123) # => "123"
+ # number_to_human(1234) # => "1.23 Thousand"
+ # number_to_human(12345) # => "12.3 Thousand"
+ # number_to_human(1234567) # => "1.23 Million"
+ # number_to_human(1234567890) # => "1.23 Billion"
+ # number_to_human(1234567890123) # => "1.23 Trillion"
+ # number_to_human(1234567890123456) # => "1.23 Quadrillion"
+ # number_to_human(1234567890123456789) # => "1230 Quadrillion"
+ # number_to_human(489939, precision: 2) # => "490 Thousand"
+ # number_to_human(489939, precision: 4) # => "489.9 Thousand"
+ # number_to_human(1234567, precision: 4,
+ # significant: false) # => "1.2346 Million"
+ # number_to_human(1234567, precision: 1,
+ # separator: ',',
+ # significant: false) # => "1,2 Million"
+ #
+ # 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"
+ #
+ # ==== Custom Unit Quantifiers
+ #
+ # You can also use your own custom unit quantifiers:
+ # number_to_human(500000, units: {unit: "ml", thousand: "lt"}) # => "500 lt"
+ #
+ # If in your I18n locale you have:
+ # distance:
+ # centi:
+ # one: "centimeter"
+ # other: "centimeters"
+ # unit:
+ # one: "meter"
+ # other: "meters"
+ # thousand:
+ # one: "kilometer"
+ # other: "kilometers"
+ # billion: "gazillion-distance"
+ #
+ # Then you could do:
+ #
+ # number_to_human(543934, units: :distance) # => "544 kilometers"
+ # number_to_human(54393498, units: :distance) # => "54400 kilometers"
+ # number_to_human(54393498000, units: :distance) # => "54.4 gazillion-distance"
+ # number_to_human(343, units: :distance, precision: 1) # => "300 meters"
+ # number_to_human(1, units: :distance) # => "1 meter"
+ # number_to_human(0.34, units: :distance) # => "34 centimeters"
+ #
+ def number_to_human(number, options = {})
+ options = escape_unsafe_delimiters_and_separators(options.symbolize_keys)
+
+ wrap_with_output_safety_handling(number, options.delete(:raise)) {
+ ActiveSupport::NumberHelper.number_to_human(number, options)
+ }
+ end
+
+ private
+
+ def escape_unsafe_delimiters_and_separators(options)
+ options[:separator] = ERB::Util.html_escape(options[:separator]) if options[:separator] && !options[:separator].html_safe?
+ options[:delimiter] = ERB::Util.html_escape(options[:delimiter]) if options[:delimiter] && !options[:delimiter].html_safe?
+ options
+ 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
+
+ formatted_number = yield
+
+ if valid_float || number.html_safe?
+ formatted_number.html_safe
+ else
+ formatted_number
+ end
+ end
+
+ def valid_float?(number)
+ !parse_float(number, false).nil?
+ end
+
+ def parse_float(number, raise_error)
+ Float(number)
+ rescue ArgumentError, TypeError
+ raise InvalidNumberError, number if raise_error
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/output_safety_helper.rb b/actionview/lib/action_view/helpers/output_safety_helper.rb
new file mode 100644
index 0000000000..60a4478c26
--- /dev/null
+++ b/actionview/lib/action_view/helpers/output_safety_helper.rb
@@ -0,0 +1,38 @@
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView #:nodoc:
+ # = Action View Raw Output Helper
+ module Helpers #:nodoc:
+ module OutputSafetyHelper
+ # This method outputs without escaping a string. Since escaping tags is
+ # now default, this can be used when you don't want Rails to automatically
+ # escape tags. This is not recommended if the data is coming from the user's
+ # input.
+ #
+ # For example:
+ #
+ # raw @user.name
+ # # => 'Jimmy <alert>Tables</alert>'
+ def raw(stringish)
+ 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.
+ #
+ # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>"], "<br />")
+ # # => "<p>foo</p>&lt;br /&gt;&lt;p&gt;bar&lt;/p&gt;"
+ #
+ # safe_join(["<p>foo</p>".html_safe, "<p>bar</p>".html_safe], "<br />".html_safe)
+ # # => "<p>foo</p><br /><p>bar</p>"
+ #
+ def safe_join(array, sep=$,)
+ sep = ERB::Util.html_escape(sep)
+
+ array.map { |i| ERB::Util.html_escape(i) }.join(sep).html_safe
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/record_tag_helper.rb b/actionview/lib/action_view/helpers/record_tag_helper.rb
new file mode 100644
index 0000000000..77c3e6d394
--- /dev/null
+++ b/actionview/lib/action_view/helpers/record_tag_helper.rb
@@ -0,0 +1,108 @@
+require 'action_view/record_identifier'
+
+module ActionView
+ # = Action View Record Tag Helpers
+ module Helpers
+ module RecordTagHelper
+ include ActionView::RecordIdentifier
+
+ # Produces a wrapper DIV element with id and class parameters that
+ # relate to the specified Active Record object. Usage example:
+ #
+ # <%= div_for(@person, class: "foo") do %>
+ # <%= @person.name %>
+ # <% end %>
+ #
+ # produces:
+ #
+ # <div id="person_123" class="person foo"> Joe Bloggs </div>
+ #
+ # You can also pass an array of Active Record objects, which will then
+ # get iterated over and yield each record as an argument for the block.
+ # For example:
+ #
+ # <%= div_for(@people, class: "foo") do |person| %>
+ # <%= person.name %>
+ # <% end %>
+ #
+ # produces:
+ #
+ # <div id="person_123" class="person foo"> Joe Bloggs </div>
+ # <div id="person_124" class="person foo"> Jane Bloggs </div>
+ #
+ def div_for(record, *args, &block)
+ content_tag_for(:div, record, *args, &block)
+ end
+
+ # content_tag_for creates an HTML element with id and class parameters
+ # that relate to the specified Active Record object. For example:
+ #
+ # <%= content_tag_for(:tr, @person) do %>
+ # <td><%= @person.first_name %></td>
+ # <td><%= @person.last_name %></td>
+ # <% end %>
+ #
+ # would produce the following HTML (assuming @person is an instance of
+ # a Person object, with an id value of 123):
+ #
+ # <tr id="person_123" class="person">....</tr>
+ #
+ # If you require the HTML id attribute to have a prefix, you can specify it:
+ #
+ # <%= content_tag_for(:tr, @person, :foo) do %> ...
+ #
+ # produces:
+ #
+ # <tr id="foo_person_123" class="person">...
+ #
+ # You can also pass an array of objects which this method will loop through
+ # and yield the current object to the supplied block, reducing the need for
+ # having to iterate through the object (using <tt>each</tt>) beforehand.
+ # For example (assuming @people is an array of Person objects):
+ #
+ # <%= content_tag_for(:tr, @people) do |person| %>
+ # <td><%= person.first_name %></td>
+ # <td><%= person.last_name %></td>
+ # <% end %>
+ #
+ # produces:
+ #
+ # <tr id="person_123" class="person">...</tr>
+ # <tr id="person_124" class="person">...</tr>
+ #
+ # content_tag_for also accepts a hash of options, which will be converted to
+ # additional HTML attributes. If you specify a <tt>:class</tt> value, it will be combined
+ # with the default class name for your object. For example:
+ #
+ # <%= content_tag_for(:li, @person, class: "bar") %>...
+ #
+ # produces:
+ #
+ # <li id="person_123" class="person bar">...
+ #
+ def content_tag_for(tag_name, single_or_multiple_records, prefix = nil, options = nil, &block)
+ options, prefix = prefix, nil if prefix.is_a?(Hash)
+
+ Array(single_or_multiple_records).map do |single_record|
+ content_tag_for_single_record(tag_name, single_record, prefix, options, &block)
+ end.join("\n").html_safe
+ end
+
+ private
+
+ # Called by <tt>content_tag_for</tt> internally to render a content tag
+ # for each record.
+ def content_tag_for_single_record(tag_name, record, prefix, options, &block)
+ options = options ? options.dup : {}
+ options[:class] = [ dom_class(record, prefix), options[:class] ].compact
+ options[:id] = dom_id(record, prefix)
+
+ if block_given?
+ content_tag(tag_name, capture(record, &block), options)
+ else
+ content_tag(tag_name, "", options)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/rendering_helper.rb b/actionview/lib/action_view/helpers/rendering_helper.rb
new file mode 100644
index 0000000000..458086de96
--- /dev/null
+++ b/actionview/lib/action_view/helpers/rendering_helper.rb
@@ -0,0 +1,90 @@
+module ActionView
+ module Helpers
+ # = Action View Rendering
+ #
+ # Implements methods that allow rendering from a view context.
+ # In order to use this module, all you need is to implement
+ # view_renderer that returns an ActionView::Renderer object.
+ module RenderingHelper
+ # Returns the result of a render that's dictated by the options hash. The primary options are:
+ #
+ # * <tt>:partial</tt> - See <tt>ActionView::PartialRenderer</tt>.
+ # * <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.
+ #
+ # 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.
+ def render(options = {}, locals = {}, &block)
+ case options
+ when Hash
+ if block_given?
+ view_renderer.render_partial(self, options.merge(:partial => options[:layout]), &block)
+ else
+ view_renderer.render(self, options)
+ end
+ else
+ view_renderer.render_partial(self, :partial => options, :locals => locals)
+ end
+ end
+
+ # Overwrites _layout_for in the context object so it supports the case a block is
+ # passed to a partial. Returns the contents that are yielded to a layout, given a
+ # name or a block.
+ #
+ # You can think of a layout as a method that is called with a block. If the user calls
+ # <tt>yield :some_name</tt>, the block, by default, returns <tt>content_for(:some_name)</tt>.
+ # If the user calls simply +yield+, the default block returns <tt>content_for(:layout)</tt>.
+ #
+ # The user can override this default by passing a block to the layout:
+ #
+ # # The template
+ # <%= render layout: "my_layout" do %>
+ # Content
+ # <% end %>
+ #
+ # # The layout
+ # <html>
+ # <%= yield %>
+ # </html>
+ #
+ # In this case, instead of the default block, which would return <tt>content_for(:layout)</tt>,
+ # this method returns the block that was passed in to <tt>render :layout</tt>, and the response
+ # would be
+ #
+ # <html>
+ # Content
+ # </html>
+ #
+ # Finally, the block can take block arguments, which can be passed in by +yield+:
+ #
+ # # The template
+ # <%= render layout: "my_layout" do |customer| %>
+ # Hello <%= customer.name %>
+ # <% end %>
+ #
+ # # The layout
+ # <html>
+ # <%= yield Struct.new(:name).new("David") %>
+ # </html>
+ #
+ # In this case, the layout would receive the block passed into <tt>render :layout</tt>,
+ # and the struct specified would be passed into the block as an argument. The result
+ # would be
+ #
+ # <html>
+ # Hello David
+ # </html>
+ #
+ def _layout_for(*args, &block)
+ name = args.first
+
+ if block && !name.is_a?(Symbol)
+ capture(*args, &block)
+ else
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/sanitize_helper.rb b/actionview/lib/action_view/helpers/sanitize_helper.rb
new file mode 100644
index 0000000000..e5cb843670
--- /dev/null
+++ b/actionview/lib/action_view/helpers/sanitize_helper.rb
@@ -0,0 +1,256 @@
+require 'active_support/core_ext/object/try'
+require 'action_view/vendor/html-scanner'
+
+module ActionView
+ # = Action View Sanitize Helpers
+ module Helpers
+ # The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
+ # These helper methods extend Action View making them callable within your template files.
+ module SanitizeHelper
+ extend ActiveSupport::Concern
+ # This +sanitize+ helper will html encode all tags and strip all attributes that
+ # aren't specifically allowed.
+ #
+ # It also strips href/src tags with invalid protocols, like javascript: especially.
+ # It does its best to counter any tricks that hackers may use, like throwing in
+ # unicode/ascii/hex values to get past the javascript: filters. Check out
+ # the extensive test suite.
+ #
+ # <%= sanitize @article.body %>
+ #
+ # You can add or remove tags/attributes if you want to customize it a bit.
+ # See ActionView::Base for full docs on the available options. You can add
+ # tags/attributes for single uses of +sanitize+ by passing either the
+ # <tt>:attributes</tt> or <tt>:tags</tt> options:
+ #
+ # Normal Use
+ #
+ # <%= sanitize @article.body %>
+ #
+ # Custom Use (only the mentioned tags and attributes are allowed, nothing else)
+ #
+ # <%= sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style) %>
+ #
+ # Add table tags to the default allowed tags
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ # Remove tags to the default allowed tags
+ #
+ # class Application < Rails::Application
+ # config.after_initialize do
+ # ActionView::Base.sanitized_allowed_tags.delete 'div'
+ # end
+ # end
+ #
+ # Change allowed default attributes
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_attributes = 'id', 'class', 'style'
+ # end
+ #
+ # Please note that sanitizing user-provided text does not guarantee that the
+ # resulting markup is valid (conforming to a document type) or even well-formed.
+ # The output may still contain e.g. unescaped '<', '>', '&' characters and
+ # confuse browsers.
+ #
+ def sanitize(html, options = {})
+ self.class.white_list_sanitizer.sanitize(html, options).try(:html_safe)
+ end
+
+ # Sanitizes a block of CSS code. Used by +sanitize+ when it comes across a style attribute.
+ def sanitize_css(style)
+ self.class.white_list_sanitizer.sanitize_css(style)
+ end
+
+ # Strips all HTML tags from the +html+, including comments. This uses the
+ # html-scanner tokenizer and so its HTML parsing ability is limited by
+ # that of html-scanner.
+ #
+ # strip_tags("Strip <i>these</i> tags!")
+ # # => Strip these tags!
+ #
+ # strip_tags("<b>Bold</b> no more! <a href='more.html'>See more here</a>...")
+ # # => Bold no more! See more here...
+ #
+ # strip_tags("<div id='top-bar'>Welcome to my website!</div>")
+ # # => Welcome to my website!
+ def strip_tags(html)
+ self.class.full_sanitizer.sanitize(html)
+ end
+
+ # Strips all link tags from +text+ leaving just the link text.
+ #
+ # strip_links('<a href="http://www.rubyonrails.org">Ruby on Rails</a>')
+ # # => Ruby on Rails
+ #
+ # strip_links('Please e-mail me at <a href="mailto:me@email.com">me@email.com</a>.')
+ # # => Please e-mail me at me@email.com.
+ #
+ # strip_links('Blog: <a href="http://www.myblog.com/" class="nav" target=\"_blank\">Visit</a>.')
+ # # => Blog: Visit.
+ def strip_links(html)
+ self.class.link_sanitizer.sanitize(html)
+ end
+
+ module ClassMethods #:nodoc:
+ attr_writer :full_sanitizer, :link_sanitizer, :white_list_sanitizer
+
+ def sanitized_protocol_separator
+ white_list_sanitizer.protocol_separator
+ end
+
+ def sanitized_uri_attributes
+ white_list_sanitizer.uri_attributes
+ end
+
+ def sanitized_bad_tags
+ white_list_sanitizer.bad_tags
+ end
+
+ def sanitized_allowed_tags
+ white_list_sanitizer.allowed_tags
+ end
+
+ def sanitized_allowed_attributes
+ white_list_sanitizer.allowed_attributes
+ end
+
+ def sanitized_allowed_css_properties
+ white_list_sanitizer.allowed_css_properties
+ end
+
+ def sanitized_allowed_css_keywords
+ white_list_sanitizer.allowed_css_keywords
+ end
+
+ def sanitized_shorthand_css_properties
+ white_list_sanitizer.shorthand_css_properties
+ end
+
+ def sanitized_allowed_protocols
+ white_list_sanitizer.allowed_protocols
+ end
+
+ def sanitized_protocol_separator=(value)
+ white_list_sanitizer.protocol_separator = value
+ end
+
+ # Gets the HTML::FullSanitizer instance used by +strip_tags+. Replace with
+ # any object that responds to +sanitize+.
+ #
+ # class Application < Rails::Application
+ # config.action_view.full_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def full_sanitizer
+ @full_sanitizer ||= HTML::FullSanitizer.new
+ end
+
+ # Gets the HTML::LinkSanitizer instance used by +strip_links+. Replace with
+ # any object that responds to +sanitize+.
+ #
+ # class Application < Rails::Application
+ # config.action_view.link_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def link_sanitizer
+ @link_sanitizer ||= HTML::LinkSanitizer.new
+ end
+
+ # Gets the HTML::WhiteListSanitizer instance used by sanitize and +sanitize_css+.
+ # Replace with any object that responds to +sanitize+.
+ #
+ # class Application < Rails::Application
+ # config.action_view.white_list_sanitizer = MySpecialSanitizer.new
+ # end
+ #
+ def white_list_sanitizer
+ @white_list_sanitizer ||= HTML::WhiteListSanitizer.new
+ end
+
+ # Adds valid HTML attributes that the +sanitize+ helper checks for URIs.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_uri_attributes = 'lowsrc', 'target'
+ # end
+ #
+ def sanitized_uri_attributes=(attributes)
+ HTML::WhiteListSanitizer.uri_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of 'bad' tags for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_bad_tags = 'embed', 'object'
+ # end
+ #
+ def sanitized_bad_tags=(attributes)
+ HTML::WhiteListSanitizer.bad_tags.merge(attributes)
+ end
+
+ # Adds to the Set of allowed tags for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+ # end
+ #
+ def sanitized_allowed_tags=(attributes)
+ HTML::WhiteListSanitizer.allowed_tags.merge(attributes)
+ end
+
+ # Adds to the Set of allowed HTML attributes for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_attributes = 'onclick', 'longdesc'
+ # end
+ #
+ def sanitized_allowed_attributes=(attributes)
+ HTML::WhiteListSanitizer.allowed_attributes.merge(attributes)
+ end
+
+ # Adds to the Set of allowed CSS properties for the #sanitize and +sanitize_css+ helpers.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_css_properties = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_properties=(attributes)
+ HTML::WhiteListSanitizer.allowed_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed CSS keywords for the +sanitize+ and +sanitize_css+ helpers.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_css_keywords = 'expression'
+ # end
+ #
+ def sanitized_allowed_css_keywords=(attributes)
+ HTML::WhiteListSanitizer.allowed_css_keywords.merge(attributes)
+ end
+
+ # Adds to the Set of allowed shorthand CSS properties for the +sanitize+ and +sanitize_css+ helpers.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_shorthand_css_properties = 'expression'
+ # end
+ #
+ def sanitized_shorthand_css_properties=(attributes)
+ HTML::WhiteListSanitizer.shorthand_css_properties.merge(attributes)
+ end
+
+ # Adds to the Set of allowed protocols for the +sanitize+ helper.
+ #
+ # class Application < Rails::Application
+ # config.action_view.sanitized_allowed_protocols = 'ssh', 'feed'
+ # end
+ #
+ def sanitized_allowed_protocols=(attributes)
+ HTML::WhiteListSanitizer.allowed_protocols.merge(attributes)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tag_helper.rb b/actionview/lib/action_view/helpers/tag_helper.rb
new file mode 100644
index 0000000000..11e41ec056
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tag_helper.rb
@@ -0,0 +1,176 @@
+require 'active_support/core_ext/string/output_safety'
+require 'set'
+
+module ActionView
+ # = Action View Tag Helpers
+ module Helpers #:nodoc:
+ # Provides methods to generate HTML tags programmatically when you can't use
+ # a Builder. By default, they output XHTML compliant tags.
+ module TagHelper
+ extend ActiveSupport::Concern
+ include CaptureHelper
+
+ BOOLEAN_ATTRIBUTES = %w(disabled readonly multiple checked autobuffer
+ autoplay controls loop selected hidden scoped async
+ defer reversed ismap seamless muted required
+ autofocus novalidate formnovalidate open pubdate
+ itemscope allowfullscreen default inert sortable
+ truespeed typemustmatch).to_set
+
+ BOOLEAN_ATTRIBUTES.merge(BOOLEAN_ATTRIBUTES.map {|attribute| attribute.to_sym })
+
+ PRE_CONTENT_STRINGS = {
+ :textarea => "\n"
+ }
+
+ # Returns an empty HTML tag of type +name+ which by default is XHTML
+ # compliant. Set +open+ to true to create an open tag compatible
+ # with HTML 4.0 and below. Add HTML attributes by passing an attributes
+ # hash to +options+. Set +escape+ to false to disable attribute value
+ # escaping.
+ #
+ # ==== Options
+ # You can use symbols or strings for the attribute names.
+ #
+ # Use +true+ with boolean attributes that can render with no value, like
+ # +disabled+ and +readonly+.
+ #
+ # HTML5 <tt>data-*</tt> attributes can be set with a single +data+ key
+ # pointing to a hash of sub-attributes.
+ #
+ # To play nicely with JavaScript conventions sub-attributes are dasherized.
+ # 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.
+ # This may come in handy when using jQuery's HTML5-aware <tt>.data()</tt>
+ # from 1.4.3.
+ #
+ # ==== Examples
+ # tag("br")
+ # # => <br />
+ #
+ # tag("br", nil, true)
+ # # => <br>
+ #
+ # tag("input", type: 'text', disabled: true)
+ # # => <input type="text" disabled="disabled" />
+ #
+ # tag("img", src: "open & shut.png")
+ # # => <img src="open &amp; shut.png" />
+ #
+ # tag("img", {src: "open &amp; shut.png"}, false, false)
+ # # => <img src="open &amp; shut.png" />
+ #
+ # tag("div", data: {name: 'Stephen', city_state: %w(Chicago IL)})
+ # # => <div data-name="Stephen" data-city-state="[&quot;Chicago&quot;,&quot;IL&quot;]" />
+ def tag(name, options = nil, open = false, escape = true)
+ "<#{name}#{tag_options(options, escape) if options}#{open ? ">" : " />"}".html_safe
+ end
+
+ # Returns an HTML block tag of type +name+ surrounding the +content+. Add
+ # HTML attributes by passing an attributes hash to +options+.
+ # Instead of passing the content as an argument, you can also use a block
+ # in which case, you pass your +options+ as the second parameter.
+ # 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
+ # <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.
+ #
+ # ==== Examples
+ # content_tag(:p, "Hello world!")
+ # # => <p>Hello world!</p>
+ # content_tag(:div, content_tag(:p, "Hello world!"), class: "strong")
+ # # => <div class="strong"><p>Hello world!</p></div>
+ # content_tag("select", options, multiple: true)
+ # # => <select multiple="multiple">...options...</select>
+ #
+ # <%= content_tag :div, class: "strong" do -%>
+ # Hello world!
+ # <% end -%>
+ # # => <div class="strong">Hello world!</div>
+ def content_tag(name, content_or_options_with_block = nil, options = nil, escape = true, &block)
+ if block_given?
+ options = content_or_options_with_block if content_or_options_with_block.is_a?(Hash)
+ content_tag_string(name, capture(&block), options, escape)
+ else
+ content_tag_string(name, content_or_options_with_block, options, escape)
+ end
+ end
+
+ # Returns a CDATA section with the given +content+. CDATA sections
+ # are used to escape blocks of text containing characters which would
+ # otherwise be recognized as markup. CDATA sections begin with the string
+ # <tt><![CDATA[</tt> and end with (and may not contain) the string <tt>]]></tt>.
+ #
+ # cdata_section("<hello world>")
+ # # => <![CDATA[<hello world>]]>
+ #
+ # cdata_section(File.read("hello_world.txt"))
+ # # => <![CDATA[<hello from a text file]]>
+ #
+ # cdata_section("hello]]>world")
+ # # => <![CDATA[hello]]]]><![CDATA[>world]]>
+ def cdata_section(content)
+ splitted = content.to_s.gsub(']]>', ']]]]><![CDATA[>')
+ "<![CDATA[#{splitted}]]>".html_safe
+ end
+
+ # Returns an escaped version of +html+ without affecting existing escaped entities.
+ #
+ # escape_once("1 < 2 &amp; 3")
+ # # => "1 &lt; 2 &amp; 3"
+ #
+ # escape_once("&lt;&lt; Accept & Checkout")
+ # # => "&lt;&lt; Accept &amp; Checkout"
+ def escape_once(html)
+ ERB::Util.html_escape_once(html)
+ end
+
+ private
+
+ def content_tag_string(name, content, options, escape = true)
+ tag_options = tag_options(options, escape) if options
+ content = ERB::Util.h(content) if escape
+ "<#{name}#{tag_options}>#{PRE_CONTENT_STRINGS[name.to_sym]}#{content}</#{name}>".html_safe
+ end
+
+ def tag_options(options, escape = true)
+ return if options.blank?
+ attrs = []
+ options.each_pair do |key, value|
+ if key.to_s == 'data' && value.is_a?(Hash)
+ value.each_pair do |k, v|
+ attrs << data_tag_option(k, v, escape)
+ end
+ elsif BOOLEAN_ATTRIBUTES.include?(key)
+ attrs << boolean_tag_option(key) if value
+ elsif !value.nil?
+ attrs << tag_option(key, value, escape)
+ end
+ end
+ " #{attrs.sort! * ' '}".html_safe unless attrs.empty?
+ end
+
+ def data_tag_option(key, value, escape)
+ key = "data-#{key.to_s.dasherize}"
+ unless value.is_a?(String) || value.is_a?(Symbol) || value.is_a?(BigDecimal)
+ value = value.to_json
+ end
+ tag_option(key, value, escape)
+ end
+
+ def boolean_tag_option(key)
+ %(#{key}="#{key}")
+ end
+
+ def tag_option(key, value, escape)
+ value = value.join(" ") if value.is_a?(Array)
+ value = ERB::Util.h(value) if escape
+ %(#{key}="#{value}")
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/helpers/tags.rb b/actionview/lib/action_view/helpers/tags.rb
index a05e16979a..a05e16979a 100644
--- a/actionpack/lib/action_view/helpers/tags.rb
+++ b/actionview/lib/action_view/helpers/tags.rb
diff --git a/actionview/lib/action_view/helpers/tags/base.rb b/actionview/lib/action_view/helpers/tags/base.rb
new file mode 100644
index 0000000000..8607da301c
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/base.rb
@@ -0,0 +1,148 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class Base # :nodoc:
+ include Helpers::ActiveModelInstanceTag, Helpers::TagHelper, Helpers::FormTagHelper
+ include FormOptionsHelper
+
+ attr_reader :object
+
+ def initialize(object_name, method_name, template_object, options = {})
+ @object_name, @method_name = object_name.to_s.dup, method_name.to_s.dup
+ @template_object = template_object
+
+ @object_name.sub!(/\[\]$/,"") || @object_name.sub!(/\[\]\]$/,"]")
+ @object = retrieve_object(options.delete(:object))
+ @options = options
+ @auto_index = retrieve_autoindex(Regexp.last_match.pre_match) if Regexp.last_match
+ end
+
+ # This is what child classes implement.
+ def render
+ raise NotImplementedError, "Subclasses must implement a render method"
+ end
+
+ private
+
+ def value(object)
+ object.send @method_name if object
+ end
+
+ def value_before_type_cast(object)
+ unless object.nil?
+ method_before_type_cast = @method_name + "_before_type_cast"
+
+ object.respond_to?(method_before_type_cast) ?
+ object.send(method_before_type_cast) :
+ value(object)
+ end
+ end
+
+ def retrieve_object(object)
+ if object
+ object
+ elsif @template_object.instance_variable_defined?("@#{@object_name}")
+ @template_object.instance_variable_get("@#{@object_name}")
+ end
+ rescue NameError
+ # As @object_name may contain the nested syntax (item[subobject]) we need to fallback to nil.
+ nil
+ end
+
+ def retrieve_autoindex(pre_match)
+ object = self.object || @template_object.instance_variable_get("@#{pre_match}")
+ if object && object.respond_to?(:to_param)
+ object.to_param
+ else
+ raise ArgumentError, "object[] naming but object param and @object var don't exist or don't respond to to_param: #{object.inspect}"
+ end
+ end
+
+ def add_default_name_and_id_for_value(tag_value, options)
+ if tag_value.nil?
+ add_default_name_and_id(options)
+ else
+ specified_id = options["id"]
+ add_default_name_and_id(options)
+
+ if specified_id.blank? && options["id"].present?
+ options["id"] += "_#{sanitized_value(tag_value)}"
+ end
+ end
+ end
+
+ def add_default_name_and_id(options)
+ if options.has_key?("index")
+ options["name"] ||= options.fetch("name"){ tag_name_with_index(options["index"], options["multiple"]) }
+ options["id"] = options.fetch("id"){ tag_id_with_index(options["index"]) }
+ options.delete("index")
+ elsif defined?(@auto_index)
+ options["name"] ||= options.fetch("name"){ tag_name_with_index(@auto_index, options["multiple"]) }
+ options["id"] = options.fetch("id"){ tag_id_with_index(@auto_index) }
+ else
+ options["name"] ||= options.fetch("name"){ tag_name(options["multiple"]) }
+ options["id"] = options.fetch("id"){ tag_id }
+ end
+
+ options["id"] = [options.delete('namespace'), options["id"]].compact.join("_").presence
+ end
+
+ def tag_name(multiple = false)
+ "#{@object_name}[#{sanitized_method_name}]#{"[]" if multiple}"
+ end
+
+ def tag_name_with_index(index, multiple = false)
+ "#{@object_name}[#{index}][#{sanitized_method_name}]#{"[]" if multiple}"
+ end
+
+ def tag_id
+ "#{sanitized_object_name}_#{sanitized_method_name}"
+ end
+
+ def tag_id_with_index(index)
+ "#{sanitized_object_name}_#{index}_#{sanitized_method_name}"
+ end
+
+ def sanitized_object_name
+ @sanitized_object_name ||= @object_name.gsub(/\]\[|[^-a-zA-Z0-9:.]/, "_").sub(/_$/, "")
+ end
+
+ def sanitized_method_name
+ @sanitized_method_name ||= @method_name.sub(/\?$/,"")
+ end
+
+ def sanitized_value(value)
+ value.to_s.gsub(/\s/, "_").gsub(/[^-\w]/, "").downcase
+ end
+
+ def select_content_tag(option_tags, options, html_options)
+ html_options = html_options.stringify_keys
+ add_default_name_and_id(html_options)
+ options[:include_blank] ||= true unless options[:prompt] || select_not_required?(html_options)
+ value = options.fetch(:selected) { value(object) }
+ select = content_tag("select", add_options(option_tags, options, value), html_options)
+
+ if html_options["multiple"] && options.fetch(:include_hidden, true)
+ tag("input", :disabled => html_options["disabled"], :name => html_options["name"], :type => "hidden", :value => "") + select
+ else
+ select
+ end
+ end
+
+ def select_not_required?(html_options)
+ !html_options["required"] || html_options["multiple"] || html_options["size"].to_i > 1
+ end
+
+ def add_options(option_tags, options, value = nil)
+ if options[:include_blank]
+ option_tags = content_tag_string('option', options[:include_blank].kind_of?(String) ? options[:include_blank] : nil, :value => '') + "\n" + option_tags
+ end
+ if value.blank? && options[:prompt]
+ option_tags = content_tag_string('option', prompt_text(options[:prompt]), :value => '') + "\n" + option_tags
+ end
+ option_tags
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/check_box.rb b/actionview/lib/action_view/helpers/tags/check_box.rb
new file mode 100644
index 0000000000..6d51f2629a
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/check_box.rb
@@ -0,0 +1,64 @@
+require 'action_view/helpers/tags/checkable'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class CheckBox < Base #:nodoc:
+ include Checkable
+
+ def initialize(object_name, method_name, template_object, checked_value, unchecked_value, options)
+ @checked_value = checked_value
+ @unchecked_value = unchecked_value
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ options = @options.stringify_keys
+ options["type"] = "checkbox"
+ options["value"] = @checked_value
+ options["checked"] = "checked" if input_checked?(object, options)
+
+ if options["multiple"]
+ add_default_name_and_id_for_value(@checked_value, options)
+ options.delete("multiple")
+ else
+ add_default_name_and_id(options)
+ end
+
+ include_hidden = options.delete("include_hidden") { true }
+ checkbox = tag("input", options)
+
+ if include_hidden
+ hidden = hidden_field_for_checkbox(options)
+ hidden + checkbox
+ else
+ checkbox
+ end
+ end
+
+ private
+
+ def checked?(value)
+ case value
+ when TrueClass, FalseClass
+ value == !!@checked_value
+ when NilClass
+ false
+ when String
+ value == @checked_value
+ else
+ if value.respond_to?(:include?)
+ value.include?(@checked_value)
+ else
+ value.to_i == @checked_value.to_i
+ end
+ end
+ end
+
+ def hidden_field_for_checkbox(options)
+ @unchecked_value ? tag("input", options.slice("name", "disabled", "form").merge!("type" => "hidden", "value" => @unchecked_value)) : "".html_safe
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/checkable.rb b/actionview/lib/action_view/helpers/tags/checkable.rb
new file mode 100644
index 0000000000..052e9df662
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/checkable.rb
@@ -0,0 +1,16 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ module Checkable # :nodoc:
+ def input_checked?(object, options)
+ if options.has_key?("checked")
+ checked = options.delete "checked"
+ checked == true || checked == "checked"
+ else
+ checked?(value(object))
+ end
+ end
+ end
+ end
+ 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
new file mode 100644
index 0000000000..9b77ebeb1b
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/collection_check_boxes.rb
@@ -0,0 +1,44 @@
+require 'action_view/helpers/tags/collection_helpers'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class CollectionCheckBoxes < Base # :nodoc:
+ include CollectionHelpers
+
+ class CheckBoxBuilder < Builder # :nodoc:
+ def check_box(extra_html_options={})
+ html_options = extra_html_options.merge(@input_html_options)
+ @template_object.check_box(@object_name, @method_name, html_options, @value, nil)
+ end
+ end
+
+ def render(&block)
+ rendered_collection = render_collection do |item, value, text, default_html_options|
+ default_html_options[:multiple] = true
+ builder = instantiate_builder(CheckBoxBuilder, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+
+ # 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
+ end
+
+ private
+
+ def render_component(builder)
+ builder.check_box + builder.label
+ end
+ 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
new file mode 100644
index 0000000000..787039c82e
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/collection_helpers.rb
@@ -0,0 +1,85 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ module CollectionHelpers # :nodoc:
+ class Builder # :nodoc:
+ attr_reader :object, :text, :value
+
+ def initialize(template_object, object_name, method_name, object,
+ sanitized_attribute_name, text, value, input_html_options)
+ @template_object = template_object
+ @object_name = object_name
+ @method_name = method_name
+ @object = object
+ @sanitized_attribute_name = sanitized_attribute_name
+ @text = text
+ @value = value
+ @input_html_options = input_html_options
+ end
+
+ def label(label_html_options={}, &block)
+ html_options = label_html_options.merge(@input_html_options)
+ @template_object.label(@object_name, @sanitized_attribute_name, @text, html_options, &block)
+ end
+ end
+
+ def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
+ @collection = collection
+ @value_method = value_method
+ @text_method = text_method
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ private
+
+ def instantiate_builder(builder_class, item, value, text, html_options)
+ builder_class.new(@template_object, @object_name, @method_name, item,
+ sanitize_attribute_name(value), text, value, html_options)
+ end
+
+ # Generate default options for collection helpers, such as :checked and
+ # :disabled.
+ def default_html_options_for_collection(item, value) #:nodoc:
+ html_options = @html_options.dup
+
+ [:checked, :selected, :disabled].each do |option|
+ current_value = @options[option]
+ next if current_value.nil?
+
+ accept = if current_value.respond_to?(:call)
+ current_value.call(item)
+ else
+ Array(current_value).map(&:to_s).include?(value.to_s)
+ end
+
+ if accept
+ html_options[option] = true
+ elsif option == :checked
+ html_options[option] = false
+ end
+ end
+
+ html_options[:object] = @object
+ html_options
+ end
+
+ def sanitize_attribute_name(value) #:nodoc:
+ "#{sanitized_method_name}_#{sanitized_value(value)}"
+ end
+
+ def render_collection #:nodoc:
+ @collection.map do |item|
+ value = value_for_collection(item, @value_method)
+ text = value_for_collection(item, @text_method)
+ default_html_options = default_html_options_for_collection(item, value)
+ additional_html_options = option_html_attributes(item)
+
+ yield item, value, text, default_html_options.merge(additional_html_options)
+ end.join.html_safe
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
new file mode 100644
index 0000000000..20be34c1f2
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/collection_radio_buttons.rb
@@ -0,0 +1,36 @@
+require 'action_view/helpers/tags/collection_helpers'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class CollectionRadioButtons < Base # :nodoc:
+ include CollectionHelpers
+
+ class RadioButtonBuilder < Builder # :nodoc:
+ def radio_button(extra_html_options={})
+ html_options = extra_html_options.merge(@input_html_options)
+ @template_object.radio_button(@object_name, @method_name, @value, html_options)
+ end
+ end
+
+ def render(&block)
+ render_collection do |item, value, text, default_html_options|
+ builder = instantiate_builder(RadioButtonBuilder, item, value, text, default_html_options)
+
+ if block_given?
+ @template_object.capture(builder, &block)
+ else
+ render_component(builder)
+ end
+ end
+ end
+
+ private
+
+ def render_component(builder)
+ builder.radio_button + builder.label
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/collection_select.rb b/actionview/lib/action_view/helpers/tags/collection_select.rb
new file mode 100644
index 0000000000..6cb2b2e0d3
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/collection_select.rb
@@ -0,0 +1,28 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class CollectionSelect < Base #:nodoc:
+ def initialize(object_name, method_name, template_object, collection, value_method, text_method, options, html_options)
+ @collection = collection
+ @value_method = value_method
+ @text_method = text_method
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ option_tags_options = {
+ :selected => @options.fetch(:selected) { value(@object) },
+ :disabled => @options[:disabled]
+ }
+
+ select_content_tag(
+ options_from_collection_for_select(@collection, @value_method, @text_method, option_tags_options),
+ @options, @html_options
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/color_field.rb b/actionview/lib/action_view/helpers/tags/color_field.rb
new file mode 100644
index 0000000000..b4bbe92746
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/color_field.rb
@@ -0,0 +1,25 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class ColorField < TextField # :nodoc:
+ def render
+ options = @options.stringify_keys
+ options["value"] ||= validate_color_string(value(object))
+ @options = options
+ super
+ end
+
+ private
+
+ def validate_color_string(string)
+ regex = /#[0-9a-fA-F]{6}/
+ if regex.match(string)
+ string.downcase
+ else
+ "#000000"
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/date_field.rb b/actionview/lib/action_view/helpers/tags/date_field.rb
new file mode 100644
index 0000000000..c22be0db29
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/date_field.rb
@@ -0,0 +1,13 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class DateField < DatetimeField # :nodoc:
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-%m-%d")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/date_select.rb b/actionview/lib/action_view/helpers/tags/date_select.rb
new file mode 100644
index 0000000000..0c4ac40070
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/date_select.rb
@@ -0,0 +1,72 @@
+require 'active_support/core_ext/time/calculations'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class DateSelect < Base # :nodoc:
+ def initialize(object_name, method_name, template_object, options, html_options)
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ error_wrapping(datetime_selector(@options, @html_options).send("select_#{select_type}").html_safe)
+ end
+
+ class << self
+ def select_type
+ @select_type ||= self.name.split("::").last.sub("Select", "").downcase
+ end
+ end
+
+ private
+
+ def select_type
+ self.class.select_type
+ end
+
+ def datetime_selector(options, html_options)
+ datetime = options.fetch(:selected) { value(object) || default_datetime(options) }
+ @auto_index ||= nil
+
+ options = options.dup
+ options[:field_name] = @method_name
+ options[:include_position] = true
+ options[:prefix] ||= @object_name
+ options[:index] = @auto_index if @auto_index && !options.has_key?(:index)
+
+ DateTimeSelector.new(datetime, options, html_options)
+ end
+
+ def default_datetime(options)
+ return if options[:include_blank] || options[:prompt]
+
+ case options[:default]
+ when nil
+ Time.current
+ when Date, Time
+ options[:default]
+ else
+ default = options[:default].dup
+
+ # Rename :minute and :second to :min and :sec
+ default[:min] ||= default[:minute]
+ default[:sec] ||= default[:second]
+
+ time = Time.current
+
+ [:year, :month, :day, :hour, :min, :sec].each do |key|
+ default[key] ||= time.send(key)
+ end
+
+ Time.utc(
+ default[:year], default[:month], default[:day],
+ default[:hour], default[:min], default[:sec]
+ )
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_field.rb b/actionview/lib/action_view/helpers/tags/datetime_field.rb
new file mode 100644
index 0000000000..25e7e05ec6
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/datetime_field.rb
@@ -0,0 +1,22 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class DatetimeField < TextField # :nodoc:
+ 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 = options
+ super
+ end
+
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-%m-%dT%T.%L%z")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_local_field.rb b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
new file mode 100644
index 0000000000..b4a74185d1
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/datetime_local_field.rb
@@ -0,0 +1,19 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class DatetimeLocalField < DatetimeField # :nodoc:
+ class << self
+ def field_type
+ @field_type ||= "datetime-local"
+ end
+ end
+
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-%m-%dT%T")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/datetime_select.rb b/actionview/lib/action_view/helpers/tags/datetime_select.rb
new file mode 100644
index 0000000000..563de1840e
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/datetime_select.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class DatetimeSelect < DateSelect # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/email_field.rb b/actionview/lib/action_view/helpers/tags/email_field.rb
new file mode 100644
index 0000000000..7ce3ccb9bf
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/email_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class EmailField < TextField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/file_field.rb b/actionview/lib/action_view/helpers/tags/file_field.rb
new file mode 100644
index 0000000000..476b820d84
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/file_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class FileField < TextField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb
new file mode 100644
index 0000000000..2ed4712dac
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/grouped_collection_select.rb
@@ -0,0 +1,29 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class GroupedCollectionSelect < Base # :nodoc:
+ def initialize(object_name, method_name, template_object, collection, group_method, group_label_method, option_key_method, option_value_method, options, html_options)
+ @collection = collection
+ @group_method = group_method
+ @group_label_method = group_label_method
+ @option_key_method = option_key_method
+ @option_value_method = option_value_method
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ option_tags_options = {
+ :selected => @options.fetch(:selected) { value(@object) },
+ :disabled => @options[:disabled]
+ }
+
+ select_content_tag(
+ option_groups_from_collection_for_select(@collection, @group_method, @group_label_method, @option_key_method, @option_value_method, option_tags_options), @options, @html_options
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/hidden_field.rb b/actionview/lib/action_view/helpers/tags/hidden_field.rb
new file mode 100644
index 0000000000..c3757c2461
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/hidden_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class HiddenField < TextField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/label.rb b/actionview/lib/action_view/helpers/tags/label.rb
new file mode 100644
index 0000000000..180aa9ac27
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/label.rb
@@ -0,0 +1,66 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class Label < Base # :nodoc:
+ def initialize(object_name, method_name, template_object, content_or_options = nil, options = nil)
+ options ||= {}
+
+ content_is_options = content_or_options.is_a?(Hash)
+ if content_is_options
+ options.merge! content_or_options
+ @content = nil
+ else
+ @content = content_or_options
+ end
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render(&block)
+ options = @options.stringify_keys
+ tag_value = options.delete("value")
+ name_and_id = options.dup
+
+ if name_and_id["for"]
+ name_and_id["id"] = name_and_id["for"]
+ else
+ name_and_id.delete("id")
+ end
+
+ add_default_name_and_id_for_value(tag_value, name_and_id)
+ options.delete("index")
+ options.delete("namespace")
+ options.delete("multiple")
+ options["for"] = name_and_id["id"] unless options.key?("for")
+
+ if block_given?
+ content = @template_object.capture(&block)
+ else
+ content = if @content.blank?
+ @object_name.gsub!(/\[(.*)_attributes\]\[\d\]/, '.\1')
+ method_and_value = tag_value.present? ? "#{@method_name}.#{tag_value}" : @method_name
+
+ if object.respond_to?(:to_model)
+ key = object.class.model_name.i18n_key
+ i18n_default = ["#{key}.#{method_and_value}".to_sym, ""]
+ end
+
+ i18n_default ||= ""
+ I18n.t("#{@object_name}.#{method_and_value}", :default => i18n_default, :scope => "helpers.label").presence
+ else
+ @content.to_s
+ end
+
+ content ||= if object && object.class.respond_to?(:human_attribute_name)
+ object.class.human_attribute_name(@method_name)
+ end
+
+ content ||= @method_name.humanize
+ end
+
+ label_tag(name_and_id["id"], content, options)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/month_field.rb b/actionview/lib/action_view/helpers/tags/month_field.rb
new file mode 100644
index 0000000000..4c0fb846ee
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/month_field.rb
@@ -0,0 +1,13 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class MonthField < DatetimeField # :nodoc:
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-%m")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/number_field.rb b/actionview/lib/action_view/helpers/tags/number_field.rb
new file mode 100644
index 0000000000..4f95b1b4de
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/number_field.rb
@@ -0,0 +1,18 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class NumberField < TextField # :nodoc:
+ def render
+ options = @options.stringify_keys
+
+ if range = options.delete("in") || options.delete("within")
+ options.update("min" => range.min, "max" => range.max)
+ end
+
+ @options = options
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/password_field.rb b/actionview/lib/action_view/helpers/tags/password_field.rb
new file mode 100644
index 0000000000..6099fa6f19
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/password_field.rb
@@ -0,0 +1,12 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class PasswordField < TextField # :nodoc:
+ def render
+ @options = {:value => nil}.merge!(@options)
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/radio_button.rb b/actionview/lib/action_view/helpers/tags/radio_button.rb
new file mode 100644
index 0000000000..4849c537a5
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/radio_button.rb
@@ -0,0 +1,31 @@
+require 'action_view/helpers/tags/checkable'
+
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class RadioButton < Base # :nodoc:
+ include Checkable
+
+ def initialize(object_name, method_name, template_object, tag_value, options)
+ @tag_value = tag_value
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ options = @options.stringify_keys
+ options["type"] = "radio"
+ options["value"] = @tag_value
+ options["checked"] = "checked" if input_checked?(object, options)
+ add_default_name_and_id_for_value(@tag_value, options)
+ tag("input", options)
+ end
+
+ private
+
+ def checked?(value)
+ value.to_s == @tag_value.to_s
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/range_field.rb b/actionview/lib/action_view/helpers/tags/range_field.rb
new file mode 100644
index 0000000000..f98ae88043
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/range_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class RangeField < NumberField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/search_field.rb b/actionview/lib/action_view/helpers/tags/search_field.rb
new file mode 100644
index 0000000000..c09e2f1be7
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/search_field.rb
@@ -0,0 +1,24 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class SearchField < TextField # :nodoc:
+ def render
+ options = @options.stringify_keys
+
+ if options["autosave"]
+ if options["autosave"] == true
+ options["autosave"] = request.host.split(".").reverse.join(".")
+ end
+ options["results"] ||= 10
+ end
+
+ if options["onsearch"]
+ options["incremental"] = true unless options.has_key?("incremental")
+ end
+
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/select.rb b/actionview/lib/action_view/helpers/tags/select.rb
new file mode 100644
index 0000000000..00881d9978
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/select.rb
@@ -0,0 +1,41 @@
+module ActionView
+ module Helpers
+ 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 = @choices.to_a if @choices.is_a?(Range)
+
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ option_tags_options = {
+ :selected => @options.fetch(:selected) { value(@object) },
+ :disabled => @options[:disabled]
+ }
+
+ option_tags = if grouped_choices?
+ grouped_options_for_select(@choices, option_tags_options)
+ else
+ options_for_select(@choices, option_tags_options)
+ end
+
+ select_content_tag(option_tags, @options, @html_options)
+ end
+
+ private
+
+ # Grouped choices look like this:
+ #
+ # [nil, []]
+ # { nil => [] }
+ def grouped_choices?
+ !@choices.empty? && @choices.first.respond_to?(:last) && Array === @choices.first.last
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/tel_field.rb b/actionview/lib/action_view/helpers/tags/tel_field.rb
new file mode 100644
index 0000000000..987bb9e67a
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/tel_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TelField < TextField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/text_area.rb b/actionview/lib/action_view/helpers/tags/text_area.rb
new file mode 100644
index 0000000000..9ee83ee7c2
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/text_area.rb
@@ -0,0 +1,18 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TextArea < Base # :nodoc:
+ def render
+ options = @options.stringify_keys
+ add_default_name_and_id(options)
+
+ if size = options.delete("size")
+ options["cols"], options["rows"] = size.split("x") if size.respond_to?(:split)
+ end
+
+ content_tag("textarea", options.delete("value") { value_before_type_cast(object) }, options)
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/text_field.rb b/actionview/lib/action_view/helpers/tags/text_field.rb
new file mode 100644
index 0000000000..e910879ebf
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/text_field.rb
@@ -0,0 +1,29 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TextField < Base # :nodoc:
+ def render
+ options = @options.stringify_keys
+ options["size"] = options["maxlength"] unless options.key?("size")
+ options["type"] ||= field_type
+ options["value"] = options.fetch("value") { value_before_type_cast(object) } unless field_type == "file"
+ options["value"] &&= ERB::Util.html_escape(options["value"])
+ add_default_name_and_id(options)
+ tag("input", options)
+ end
+
+ class << self
+ def field_type
+ @field_type ||= self.name.split("::").last.sub("Field", "").downcase
+ end
+ end
+
+ private
+
+ def field_type
+ self.class.field_type
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/time_field.rb b/actionview/lib/action_view/helpers/tags/time_field.rb
new file mode 100644
index 0000000000..0e90a3aed7
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/time_field.rb
@@ -0,0 +1,13 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TimeField < DatetimeField # :nodoc:
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%T.%L")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/time_select.rb b/actionview/lib/action_view/helpers/tags/time_select.rb
new file mode 100644
index 0000000000..0b06311d25
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/time_select.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TimeSelect < DateSelect # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/time_zone_select.rb b/actionview/lib/action_view/helpers/tags/time_zone_select.rb
new file mode 100644
index 0000000000..80d165ec7e
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/time_zone_select.rb
@@ -0,0 +1,20 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class TimeZoneSelect < Base # :nodoc:
+ def initialize(object_name, method_name, template_object, priority_zones, options, html_options)
+ @priority_zones = priority_zones
+ @html_options = html_options
+
+ super(object_name, method_name, template_object, options)
+ end
+
+ def render
+ select_content_tag(
+ time_zone_options_for_select(value(@object) || @options[:default], @priority_zones, @options[:model] || ActiveSupport::TimeZone), @options, @html_options
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/url_field.rb b/actionview/lib/action_view/helpers/tags/url_field.rb
new file mode 100644
index 0000000000..d76340178d
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/url_field.rb
@@ -0,0 +1,8 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class UrlField < TextField # :nodoc:
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/tags/week_field.rb b/actionview/lib/action_view/helpers/tags/week_field.rb
new file mode 100644
index 0000000000..5b3d0494e9
--- /dev/null
+++ b/actionview/lib/action_view/helpers/tags/week_field.rb
@@ -0,0 +1,13 @@
+module ActionView
+ module Helpers
+ module Tags # :nodoc:
+ class WeekField < DatetimeField # :nodoc:
+ private
+
+ def format_date(value)
+ value.try(:strftime, "%Y-W%W")
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/text_helper.rb b/actionview/lib/action_view/helpers/text_helper.rb
new file mode 100644
index 0000000000..c23d605c5f
--- /dev/null
+++ b/actionview/lib/action_view/helpers/text_helper.rb
@@ -0,0 +1,445 @@
+require 'active_support/core_ext/string/filters'
+require 'active_support/core_ext/array/extract_options'
+
+module ActionView
+ # = Action View Text Helpers
+ module Helpers #:nodoc:
+ # The TextHelper module provides a set of methods for filtering, formatting
+ # and transforming strings, which can reduce the amount of inline Ruby code in
+ # your views. These helper methods extend Action View making them callable
+ # within your template files.
+ #
+ # ==== Sanitization
+ #
+ # Most text helpers by default sanitize the given content, but do not escape it.
+ # This means HTML tags will appear in the page but all malicious code will be removed.
+ # Let's look at some examples using the +simple_format+ method:
+ #
+ # simple_format('<a href="http://example.com/">Example</a>')
+ # # => "<p><a href=\"http://example.com/\">Example</a></p>"
+ #
+ # simple_format('<a href="javascript:alert(\'no!\')">Example</a>')
+ # # => "<p><a>Example</a></p>"
+ #
+ # If you want to escape all content, you should invoke the +h+ method before
+ # calling the text helper.
+ #
+ # simple_format h('<a href="http://example.com/">Example</a>')
+ # # => "<p>&lt;a href=\"http://example.com/\"&gt;Example&lt;/a&gt;</p>"
+ module TextHelper
+ extend ActiveSupport::Concern
+
+ include SanitizeHelper
+ include TagHelper
+ # The preferred method of outputting text in your views is to use the
+ # <%= "text" %> eRuby syntax. The regular _puts_ and _print_ methods
+ # do not operate as expected in an eRuby code block. If you absolutely must
+ # output text within a non-output code block (i.e., <% %>), you can use the concat method.
+ #
+ # <%
+ # concat "hello"
+ # # is the equivalent of <%= "hello" %>
+ #
+ # if logged_in
+ # concat "Logged in!"
+ # else
+ # concat link_to('login', action: :login)
+ # end
+ # # will either display "Logged in!" or a login link
+ # %>
+ def concat(string)
+ output_buffer << string
+ end
+
+ def safe_concat(string)
+ output_buffer.respond_to?(:safe_concat) ? output_buffer.safe_concat(string) : concat(string)
+ end
+
+ # Truncates a given +text+ after a given <tt>:length</tt> if +text+ is longer than <tt>:length</tt>
+ # (defaults to 30). The last characters will be replaced with the <tt>:omission</tt> (defaults to "...")
+ # for a total length not exceeding <tt>:length</tt>.
+ #
+ # Pass a <tt>:separator</tt> to truncate +text+ at a natural break.
+ #
+ # Pass a block if you want to show extra content when the text is truncated.
+ #
+ # The result is marked as HTML-safe, but it is escaped by default, unless <tt>:escape</tt> is
+ # +false+. Care should be taken if +text+ contains HTML tags or entities, because truncation
+ # may produce invalid HTML (such as unbalanced or incomplete tags).
+ #
+ # truncate("Once upon a time in a world far far away")
+ # # => "Once upon a time in a world..."
+ #
+ # truncate("Once upon a time in a world far far away", length: 17)
+ # # => "Once upon a ti..."
+ #
+ # truncate("Once upon a time in a world far far away", length: 17, separator: ' ')
+ # # => "Once upon a..."
+ #
+ # truncate("And they found that many people were sleeping better.", length: 25, omission: '... (continued)')
+ # # => "And they f... (continued)"
+ #
+ # truncate("<p>Once upon a time in a world far far away</p>")
+ # # => "<p>Once upon a time in a wo..."
+ #
+ # truncate("Once upon a time in a world far far away") { link_to "Continue", "#" }
+ # # => "Once upon a time in a wo...<a href="#">Continue</a>"
+ def truncate(text, options = {}, &block)
+ if text
+ length = options.fetch(:length, 30)
+
+ content = text.truncate(length, options)
+ content = options[:escape] == false ? content.html_safe : ERB::Util.html_escape(content)
+ content << capture(&block) if block_given? && text.length > length
+ content
+ end
+ end
+
+ # 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>')
+ #
+ # highlight('You searched for: rails', 'rails')
+ # # => You searched for: <mark>rails</mark>
+ #
+ # highlight('You searched for: ruby, rails, dhh', 'actionpack')
+ # # => You searched for: ruby, rails, dhh
+ #
+ # highlight('You searched for: rails', ['for', 'rails'], highlighter: '<em>\1</em>')
+ # # => You searched <em>for</em>: <em>rails</em>
+ #
+ # highlight('You searched for: rails', 'rails', highlighter: '<a href="search?q=\1">\1</a>')
+ # # => 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)
+ end.html_safe
+ end
+
+ # Extracts an excerpt from +text+ that matches the first instance of +phrase+.
+ # The <tt>:radius</tt> option expands the excerpt on each side of the first occurrence of +phrase+ by the number of characters
+ # defined in <tt>:radius</tt> (which defaults to 100). If the excerpt radius overflows the beginning or end of the +text+,
+ # then the <tt>:omission</tt> option (which defaults to "...") will be prepended/appended accordingly. Use the
+ # <tt>:separator</tt> option to choose the delimitation. The resulting string will be stripped in any case. If the +phrase+
+ # isn't found, nil is returned.
+ #
+ # excerpt('This is an example', 'an', radius: 5)
+ # # => ...s is an exam...
+ #
+ # excerpt('This is an example', 'is', radius: 5)
+ # # => This is a...
+ #
+ # excerpt('This is an example', 'is')
+ # # => This is an example
+ #
+ # excerpt('This next thing is an example', 'ex', radius: 2)
+ # # => ...next...
+ #
+ # excerpt('This is also an example', 'an', radius: 8, omission: '<chop> ')
+ # # => <chop> is also an example
+ #
+ # excerpt('This is a very beautiful morning', 'very', separator: ' ', radius: 1)
+ # # => ...a very beautiful...
+ def excerpt(text, phrase, options = {})
+ return unless text && phrase
+
+ separator = options[:separator] || ''
+ phrase = Regexp.escape(phrase)
+ regex = /#{phrase}/i
+
+ return unless matches = text.match(regex)
+ phrase = matches[0]
+
+ unless separator.empty?
+ text.split(separator).each do |value|
+ if value.match(regex)
+ regex = phrase = value
+ break
+ end
+ end
+ end
+
+ first_part, second_part = text.split(regex, 2)
+
+ prefix, first_part = cut_excerpt_part(:first, first_part, separator, options)
+ postfix, second_part = cut_excerpt_part(:second, second_part, separator, options)
+
+ affix = [first_part, separator, phrase, separator, second_part].join.strip
+ [prefix, affix, postfix].join
+ end
+
+ # Attempts to pluralize the +singular+ word unless +count+ is 1. If
+ # +plural+ is supplied, it will use that when count is > 1, otherwise
+ # it will use the Inflector to determine the plural form.
+ #
+ # pluralize(1, 'person')
+ # # => 1 person
+ #
+ # pluralize(2, 'person')
+ # # => 2 people
+ #
+ # pluralize(3, 'person', 'users')
+ # # => 3 users
+ #
+ # pluralize(0, 'person')
+ # # => 0 people
+ def pluralize(count, singular, plural = nil)
+ word = if (count == 1 || count =~ /^1(\.0+)?$/)
+ singular
+ else
+ plural || singular.pluralize
+ end
+
+ "#{count || 0} #{word}"
+ end
+
+ # Wraps the +text+ into lines no longer than +line_width+ width. This method
+ # breaks on the first whitespace character that does not exceed +line_width+
+ # (which is 80 by default).
+ #
+ # word_wrap('Once upon a time')
+ # # => Once upon a time
+ #
+ # word_wrap('Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding a successor to the throne turned out to be more trouble than anyone could have imagined...')
+ # # => Once upon a time, in a kingdom called Far Far Away, a king fell ill, and finding\na successor to the throne turned out to be more trouble than anyone could have\nimagined...
+ #
+ # word_wrap('Once upon a time', line_width: 8)
+ # # => Once\nupon a\ntime
+ #
+ # word_wrap('Once upon a time', line_width: 1)
+ # # => Once\nupon\na\ntime
+ def word_wrap(text, options = {})
+ line_width = options.fetch(:line_width, 80)
+
+ text.split("\n").collect! do |line|
+ line.length > line_width ? line.gsub(/(.{1,#{line_width}})(\s+|$)/, "\\1\n").strip : line
+ end * "\n"
+ end
+
+ # Returns +text+ transformed into HTML using simple formatting rules.
+ # Two or more consecutive newlines(<tt>\n\n</tt>) are considered as a
+ # paragraph and wrapped in <tt><p></tt> tags. One newline (<tt>\n</tt>) is
+ # considered as a linebreak and a <tt><br /></tt> tag is appended. This
+ # method does not remove the newlines from the +text+.
+ #
+ # You can pass any HTML attributes into <tt>html_options</tt>. These
+ # will be added to all created paragraphs.
+ #
+ # ==== Options
+ # * <tt>:sanitize</tt> - If +false+, does not sanitize +text+.
+ # * <tt>:wrapper_tag</tt> - String representing the wrapper tag, defaults to <tt>"p"</tt>
+ #
+ # ==== Examples
+ # my_text = "Here is some basic text...\n...with a line break."
+ #
+ # simple_format(my_text)
+ # # => "<p>Here is some basic text...\n<br />...with a line break.</p>"
+ #
+ # simple_format(my_text, {}, wrapper_tag: "div")
+ # # => "<div>Here is some basic text...\n<br />...with a line break.</div>"
+ #
+ # more_text = "We want to put a paragraph...\n\n...right there."
+ #
+ # simple_format(more_text)
+ # # => "<p>We want to put a paragraph...</p>\n\n<p>...right there.</p>"
+ #
+ # simple_format("Look ma! A class!", class: 'description')
+ # # => "<p class='description'>Look ma! A class!</p>"
+ #
+ # simple_format("<blink>Unblinkable.</blink>")
+ # # => "<p>Unblinkable.</p>"
+ #
+ # simple_format("<blink>Blinkable!</blink> It's true.", {}, sanitize: false)
+ # # => "<p><blink>Blinkable!</blink> It's true.</p>"
+ def simple_format(text, html_options = {}, options = {})
+ wrapper_tag = options.fetch(:wrapper_tag, :p)
+
+ text = sanitize(text) if options.fetch(:sanitize, true)
+ paragraphs = split_paragraphs(text)
+
+ if paragraphs.empty?
+ content_tag(wrapper_tag, nil, html_options)
+ else
+ paragraphs.map! { |paragraph|
+ content_tag(wrapper_tag, paragraph, html_options, options[:sanitize])
+ }.join("\n\n").html_safe
+ end
+ end
+
+ # Creates a Cycle object whose _to_s_ method cycles through elements of an
+ # array every time it is called. This can be used for example, to alternate
+ # classes for table rows. You can use named cycles to allow nesting in loops.
+ # Passing a Hash as the last parameter with a <tt>:name</tt> key will create a
+ # named cycle. The default name for a cycle without a +:name+ key is
+ # <tt>"default"</tt>. You can manually reset a cycle by calling reset_cycle
+ # and passing the name of the cycle. The current cycle string can be obtained
+ # anytime using the current_cycle method.
+ #
+ # # Alternate CSS classes for even and odd numbers...
+ # @items = [1,2,3,4]
+ # <table>
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("odd", "even") -%>">
+ # <td>item</td>
+ # </tr>
+ # <% end %>
+ # </table>
+ #
+ #
+ # # Cycle CSS classes for rows, and text colors for values within each row
+ # @items = x = [{first: 'Robert', middle: 'Daniel', last: 'James'},
+ # {first: 'Emily', middle: 'Shannon', maiden: 'Pike', last: 'Hicks'},
+ # {first: 'June', middle: 'Dae', last: 'Jones'}]
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("odd", "even", name: "row_class") -%>">
+ # <td>
+ # <% item.values.each do |value| %>
+ # <%# Create a named cycle "colors" %>
+ # <span style="color:<%= cycle("red", "green", "blue", name: "colors") -%>">
+ # <%= value %>
+ # </span>
+ # <% end %>
+ # <% reset_cycle("colors") %>
+ # </td>
+ # </tr>
+ # <% end %>
+ def cycle(first_value, *values)
+ options = values.extract_options!
+ name = options.fetch(:name, 'default')
+
+ values.unshift(first_value)
+
+ cycle = get_cycle(name)
+ unless cycle && cycle.values == values
+ cycle = set_cycle(name, Cycle.new(*values))
+ end
+ cycle.to_s
+ end
+
+ # Returns the current cycle string after a cycle has been started. Useful
+ # for complex table highlighting or any other design need which requires
+ # the current cycle string in more than one place.
+ #
+ # # Alternate background colors
+ # @items = [1,2,3,4]
+ # <% @items.each do |item| %>
+ # <div style="background-color:<%= cycle("red","white","blue") %>">
+ # <span style="background-color:<%= current_cycle %>"><%= item %></span>
+ # </div>
+ # <% end %>
+ def current_cycle(name = "default")
+ cycle = get_cycle(name)
+ cycle.current_value if cycle
+ end
+
+ # Resets a cycle so that it starts from the first element the next time
+ # it is called. Pass in +name+ to reset a named cycle.
+ #
+ # # Alternate CSS classes for even and odd numbers...
+ # @items = [[1,2,3,4], [5,6,3], [3,4,5,6,7,4]]
+ # <table>
+ # <% @items.each do |item| %>
+ # <tr class="<%= cycle("even", "odd") -%>">
+ # <% item.each do |value| %>
+ # <span style="color:<%= cycle("#333", "#666", "#999", name: "colors") -%>">
+ # <%= value %>
+ # </span>
+ # <% end %>
+ #
+ # <% reset_cycle("colors") %>
+ # </tr>
+ # <% end %>
+ # </table>
+ def reset_cycle(name = "default")
+ cycle = get_cycle(name)
+ cycle.reset if cycle
+ end
+
+ class Cycle #:nodoc:
+ attr_reader :values
+
+ def initialize(first_value, *values)
+ @values = values.unshift(first_value)
+ reset
+ end
+
+ def reset
+ @index = 0
+ end
+
+ def current_value
+ @values[previous_index].to_s
+ end
+
+ def to_s
+ value = @values[@index].to_s
+ @index = next_index
+ return value
+ end
+
+ private
+
+ def next_index
+ step_index(1)
+ end
+
+ def previous_index
+ step_index(-1)
+ end
+
+ def step_index(n)
+ (@index + n) % @values.size
+ end
+ end
+
+ private
+ # The cycle helpers need to store the cycles in a place that is
+ # guaranteed to be reset every time a page is rendered, so it
+ # uses an instance variable of ActionView::Base.
+ def get_cycle(name)
+ @_cycles = Hash.new unless defined?(@_cycles)
+ return @_cycles[name]
+ end
+
+ def set_cycle(name, cycle_object)
+ @_cycles = Hash.new unless defined?(@_cycles)
+ @_cycles[name] = cycle_object
+ end
+
+ def split_paragraphs(text)
+ return [] if text.blank?
+
+ text.to_str.gsub(/\r\n?/, "\n").split(/\n\n+/).map! do |t|
+ t.gsub!(/([^\n]\n)(?=[^\n])/, '\1<br />') || t
+ end
+ end
+
+ def cut_excerpt_part(part_position, part, separator, options)
+ return "", "" unless part
+
+ radius = options.fetch(:radius, 100)
+ omission = options.fetch(:omission, "...")
+
+ part = part.split(separator)
+ part.delete("")
+ affix = part.size > radius ? omission : ""
+
+ part = if part_position == :first
+ drop_index = [part.length - radius, 0].max
+ part.drop(drop_index)
+ else
+ part.first(radius)
+ end
+
+ return affix, part.join(separator)
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/translation_helper.rb b/actionview/lib/action_view/helpers/translation_helper.rb
new file mode 100644
index 0000000000..ad8eb47f1f
--- /dev/null
+++ b/actionview/lib/action_view/helpers/translation_helper.rb
@@ -0,0 +1,107 @@
+require 'action_view/helpers/tag_helper'
+require 'i18n/exceptions'
+
+module I18n
+ class ExceptionHandler
+ include Module.new {
+ def call(exception, locale, key, options)
+ exception.is_a?(MissingTranslation) && options[:rescue_format] == :html ? super.html_safe : super
+ end
+ }
+ end
+end
+
+module ActionView
+ # = Action View Translation Helpers
+ module Helpers
+ module TranslationHelper
+ # Delegates to <tt>I18n#translate</tt> but also performs three additional functions.
+ #
+ # First, it'll pass the <tt>rescue_format: :html</tt> option to I18n so that any
+ # thrown +MissingTranslation+ messages will be turned into inline spans that
+ #
+ # * have a "translation-missing" class set,
+ # * contain the missing key as a title attribute and
+ # * a titleized version of the last key segment as a text.
+ #
+ # E.g. the value returned for a missing translation key :"blog.post.title" will be
+ # <span class="translation_missing" title="translation missing: en.blog.post.title">Title</span>.
+ # This way your views will display rather reasonable strings but it will still
+ # be easy to spot missing translations.
+ #
+ # Second, it'll scope the key by the current partial if the key starts
+ # with a period. So if you call <tt>translate(".foo")</tt> from the
+ # <tt>people/index.html.erb</tt> template, you'll actually be calling
+ # <tt>I18n.translate("people.index.foo")</tt>. This makes it less repetitive
+ # to translate many keys within the same partials and gives you a simple framework
+ # for scoping them consistently. If you don't prepend the key with a period,
+ # nothing is converted.
+ #
+ # Third, it'll mark the translation as safe HTML if the key has the suffix
+ # "_html" or the last element of the key is the word "html". For example,
+ # calling translate("footer_html") or translate("footer.html") will return
+ # a safe HTML string that won't be escaped by other HTML helper methods. This
+ # naming convention helps to identify translations that include HTML tags so that
+ # you know what kind of output to expect when you call translate in a template.
+ def translate(key, options = {})
+ options.merge!(:rescue_format => :html) unless options.key?(:rescue_format)
+ options[:default] = wrap_translate_defaults(options[:default]) if options[:default]
+ if html_safe_translation_key?(key)
+ html_safe_options = options.dup
+ options.except(*I18n::RESERVED_KEYS).each do |name, value|
+ unless name == :count && value.is_a?(Numeric)
+ html_safe_options[name] = ERB::Util.html_escape(value.to_s)
+ end
+ end
+ translation = I18n.translate(scope_key_by_partial(key), html_safe_options)
+
+ translation.respond_to?(:html_safe) ? translation.html_safe : translation
+ else
+ I18n.translate(scope_key_by_partial(key), options)
+ end
+ end
+ alias :t :translate
+
+ # Delegates to <tt>I18n.localize</tt> with no additional functionality.
+ #
+ # See http://rubydoc.info/github/svenfuchs/i18n/master/I18n/Backend/Base:localize
+ # for more information.
+ def localize(*args)
+ I18n.localize(*args)
+ end
+ alias :l :localize
+
+ private
+ def scope_key_by_partial(key)
+ if key.to_s.first == "."
+ if @virtual_path
+ @virtual_path.gsub(%r{/_?}, ".") + key.to_s
+ else
+ raise "Cannot use t(#{key.inspect}) shortcut because path is not available"
+ end
+ else
+ key
+ end
+ end
+
+ def html_safe_translation_key?(key)
+ key.to_s =~ /(\b|_|\.)html$/
+ end
+
+ def wrap_translate_defaults(defaults)
+ new_defaults = []
+ defaults = Array(defaults)
+ while key = defaults.shift
+ if key.is_a?(Symbol)
+ new_defaults << lambda { |_, options| translate key, options.merge(:default => defaults) }
+ break
+ else
+ new_defaults << key
+ end
+ end
+
+ new_defaults
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/helpers/url_helper.rb b/actionview/lib/action_view/helpers/url_helper.rb
new file mode 100644
index 0000000000..56dd7a4390
--- /dev/null
+++ b/actionview/lib/action_view/helpers/url_helper.rb
@@ -0,0 +1,625 @@
+require 'action_view/helpers/javascript_helper'
+require 'active_support/core_ext/array/access'
+require 'active_support/core_ext/hash/keys'
+require 'active_support/core_ext/string/output_safety'
+
+module ActionView
+ # = Action View URL Helpers
+ module Helpers #:nodoc:
+ # Provides a set of methods for making links and getting URLs that
+ # depend on the routing subsystem (see ActionDispatch::Routing).
+ # This allows you to use the same format for links in views
+ # and controllers.
+ module UrlHelper
+ # This helper may be included in any class that includes the
+ # URL helpers of a routes (routes.url_helpers). Some methods
+ # provided here will only work in the context of a request
+ # (link_to_unless_current, for instance), which must be provided
+ # as a method called #request on the context.
+ BUTTON_TAG_METHOD_VERBS = %w{patch put delete}
+ extend ActiveSupport::Concern
+
+ include TagHelper
+
+ module ClassMethods
+ def _url_for_modules
+ ActionView::RoutingUrlFor
+ end
+ end
+
+ # Basic implementation of url_for to allow use helpers without routes existence
+ def url_for(options = nil) # :nodoc:
+ case options
+ when String
+ options
+ when :back
+ _back_url
+ else
+ raise ArgumentError, "arguments passed to url_for can't be handled. Please require " +
+ "routes or provide your own implementation"
+ end
+ end
+
+ def _back_url # :nodoc:
+ referrer = controller.respond_to?(:request) && controller.request.env["HTTP_REFERER"]
+ referrer || 'javascript:history.back()'
+ end
+ protected :_back_url
+
+ # Creates a link tag of the given +name+ using a URL created by the set of +options+.
+ # See the valid options in the documentation for +url_for+. It's also possible to
+ # pass a String instead of an options hash, which generates a link tag that uses the
+ # value of the String as the href for the link. Using a <tt>:back</tt> Symbol instead
+ # of an options hash will generate a link to the referrer (a JavaScript back link
+ # will be used in place of a referrer if none exists). If +nil+ is passed as the name
+ # the value of the link itself will become the name.
+ #
+ # ==== Signatures
+ #
+ # link_to(body, url, html_options = {})
+ # # url is a String; you can use URL helpers like
+ # # posts_path
+ #
+ # link_to(body, url_options = {}, html_options = {})
+ # # url_options, except :method, is passed to url_for
+ #
+ # link_to(options = {}, html_options = {}) do
+ # # name
+ # end
+ #
+ # link_to(url, html_options = {}) do
+ # # name
+ # end
+ #
+ # ==== Options
+ # * <tt>:data</tt> - This option can be used to add custom data attributes.
+ # * <tt>method: symbol of HTTP verb</tt> - This modifier will dynamically
+ # create an HTML form and immediately submit the form for processing using
+ # the HTTP verb specified. Useful for having links perform a POST operation
+ # in dangerous actions like deleting a record (which search bots can follow
+ # while spidering your site). Supported verbs are <tt>:post</tt>, <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>.
+ # Note that if the user has JavaScript disabled, the request will fall back
+ # to using GET. If <tt>href: '#'</tt> is used and the user has JavaScript
+ # disabled clicking the link will have no effect. If you are relying on the
+ # POST behavior, 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>.
+ # * <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
+ # completion of the Ajax request and performing JavaScript operations once
+ # they're complete
+ #
+ # ==== Data attributes
+ #
+ # * <tt>confirm: 'question?'</tt> - This will allow the unobtrusive JavaScript
+ # driver to prompt with the question specified (in this case, the
+ # resulting text would be <tt>question?</tt>. If the user accepts, the
+ # link is processed normally, otherwise no action is taken.
+ # * <tt>:disable_with</tt> - Value of this parameter will be
+ # used as the value for a disabled version of the submit
+ # button when the form is submitted. This feature is provided
+ # by the unobtrusive JavaScript driver.
+ #
+ # ==== Examples
+ # Because it relies on +url_for+, +link_to+ supports both older-style controller/action/id arguments
+ # and newer RESTful routes. Current Rails style favors RESTful routes whenever possible, so base
+ # your application on resources and use
+ #
+ # link_to "Profile", profile_path(@profile)
+ # # => <a href="/profiles/1">Profile</a>
+ #
+ # or the even pithier
+ #
+ # link_to "Profile", @profile
+ # # => <a href="/profiles/1">Profile</a>
+ #
+ # in place of the older more verbose, non-resource-oriented
+ #
+ # link_to "Profile", controller: "profiles", action: "show", id: @profile
+ # # => <a href="/profiles/show/1">Profile</a>
+ #
+ # Similarly,
+ #
+ # link_to "Profiles", profiles_path
+ # # => <a href="/profiles">Profiles</a>
+ #
+ # is better than
+ #
+ # link_to "Profiles", controller: "profiles"
+ # # => <a href="/profiles">Profiles</a>
+ #
+ # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
+ #
+ # <%= link_to(@profile) do %>
+ # <strong><%= @profile.name %></strong> -- <span>Check it out!</span>
+ # <% end %>
+ # # => <a href="/profiles/1">
+ # <strong>David</strong> -- <span>Check it out!</span>
+ # </a>
+ #
+ # Classes and ids for CSS are easy to produce:
+ #
+ # link_to "Articles", articles_path, id: "news", class: "article"
+ # # => <a href="/articles" class="article" id="news">Articles</a>
+ #
+ # Be careful when using the older argument style, as an extra literal hash is needed:
+ #
+ # link_to "Articles", { controller: "articles" }, id: "news", class: "article"
+ # # => <a href="/articles" class="article" id="news">Articles</a>
+ #
+ # Leaving the hash off gives the wrong link:
+ #
+ # link_to "WRONG!", controller: "articles", id: "news", class: "article"
+ # # => <a href="/articles/index/news?class=article">WRONG!</a>
+ #
+ # +link_to+ can also produce links with anchors or query strings:
+ #
+ # link_to "Comment wall", profile_path(@profile, anchor: "wall")
+ # # => <a href="/profiles/1#wall">Comment wall</a>
+ #
+ # link_to "Ruby on Rails search", controller: "searches", query: "ruby on rails"
+ # # => <a href="/searches?query=ruby+on+rails">Ruby on Rails search</a>
+ #
+ # link_to "Nonsense search", searches_path(foo: "bar", baz: "quux")
+ # # => <a href="/searches?foo=bar&amp;baz=quux">Nonsense search</a>
+ #
+ # The only option specific to +link_to+ (<tt>:method</tt>) is used as follows:
+ #
+ # link_to("Destroy", "http://www.example.com", method: :delete)
+ # # => <a href='http://www.example.com' rel="nofollow" data-method="delete">Destroy</a>
+ #
+ # You can also use custom data attributes using the <tt>:data</tt> option:
+ #
+ # link_to "Visit Other Site", "http://www.rubyonrails.org/", data: { confirm: "Are you sure?" }
+ # # => <a href="http://www.rubyonrails.org/" data-confirm="Are you sure?">Visit Other Site</a>
+ def link_to(name = nil, options = nil, html_options = nil, &block)
+ html_options, options, name = options, name, block if block_given?
+ options ||= {}
+
+ html_options = convert_options_to_data_attributes(options, html_options)
+
+ url = url_for(options)
+ html_options['href'] ||= url
+
+ content_tag(:a, name || url, html_options, &block)
+ end
+
+ # Generates a form containing a single button that submits to the URL created
+ # by the set of +options+. This is the safest method to ensure links that
+ # cause changes to your data are not triggered by search bots or accelerators.
+ # If the HTML button does not work with your layout, you can also consider
+ # using the +link_to+ method with the <tt>:method</tt> modifier as described in
+ # the +link_to+ documentation.
+ #
+ # By default, the generated form element has a class name of <tt>button_to</tt>
+ # to allow styling of the form itself and its children. This can be changed
+ # using the <tt>:form_class</tt> modifier within +html_options+. You can control
+ # the form submission and input element behavior using +html_options+.
+ # This method accepts the <tt>:method</tt> modifier described in the +link_to+ documentation.
+ # If no <tt>:method</tt> modifier is given, it will default to performing a POST operation.
+ # You can also disable the button by passing <tt>disabled: true</tt> in +html_options+.
+ # If you are using RESTful routes, you can pass the <tt>:method</tt>
+ # to change the HTTP verb used to submit the form.
+ #
+ # ==== Options
+ # The +options+ hash accepts the same options as +url_for+.
+ #
+ # There are a few special +html_options+:
+ # * <tt>:method</tt> - Symbol of HTTP verb. Supported verbs are <tt>:post</tt>, <tt>:get</tt>,
+ # <tt>:delete</tt>, <tt>:patch</tt>, and <tt>:put</tt>. By default it will be <tt>:post</tt>.
+ # * <tt>:disabled</tt> - If set to true, it will generate a disabled button.
+ # * <tt>:data</tt> - This option can be used to add custom data attributes.
+ # * <tt>:remote</tt> - If set to true, will allow the Unobtrusive JavaScript drivers to control the
+ # submit behavior. By default this behavior is an ajax submit.
+ # * <tt>:form</tt> - This hash will be form attributes
+ # * <tt>:form_class</tt> - This controls the class of the form within which the submit button will
+ # be placed
+ # * <tt>:params</tt> - Hash of parameters to be rendered as hidden fields within the form.
+ #
+ # ==== Data attributes
+ #
+ # * <tt>:confirm</tt> - This will use the unobtrusive JavaScript driver to
+ # prompt with the question specified. If the user accepts, the link is
+ # processed normally, otherwise no action is taken.
+ # * <tt>:disable_with</tt> - Value of this parameter will be
+ # used as the value for a disabled version of the submit
+ # button when the form is submitted. This feature is provided
+ # by the unobtrusive JavaScript driver.
+ #
+ # ==== Examples
+ # <%= button_to "New", action: "new" %>
+ # # => "<form method="post" action="/controller/new" class="button_to">
+ # # <div><input value="New" type="submit" /></div>
+ # # </form>"
+ #
+ # <%= button_to [:make_happy, @user] do %>
+ # Make happy <strong><%= @user.name %></strong>
+ # <% end %>
+ # # => "<form method="post" action="/users/1/make_happy" class="button_to">
+ # # <div>
+ # # <button type="submit">
+ # # Make happy <strong><%= @user.name %></strong>
+ # # </button>
+ # # </div>
+ # # </form>"
+ #
+ # <%= button_to "New", { action: "new" }, form_class: "new-thing" %>
+ # # => "<form method="post" action="/controller/new" class="new-thing">
+ # # <div><input value="New" type="submit" /></div>
+ # # </form>"
+ #
+ #
+ # <%= button_to "Create", { action: "create" }, remote: true, form: { "data-type" => "json" } %>
+ # # => "<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
+ # # <div>
+ # # <input value="Create" type="submit" />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
+ # # </div>
+ # # </form>"
+ #
+ #
+ # <%= button_to "Delete Image", { action: "delete", id: @image.id },
+ # method: :delete, data: { confirm: "Are you sure?" } %>
+ # # => "<form method="post" action="/images/delete/1" class="button_to">
+ # # <div>
+ # # <input type="hidden" name="_method" value="delete" />
+ # # <input data-confirm='Are you sure?' value="Delete Image" type="submit" />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
+ # # </div>
+ # # </form>"
+ #
+ #
+ # <%= button_to('Destroy', 'http://www.example.com',
+ # method: "delete", remote: true, data: { confirm: 'Are you sure?', disable_with: 'loading...' }) %>
+ # # => "<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
+ # # <div>
+ # # <input name='_method' value='delete' type='hidden' />
+ # # <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
+ # # <input name="authenticity_token" type="hidden" value="10f2163b45388899ad4d5ae948988266befcb6c3d1b2451cf657a0c293d605a6"/>
+ # # </div>
+ # # </form>"
+ # #
+ def button_to(name = nil, options = nil, html_options = nil, &block)
+ html_options, options = options, name if block_given?
+ options ||= {}
+ html_options ||= {}
+
+ html_options = html_options.stringify_keys
+ convert_boolean_attributes!(html_options, %w(disabled))
+
+ url = options.is_a?(String) ? options : url_for(options)
+ remote = html_options.delete('remote')
+ params = html_options.delete('params')
+
+ method = html_options.delete('method').to_s
+ method_tag = BUTTON_TAG_METHOD_VERBS.include?(method) ? method_tag(method) : ''.html_safe
+
+ form_method = method == 'get' ? 'get' : 'post'
+ form_options = html_options.delete('form') || {}
+ form_options[:class] ||= html_options.delete('form_class') || 'button_to'
+ form_options.merge!(method: form_method, action: url)
+ form_options.merge!("data-remote" => "true") if remote
+
+ request_token_tag = form_method == 'post' ? token_tag : ''
+
+ html_options = convert_options_to_data_attributes(options, html_options)
+ html_options['type'] = 'submit'
+
+ button = if block_given?
+ content_tag('button', html_options, &block)
+ else
+ html_options['value'] = name || url
+ tag('input', html_options)
+ end
+
+ inner_tags = method_tag.safe_concat(button).safe_concat(request_token_tag)
+ if params
+ params.each do |param_name, value|
+ inner_tags.safe_concat tag(:input, type: "hidden", name: param_name, value: value.to_param)
+ end
+ end
+ content_tag('form', content_tag('div', inner_tags), form_options)
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ unless the current request URI is the same as the links, in
+ # which case only the name is returned (or the given block is yielded, if
+ # one exists). You can give +link_to_unless_current+ a block which will
+ # specialize the default behavior (e.g., show a "Start Here" link rather
+ # than the link's text).
+ #
+ # ==== Examples
+ # Let's say you have a navigation menu...
+ #
+ # <ul id="navbar">
+ # <li><%= link_to_unless_current("Home", { action: "index" }) %></li>
+ # <li><%= link_to_unless_current("About Us", { action: "about" }) %></li>
+ # </ul>
+ #
+ # If in the "about" action, it will render...
+ #
+ # <ul id="navbar">
+ # <li><a href="/controller/index">Home</a></li>
+ # <li>About Us</li>
+ # </ul>
+ #
+ # ...but if in the "index" action, it will render:
+ #
+ # <ul id="navbar">
+ # <li>Home</li>
+ # <li><a href="/controller/about">About Us</a></li>
+ # </ul>
+ #
+ # The implicit block given to +link_to_unless_current+ is evaluated if the current
+ # action is the action given. So, if we had a comments page and wanted to render a
+ # "Go Back" link instead of a link to the comments page, we could do something like this...
+ #
+ # <%=
+ # link_to_unless_current("Comment", { controller: "comments", action: "new" }) do
+ # link_to("Go back", { controller: "posts", action: "index" })
+ # end
+ # %>
+ def link_to_unless_current(name, options = {}, html_options = {}, &block)
+ link_to_unless current_page?(options), name, options, html_options, &block
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ unless +condition+ is true, in which case only the name is
+ # returned. To specialize the default behavior (i.e., show a login link rather
+ # than just the plaintext link text), you can pass a block that
+ # accepts the name or the full argument list for +link_to_unless+.
+ #
+ # ==== Examples
+ # <%= link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) %>
+ # # If the user is logged in...
+ # # => <a href="/controller/reply/">Reply</a>
+ #
+ # <%=
+ # link_to_unless(@current_user.nil?, "Reply", { action: "reply" }) do |name|
+ # link_to(name, { controller: "accounts", action: "signup" })
+ # end
+ # %>
+ # # If the user is logged in...
+ # # => <a href="/controller/reply/">Reply</a>
+ # # 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
+ end
+
+ # Creates a link tag of the given +name+ using a URL created by the set of
+ # +options+ if +condition+ is true, otherwise only the name is
+ # returned. To specialize the default behavior, you can pass a block that
+ # accepts the name or the full argument list for +link_to_unless+ (see the examples
+ # in +link_to_unless+).
+ #
+ # ==== Examples
+ # <%= link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) %>
+ # # If the user isn't logged in...
+ # # => <a href="/sessions/new/">Login</a>
+ #
+ # <%=
+ # link_to_if(@current_user.nil?, "Login", { controller: "sessions", action: "new" }) do
+ # link_to(@current_user.login, { controller: "accounts", action: "show", id: @current_user })
+ # end
+ # %>
+ # # If the user isn't logged in...
+ # # => <a href="/sessions/new/">Login</a>
+ # # 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
+ end
+
+ # Creates a mailto link tag to the specified +email_address+, which is
+ # also used as the name of the link unless +name+ is specified. Additional
+ # HTML attributes for the link can be passed in +html_options+.
+ #
+ # +mail_to+ has several methods for customizing the email itself by
+ # passing special keys to +html_options+.
+ #
+ # ==== Options
+ # * <tt>:subject</tt> - Preset the subject line of the email.
+ # * <tt>:body</tt> - Preset the body of the email.
+ # * <tt>:cc</tt> - Carbon Copy additional recipients on the email.
+ # * <tt>:bcc</tt> - Blind Carbon Copy additional recipients on the email.
+ #
+ # ==== Obfuscation
+ # Prior to Rails 4.0, +mail_to+ provided options for encoding the address
+ # in order to hinder email harvesters. To take advantage of these options,
+ # install the +actionview-encoded_mail_to+ gem.
+ #
+ # ==== Examples
+ # mail_to "me@domain.com"
+ # # => <a href="mailto:me@domain.com">me@domain.com</a>
+ #
+ # mail_to "me@domain.com", "My email"
+ # # => <a href="mailto:me@domain.com">My email</a>
+ #
+ # mail_to "me@domain.com", "My email", cc: "ccaddress@domain.com",
+ # subject: "This is an example email"
+ # # => <a href="mailto:me@domain.com?cc=ccaddress@domain.com&subject=This%20is%20an%20example%20email">My email</a>
+ #
+ # You can use a block as well if your link target is hard to fit into the name parameter. ERB example:
+ #
+ # <%= mail_to "me@domain.com" do %>
+ # <strong>Email me:</strong> <span>me@domain.com</span>
+ # <% end %>
+ # # => <a href="mailto:me@domain.com">
+ # <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
+
+ extras = %w{ cc bcc body subject }.map! { |item|
+ option = html_options.delete(item) || next
+ "#{item}=#{Rack::Utils.escape_path(option)}"
+ }.compact
+ extras = extras.empty? ? '' : '?' + ERB::Util.html_escape(extras.join('&'))
+
+ html_options["href"] = "mailto:#{email_address}#{extras}".html_safe
+
+ content_tag(:a, name || email_address.html_safe, html_options, &block)
+ end
+
+ # True if the current request URI was generated by the given +options+.
+ #
+ # ==== Examples
+ # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc</tt> action.
+ #
+ # current_page?(action: 'process')
+ # # => false
+ #
+ # current_page?(controller: 'shop', action: 'checkout')
+ # # => true
+ #
+ # current_page?(controller: 'shop', action: 'checkout', order: 'asc')
+ # # => false
+ #
+ # current_page?(action: 'checkout')
+ # # => true
+ #
+ # current_page?(controller: 'library', action: 'checkout')
+ # # => false
+ #
+ # current_page?('http://www.example.com/shop/checkout')
+ # # => true
+ #
+ # current_page?('/shop/checkout')
+ # # => true
+ #
+ # Let's say we're in the <tt>http://www.example.com/shop/checkout?order=desc&page=1</tt> action.
+ #
+ # current_page?(action: 'process')
+ # # => false
+ #
+ # current_page?(controller: 'shop', action: 'checkout')
+ # # => true
+ #
+ # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '1')
+ # # => true
+ #
+ # current_page?(controller: 'shop', action: 'checkout', order: 'desc', page: '2')
+ # # => false
+ #
+ # current_page?(controller: 'shop', action: 'checkout', order: 'desc')
+ # # => false
+ #
+ # current_page?(action: 'checkout')
+ # # => true
+ #
+ # current_page?(controller: 'library', action: 'checkout')
+ # # => false
+ #
+ # Let's say we're in the <tt>http://www.example.com/products</tt> action with method POST in case of invalid product.
+ #
+ # current_page?(controller: 'product', action: 'index')
+ # # => false
+ #
+ def current_page?(options)
+ unless request
+ raise "You cannot use helpers that need to determine the current " \
+ "page unless your view context provides a Request object " \
+ "in a #request method"
+ end
+
+ return false unless request.get? || request.head?
+
+ url_string = URI.parser.unescape(url_for(options)).force_encoding(Encoding::BINARY)
+
+ # We ignore any extra parameters in the request_uri if the
+ # submitted url doesn't have any either. This lets the function
+ # work with things like ?order=asc
+ request_uri = url_string.index("?") ? request.fullpath : request.path
+ request_uri = URI.parser.unescape(request_uri).force_encoding(Encoding::BINARY)
+
+ if url_string =~ /^\w+:\/\//
+ url_string == "#{request.protocol}#{request.host_with_port}#{request_uri}"
+ else
+ url_string == request_uri
+ end
+ end
+
+ private
+ def convert_options_to_data_attributes(options, html_options)
+ if html_options
+ html_options = html_options.stringify_keys
+ html_options['data-remote'] = 'true' if link_to_remote_options?(options) || link_to_remote_options?(html_options)
+
+ method = html_options.delete('method')
+
+ add_method_to_attributes!(html_options, method) if method
+
+ html_options
+ else
+ link_to_remote_options?(options) ? {'data-remote' => 'true'} : {}
+ end
+ end
+
+ def link_to_remote_options?(options)
+ if options.is_a?(Hash)
+ options.delete('remote') || options.delete(:remote)
+ end
+ end
+
+ def add_method_to_attributes!(html_options, method)
+ if method && method.to_s.downcase != "get" && html_options["rel"] !~ /nofollow/
+ html_options["rel"] = "#{html_options["rel"]} nofollow".lstrip
+ end
+ html_options["data-method"] = method
+ end
+
+ # Processes the +html_options+ hash, converting the boolean
+ # attributes from true/false form into the form required by
+ # HTML/XHTML. (An attribute is considered to be boolean if
+ # its name is listed in the given +bool_attrs+ array.)
+ #
+ # More specifically, for each boolean attribute in +html_options+
+ # given as:
+ #
+ # "attr" => bool_value
+ #
+ # if the associated +bool_value+ evaluates to true, it is
+ # replaced with the attribute's name; otherwise the attribute is
+ # removed from the +html_options+ hash. (See the XHTML 1.0 spec,
+ # section 4.5 "Attribute Minimization" for more:
+ # http://www.w3.org/TR/xhtml1/#h-4.5)
+ #
+ # Returns the updated +html_options+ hash, which is also modified
+ # in place.
+ #
+ # Example:
+ #
+ # convert_boolean_attributes!( html_options,
+ # %w( checked disabled readonly ) )
+ def convert_boolean_attributes!(html_options, bool_attrs)
+ bool_attrs.each { |x| html_options[x] = x if html_options.delete(x) }
+ html_options
+ end
+
+ def token_tag(token=nil)
+ if token != false && protect_against_forgery?
+ token ||= form_authenticity_token
+ tag(:input, type: "hidden", name: request_forgery_protection_token.to_s, value: token)
+ else
+ ''
+ end
+ end
+
+ def method_tag(method)
+ tag('input', type: 'hidden', name: '_method', value: method.to_s)
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/layouts.rb b/actionview/lib/action_view/layouts.rb
new file mode 100644
index 0000000000..d8de1d95df
--- /dev/null
+++ b/actionview/lib/action_view/layouts.rb
@@ -0,0 +1,424 @@
+require "action_view/rendering"
+require "active_support/core_ext/module/remove_method"
+
+module ActionView
+ # Layouts reverse the common pattern of including shared headers and footers in many templates to isolate changes in
+ # repeated setups. The inclusion pattern has pages that look like this:
+ #
+ # <%= render "shared/header" %>
+ # Hello World
+ # <%= render "shared/footer" %>
+ #
+ # This approach is a decent way of keeping common structures isolated from the changing content, but it's verbose
+ # and if you ever want to change the structure of these two includes, you'll have to change all the templates.
+ #
+ # With layouts, you can flip it around and have the common structure know where to insert changing content. This means
+ # that the header and footer are only mentioned in one place, like this:
+ #
+ # // The header part of this layout
+ # <%= yield %>
+ # // The footer part of this layout
+ #
+ # And then you have content pages that look like this:
+ #
+ # hello world
+ #
+ # At rendering time, the content page is computed and then inserted in the layout, like this:
+ #
+ # // The header part of this layout
+ # hello world
+ # // The footer part of this layout
+ #
+ # == Accessing shared variables
+ #
+ # Layouts have access to variables specified in the content pages and vice versa. This allows you to have layouts with
+ # references that won't materialize before rendering time:
+ #
+ # <h1><%= @page_title %></h1>
+ # <%= yield %>
+ #
+ # ...and content pages that fulfill these references _at_ rendering time:
+ #
+ # <% @page_title = "Welcome" %>
+ # Off-world colonies offers you a chance to start a new life
+ #
+ # The result after rendering is:
+ #
+ # <h1>Welcome</h1>
+ # Off-world colonies offers you a chance to start a new life
+ #
+ # == Layout assignment
+ #
+ # You can either specify a layout declaratively (using the #layout class method) or give
+ # it the same name as your controller, and place it in <tt>app/views/layouts</tt>.
+ # If a subclass does not have a layout specified, it inherits its layout using normal Ruby inheritance.
+ #
+ # For instance, if you have PostsController and a template named <tt>app/views/layouts/posts.html.erb</tt>,
+ # that template will be used for all actions in PostsController and controllers inheriting
+ # from PostsController.
+ #
+ # If you use a module, for instance Weblog::PostsController, you will need a template named
+ # <tt>app/views/layouts/weblog/posts.html.erb</tt>.
+ #
+ # Since all your controllers inherit from ApplicationController, they will use
+ # <tt>app/views/layouts/application.html.erb</tt> if no other layout is specified
+ # or provided.
+ #
+ # == Inheritance Examples
+ #
+ # class BankController < ActionController::Base
+ # # bank.html.erb exists
+ #
+ # class ExchangeController < BankController
+ # # exchange.html.erb exists
+ #
+ # class CurrencyController < BankController
+ #
+ # class InformationController < BankController
+ # layout "information"
+ #
+ # class TellerController < InformationController
+ # # teller.html.erb exists
+ #
+ # class EmployeeController < InformationController
+ # # employee.html.erb exists
+ # layout nil
+ #
+ # class VaultController < BankController
+ # layout :access_level_layout
+ #
+ # class TillController < BankController
+ # layout false
+ #
+ # In these examples, we have three implicit lookup scenarios:
+ # * The BankController uses the "bank" layout.
+ # * The ExchangeController uses the "exchange" layout.
+ # * The CurrencyController inherits the layout from BankController.
+ #
+ # However, when a layout is explicitly set, the explicitly set layout wins:
+ # * The InformationController uses the "information" layout, explicitly set.
+ # * The TellerController also uses the "information" layout, because the parent explicitly set it.
+ # * The EmployeeController uses the "employee" layout, because it set the layout to nil, resetting the parent configuration.
+ # * The VaultController chooses a layout dynamically by calling the <tt>access_level_layout</tt> method.
+ # * The TillController does not use a layout at all.
+ #
+ # == Types of layouts
+ #
+ # Layouts are basically just regular templates, but the name of this template needs not be specified statically. Sometimes
+ # you want to alternate layouts depending on runtime information, such as whether someone is logged in or not. This can
+ # be done either by specifying a method reference as a symbol or using an inline method (as a proc).
+ #
+ # The method reference is the preferred approach to variable layouts and is used like this:
+ #
+ # class WeblogController < ActionController::Base
+ # layout :writers_and_readers
+ #
+ # def index
+ # # fetching posts
+ # end
+ #
+ # private
+ # def writers_and_readers
+ # logged_in? ? "writer_layout" : "reader_layout"
+ # end
+ # end
+ #
+ # Now when a new request for the index action is processed, the layout will vary depending on whether the person accessing
+ # is logged in or not.
+ #
+ # If you want to use an inline method, such as a proc, do something like this:
+ #
+ # class WeblogController < ActionController::Base
+ # layout proc { |controller| controller.logged_in? ? "writer_layout" : "reader_layout" }
+ # end
+ #
+ # If an argument isn't given to the proc, it's evaluated in the context of
+ # the current controller anyway.
+ #
+ # class WeblogController < ActionController::Base
+ # layout proc { logged_in? ? "writer_layout" : "reader_layout" }
+ # end
+ #
+ # Of course, the most common way of specifying a layout is still just as a plain template name:
+ #
+ # class WeblogController < ActionController::Base
+ # layout "weblog_standard"
+ # end
+ #
+ # The template will be looked always in <tt>app/views/layouts/</tt> folder. But you can point
+ # <tt>layouts</tt> folder direct also. <tt>layout "layouts/demo"</tt> is the same as <tt>layout "demo"</tt>.
+ #
+ # Setting the layout to nil forces it to be looked up in the filesystem and fallbacks to the parent behavior if none exists.
+ # Setting it to nil is useful to re-enable template lookup overriding a previous configuration set in the parent:
+ #
+ # class ApplicationController < ActionController::Base
+ # layout "application"
+ # end
+ #
+ # class PostsController < ApplicationController
+ # # Will use "application" layout
+ # end
+ #
+ # class CommentsController < ApplicationController
+ # # Will search for "comments" layout and fallback "application" layout
+ # layout nil
+ # end
+ #
+ # == Conditional layouts
+ #
+ # If you have a layout that by default is applied to all the actions of a controller, you still have the option of rendering
+ # a given action or set of actions without a layout, or restricting a layout to only a single action or a set of actions. The
+ # <tt>:only</tt> and <tt>:except</tt> options can be passed to the layout call. For example:
+ #
+ # class WeblogController < ActionController::Base
+ # layout "weblog_standard", except: :rss
+ #
+ # # ...
+ #
+ # end
+ #
+ # This will assign "weblog_standard" as the WeblogController's layout for all actions except for the +rss+ action, which will
+ # be rendered directly, without wrapping a layout around the rendered view.
+ #
+ # Both the <tt>:only</tt> and <tt>:except</tt> condition can accept an arbitrary number of method references, so
+ # #<tt>except: [ :rss, :text_only ]</tt> is valid, as is <tt>except: :rss</tt>.
+ #
+ # == Using a different layout in the action render call
+ #
+ # If most of your actions use the same layout, it makes perfect sense to define a controller-wide layout as described above.
+ # Sometimes you'll have exceptions where one action wants to use a different layout than the rest of the controller.
+ # You can do this by passing a <tt>:layout</tt> option to the <tt>render</tt> call. For example:
+ #
+ # class WeblogController < ActionController::Base
+ # layout "weblog_standard"
+ #
+ # def help
+ # render action: "help", layout: "help"
+ # end
+ # end
+ #
+ # This will override the controller-wide "weblog_standard" layout, and will render the help action with the "help" layout instead.
+ module Layouts
+ extend ActiveSupport::Concern
+
+ include ActionView::Rendering
+
+ included do
+ class_attribute :_layout, :_layout_conditions, :instance_accessor => false
+ self._layout = nil
+ self._layout_conditions = {}
+ _write_layout_method
+ end
+
+ delegate :_layout_conditions, to: :class
+
+ module ClassMethods
+ def inherited(klass) # :nodoc:
+ super
+ klass._write_layout_method
+ end
+
+ # This module is mixed in if layout conditions are provided. This means
+ # that if no layout conditions are used, this method is not used
+ module LayoutConditions # :nodoc:
+ private
+
+ # Determines whether the current action has a layout definition by
+ # checking the action name against the :only and :except conditions
+ # set by the <tt>layout</tt> method.
+ #
+ # ==== Returns
+ # * <tt> Boolean</tt> - True if the action has a layout definition, false otherwise.
+ def _conditional_layout?
+ return unless super
+
+ conditions = _layout_conditions
+
+ if only = conditions[:only]
+ only.include?(action_name)
+ elsif except = conditions[:except]
+ !except.include?(action_name)
+ else
+ true
+ end
+ end
+ end
+
+ # Specify the layout to use for this class.
+ #
+ # If the specified layout is a:
+ # String:: the String is the template name
+ # Symbol:: call the method specified by the symbol, which will return the template name
+ # false:: There is no layout
+ # true:: raise an ArgumentError
+ # nil:: Force default layout behavior with inheritance
+ #
+ # ==== Parameters
+ # * <tt>layout</tt> - The layout to use.
+ #
+ # ==== Options (conditions)
+ # * :only - A list of actions to apply this layout to.
+ # * :except - Apply this layout to all actions but this one.
+ def layout(layout, conditions = {})
+ include LayoutConditions unless conditions.empty?
+
+ conditions.each {|k, v| conditions[k] = Array(v).map {|a| a.to_s} }
+ self._layout_conditions = conditions
+
+ self._layout = layout
+ _write_layout_method
+ end
+
+ # If no layout is supplied, look for a template named the return
+ # value of this method.
+ #
+ # ==== Returns
+ # * <tt>String</tt> - A template name
+ def _implied_layout_name # :nodoc:
+ controller_path
+ end
+
+ # Creates a _layout method to be called by _default_layout .
+ #
+ # If a layout is not explicitly mentioned then look for a layout with the controller's name.
+ # if nothing is found then try same procedure to find super class's layout.
+ def _write_layout_method # :nodoc:
+ remove_possible_method(:_layout)
+
+ prefixes = _implied_layout_name =~ /\blayouts/ ? [] : ["layouts"]
+ default_behavior = "lookup_context.find_all('#{_implied_layout_name}', #{prefixes.inspect}).first || super"
+ name_clause = if name
+ default_behavior
+ else
+ <<-RUBY
+ super
+ RUBY
+ end
+
+ layout_definition = case _layout
+ when String
+ _layout.inspect
+ when Symbol
+ <<-RUBY
+ #{_layout}.tap do |layout|
+ return #{default_behavior} if layout.nil?
+ unless layout.is_a?(String) || !layout
+ raise ArgumentError, "Your layout method :#{_layout} returned \#{layout}. It " \
+ "should have returned a String, false, or nil"
+ end
+ end
+ RUBY
+ when Proc
+ define_method :_layout_from_proc, &_layout
+ protected :_layout_from_proc
+ <<-RUBY
+ result = _layout_from_proc(#{_layout.arity == 0 ? '' : 'self'})
+ return #{default_behavior} if result.nil?
+ result
+ RUBY
+ when false
+ nil
+ when true
+ raise ArgumentError, "Layouts must be specified as a String, Symbol, Proc, false, or nil"
+ when nil
+ name_clause
+ end
+
+ self.class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def _layout
+ if _conditional_layout?
+ #{layout_definition}
+ else
+ #{name_clause}
+ end
+ end
+ private :_layout
+ RUBY
+ end
+ end
+
+ def _normalize_options(options) # :nodoc:
+ super
+
+ if _include_layout?(options)
+ layout = options.delete(:layout) { :default }
+ options[:layout] = _layout_for_option(layout)
+ end
+ end
+
+ attr_internal_writer :action_has_layout
+
+ def initialize(*) # :nodoc:
+ @_action_has_layout = true
+ super
+ end
+
+ # Controls whether an action should be rendered using a layout.
+ # If you want to disable any <tt>layout</tt> settings for the
+ # current action so that it is rendered without a layout then
+ # either override this method in your controller to return false
+ # for that action or set the <tt>action_has_layout</tt> attribute
+ # to false before rendering.
+ def action_has_layout?
+ @_action_has_layout
+ end
+
+ private
+
+ def _conditional_layout?
+ true
+ end
+
+ # This will be overwritten by _write_layout_method
+ def _layout; end
+
+ # Determine the layout for a given name, taking into account the name type.
+ #
+ # ==== Parameters
+ # * <tt>name</tt> - The name of the template
+ def _layout_for_option(name)
+ case name
+ when String then _normalize_layout(name)
+ when Proc then name
+ when true then Proc.new { _default_layout(true) }
+ when :default then Proc.new { _default_layout(false) }
+ when false, nil then nil
+ else
+ raise ArgumentError,
+ "String, Proc, :default, true, or false, expected for `layout'; you passed #{name.inspect}"
+ end
+ end
+
+ def _normalize_layout(value)
+ value.is_a?(String) && value !~ /\blayouts/ ? "layouts/#{value}" : value
+ end
+
+ # Returns the default layout for this controller.
+ # Optionally raises an exception if the layout could not be found.
+ #
+ # ==== Parameters
+ # * <tt>require_layout</tt> - If set to true and layout is not found,
+ # an ArgumentError exception is raised (defaults to false)
+ #
+ # ==== Returns
+ # * <tt>template</tt> - The template object for the default layout (or nil)
+ def _default_layout(require_layout = false)
+ begin
+ value = _layout if action_has_layout?
+ rescue NameError => e
+ raise e, "Could not render layout: #{e.message}"
+ end
+
+ if require_layout && action_has_layout? && !value
+ raise ArgumentError,
+ "There was no default layout for #{self.class} in #{view_paths.inspect}"
+ end
+
+ _normalize_layout(value)
+ end
+
+ def _include_layout?(options)
+ (options.keys & [:text, :inline, :partial]).empty? || options.key?(:layout)
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/locale/en.yml b/actionview/lib/action_view/locale/en.yml
index 8a56f147b8..8a56f147b8 100644
--- a/actionpack/lib/action_view/locale/en.yml
+++ b/actionview/lib/action_view/locale/en.yml
diff --git a/actionview/lib/action_view/log_subscriber.rb b/actionview/lib/action_view/log_subscriber.rb
new file mode 100644
index 0000000000..354a136894
--- /dev/null
+++ b/actionview/lib/action_view/log_subscriber.rb
@@ -0,0 +1,32 @@
+require 'active_support/log_subscriber'
+
+module ActionView
+ # = Action View Log Subscriber
+ #
+ # Provides functionality so that Rails can output logs from Action View.
+ class LogSubscriber < ActiveSupport::LogSubscriber
+ VIEWS_PATTERN = /^app\/views\//.freeze
+
+ 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)
+ end
+ alias :render_partial :render_template
+ alias :render_collection :render_template
+
+ def logger
+ ActionView::Base.logger
+ end
+
+ protected
+
+ def from_rails_root(string)
+ string.sub("#{Rails.root}/", "").sub(VIEWS_PATTERN, "")
+ end
+ end
+end
+
+ActionView::LogSubscriber.attach_to :action_view
diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb
new file mode 100644
index 0000000000..f9d5b97fe3
--- /dev/null
+++ b/actionview/lib/action_view/lookup_context.rb
@@ -0,0 +1,241 @@
+require 'thread_safe'
+require 'active_support/core_ext/module/remove_method'
+require 'active_support/core_ext/module/attribute_accessors'
+
+module ActionView
+ # = Action View Lookup Context
+ #
+ # LookupContext is the object responsible to hold all information required to lookup
+ # templates, i.e. view paths and details. The LookupContext is also responsible to
+ # generate a key, given to view paths, used in the resolver cache lookup. Since
+ # this key is generated just once during the request, it speeds up all cache accesses.
+ class LookupContext #:nodoc:
+ attr_accessor :prefixes, :rendered_format
+
+ mattr_accessor :fallbacks
+ @@fallbacks = FallbackFileSystemResolver.instances
+
+ mattr_accessor :registered_details
+ self.registered_details = []
+
+ def self.register_detail(name, options = {}, &block)
+ self.registered_details << name
+ initialize = registered_details.map { |n| "@details[:#{n}] = details[:#{n}] || default_#{n}" }
+
+ Accessors.send :define_method, :"default_#{name}", &block
+ Accessors.module_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def #{name}
+ @details.fetch(:#{name}, [])
+ end
+
+ def #{name}=(value)
+ value = value.present? ? Array(value) : default_#{name}
+ _set_detail(:#{name}, value) if value != @details[:#{name}]
+ end
+
+ remove_possible_method :initialize_details
+ def initialize_details(details)
+ #{initialize.join("\n")}
+ end
+ METHOD
+ end
+
+ # Holds accessors for the registered details.
+ module Accessors #:nodoc:
+ end
+
+ register_detail(:locale) do
+ locales = [I18n.locale]
+ locales.concat(I18n.fallbacks[I18n.locale]) if I18n.respond_to? :fallbacks
+ locales << I18n.default_locale
+ locales.uniq!
+ locales
+ end
+ register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] }
+ register_detail(:handlers){ Template::Handlers.extensions }
+
+ class DetailsKey #:nodoc:
+ alias :eql? :equal?
+ alias :object_hash :hash
+
+ attr_reader :hash
+ @details_keys = ThreadSafe::Cache.new
+
+ def self.get(details)
+ @details_keys[details] ||= new
+ end
+
+ def self.clear
+ @details_keys.clear
+ end
+
+ def initialize
+ @hash = object_hash
+ end
+ end
+
+ # Add caching behavior on top of Details.
+ module DetailsCache
+ attr_accessor :cache
+
+ # Calculate the details key. Remove the handlers from calculation to improve performance
+ # since the user cannot modify it explicitly.
+ def details_key #:nodoc:
+ @details_key ||= DetailsKey.get(@details) if @cache
+ end
+
+ # Temporary skip passing the details_key forward.
+ def disable_cache
+ old_value, @cache = @cache, false
+ yield
+ ensure
+ @cache = old_value
+ end
+
+ protected
+
+ def _set_detail(key, value)
+ @details = @details.dup if @details_key
+ @details_key = nil
+ @details[key] = value
+ end
+ end
+
+ # Helpers related to template lookup using the lookup context information.
+ module ViewPaths
+ attr_reader :view_paths, :html_fallback_for_js
+
+ # Whenever setting view paths, makes a copy so we can manipulate then in
+ # instance objects as we wish.
+ def view_paths=(paths)
+ @view_paths = ActionView::PathSet.new(Array(paths))
+ end
+
+ def find(name, prefixes = [], partial = false, keys = [], options = {})
+ @view_paths.find(*args_for_lookup(name, prefixes, partial, keys, options))
+ end
+ alias :find_template :find
+
+ def find_all(name, prefixes = [], partial = false, keys = [], options = {})
+ @view_paths.find_all(*args_for_lookup(name, prefixes, partial, keys, options))
+ end
+
+ def exists?(name, prefixes = [], partial = false, keys = [], options = {})
+ @view_paths.exists?(*args_for_lookup(name, prefixes, partial, keys, options))
+ end
+ alias :template_exists? :exists?
+
+ # Add fallbacks to the view paths. Useful in cases you are rendering a :file.
+ def with_fallbacks
+ added_resolvers = 0
+ self.class.fallbacks.each do |resolver|
+ next if view_paths.include?(resolver)
+ view_paths.push(resolver)
+ added_resolvers += 1
+ end
+ yield
+ ensure
+ added_resolvers.times { view_paths.pop }
+ end
+
+ protected
+
+ def args_for_lookup(name, prefixes, partial, keys, details_options) #:nodoc:
+ name, prefixes = normalize_name(name, prefixes)
+ details, details_key = detail_args_for(details_options)
+ [name, prefixes, partial || false, details, details_key, keys]
+ end
+
+ # Compute details hash and key according to user options (e.g. passed from #render).
+ 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)]
+ end
+
+ # Support legacy foo.erb names even though we now ignore .erb
+ # as well as incorrectly putting part of the path in the template
+ # name instead of the prefix.
+ def normalize_name(name, prefixes) #:nodoc:
+ prefixes = prefixes.presence
+ parts = name.to_s.split('/')
+ parts.shift if parts.first.empty?
+ name = parts.pop
+
+ return name, prefixes || [""] if parts.empty?
+
+ parts = parts.join('/')
+ prefixes = prefixes ? prefixes.map { |p| "#{p}/#{parts}" } : [parts]
+
+ return name, prefixes
+ end
+ end
+
+ include Accessors
+ include DetailsCache
+ include ViewPaths
+
+ def initialize(view_paths, details = {}, prefixes = [])
+ @details, @details_key = {}, nil
+ @skip_default_locale = false
+ @cache = true
+ @prefixes = prefixes
+ @rendered_format = nil
+
+ self.view_paths = view_paths
+ initialize_details(details)
+ end
+
+ # Override formats= to expand ["*/*"] values and automatically
+ # add :html as fallback to :js.
+ def formats=(values)
+ if values
+ values.concat(default_formats) if values.delete "*/*"
+ if values == [:js]
+ values << :html
+ @html_fallback_for_js = true
+ end
+ end
+ super(values)
+ end
+
+ # Do not use the default locale on template lookup.
+ def skip_default_locale!
+ @skip_default_locale = true
+ self.locale = nil
+ end
+
+ # Override locale to return a symbol instead of array.
+ def locale
+ @details[:locale].first
+ 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
+ # acting as proxy, which we need to skip.
+ def locale=(value)
+ if value
+ config = I18n.config.respond_to?(:original_config) ? I18n.config.original_config : I18n.config
+ config.locale = value
+ end
+
+ super(@skip_default_locale ? I18n.locale : default_locale)
+ end
+
+ # A method which only uses the first format in the formats array for layout lookup.
+ def with_layout_format
+ if formats.size == 1
+ yield
+ else
+ old_formats = formats
+ _set_detail(:formats, formats[0,1])
+
+ begin
+ yield
+ ensure
+ _set_detail(:formats, old_formats)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/model_naming.rb b/actionview/lib/action_view/model_naming.rb
index e09ebd60df..e09ebd60df 100644
--- a/actionpack/lib/action_view/model_naming.rb
+++ b/actionview/lib/action_view/model_naming.rb
diff --git a/actionview/lib/action_view/path_set.rb b/actionview/lib/action_view/path_set.rb
new file mode 100644
index 0000000000..91ee2ea8f5
--- /dev/null
+++ b/actionview/lib/action_view/path_set.rb
@@ -0,0 +1,77 @@
+module ActionView #:nodoc:
+ # = Action View PathSet
+ #
+ # This class is used to store and access paths in Action View. A number of
+ # operations are defined so that you can search among the paths in this
+ # set and also perform operations on other +PathSet+ objects.
+ #
+ # A +LookupContext+ will use a +PathSet+ to store the paths in its context.
+ class PathSet #:nodoc:
+ include Enumerable
+
+ attr_reader :paths
+
+ delegate :[], :include?, :pop, :size, :each, to: :paths
+
+ def initialize(paths = [])
+ @paths = typecast paths
+ end
+
+ def initialize_copy(other)
+ @paths = other.paths.dup
+ self
+ end
+
+ def to_ary
+ paths.dup
+ end
+
+ def compact
+ PathSet.new paths.compact
+ end
+
+ def +(array)
+ PathSet.new(paths + array)
+ end
+
+ %w(<< concat push insert unshift).each do |method|
+ class_eval <<-METHOD, __FILE__, __LINE__ + 1
+ def #{method}(*args)
+ paths.#{method}(*typecast(args))
+ end
+ METHOD
+ end
+
+ def find(*args)
+ find_all(*args).first || raise(MissingTemplate.new(self, *args))
+ end
+
+ def find_all(path, prefixes = [], *args)
+ prefixes = [prefixes] if String === prefixes
+ prefixes.each do |prefix|
+ paths.each do |resolver|
+ templates = resolver.find_all(path, prefix, *args)
+ return templates unless templates.empty?
+ end
+ end
+ []
+ end
+
+ def exists?(path, prefixes, *args)
+ find_all(path, prefixes, *args).any?
+ end
+
+ private
+
+ def typecast(paths)
+ paths.map do |path|
+ case path
+ when Pathname, String
+ OptimizedFileSystemResolver.new path.to_s
+ else
+ path
+ end
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/railtie.rb b/actionview/lib/action_view/railtie.rb
new file mode 100644
index 0000000000..c2783f6377
--- /dev/null
+++ b/actionview/lib/action_view/railtie.rb
@@ -0,0 +1,56 @@
+require "action_view"
+require "rails"
+
+module ActionView
+ # = Action View Railtie
+ class Railtie < Rails::Railtie # :nodoc:
+ config.action_view = ActiveSupport::OrderedOptions.new
+ config.action_view.embed_authenticity_token_in_remote_forms = false
+
+ config.eager_load_namespaces << ActionView
+
+ initializer "action_view.embed_authenticity_token_in_remote_forms" do |app|
+ ActiveSupport.on_load(:action_view) do
+ ActionView::Helpers::FormTagHelper.embed_authenticity_token_in_remote_forms =
+ app.config.action_view.delete(:embed_authenticity_token_in_remote_forms)
+ end
+ end
+
+ initializer "action_view.logger" do
+ ActiveSupport.on_load(:action_view) { self.logger ||= Rails.logger }
+ end
+
+ initializer "action_view.set_configs" do |app|
+ ActiveSupport.on_load(:action_view) do
+ app.config.action_view.each do |k,v|
+ send "#{k}=", v
+ end
+ end
+ end
+
+ initializer "action_view.caching" do |app|
+ ActiveSupport.on_load(:action_view) do
+ if app.config.action_view.cache_template_loading.nil?
+ ActionView::Resolver.caching = app.config.cache_classes
+ end
+ end
+ end
+
+ initializer "action_view.setup_action_pack", before: :add_view_paths do |app|
+ ActiveSupport.on_load(:action_controller) do
+ ActionController::Base.superclass.send(:include, ActionView::Layouts)
+ ActionView::RoutingUrlFor.send(:include, ActionDispatch::Routing::UrlFor)
+ end
+ end
+
+ initializer "action_view.setup_action_mailer", before: :add_view_paths do |app|
+ ActiveSupport.on_load(:action_mailer) do
+ ActionMailer::Base.send(:include, ActionView::Layouts)
+ end
+ end
+
+ rake_tasks do
+ load "action_view/tasks/dependencies.rake"
+ end
+ end
+end
diff --git a/actionview/lib/action_view/record_identifier.rb b/actionview/lib/action_view/record_identifier.rb
new file mode 100644
index 0000000000..63f645431a
--- /dev/null
+++ b/actionview/lib/action_view/record_identifier.rb
@@ -0,0 +1,84 @@
+require 'active_support/core_ext/module'
+require 'action_view/model_naming'
+
+module ActionView
+ # The record identifier encapsulates a number of naming conventions for dealing with records, like Active Records or
+ # pretty much any other model type that has an id. These patterns are then used to try elevate the view actions to
+ # a higher logical level.
+ #
+ # # routes
+ # resources :posts
+ #
+ # # view
+ # <%= div_for(post) do %> <div id="post_45" class="post">
+ # <%= post.body %> What a wonderful world!
+ # <% end %> </div>
+ #
+ # # controller
+ # def update
+ # post = Post.find(params[:id])
+ # post.update(params[:post])
+ #
+ # redirect_to(post) # Calls polymorphic_url(post) which in turn calls post_url(post)
+ # end
+ #
+ # As the example above shows, you can stop caring to a large extent what the actual id of the post is.
+ # You just know that one is being assigned and that the subsequent calls in redirect_to expect that
+ # same naming convention and allows you to write less code if you follow it.
+ module RecordIdentifier
+ extend self
+ extend ModelNaming
+
+ include ModelNaming
+
+ JOIN = '_'.freeze
+ NEW = 'new'.freeze
+
+ # The DOM class convention is to use the singular form of an object or class.
+ #
+ # dom_class(post) # => "post"
+ # dom_class(Person) # => "person"
+ #
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_class:
+ #
+ # dom_class(post, :edit) # => "edit_post"
+ # dom_class(Person, :edit) # => "edit_person"
+ def dom_class(record_or_class, prefix = nil)
+ singular = model_name_from_record_or_class(record_or_class).param_key
+ prefix ? "#{prefix}#{JOIN}#{singular}" : singular
+ end
+
+ # The DOM id convention is to use the singular form of an object or class with the id following an underscore.
+ # If no id is found, prefix with "new_" instead.
+ #
+ # dom_id(Post.find(45)) # => "post_45"
+ # dom_id(Post.new) # => "new_post"
+ #
+ # If you need to address multiple instances of the same class in the same view, you can prefix the dom_id:
+ #
+ # dom_id(Post.find(45), :edit) # => "edit_post_45"
+ # dom_id(Post.new, :custom) # => "custom_post"
+ def dom_id(record, prefix = nil)
+ if record_id = record_key_for_dom_id(record)
+ "#{dom_class(record, prefix)}#{JOIN}#{record_id}"
+ else
+ dom_class(record, prefix || NEW)
+ end
+ end
+
+ protected
+
+ # Returns a string representation of the key attribute(s) that is suitable for use in an HTML DOM id.
+ # This can be overwritten to customize the default generated string representation if desired.
+ # If you need to read back a key from a dom_id in order to query for the underlying database record,
+ # you should write a helper like 'person_record_from_dom_id' that will extract the key either based
+ # on the default implementation (which just joins all key attributes with '_') or on your own
+ # overwritten version of the method. By default, this implementation passes the key string through a
+ # method that replaces all characters that are invalid inside DOM ids, with valid ones. You need to
+ # make sure yourself that your dom ids are valid, in case you overwrite this method.
+ def record_key_for_dom_id(record)
+ key = convert_to_model(record).to_key
+ key ? key.join('_') : key
+ end
+ end
+end
diff --git a/actionview/lib/action_view/renderer/abstract_renderer.rb b/actionview/lib/action_view/renderer/abstract_renderer.rb
new file mode 100644
index 0000000000..73c19a0ae2
--- /dev/null
+++ b/actionview/lib/action_view/renderer/abstract_renderer.rb
@@ -0,0 +1,47 @@
+module ActionView
+ # This class defines the interface for a renderer. Each class that
+ # subclasses +AbstractRenderer+ is used by the base +Renderer+ class to
+ # render a specific type of object.
+ #
+ # The base +Renderer+ class uses its +render+ method to delegate to the
+ # renderers. These currently consist of
+ #
+ # PartialRenderer - Used for rendering partials
+ # TemplateRenderer - Used for rendering other types of templates
+ # StreamingTemplateRenderer - Used for streaming
+ #
+ # Whenever the +render+ method is called on the base +Renderer+ class, a new
+ # renderer object of the correct type is created, and the +render+ method on
+ # that new object is called in turn. This abstracts the setup and rendering
+ # into a separate classes for partials and templates.
+ class AbstractRenderer #:nodoc:
+ delegate :find_template, :template_exists?, :with_fallbacks, :with_layout_format, :formats, :to => :@lookup_context
+
+ def initialize(lookup_context)
+ @lookup_context = lookup_context
+ end
+
+ def render
+ raise NotImplementedError
+ end
+
+ protected
+
+ def extract_details(options)
+ @lookup_context.registered_details.each_with_object({}) do |key, details|
+ next unless value = options[key]
+ details[key] = Array(value)
+ end
+ end
+
+ def instrument(name, options={})
+ ActiveSupport::Notifications.instrument("render_#{name}.action_view", options){ yield }
+ end
+
+ 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
+end
diff --git a/actionview/lib/action_view/renderer/partial_renderer.rb b/actionview/lib/action_view/renderer/partial_renderer.rb
new file mode 100644
index 0000000000..821026268a
--- /dev/null
+++ b/actionview/lib/action_view/renderer/partial_renderer.rb
@@ -0,0 +1,492 @@
+require 'thread_safe'
+
+module ActionView
+ # = Action View Partials
+ #
+ # There's also a convenience method for rendering sub templates within the current controller that depends on a
+ # single object (we call this kind of sub templates for partials). It relies on the fact that partials should
+ # follow the naming convention of being prefixed with an underscore -- as to separate them from regular
+ # templates that could be rendered on their own.
+ #
+ # In a template for Advertiser#account:
+ #
+ # <%= render partial: "account" %>
+ #
+ # This would render "advertiser/_account.html.erb".
+ #
+ # In another template for Advertiser#buy, we could have:
+ #
+ # <%= render partial: "account", locals: { account: @buyer } %>
+ #
+ # <% @advertisements.each do |ad| %>
+ # <%= render partial: "ad", locals: { ad: ad } %>
+ # <% end %>
+ #
+ # This would first render "advertiser/_account.html.erb" with @buyer passed in as the local variable +account+, then
+ # render "advertiser/_ad.html.erb" and pass the local variable +ad+ to the template for display.
+ #
+ # == The :as and :object options
+ #
+ # By default <tt>ActionView::PartialRenderer</tt> doesn't have any local variables.
+ # The <tt>:object</tt> option can be used to pass an object to the partial. For instance:
+ #
+ # <%= render partial: "account", object: @buyer %>
+ #
+ # would provide the <tt>@buyer</tt> object to the partial, available under the local variable +account+ and is
+ # equivalent to:
+ #
+ # <%= render partial: "account", locals: { account: @buyer } %>
+ #
+ # With the <tt>:as</tt> option we can specify a different name for said local variable. For example, if we
+ # wanted it to be +user+ instead of +account+ we'd do:
+ #
+ # <%= render partial: "account", object: @buyer, as: 'user' %>
+ #
+ # This is equivalent to
+ #
+ # <%= render partial: "account", locals: { user: @buyer } %>
+ #
+ # == Rendering a collection of partials
+ #
+ # The example of partial use describes a familiar pattern where a template needs to iterate over an array and
+ # render a sub template for each of the elements. This pattern has been implemented as a single method that
+ # accepts an array and renders a partial by the same name as the elements contained within. So the three-lined
+ # example in "Using partials" can be rewritten with a single line:
+ #
+ # <%= 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+.
+ #
+ # The <tt>:as</tt> option may be used when rendering partials.
+ #
+ # You can specify a partial to be rendered between elements via the <tt>:spacer_template</tt> option.
+ # The following example will render <tt>advertiser/_ad_divider.html.erb</tt> between each ad partial:
+ #
+ # <%= render partial: "ad", collection: @advertisements, spacer_template: "ad_divider" %>
+ #
+ # If the given <tt>:collection</tt> is nil or empty, <tt>render</tt> will return nil. This will allow you
+ # to specify a text which will displayed instead by using this form:
+ #
+ # <%= render(partial: "ad", collection: @advertisements) || "There's no ad to be displayed" %>
+ #
+ # NOTE: Due to backwards compatibility concerns, the collection can't be one of hashes. Normally you'd also
+ # just keep domain objects, like Active Records, in there.
+ #
+ # == Rendering shared partials
+ #
+ # Two controllers can share a set of partials and render them like this:
+ #
+ # <%= render partial: "advertisement/ad", locals: { ad: @advertisement } %>
+ #
+ # This will render the partial "advertisement/_ad.html.erb" regardless of which controller this is being called from.
+ #
+ # == Rendering objects that respond to `to_partial_path`
+ #
+ # Instead of explicitly naming the location of a partial, you can also let PartialRenderer do the work
+ # and pick the proper path by checking `to_partial_path` method.
+ #
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
+ # <%= render partial: @account %>
+ #
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
+ # # that's why we can replace:
+ # # <%= render partial: "posts/post", collection: @posts %>
+ # <%= render partial: @posts %>
+ #
+ # == Rendering the default case
+ #
+ # If you're not going to be using any of the options like collections or layouts, you can also use the short-hand
+ # defaults of render to render partials. Examples:
+ #
+ # # Instead of <%= render partial: "account" %>
+ # <%= render "account" %>
+ #
+ # # Instead of <%= render partial: "account", locals: { account: @buyer } %>
+ # <%= render "account", account: @buyer %>
+ #
+ # # @account.to_partial_path returns 'accounts/account', so it can be used to replace:
+ # # <%= render partial: "accounts/account", locals: { account: @account} %>
+ # <%= render @account %>
+ #
+ # # @posts is an array of Post instances, so every post record returns 'posts/post' on `to_partial_path`,
+ # # that's why we can replace:
+ # # <%= render partial: "posts/post", collection: @posts %>
+ # <%= render @posts %>
+ #
+ # == Rendering partials with layouts
+ #
+ # Partials can have their own layouts applied to them. These layouts are different than the ones that are
+ # specified globally for the entire action, but they work in a similar fashion. Imagine a list with two types
+ # of users:
+ #
+ # <%# app/views/users/index.html.erb &>
+ # Here's the administrator:
+ # <%= render partial: "user", layout: "administrator", locals: { user: administrator } %>
+ #
+ # Here's the editor:
+ # <%= render partial: "user", layout: "editor", locals: { user: editor } %>
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # Name: <%= user.name %>
+ #
+ # <%# app/views/users/_administrator.html.erb &>
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # <%= yield %>
+ # </div>
+ #
+ # <%# app/views/users/_editor.html.erb &>
+ # <div id="editor">
+ # Deadline: <%= user.deadline %>
+ # <%= yield %>
+ # </div>
+ #
+ # ...this will return:
+ #
+ # Here's the administrator:
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # Here's the editor:
+ # <div id="editor">
+ # Deadline: <%= user.deadline %>
+ # Name: <%= user.name %>
+ # </div>
+ #
+ # If a collection is given, the layout will be rendered once for each item in
+ # the collection. Just think these two snippets have the same output:
+ #
+ # <%# app/views/users/_user.html.erb %>
+ # Name: <%= user.name %>
+ #
+ # <%# app/views/users/index.html.erb %>
+ # <%# This does not use layouts %>
+ # <ul>
+ # <% users.each do |user| -%>
+ # <li>
+ # <%= render partial: "user", locals: { user: user } %>
+ # </li>
+ # <% end -%>
+ # </ul>
+ #
+ # <%# app/views/users/_li_layout.html.erb %>
+ # <li>
+ # <%= yield %>
+ # </li>
+ #
+ # <%# app/views/users/index.html.erb %>
+ # <ul>
+ # <%= render partial: "user", layout: "li_layout", collection: users %>
+ # </ul>
+ #
+ # Given two users whose names are Alice and Bob, these snippets return:
+ #
+ # <ul>
+ # <li>
+ # Name: Alice
+ # </li>
+ # <li>
+ # Name: Bob
+ # </li>
+ # </ul>
+ #
+ # The current object being rendered, as well as the object_counter, will be
+ # available as local variables inside the layout template under the same names
+ # as available in the partial.
+ #
+ # You can also apply a layout to a block within any template:
+ #
+ # <%# app/views/users/_chief.html.erb &>
+ # <%= render(layout: "administrator", locals: { user: chief }) do %>
+ # Title: <%= chief.title %>
+ # <% end %>
+ #
+ # ...this will return:
+ #
+ # <div id="administrator">
+ # Budget: $<%= user.budget %>
+ # Title: <%= chief.name %>
+ # </div>
+ #
+ # As you can see, the <tt>:locals</tt> hash is shared between both the partial and its layout.
+ #
+ # If you pass arguments to "yield" then this will be passed to the block. One way to use this is to pass
+ # an array to layout and treat it as an enumerable.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # Budget: $<%= user.budget %>
+ # <%= yield user %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <%= render layout: @users do |user| %>
+ # Title: <%= user.title %>
+ # <% end %>
+ #
+ # This will render the layout for each user and yield to the block, passing the user, each time.
+ #
+ # You can also yield multiple times in one layout and use block arguments to differentiate the sections.
+ #
+ # <%# app/views/users/_user.html.erb &>
+ # <div class="user">
+ # <%= yield user, :header %>
+ # Budget: $<%= user.budget %>
+ # <%= yield user, :footer %>
+ # </div>
+ #
+ # <%# app/views/users/index.html.erb &>
+ # <%= render layout: @users do |user, section| %>
+ # <%- case section when :header -%>
+ # Title: <%= user.title %>
+ # <%- when :footer -%>
+ # Deadline: <%= user.deadline %>
+ # <%- end -%>
+ # <% end %>
+ class PartialRenderer < AbstractRenderer
+ PREFIXED_PARTIAL_NAMES = ThreadSafe::Cache.new do |h, k|
+ h[k] = ThreadSafe::Cache.new
+ end
+
+ def initialize(*)
+ super
+ @context_prefix = @lookup_context.prefixes.first
+ end
+
+ def render(context, options, block)
+ setup(context, options, block)
+ identifier = (@template = find_partial) ? @template.identifier : @path
+
+ @lookup_context.rendered_format ||= begin
+ if @template && @template.formats.present?
+ @template.formats.first
+ else
+ formats.first
+ end
+ end
+
+ if @collection
+ instrument(:collection, :identifier => identifier || "collection", :count => @collection.size) do
+ render_collection
+ end
+ else
+ instrument(:partial, :identifier => identifier) do
+ render_partial
+ end
+ end
+ end
+
+ def render_collection
+ return nil if @collection.blank?
+
+ if @options.key?(:spacer_template)
+ spacer = find_template(@options[:spacer_template], @locals.keys).render(@view, @locals)
+ end
+
+ result = @template ? collection_with_template : collection_without_template
+ result.join(spacer).html_safe
+ end
+
+ def render_partial
+ view, locals, block = @view, @locals, @block
+ object, as = @object, @variable
+
+ if !block && (layout = @options[:layout])
+ layout = find_template(layout.to_s, @template_keys)
+ end
+
+ object ||= locals[as]
+ locals[as] = object
+
+ content = @template.render(view, locals) do |*name|
+ view._layout_for(*name, &block)
+ end
+
+ content = layout.render(view, locals){ content } if layout
+ content
+ end
+
+ private
+
+ # Sets up instance variables needed for rendering a partial. This method
+ # finds the options and details and extracts them. The method also contains
+ # logic that handles the type of object passed in as the partial.
+ #
+ # If +options[:partial]+ is a string, then the +@path+ instance variable is
+ # set to that string. Otherwise, the +options[:partial]+ object must
+ # 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
+ @details = extract_details(options)
+
+ prepend_formats(options[:formats])
+
+ if String === partial
+ @object = options[:object]
+ @path = partial
+ @collection = collection
+ else
+ @object = partial
+
+ if @collection = collection_from_object || collection
+ paths = @collection_data = @collection.map { |o| partial_path(o) }
+ @path = paths.uniq.size == 1 ? paths.first : nil
+ else
+ @path = partial_path
+ end
+ end
+
+ if as = options[:as]
+ raise_invalid_identifier(as) unless as.to_s =~ /\A[a-z_]\w*\z/
+ as = as.to_sym
+ end
+
+ if @path
+ @variable, @variable_counter = retrieve_variable(@path, as)
+ @template_keys = retrieve_template_keys
+ else
+ paths.map! { |path| retrieve_variable(path, as).unshift(path) }
+ end
+
+ self
+ end
+
+ def collection
+ if @options.key?(:collection)
+ collection = @options[:collection]
+ collection.respond_to?(:to_ary) ? collection.to_ary : []
+ end
+ end
+
+ def collection_from_object
+ @object.to_ary if @object.respond_to?(:to_ary)
+ end
+
+ def find_partial
+ if path = @path
+ find_template(path, @template_keys)
+ end
+ end
+
+ def find_template(path, locals)
+ prefixes = path.include?(?/) ? [] : @lookup_context.prefixes
+ @lookup_context.find_template(path, prefixes, true, locals, @details)
+ end
+
+ def collection_with_template
+ view, locals, template = @view, @locals, @template
+ as, counter = @variable, @variable_counter
+
+ if layout = @options[:layout]
+ layout = find_template(layout, @template_keys)
+ end
+
+ index = -1
+ @collection.map do |object|
+ locals[as] = object
+ locals[counter] = (index += 1)
+
+ content = template.render(view, locals)
+ content = layout.render(view, locals) { content } if layout
+ content
+ end
+ end
+
+ def collection_without_template
+ view, locals, collection_data = @view, @locals, @collection_data
+ cache = {}
+ keys = @locals.keys
+
+ index = -1
+ @collection.map do |object|
+ index += 1
+ path, as, counter = collection_data[index]
+
+ locals[as] = object
+ locals[counter] = index
+
+ template = (cache[path] ||= find_template(path, keys + [as, counter]))
+ template.render(view, locals)
+ end
+ end
+
+ # Obtains the path to where the object's partial is located. If the object
+ # responds to +to_partial_path+, then +to_partial_path+ will be called and
+ # will provide the path. If the object does not respond to +to_partial_path+,
+ # then an +ArgumentError+ is raised.
+ #
+ # If +prefix_partial_path_with_controller_namespace+ is true, then this
+ # method will prefix the partial paths with a namespace.
+ def partial_path(object = @object)
+ object = object.to_model if object.respond_to?(:to_model)
+
+ path = if object.respond_to?(:to_partial_path)
+ object.to_partial_path
+ else
+ raise ArgumentError.new("'#{object.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.")
+ end
+
+ if @view.prefix_partial_path_with_controller_namespace
+ prefixed_partial_names[path] ||= merge_prefix_into_object_path(@context_prefix, path.dup)
+ else
+ path
+ end
+ end
+
+ def prefixed_partial_names
+ @prefixed_partial_names ||= PREFIXED_PARTIAL_NAMES[@context_prefix]
+ end
+
+ def merge_prefix_into_object_path(prefix, object_path)
+ if prefix.include?(?/) && object_path.include?(?/)
+ prefixes = []
+ prefix_array = File.dirname(prefix).split('/')
+ object_path_array = object_path.split('/')[0..-3] # skip model dir & partial
+
+ prefix_array.each_with_index do |dir, index|
+ break if dir == object_path_array[index]
+ prefixes << dir
+ end
+
+ (prefixes << object_path).join("/")
+ else
+ object_path
+ end
+ end
+
+ def retrieve_template_keys
+ keys = @locals.keys
+ keys << @variable if @object || @collection
+ keys << @variable_counter if @collection
+ keys
+ end
+
+ def retrieve_variable(path, as)
+ variable = as || begin
+ base = path[-1] == "/" ? "" : File.basename(path)
+ 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]
+ end
+
+ IDENTIFIER_ERROR_MESSAGE = "The partial name (%s) is not a valid Ruby identifier; " +
+ "make sure your partial name starts with a lowercase letter or underscore, " +
+ "and is followed by any combination of letters, numbers and underscores."
+
+ def raise_invalid_identifier(path)
+ raise ArgumentError.new(IDENTIFIER_ERROR_MESSAGE % (path))
+ end
+ end
+end
diff --git a/actionview/lib/action_view/renderer/renderer.rb b/actionview/lib/action_view/renderer/renderer.rb
new file mode 100644
index 0000000000..964b18337e
--- /dev/null
+++ b/actionview/lib/action_view/renderer/renderer.rb
@@ -0,0 +1,50 @@
+module ActionView
+ # This is the main entry point for rendering. It basically delegates
+ # to other objects like TemplateRenderer and PartialRenderer which
+ # actually renders the template.
+ #
+ # The Renderer will parse the options from the +render+ or +render_body+
+ # method and render a partial or a template based on the options. The
+ # +TemplateRenderer+ and +PartialRenderer+ objects are wrappers which do all
+ # the setup and logic necessary to render a view and a new object is created
+ # each time +render+ is called.
+ class Renderer
+ attr_accessor :lookup_context
+
+ def initialize(lookup_context)
+ @lookup_context = lookup_context
+ end
+
+ # Main render entry point shared by AV and AC.
+ def render(context, options)
+ if options.key?(:partial)
+ render_partial(context, options)
+ else
+ render_template(context, options)
+ end
+ end
+
+ # Render but returns a valid Rack body. If fibers are defined, we return
+ # a streaming body that renders the template piece by piece.
+ #
+ # Note that partials are not supported to be rendered with streaming,
+ # so in such cases, we just wrap them in an array.
+ def render_body(context, options)
+ if options.key?(:partial)
+ [render_partial(context, options)]
+ else
+ StreamingTemplateRenderer.new(@lookup_context).render(context, options)
+ end
+ end
+
+ # Direct accessor to template rendering.
+ def render_template(context, options) #:nodoc:
+ TemplateRenderer.new(@lookup_context).render(context, options)
+ end
+
+ # Direct access to partial rendering.
+ def render_partial(context, options, &block) #:nodoc:
+ PartialRenderer.new(@lookup_context).render(context, options, block)
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
index 9cf6eb0c65..9cf6eb0c65 100644
--- a/actionpack/lib/action_view/renderer/streaming_template_renderer.rb
+++ b/actionview/lib/action_view/renderer/streaming_template_renderer.rb
diff --git a/actionview/lib/action_view/renderer/template_renderer.rb b/actionview/lib/action_view/renderer/template_renderer.rb
new file mode 100644
index 0000000000..668831dff3
--- /dev/null
+++ b/actionview/lib/action_view/renderer/template_renderer.rb
@@ -0,0 +1,96 @@
+require 'active_support/core_ext/object/try'
+
+module ActionView
+ class TemplateRenderer < AbstractRenderer #:nodoc:
+ def render(context, options)
+ @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
+
+ render_template(template, options[:layout], options[:locals])
+ end
+
+ # Determine the template to be rendered using the given options.
+ def determine_template(options) #:nodoc:
+ keys = options.fetch(:locals, {}).keys
+
+ if options.key?(:text)
+ Template::Text.new(options[:text], formats.first)
+ elsif options.key?(:file)
+ with_fallbacks { find_template(options[:file], nil, false, keys, @details) }
+ elsif options.key?(:inline)
+ handler = Template.handler_for_extension(options[:type] || "erb")
+ Template.new(options[:inline], "inline template", handler, :locals => keys)
+ elsif options.key?(:template)
+ if options[:template].respond_to?(:render)
+ options[:template]
+ else
+ 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."
+ end
+ end
+
+ # Renders the given template. A string representing the layout can be
+ # supplied as well.
+ def render_template(template, layout_name = nil, locals = nil) #:nodoc:
+ view, locals = @view, locals || {}
+
+ render_with_layout(layout_name, locals) do |layout|
+ instrument(:template, :identifier => template.identifier, :layout => layout.try(:virtual_path)) do
+ template.render(view, locals) { |*name| view._layout_for(*name) }
+ end
+ end
+ end
+
+ def render_with_layout(path, locals) #:nodoc:
+ layout = path && find_layout(path, locals.keys)
+ content = yield(layout)
+
+ if layout
+ view = @view
+ view.view_flow.set(:layout, content)
+ layout.render(view, locals){ |*name| view._layout_for(*name) }
+ else
+ content
+ end
+ end
+
+ # This is the method which actually finds the layout using details in the lookup
+ # context object. If no layout is found, it checks if at least a layout with
+ # the given name exists across all details before raising the error.
+ def find_layout(layout, keys)
+ with_layout_format { resolve_layout(layout, keys) }
+ end
+
+ def resolve_layout(layout, keys)
+ case layout
+ when String
+ begin
+ if layout =~ /^\//
+ with_fallbacks { find_template(layout, nil, false, keys, @details) }
+ else
+ find_template(layout, nil, false, keys, @details)
+ end
+ rescue ActionView::MissingTemplate
+ all_details = @details.merge(:formats => @lookup_context.default_formats)
+ raise unless template_exists?(layout, nil, false, keys, all_details)
+ end
+ when Proc
+ resolve_layout(layout.call, keys)
+ when FalseClass
+ nil
+ else
+ layout
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb
new file mode 100644
index 0000000000..18a0788f8e
--- /dev/null
+++ b/actionview/lib/action_view/rendering.rb
@@ -0,0 +1,141 @@
+require "action_view/view_paths"
+
+module ActionView
+ # This is a class to fix I18n global state. Whenever you provide I18n.locale during a request,
+ # it will trigger the lookup_context and consequently expire the cache.
+ class I18nProxy < ::I18n::Config #:nodoc:
+ attr_reader :original_config, :lookup_context
+
+ def initialize(original_config, lookup_context)
+ original_config = original_config.original_config if original_config.respond_to?(:original_config)
+ @original_config, @lookup_context = original_config, lookup_context
+ end
+
+ def locale
+ @original_config.locale
+ end
+
+ def locale=(value)
+ @lookup_context.locale = value
+ end
+ end
+
+ module Rendering
+ extend ActiveSupport::Concern
+ include ActionView::ViewPaths
+
+ # Overwrite process to setup I18n proxy.
+ def process(*) #:nodoc:
+ old_config, I18n.config = I18n.config, I18nProxy.new(I18n.config, lookup_context)
+ super
+ ensure
+ I18n.config = old_config
+ end
+
+ module ClassMethods
+ def view_context_class
+ @view_context_class ||= begin
+ routes = respond_to?(:_routes) && _routes
+ helpers = respond_to?(:_helpers) && _helpers
+
+ Class.new(ActionView::Base) do
+ if routes
+ include routes.url_helpers
+ include routes.mounted_helpers
+ end
+
+ if helpers
+ include helpers
+ end
+ end
+ end
+ end
+ end
+
+ attr_internal_writer :view_context_class
+
+ def view_context_class
+ @_view_context_class ||= self.class.view_context_class
+ end
+
+ # An instance of a view class. The default view class is ActionView::Base
+ #
+ # The view class must have the following methods:
+ # View.new[lookup_context, assigns, controller]
+ # Create a new ActionView instance for a controller
+ # View#render[options]
+ # Returns String with the rendered template
+ #
+ # Override this method in a module to change the default behavior.
+ def view_context
+ view_context_class.new(view_renderer, view_assigns, self)
+ end
+
+ # Returns an object that is able to render templates.
+ # :api: private
+ def view_renderer
+ @_view_renderer ||= ActionView::Renderer.new(lookup_context)
+ end
+
+ # Find and renders a template based on the options given.
+ # :api: private
+ def _render_template(options) #:nodoc:
+ lookup_context.rendered_format = nil if options[:formats]
+ view_renderer.render(view_context, options)
+ end
+
+ def render_to_body(options = {})
+ _process_options(options)
+ _render_template(options)
+ end
+
+ def rendered_format
+ Mime[lookup_context.rendered_format]
+ end
+
+ private
+
+ # Assign the rendered format to lookup context.
+ def _process_format(format) #:nodoc:
+ super
+ lookup_context.formats = [format.to_sym]
+ lookup_context.rendered_format = lookup_context.formats.first
+ end
+
+ # Normalize args by converting render "foo" to render :action => "foo" and
+ # render "foo/bar" to render :file => "foo/bar".
+ # :api: private
+ def _normalize_args(action=nil, options={})
+ options = super(action, options)
+ case action
+ when NilClass
+ when Hash
+ options = action
+ when String, Symbol
+ action = action.to_s
+ key = action.include?(?/) ? :file : :action
+ options[key] = action
+ else
+ options[:partial] = action
+ end
+
+ options
+ end
+
+ # Normalize options.
+ # :api: private
+ def _normalize_options(options)
+ options = super(options)
+ if options[:partial] == true
+ options[:partial] = action_name
+ end
+
+ if (options.keys & [:partial, :file, :template]).empty?
+ options[:prefixes] ||= _prefixes
+ end
+
+ options[:template] ||= (options[:action] || action_name).to_s
+ options
+ end
+ end
+end
diff --git a/actionview/lib/action_view/routing_url_for.rb b/actionview/lib/action_view/routing_url_for.rb
new file mode 100644
index 0000000000..f10e7e88ba
--- /dev/null
+++ b/actionview/lib/action_view/routing_url_for.rb
@@ -0,0 +1,107 @@
+module ActionView
+ module RoutingUrlFor
+
+ # Returns the URL for the set of +options+ provided. This takes the
+ # same options as +url_for+ in Action Controller (see the
+ # documentation for <tt>ActionController::Base#url_for</tt>). Note that by default
+ # <tt>:only_path</tt> is <tt>true</tt> so you'll get the relative "/controller/action"
+ # instead of the fully qualified URL like "http://example.com/controller/action".
+ #
+ # ==== Options
+ # * <tt>:anchor</tt> - Specifies the anchor name to be appended to the path.
+ # * <tt>:only_path</tt> - If true, returns the relative URL (omitting the protocol, host name, and port) (<tt>true</tt> by default unless <tt>:host</tt> is specified).
+ # * <tt>:trailing_slash</tt> - If true, adds a trailing slash, as in "/archive/2005/". Note that this
+ # is currently not recommended since it breaks caching.
+ # * <tt>:host</tt> - Overrides the default (current) host if provided.
+ # * <tt>:protocol</tt> - Overrides the default (current) protocol if provided.
+ # * <tt>:user</tt> - Inline HTTP authentication (only plucked out if <tt>:password</tt> is also present).
+ # * <tt>:password</tt> - Inline HTTP authentication (only plucked out if <tt>:user</tt> is also present).
+ #
+ # ==== Relying on named routes
+ #
+ # Passing a record (like an Active Record) instead of a hash as the options parameter will
+ # trigger the named route for that record. The lookup will happen on the name of the class. So passing a
+ # Workshop object will attempt to use the +workshop_path+ route. If you have a nested route, such as
+ # +admin_workshop_path+ you'll have to call that explicitly (it's impossible for +url_for+ to guess that route).
+ #
+ # ==== Implicit Controller Namespacing
+ #
+ # Controllers passed in using the +:controller+ option will retain their namespace unless it is an absolute one.
+ #
+ # ==== Examples
+ # <%= url_for(action: 'index') %>
+ # # => /blog/
+ #
+ # <%= url_for(action: 'find', controller: 'books') %>
+ # # => /books/find
+ #
+ # <%= url_for(action: 'login', controller: 'members', only_path: false, protocol: 'https') %>
+ # # => https://www.example.com/members/login/
+ #
+ # <%= url_for(action: 'play', anchor: 'player') %>
+ # # => /messages/play/#player
+ #
+ # <%= url_for(action: 'jump', anchor: 'tax&ship') %>
+ # # => /testing/jump/#tax&ship
+ #
+ # <%= url_for(Workshop.new) %>
+ # # relies on Workshop answering a persisted? call (and in this case returning false)
+ # # => /workshops
+ #
+ # <%= url_for(@workshop) %>
+ # # calls @workshop.to_param which by default returns the id
+ # # => /workshops/5
+ #
+ # # to_param can be re-defined in a model to provide different URL names:
+ # # => /workshops/1-workshop-name
+ #
+ # <%= url_for("http://www.example.com") %>
+ # # => http://www.example.com
+ #
+ # <%= url_for(:back) %>
+ # # if request.env["HTTP_REFERER"] is set to "http://www.example.com"
+ # # => http://www.example.com
+ #
+ # <%= url_for(:back) %>
+ # # if request.env["HTTP_REFERER"] is not set or is blank
+ # # => javascript:history.back()
+ #
+ # <%= url_for(action: 'index', controller: 'users') %>
+ # # Assuming an "admin" namespace
+ # # => /admin/users
+ #
+ # <%= url_for(action: 'index', controller: '/users') %>
+ # # Specify absolute path with beginning slash
+ # # => /users
+ def url_for(options = nil)
+ case options
+ when String
+ options
+ when nil, Hash
+ options ||= {}
+ options = { :only_path => options[:host].nil? }.merge!(options.symbolize_keys)
+ super
+ when :back
+ _back_url
+ else
+ polymorphic_path(options)
+ end
+ end
+
+ def url_options #:nodoc:
+ return super unless controller.respond_to?(:url_options)
+ controller.url_options
+ end
+
+ def _routes_context #:nodoc:
+ controller
+ end
+ protected :_routes_context
+
+ def optimize_routes_generation? #:nodoc:
+ controller.respond_to?(:optimize_routes_generation?, true) ?
+ controller.optimize_routes_generation? : super
+ end
+ protected :optimize_routes_generation?
+ end
+end
diff --git a/actionview/lib/action_view/tasks/dependencies.rake b/actionview/lib/action_view/tasks/dependencies.rake
new file mode 100644
index 0000000000..1b9426c0e5
--- /dev/null
+++ b/actionview/lib/action_view/tasks/dependencies.rake
@@ -0,0 +1,17 @@
+namespace :cache_digests do
+ desc 'Lookup nested dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
+ task :nested_dependencies => :environment do
+ abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
+ template, format = ENV['TEMPLATE'].split(".")
+ format ||= :html
+ puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).nested_dependencies
+ end
+
+ desc 'Lookup first-level dependencies for TEMPLATE (like messages/show or comments/_comment.html)'
+ task :dependencies => :environment do
+ abort 'You must provide TEMPLATE for the task to run' unless ENV['TEMPLATE'].present?
+ template, format = ENV['TEMPLATE'].split(".")
+ format ||= :html
+ puts JSON.pretty_generate ActionView::Digestor.new(template, format, ApplicationController.new.lookup_context).dependencies
+ end
+end
diff --git a/actionview/lib/action_view/template.rb b/actionview/lib/action_view/template.rb
new file mode 100644
index 0000000000..9b0619f1aa
--- /dev/null
+++ b/actionview/lib/action_view/template.rb
@@ -0,0 +1,340 @@
+require 'active_support/core_ext/object/try'
+require 'active_support/core_ext/kernel/singleton_class'
+require 'thread'
+
+module ActionView
+ # = Action View Template
+ class Template
+ extend ActiveSupport::Autoload
+
+ # === Encodings in ActionView::Template
+ #
+ # ActionView::Template is one of a few sources of potential
+ # encoding issues in Rails. This is because the source for
+ # templates are usually read from disk, and Ruby (like most
+ # encoding-aware programming languages) assumes that the
+ # String retrieved through File IO is encoded in the
+ # <tt>default_external</tt> encoding. In Rails, the default
+ # <tt>default_external</tt> encoding is UTF-8.
+ #
+ # As a result, if a user saves their template as ISO-8859-1
+ # (for instance, using a non-Unicode-aware text editor),
+ # and uses characters outside of the ASCII range, their
+ # users will see diamonds with question marks in them in
+ # the browser.
+ #
+ # For the rest of this documentation, when we say "UTF-8",
+ # we mean "UTF-8 or whatever the default_internal encoding
+ # is set to". By default, it will be UTF-8.
+ #
+ # To mitigate this problem, we use a few strategies:
+ # 1. If the source is not valid UTF-8, we raise an exception
+ # when the template is compiled to alert the user
+ # to the problem.
+ # 2. The user can specify the encoding using Ruby-style
+ # encoding comments in any template engine. If such
+ # a comment is supplied, Rails will apply that encoding
+ # to the resulting compiled source returned by the
+ # template handler.
+ # 3. In all cases, we transcode the resulting String to
+ # the UTF-8.
+ #
+ # This means that other parts of Rails can always assume
+ # that templates are encoded in UTF-8, even if the original
+ # source of the template was not UTF-8.
+ #
+ # From a user's perspective, the easiest thing to do is
+ # to save your templates as UTF-8. If you do this, you
+ # do not need to do anything else for things to "just work".
+ #
+ # === Instructions for template handlers
+ #
+ # The easiest thing for you to do is to simply ignore
+ # encodings. Rails will hand you the template source
+ # as the default_internal (generally UTF-8), raising
+ # an exception for the user before sending the template
+ # to you if it could not determine the original encoding.
+ #
+ # For the greatest simplicity, you can support only
+ # UTF-8 as the <tt>default_internal</tt>. This means
+ # that from the perspective of your handler, the
+ # entire pipeline is just UTF-8.
+ #
+ # === Advanced: Handlers with alternate metadata sources
+ #
+ # If you want to provide an alternate mechanism for
+ # specifying encodings (like ERB does via <%# encoding: ... %>),
+ # you may indicate that you will handle encodings yourself
+ # by implementing <tt>self.handles_encoding?</tt>
+ # on your handler.
+ #
+ # If you do, Rails will not try to encode the String
+ # into the default_internal, passing you the unaltered
+ # bytes tagged with the assumed encoding (from
+ # default_external).
+ #
+ # In this case, make sure you return a String from
+ # your handler encoded in the default_internal. Since
+ # you are handling out-of-band metadata, you are
+ # also responsible for alerting the user to any
+ # problems with converting the user's data to
+ # the <tt>default_internal</tt>.
+ #
+ # To do so, simply raise +WrongEncodingError+ as follows:
+ #
+ # raise WrongEncodingError.new(
+ # problematic_string,
+ # expected_encoding
+ # )
+
+ eager_autoload do
+ autoload :Error
+ autoload :Handlers
+ autoload :Text
+ autoload :Types
+ end
+
+ extend Template::Handlers
+
+ attr_accessor :locals, :formats, :virtual_path
+
+ attr_reader :source, :identifier, :handler, :original_encoding, :updated_at
+
+ # This finalizer is needed (and exactly with a proc inside another proc)
+ # otherwise templates leak in development.
+ Finalizer = proc do |method_name, mod|
+ proc do
+ mod.module_eval do
+ remove_possible_method method_name
+ end
+ end
+ end
+
+ def initialize(source, identifier, handler, details)
+ format = details[:format] || (handler.default_format if handler.respond_to?(:default_format))
+
+ @source = source
+ @identifier = identifier
+ @handler = handler
+ @compiled = false
+ @original_encoding = nil
+ @locals = details[:locals] || []
+ @virtual_path = details[:virtual_path]
+ @updated_at = details[:updated_at] || Time.now
+ @formats = Array(format).map { |f| f.respond_to?(:ref) ? f.ref : f }
+ @compile_mutex = Mutex.new
+ end
+
+ # Returns if the underlying handler supports streaming. If so,
+ # a streaming buffer *may* be passed when it start rendering.
+ def supports_streaming?
+ handler.respond_to?(:supports_streaming?) && handler.supports_streaming?
+ end
+
+ # Render a template. If the template was not compiled yet, it is done
+ # exactly before rendering.
+ #
+ # This method is instrumented as "!render_template.action_view". Notice that
+ # we use a bang in this instrumentation because you don't want to
+ # consume this in production. This is only slow if it's being listened to.
+ def render(view, locals, buffer=nil, &block)
+ instrument("!render_template") do
+ compile!(view)
+ view.send(method_name, locals, buffer, &block)
+ end
+ rescue => e
+ handle_render_error(view, e)
+ end
+
+ def type
+ @type ||= Types[@formats.first] if @formats.first
+ end
+
+ # Receives a view object and return a template similar to self by using @virtual_path.
+ #
+ # This method is useful if you have a template object but it does not contain its source
+ # anymore since it was already compiled. In such cases, all you need to do is to call
+ # refresh passing in the view object.
+ #
+ # Notice this method raises an error if the template to be refreshed does not have a
+ # virtual path set (true just for inline templates).
+ def refresh(view)
+ raise "A template needs to have a virtual path in order to be refreshed" unless @virtual_path
+ lookup = view.lookup_context
+ pieces = @virtual_path.split("/")
+ name = pieces.pop
+ partial = !!name.sub!(/^_/, "")
+ lookup.disable_cache do
+ lookup.find_template(name, [ pieces.join('/') ], partial, @locals)
+ end
+ end
+
+ def inspect
+ @inspect ||= defined?(Rails.root) ? identifier.sub("#{Rails.root}/", '') : identifier
+ end
+
+ # This method is responsible for properly setting the encoding of the
+ # source. Until this point, we assume that the source is BINARY data.
+ # If no additional information is supplied, we assume the encoding is
+ # the same as <tt>Encoding.default_external</tt>.
+ #
+ # The user can also specify the encoding via a comment on the first
+ # line of the template (# encoding: NAME-OF-ENCODING). This will work
+ # with any template engine, as we process out the encoding comment
+ # before passing the source on to the template engine, leaving a
+ # blank line in its stead.
+ def encode!
+ return unless source.encoding == Encoding::BINARY
+
+ # Look for # encoding: *. If we find one, we'll encode the
+ # String in that encoding, otherwise, we'll use the
+ # default external encoding.
+ if source.sub!(/\A#{ENCODING_FLAG}/, '')
+ encoding = magic_encoding = $1
+ else
+ encoding = Encoding.default_external
+ end
+
+ # Tag the source with the default external encoding
+ # or the encoding specified in the file
+ source.force_encoding(encoding)
+
+ # If the user didn't specify an encoding, and the handler
+ # handles encodings, we simply pass the String as is to
+ # the handler (with the default_external tag)
+ if !magic_encoding && @handler.respond_to?(:handles_encoding?) && @handler.handles_encoding?
+ source
+ # Otherwise, if the String is valid in the encoding,
+ # encode immediately to default_internal. This means
+ # that if a handler doesn't handle encodings, it will
+ # always get Strings in the default_internal
+ elsif source.valid_encoding?
+ source.encode!
+ # Otherwise, since the String is invalid in the encoding
+ # specified, raise an exception
+ else
+ raise WrongEncodingError.new(source, encoding)
+ end
+ end
+
+ protected
+
+ # Compile a template. This method ensures a template is compiled
+ # just once and removes the source after it is compiled.
+ def compile!(view) #:nodoc:
+ return if @compiled
+
+ # Templates can be used concurrently in threaded environments
+ # so compilation and any instance variable modification must
+ # be synchronized
+ @compile_mutex.synchronize do
+ # Any thread holding this lock will be compiling the template needed
+ # by the threads waiting. So re-check the @compiled flag to avoid
+ # re-compilation
+ return if @compiled
+
+ if view.is_a?(ActionView::CompiledTemplates)
+ mod = ActionView::CompiledTemplates
+ else
+ mod = view.singleton_class
+ end
+
+ instrument("!compile_template") do
+ compile(view, mod)
+ end
+
+ # Just discard the source if we have a virtual path. This
+ # means we can get the template back.
+ @source = nil if @virtual_path
+ @compiled = true
+ end
+ end
+
+ # Among other things, this method is responsible for properly setting
+ # the encoding of the compiled template.
+ #
+ # If the template engine handles encodings, we send the encoded
+ # String to the engine without further processing. This allows
+ # the template engine to support additional mechanisms for
+ # specifying the encoding. For instance, ERB supports <%# encoding: %>
+ #
+ # Otherwise, after we figure out the correct encoding, we then
+ # encode the source into <tt>Encoding.default_internal</tt>.
+ # In general, this means that templates will be UTF-8 inside of Rails,
+ # regardless of the original source encoding.
+ def compile(view, mod) #:nodoc:
+ encode!
+ method_name = self.method_name
+ code = @handler.call(self)
+
+ # Make sure that the resulting String to be eval'd is in the
+ # encoding of the code
+ source = <<-end_src
+ def #{method_name}(local_assigns, output_buffer)
+ _old_virtual_path, @virtual_path = @virtual_path, #{@virtual_path.inspect};_old_output_buffer = @output_buffer;#{locals_code};#{code}
+ ensure
+ @virtual_path, @output_buffer = _old_virtual_path, _old_output_buffer
+ end
+ end_src
+
+ # Make sure the source is in the encoding of the returned code
+ source.force_encoding(code.encoding)
+
+ # In case we get back a String from a handler that is not in
+ # BINARY or the default_internal, encode it to the default_internal
+ source.encode!
+
+ # Now, validate that the source we got back from the template
+ # handler is valid in the default_internal. This is for handlers
+ # that handle encoding but screw up
+ unless source.valid_encoding?
+ raise WrongEncodingError.new(@source, Encoding.default_internal)
+ end
+
+ begin
+ mod.module_eval(source, identifier, 0)
+ ObjectSpace.define_finalizer(self, Finalizer[method_name, mod])
+ rescue => e # errors from template code
+ if logger = (view && view.logger)
+ logger.debug "ERROR: compiling #{method_name} RAISED #{e}"
+ logger.debug "Function body: #{source}"
+ logger.debug "Backtrace: #{e.backtrace.join("\n")}"
+ end
+
+ raise ActionView::Template::Error.new(self, e)
+ end
+ end
+
+ def handle_render_error(view, e) #:nodoc:
+ if e.is_a?(Template::Error)
+ e.sub_template_of(self)
+ raise e
+ else
+ template = self
+ unless template.source
+ template = refresh(view)
+ template.encode!
+ end
+ raise Template::Error.new(template, e)
+ end
+ end
+
+ def locals_code #:nodoc:
+ # Double assign to suppress the dreaded 'assigned but unused variable' warning
+ @locals.map { |key| "#{key} = #{key} = local_assigns[:#{key}];" }.join
+ end
+
+ def method_name #:nodoc:
+ @method_name ||= "_#{identifier_method_name}__#{@identifier.hash}_#{__id__}".gsub('-', "_")
+ end
+
+ def identifier_method_name #:nodoc:
+ inspect.gsub(/[^a-z_]/, '_')
+ end
+
+ def instrument(action, &block)
+ payload = { virtual_path: @virtual_path, identifier: @identifier }
+ ActiveSupport::Notifications.instrument("#{action}.action_view", payload, &block)
+ end
+ end
+end
diff --git a/actionview/lib/action_view/template/error.rb b/actionview/lib/action_view/template/error.rb
new file mode 100644
index 0000000000..a89d51221e
--- /dev/null
+++ b/actionview/lib/action_view/template/error.rb
@@ -0,0 +1,138 @@
+require "active_support/core_ext/enumerable"
+
+module ActionView
+ # = Action View Errors
+ class ActionViewError < StandardError #:nodoc:
+ end
+
+ class EncodingError < StandardError #:nodoc:
+ end
+
+ class MissingRequestError < StandardError #:nodoc:
+ end
+
+ class WrongEncodingError < EncodingError #:nodoc:
+ def initialize(string, encoding)
+ @string, @encoding = string, encoding
+ end
+
+ def message
+ @string.force_encoding(Encoding::ASCII_8BIT)
+ "Your template was not saved as valid #{@encoding}. Please " \
+ "either specify #{@encoding} as the encoding for your template " \
+ "in your text editor, or mark the template with its " \
+ "encoding by inserting the following as the first line " \
+ "of the template:\n\n# encoding: <name of correct encoding>.\n\n" \
+ "The source of your template was:\n\n#{@string}"
+ end
+ end
+
+ class MissingTemplate < ActionViewError #:nodoc:
+ attr_reader :path
+
+ def initialize(paths, path, prefixes, partial, details, *)
+ @path = path
+ prefixes = Array(prefixes)
+ template_type = if partial
+ "partial"
+ elsif path =~ /layouts/i
+ 'layout'
+ else
+ 'template'
+ end
+
+ searched_paths = prefixes.map { |prefix| [prefix, path].join("/") }
+
+ out = "Missing #{template_type} #{searched_paths.join(", ")} with #{details.inspect}. Searched in:\n"
+ out += paths.compact.map { |p| " * #{p.to_s.inspect}\n" }.join
+ super out
+ end
+ end
+
+ class Template
+ # The Template::Error exception is raised when the compilation or rendering of the template
+ # fails. This exception then gathers a bunch of intimate details and uses it to report a
+ # precise exception message.
+ class Error < ActionViewError #:nodoc:
+ SOURCE_CODE_RADIUS = 3
+
+ attr_reader :original_exception, :backtrace
+
+ def initialize(template, original_exception)
+ super(original_exception.message)
+ @template, @original_exception = template, original_exception
+ @sub_templates = nil
+ @backtrace = original_exception.backtrace
+ end
+
+ def file_name
+ @template.identifier
+ end
+
+ def sub_template_message
+ if @sub_templates
+ "Trace of template inclusion: " +
+ @sub_templates.collect { |template| template.inspect }.join(", ")
+ else
+ ""
+ end
+ end
+
+ def source_extract(indentation = 0, output = :console)
+ return unless num = line_number
+ num = num.to_i
+
+ source_code = @template.source.split("\n")
+
+ start_on_line = [ num - SOURCE_CODE_RADIUS - 1, 0 ].max
+ end_on_line = [ num + SOURCE_CODE_RADIUS - 1, source_code.length].min
+
+ indent = end_on_line.to_s.size + indentation
+ return unless source_code = source_code[start_on_line..end_on_line]
+
+ formatted_code_for(source_code, start_on_line, indent, output)
+ end
+
+ def sub_template_of(template_path)
+ @sub_templates ||= []
+ @sub_templates << template_path
+ end
+
+ def line_number
+ @line_number ||=
+ if file_name
+ regexp = /#{Regexp.escape File.basename(file_name)}:(\d+)/
+ $1 if message =~ regexp || backtrace.find { |line| line =~ regexp }
+ end
+ end
+
+ def annoted_source_code
+ source_extract(4)
+ end
+
+ private
+
+ def source_location
+ if line_number
+ "on line ##{line_number} of "
+ else
+ 'in '
+ end + file_name
+ end
+
+ def formatted_code_for(source_code, line_counter, indent, output)
+ start_value = (output == :html) ? {} : ""
+ source_code.inject(start_value) do |result, line|
+ line_counter += 1
+ if output == :html
+ result.update(line_counter.to_s => "%#{indent}s %s\n" % ["", line])
+ else
+ result << "%#{indent}s: %s\n" % [line_counter, line]
+ end
+ end
+ end
+ end
+ end
+
+ TemplateError = Template::Error
+end
diff --git a/actionpack/lib/action_view/template/handlers.rb b/actionview/lib/action_view/template/handlers.rb
index d9cddc0040..d9cddc0040 100644
--- a/actionpack/lib/action_view/template/handlers.rb
+++ b/actionview/lib/action_view/template/handlers.rb
diff --git a/actionpack/lib/action_view/template/handlers/builder.rb b/actionview/lib/action_view/template/handlers/builder.rb
index d90b0c6378..d90b0c6378 100644
--- a/actionpack/lib/action_view/template/handlers/builder.rb
+++ b/actionview/lib/action_view/template/handlers/builder.rb
diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb
new file mode 100644
index 0000000000..c8a0059596
--- /dev/null
+++ b/actionview/lib/action_view/template/handlers/erb.rb
@@ -0,0 +1,145 @@
+require 'erubis'
+
+module ActionView
+ class Template
+ module Handlers
+ class Erubis < ::Erubis::Eruby
+ def add_preamble(src)
+ @newline_pending = 0
+ src << "@output_buffer = output_buffer || ActionView::OutputBuffer.new;"
+ end
+
+ def add_text(src, text)
+ return if text.empty?
+
+ if text == "\n"
+ @newline_pending += 1
+ else
+ src << "@output_buffer.safe_append='"
+ src << "\n" * @newline_pending if @newline_pending > 0
+ src << escape_text(text)
+ src << "';"
+
+ @newline_pending = 0
+ end
+ end
+
+ # Erubis toggles <%= and <%== behavior when escaping is enabled.
+ # We override to always treat <%== as escaped.
+ def add_expr(src, code, indicator)
+ case indicator
+ when '=='
+ add_expr_escaped(src, code)
+ else
+ super
+ end
+ end
+
+ BLOCK_EXPR = /\s+(do|\{)(\s*\|[^|]*\|)?\s*\Z/
+
+ def add_expr_literal(src, code)
+ flush_newline_if_pending(src)
+ if code =~ BLOCK_EXPR
+ src << '@output_buffer.append= ' << code
+ else
+ src << '@output_buffer.append=(' << code << ');'
+ end
+ end
+
+ def add_expr_escaped(src, code)
+ flush_newline_if_pending(src)
+ if code =~ BLOCK_EXPR
+ src << "@output_buffer.safe_append= " << code
+ else
+ src << "@output_buffer.safe_append=(" << code << ");"
+ end
+ end
+
+ def add_stmt(src, code)
+ flush_newline_if_pending(src)
+ super
+ end
+
+ def add_postamble(src)
+ flush_newline_if_pending(src)
+ src << '@output_buffer.to_s'
+ end
+
+ def flush_newline_if_pending(src)
+ if @newline_pending > 0
+ src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';"
+ @newline_pending = 0
+ end
+ end
+ end
+
+ class ERB
+ # Specify trim mode for the ERB compiler. Defaults to '-'.
+ # See ERB documentation for suitable values.
+ class_attribute :erb_trim_mode
+ self.erb_trim_mode = '-'
+
+ # Default implementation used.
+ class_attribute :erb_implementation
+ self.erb_implementation = Erubis
+
+ # Do not escape templates of these mime types.
+ class_attribute :escape_whitelist
+ self.escape_whitelist = ["text/plain"]
+
+ ENCODING_TAG = Regexp.new("\\A(<%#{ENCODING_FLAG}-?%>)[ \\t]*")
+
+ def self.call(template)
+ new.call(template)
+ end
+
+ def supports_streaming?
+ true
+ end
+
+ def handles_encoding?
+ true
+ end
+
+ def call(template)
+ # First, convert to BINARY, so in case the encoding is
+ # wrong, we can still find an encoding tag
+ # (<%# encoding %>) inside the String using a regular
+ # expression
+ template_source = template.source.dup.force_encoding(Encoding::ASCII_8BIT)
+
+ erb = template_source.gsub(ENCODING_TAG, '')
+ encoding = $2
+
+ erb.force_encoding valid_encoding(template.source.dup, encoding)
+
+ # Always make sure we return a String in the default_internal
+ erb.encode!
+
+ self.class.erb_implementation.new(
+ erb,
+ :escape => (self.class.escape_whitelist.include? template.type),
+ :trim => (self.class.erb_trim_mode == "-")
+ ).src
+ end
+
+ private
+
+ def valid_encoding(string, encoding)
+ # If a magic encoding comment was found, tag the
+ # String with this encoding. This is for a case
+ # where the original String was assumed to be,
+ # for instance, UTF-8, but a magic comment
+ # proved otherwise
+ string.force_encoding(encoding) if encoding
+
+ # If the String is valid, return the encoding we found
+ return string.encoding if string.valid_encoding?
+
+ # Otherwise, raise an exception
+ raise WrongEncodingError.new(string, string.encoding)
+ end
+ end
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/handlers/raw.rb b/actionview/lib/action_view/template/handlers/raw.rb
index 0c0d1fffcb..0c0d1fffcb 100644
--- a/actionpack/lib/action_view/template/handlers/raw.rb
+++ b/actionview/lib/action_view/template/handlers/raw.rb
diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb
new file mode 100644
index 0000000000..3304605c1a
--- /dev/null
+++ b/actionview/lib/action_view/template/resolver.rb
@@ -0,0 +1,326 @@
+require "pathname"
+require "active_support/core_ext/class"
+require "active_support/core_ext/class/attribute_accessors"
+require "action_view/template"
+require "thread"
+require "thread_safe"
+
+module ActionView
+ # = Action View Resolver
+ class Resolver
+ # Keeps all information about view path and builds virtual path.
+ class Path
+ attr_reader :name, :prefix, :partial, :virtual
+ alias_method :partial?, :partial
+
+ def self.build(name, prefix, partial)
+ virtual = ""
+ virtual << "#{prefix}/" unless prefix.empty?
+ virtual << (partial ? "_#{name}" : name)
+ new name, prefix, partial, virtual
+ end
+
+ def initialize(name, prefix, partial, virtual)
+ @name = name
+ @prefix = prefix
+ @partial = partial
+ @virtual = virtual
+ end
+
+ def to_str
+ @virtual
+ end
+ alias :to_s :to_str
+ end
+
+ # Threadsafe template cache
+ class Cache #:nodoc:
+ class SmallCache < ThreadSafe::Cache
+ def initialize(options = {})
+ super(options.merge(:initial_capacity => 2))
+ end
+ end
+
+ # preallocate all the default blocks for performance/memory consumption reasons
+ PARTIAL_BLOCK = lambda {|cache, partial| cache[partial] = SmallCache.new}
+ PREFIX_BLOCK = lambda {|cache, prefix| cache[prefix] = SmallCache.new(&PARTIAL_BLOCK)}
+ NAME_BLOCK = lambda {|cache, name| cache[name] = SmallCache.new(&PREFIX_BLOCK)}
+ KEY_BLOCK = lambda {|cache, key| cache[key] = SmallCache.new(&NAME_BLOCK)}
+
+ # usually a majority of template look ups return nothing, use this canonical preallocated array to save memory
+ NO_TEMPLATES = [].freeze
+
+ def initialize
+ @data = SmallCache.new(&KEY_BLOCK)
+ end
+
+ # Cache the templates returned by the block
+ def cache(key, name, prefix, partial, locals)
+ if Resolver.caching?
+ @data[key][name][prefix][partial][locals] ||= canonical_no_templates(yield)
+ else
+ fresh_templates = yield
+ cached_templates = @data[key][name][prefix][partial][locals]
+
+ if templates_have_changed?(cached_templates, fresh_templates)
+ @data[key][name][prefix][partial][locals] = canonical_no_templates(fresh_templates)
+ else
+ cached_templates || NO_TEMPLATES
+ end
+ end
+ end
+
+ def clear
+ @data.clear
+ end
+
+ private
+
+ def canonical_no_templates(templates)
+ templates.empty? ? NO_TEMPLATES : templates
+ end
+
+ def templates_have_changed?(cached_templates, fresh_templates)
+ # if either the old or new template list is empty, we don't need to (and can't)
+ # compare modification times, and instead just check whether the lists are different
+ if cached_templates.blank? || fresh_templates.blank?
+ return fresh_templates.blank? != cached_templates.blank?
+ end
+
+ cached_templates_max_updated_at = cached_templates.map(&:updated_at).max
+
+ # if a template has changed, it will be now be newer than all the cached templates
+ fresh_templates.any? { |t| t.updated_at > cached_templates_max_updated_at }
+ end
+ end
+
+ cattr_accessor :caching
+ self.caching = true
+
+ class << self
+ alias :caching? :caching
+ end
+
+ def initialize
+ @cache = Cache.new
+ end
+
+ def clear_cache
+ @cache.clear
+ end
+
+ # Normalizes the arguments and passes it on to find_templates.
+ def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
+ cached(key, [name, prefix, partial], details, locals) do
+ find_templates(name, prefix, partial, details)
+ end
+ end
+
+ private
+
+ delegate :caching?, to: :class
+
+ # This is what child classes implement. No defaults are needed
+ # because Resolver guarantees that the arguments are present and
+ # normalized.
+ def find_templates(name, prefix, partial, details)
+ raise NotImplementedError, "Subclasses must implement a find_templates(name, prefix, partial, details) method"
+ end
+
+ # Helpers that builds a path. Useful for building virtual paths.
+ def build_path(name, prefix, partial)
+ Path.build(name, prefix, partial)
+ end
+
+ # Handles templates caching. If a key is given and caching is on
+ # always check the cache before hitting the resolver. Otherwise,
+ # it always hits the resolver but if the key is present, check if the
+ # resolver is fresher before returning it.
+ def cached(key, path_info, details, locals) #:nodoc:
+ name, prefix, partial = path_info
+ locals = locals.map { |x| x.to_s }.sort!
+
+ if key
+ @cache.cache(key, name, prefix, partial, locals) do
+ decorate(yield, path_info, details, locals)
+ end
+ else
+ decorate(yield, path_info, details, locals)
+ end
+ end
+
+ # Ensures all the resolver information is set in the template.
+ def decorate(templates, path_info, details, locals) #:nodoc:
+ cached = nil
+ templates.each do |t|
+ t.locals = locals
+ t.formats = details[:formats] || [:html] if t.formats.empty?
+ t.virtual_path ||= (cached ||= build_path(*path_info))
+ end
+ end
+ end
+
+ # An abstract class that implements a Resolver with path semantics.
+ class PathResolver < Resolver #:nodoc:
+ EXTENSIONS = [:locale, :formats, :handlers]
+ DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}"
+
+ def initialize(pattern=nil)
+ @pattern = pattern || DEFAULT_PATTERN
+ super()
+ end
+
+ private
+
+ def find_templates(name, prefix, partial, details)
+ path = Path.build(name, prefix, partial)
+ query(path, details, details[:formats])
+ end
+
+ 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.map { |template|
+ handler, format = extract_handler_and_format(template, formats)
+ contents = File.binread template
+
+ Template.new(contents, File.expand_path(template), handler,
+ :virtual_path => path.virtual,
+ :format => format,
+ :updated_at => mtime(template))
+ }
+ end
+
+ # Helper for building query glob string based on resolver's pattern.
+ def build_query(path, details)
+ query = @pattern.dup
+
+ prefix = path.prefix.empty? ? "" : "#{escape_entry(path.prefix)}\\1"
+ query.gsub!(/\:prefix(\/)?/, prefix)
+
+ partial = escape_entry(path.partial? ? "_#{path.name}" : path.name)
+ query.gsub!(/\:action/, partial)
+
+ details.each do |ext, variants|
+ query.gsub!(/\:#{ext}/, "{#{variants.compact.uniq.join(',')}}")
+ end
+
+ File.expand_path(query, @path)
+ end
+
+ def escape_entry(entry)
+ entry.gsub(/[*?{}\[\]]/, '\\\\\\&')
+ end
+
+ # Returns the file mtime from the filesystem.
+ def mtime(p)
+ File.mtime(p)
+ end
+
+ # Extract handler and formats from path. If a format cannot be a 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)
+ pieces = File.basename(path).split(".")
+ pieces.shift
+
+ extension = pieces.pop
+ unless extension
+ message = "The file #{path} did not specify a template handler. The default is currently ERB, " \
+ "but will change to RAW in the future."
+ ActiveSupport::Deprecation.warn message
+ end
+
+ handler = Template.handler_for_extension(extension)
+ format = pieces.last && Template::Types[pieces.last]
+ [handler, format]
+ end
+ end
+
+ # A resolver that loads files from the filesystem. It allows setting your own
+ # resolving pattern. Such pattern can be a glob string supported by some variables.
+ #
+ # ==== Examples
+ #
+ # Default pattern, loads views the same way as previous versions of rails, eg. when you're
+ # looking for `users/new` it will produce query glob: `users/new{.{en},}{.{html,js},}{.{erb,haml},}`
+ #
+ # FileSystemResolver.new("/path/to/views", ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}")
+ #
+ # This one allows you to keep files with different formats in separate subdirectories,
+ # eg. `users/new.html` will be loaded from `users/html/new.erb` or `users/new.html.erb`,
+ # `users/new.js` from `users/js/new.erb` or `users/new.js.erb`, etc.
+ #
+ # FileSystemResolver.new("/path/to/views", ":prefix/{:formats/,}:action{.:locale,}{.:formats,}{.:handlers,}")
+ #
+ # If you don't specify a pattern then the default will be used.
+ #
+ # In order to use any of the customized resolvers above in a Rails application, you just need
+ # to configure ActionController::Base.view_paths in an initializer, for example:
+ #
+ # ActionController::Base.view_paths = FileSystemResolver.new(
+ # Rails.root.join("app/views"),
+ # ":prefix{/:locale}/:action{.:formats,}{.:handlers,}"
+ # )
+ #
+ # ==== Pattern format and variables
+ #
+ # Pattern has to be a valid glob string, and it allows you to use the
+ # following variables:
+ #
+ # * <tt>:prefix</tt> - usually the controller path
+ # * <tt>:action</tt> - name of the action
+ # * <tt>:locale</tt> - possible locale versions
+ # * <tt>:formats</tt> - possible request formats (for example html, json, xml...)
+ # * <tt>:handlers</tt> - possible handlers (for example erb, haml, builder...)
+ #
+ class FileSystemResolver < PathResolver
+ def initialize(path, pattern=nil)
+ raise ArgumentError, "path already is a Resolver class" if path.is_a?(Resolver)
+ super(pattern)
+ @path = File.expand_path(path)
+ end
+
+ def to_s
+ @path.to_s
+ end
+ alias :to_path :to_s
+
+ def eql?(resolver)
+ self.class.equal?(resolver.class) && to_path == resolver.to_path
+ end
+ alias :== :eql?
+ end
+
+ # An Optimized resolver for Rails' most common case.
+ class OptimizedFileSystemResolver < FileSystemResolver #:nodoc:
+ def build_query(path, details)
+ exts = EXTENSIONS.map { |ext| details[ext] }
+ query = escape_entry(File.join(@path, path))
+
+ query + exts.map { |ext|
+ "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}"
+ }.join
+ end
+ end
+
+ # The same as FileSystemResolver but does not allow templates to store
+ # a virtual path since it is invalid for such resolvers.
+ class FallbackFileSystemResolver < FileSystemResolver #:nodoc:
+ def self.instances
+ [new(""), new("/")]
+ end
+
+ def decorate(*)
+ super.each { |t| t.virtual_path = nil }
+ end
+ end
+end
diff --git a/actionpack/lib/action_view/template/text.rb b/actionview/lib/action_view/template/text.rb
index 859c7bc3ce..859c7bc3ce 100644
--- a/actionpack/lib/action_view/template/text.rb
+++ b/actionview/lib/action_view/template/text.rb
diff --git a/actionview/lib/action_view/template/types.rb b/actionview/lib/action_view/template/types.rb
new file mode 100644
index 0000000000..db77cb5d19
--- /dev/null
+++ b/actionview/lib/action_view/template/types.rb
@@ -0,0 +1,57 @@
+require 'set'
+require 'active_support/core_ext/class/attribute_accessors'
+
+module ActionView
+ class Template
+ class Types
+ class Type
+ cattr_accessor :types
+ self.types = Set.new
+
+ def self.register(*t)
+ types.merge(t.map { |type| type.to_s })
+ end
+
+ register :html, :text, :js, :css, :xml, :json
+
+ def self.[](type)
+ return type if type.is_a?(self)
+
+ if type.is_a?(Symbol) || types.member?(type.to_s)
+ new(type)
+ end
+ end
+
+ attr_reader :symbol
+
+ def initialize(symbol)
+ @symbol = symbol.to_sym
+ end
+
+ delegate :to_s, :to_sym, :to => :symbol
+ alias to_str to_s
+
+ def ref
+ to_sym || to_s
+ end
+
+ def ==(type)
+ return false if type.blank?
+ symbol.to_sym == type.to_sym
+ end
+ end
+
+ cattr_accessor :type_klass
+
+ def self.delegate_to(klass)
+ self.type_klass = klass
+ end
+
+ delegate_to Type
+
+ def self.[](type)
+ type_klass[type]
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/test_case.rb b/actionview/lib/action_view/test_case.rb
new file mode 100644
index 0000000000..3145446114
--- /dev/null
+++ b/actionview/lib/action_view/test_case.rb
@@ -0,0 +1,272 @@
+require 'active_support/core_ext/module/remove_method'
+require 'action_controller'
+require 'action_controller/test_case'
+require 'action_view'
+
+module ActionView
+ # = Action View Test Case
+ class TestCase < ActiveSupport::TestCase
+ class TestController < ActionController::Base
+ include ActionDispatch::TestProcess
+
+ attr_accessor :request, :response, :params
+
+ class << self
+ attr_writer :controller_path
+ end
+
+ def controller_path=(path)
+ self.class.controller_path=(path)
+ end
+
+ def initialize
+ super
+ self.class.controller_path = ""
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+
+ @request.env.delete('PATH_INFO')
+ @params = {}
+ end
+ end
+
+ module Behavior
+ extend ActiveSupport::Concern
+
+ include ActionDispatch::Assertions, ActionDispatch::TestProcess
+ include ActionController::TemplateAssertions
+ include ActionView::Context
+
+ include ActionDispatch::Routing::PolymorphicRoutes
+
+ include AbstractController::Helpers
+ include ActionView::Helpers
+ include ActionView::RecordIdentifier
+ include ActionView::RoutingUrlFor
+
+ include ActiveSupport::Testing::ConstantLookup
+
+ delegate :lookup_context, :to => :controller
+ attr_accessor :controller, :output_buffer, :rendered
+
+ module ClassMethods
+ def tests(helper_class)
+ case helper_class
+ when String, Symbol
+ self.helper_class = "#{helper_class.to_s.underscore}_helper".camelize.safe_constantize
+ when Module
+ self.helper_class = helper_class
+ end
+ end
+
+ def determine_default_helper_class(name)
+ determine_constant_from_test_name(name) do |constant|
+ Module === constant && !(Class === constant)
+ end
+ end
+
+ def helper_method(*methods)
+ # Almost a duplicate from ActionController::Helpers
+ methods.flatten.each do |method|
+ _helpers.module_eval <<-end_eval
+ def #{method}(*args, &block) # def current_user(*args, &block)
+ _test_case.send(%(#{method}), *args, &block) # _test_case.send(%(current_user), *args, &block)
+ end # end
+ end_eval
+ end
+ end
+
+ attr_writer :helper_class
+
+ def helper_class
+ @helper_class ||= determine_default_helper_class(name)
+ end
+
+ def new(*)
+ include_helper_modules!
+ super
+ end
+
+ private
+
+ def include_helper_modules!
+ helper(helper_class) if helper_class
+ include _helpers
+ end
+
+ end
+
+ def setup_with_controller
+ @controller = ActionView::TestCase::TestController.new
+ @request = @controller.request
+ @output_buffer = ActiveSupport::SafeBuffer.new
+ @rendered = ''
+
+ make_test_case_available_to_view!
+ say_no_to_protect_against_forgery!
+ end
+
+ def config
+ @controller.config if @controller.respond_to?(:config)
+ end
+
+ def render(options = {}, local_assigns = {}, &block)
+ view.assign(view_assigns)
+ @rendered << output = view.render(options, local_assigns, &block)
+ output
+ end
+
+ def rendered_views
+ @_rendered_views ||= RenderedViewsCollection.new
+ end
+
+ class RenderedViewsCollection
+ def initialize
+ @rendered_views ||= Hash.new { |hash, key| hash[key] = [] }
+ end
+
+ def add(view, locals)
+ @rendered_views[view] ||= []
+ @rendered_views[view] << locals
+ end
+
+ def locals_for(view)
+ @rendered_views[view]
+ end
+
+ def rendered_views
+ @rendered_views.keys
+ end
+
+ def view_rendered?(view, expected_locals)
+ locals_for(view).any? do |actual_locals|
+ expected_locals.all? {|key, value| value == actual_locals[key] }
+ end
+ end
+ end
+
+ included do
+ setup :setup_with_controller
+ end
+
+ private
+
+ # Support the selector assertions
+ #
+ # Need to experiment if this priority is the best one: rendered => output_buffer
+ def response_from_page
+ HTML::Document.new(@rendered.blank? ? @output_buffer : @rendered).root
+ end
+
+ def say_no_to_protect_against_forgery!
+ _helpers.module_eval do
+ remove_possible_method :protect_against_forgery?
+ def protect_against_forgery?
+ false
+ end
+ end
+ end
+
+ def make_test_case_available_to_view!
+ test_case_instance = self
+ _helpers.module_eval do
+ unless private_method_defined?(:_test_case)
+ define_method(:_test_case) { test_case_instance }
+ private :_test_case
+ end
+ end
+ end
+
+ module Locals
+ attr_accessor :rendered_views
+
+ def render(options = {}, local_assigns = {})
+ case options
+ when Hash
+ if block_given?
+ rendered_views.add options[:layout], options[:locals]
+ elsif options.key?(:partial)
+ rendered_views.add options[:partial], options[:locals]
+ end
+ else
+ rendered_views.add options, local_assigns
+ end
+
+ super
+ end
+ end
+
+ # The instance of ActionView::Base that is used by +render+.
+ def view
+ @view ||= begin
+ view = @controller.view_context
+ view.singleton_class.send :include, _helpers
+ view.extend(Locals)
+ view.rendered_views = self.rendered_views
+ view.output_buffer = self.output_buffer
+ view
+ end
+ end
+
+ alias_method :_view, :view
+
+ INTERNAL_IVARS = [
+ :@NAME,
+ :@failures,
+ :@assertions,
+ :@__io__,
+ :@_assertion_wrapped,
+ :@_assertions,
+ :@_result,
+ :@_routes,
+ :@controller,
+ :@_layouts,
+ :@_files,
+ :@_rendered_views,
+ :@method_name,
+ :@output_buffer,
+ :@_partials,
+ :@passed,
+ :@rendered,
+ :@request,
+ :@routes,
+ :@tagged_logger,
+ :@_templates,
+ :@options,
+ :@test_passed,
+ :@view,
+ :@view_context_class
+ ]
+
+ def _user_defined_ivars
+ instance_variables - INTERNAL_IVARS
+ end
+
+ # Returns a Hash of instance variables and their values, as defined by
+ # the user in the test case, which are then assigned to the view being
+ # rendered. This is generally intended for internal use and extension
+ # frameworks.
+ def view_assigns
+ Hash[_user_defined_ivars.map do |ivar|
+ [ivar[1..-1].to_sym, instance_variable_get(ivar)]
+ end]
+ end
+
+ def _routes
+ @controller._routes if @controller.respond_to?(:_routes)
+ end
+
+ def method_missing(selector, *args)
+ if @controller.respond_to?(:_routes) &&
+ ( @controller._routes.named_routes.helpers.include?(selector) ||
+ @controller._routes.mounted_helpers.method_defined?(selector) )
+ @controller.__send__(selector, *args)
+ else
+ super
+ end
+ end
+ end
+
+ include Behavior
+ end
+end
diff --git a/actionpack/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb
index 7afa2fa613..7afa2fa613 100644
--- a/actionpack/lib/action_view/testing/resolvers.rb
+++ b/actionview/lib/action_view/testing/resolvers.rb
diff --git a/actionview/lib/action_view/vendor/html-scanner.rb b/actionview/lib/action_view/vendor/html-scanner.rb
new file mode 100644
index 0000000000..775b827529
--- /dev/null
+++ b/actionview/lib/action_view/vendor/html-scanner.rb
@@ -0,0 +1,20 @@
+$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/html-scanner"
+
+module HTML
+ extend ActiveSupport::Autoload
+
+ eager_autoload do
+ autoload :CDATA, 'html/node'
+ autoload :Document, 'html/document'
+ autoload :FullSanitizer, 'html/sanitizer'
+ autoload :LinkSanitizer, 'html/sanitizer'
+ autoload :Node, 'html/node'
+ autoload :Sanitizer, 'html/sanitizer'
+ autoload :Selector, 'html/selector'
+ autoload :Tag, 'html/node'
+ autoload :Text, 'html/node'
+ autoload :Tokenizer, 'html/tokenizer'
+ autoload :Version, 'html/version'
+ autoload :WhiteListSanitizer, 'html/sanitizer'
+ end
+end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/document.rb b/actionview/lib/action_view/vendor/html-scanner/html/document.rb
index 386820300a..386820300a 100644
--- a/actionpack/lib/action_view/vendor/html-scanner/html/document.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/document.rb
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/node.rb b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
new file mode 100644
index 0000000000..7e7cd4f7b6
--- /dev/null
+++ b/actionview/lib/action_view/vendor/html-scanner/html/node.rb
@@ -0,0 +1,532 @@
+require 'strscan'
+
+module HTML #:nodoc:
+
+ class Conditions < Hash #:nodoc:
+ def initialize(hash)
+ super()
+ hash = { :content => hash } unless Hash === hash
+ hash = keys_to_symbols(hash)
+ hash.each do |k,v|
+ case k
+ when :tag, :content then
+ # keys are valid, and require no further processing
+ when :attributes then
+ hash[k] = keys_to_strings(v)
+ when :parent, :child, :ancestor, :descendant, :sibling, :before,
+ :after
+ hash[k] = Conditions.new(v)
+ when :children
+ hash[k] = v = keys_to_symbols(v)
+ v.each do |key,value|
+ case key
+ when :count, :greater_than, :less_than
+ # keys are valid, and require no further processing
+ when :only
+ v[key] = Conditions.new(value)
+ else
+ raise "illegal key #{key.inspect} => #{value.inspect}"
+ end
+ end
+ else
+ raise "illegal key #{k.inspect} => #{v.inspect}"
+ end
+ end
+ update hash
+ end
+
+ private
+
+ def keys_to_strings(hash)
+ Hash[hash.keys.map {|k| [k.to_s, hash[k]]}]
+ end
+
+ def keys_to_symbols(hash)
+ Hash[hash.keys.map do |k|
+ raise "illegal key #{k.inspect}" unless k.respond_to?(:to_sym)
+ [k.to_sym, hash[k]]
+ end]
+ end
+ end
+
+ # The base class of all nodes, textual and otherwise, in an HTML document.
+ class Node #:nodoc:
+ # The array of children of this node. Not all nodes have children.
+ attr_reader :children
+
+ # The parent node of this node. All nodes have a parent, except for the
+ # root node.
+ attr_reader :parent
+
+ # The line number of the input where this node was begun
+ attr_reader :line
+
+ # The byte position in the input where this node was begun
+ attr_reader :position
+
+ # Create a new node as a child of the given parent.
+ def initialize(parent, line=0, pos=0)
+ @parent = parent
+ @children = []
+ @line, @position = line, pos
+ end
+
+ # Return a textual representation of the node.
+ def to_s
+ @children.join()
+ end
+
+ # Return false (subclasses must override this to provide specific matching
+ # behavior.) +conditions+ may be of any type.
+ def match(conditions)
+ false
+ end
+
+ # Search the children of this node for the first node for which #find
+ # returns non +nil+. Returns the result of the #find call that succeeded.
+ def find(conditions)
+ conditions = validate_conditions(conditions)
+ @children.each do |child|
+ node = child.find(conditions)
+ return node if node
+ end
+ nil
+ end
+
+ # Search for all nodes that match the given conditions, and return them
+ # as an array.
+ def find_all(conditions)
+ conditions = validate_conditions(conditions)
+
+ matches = []
+ matches << self if match(conditions)
+ @children.each do |child|
+ matches.concat child.find_all(conditions)
+ end
+ matches
+ end
+
+ # Returns +false+. Subclasses may override this if they define a kind of
+ # tag.
+ def tag?
+ false
+ end
+
+ def validate_conditions(conditions)
+ Conditions === conditions ? conditions : Conditions.new(conditions)
+ end
+
+ def ==(node)
+ return false unless self.class == node.class && children.size == node.children.size
+
+ equivalent = true
+
+ children.size.times do |i|
+ equivalent &&= children[i] == node.children[i]
+ end
+
+ equivalent
+ end
+
+ class <<self
+ def parse(parent, line, pos, content, strict=true)
+ if content !~ /^<\S/
+ Text.new(parent, line, pos, content)
+ else
+ scanner = StringScanner.new(content)
+
+ unless scanner.skip(/</)
+ if strict
+ raise "expected <"
+ else
+ return Text.new(parent, line, pos, content)
+ end
+ end
+
+ if scanner.skip(/!\[CDATA\[/)
+ unless scanner.skip_until(/\]\]>/)
+ if strict
+ raise "expected ]]> (got #{scanner.rest.inspect} for #{content})"
+ else
+ scanner.skip_until(/\Z/)
+ end
+ end
+
+ return CDATA.new(parent, line, pos, scanner.pre_match.gsub(/<!\[CDATA\[/, ''))
+ end
+
+ closing = ( scanner.scan(/\//) ? :close : nil )
+ return Text.new(parent, line, pos, content) unless name = scanner.scan(/[^\s!>\/]+/)
+ name.downcase!
+
+ unless closing
+ scanner.skip(/\s*/)
+ attributes = {}
+ while attr = scanner.scan(/[-\w:]+/)
+ value = true
+ if scanner.scan(/\s*=\s*/)
+ if delim = scanner.scan(/['"]/)
+ value = ""
+ while text = scanner.scan(/[^#{delim}\\]+|./)
+ case text
+ when "\\" then
+ value << text
+ break if scanner.eos?
+ value << scanner.getch
+ when delim
+ break
+ else value << text
+ end
+ end
+ else
+ value = scanner.scan(/[^\s>\/]+/)
+ end
+ end
+ attributes[attr.downcase] = value
+ scanner.skip(/\s*/)
+ end
+
+ closing = ( scanner.scan(/\//) ? :self : nil )
+ end
+
+ unless scanner.scan(/\s*>/)
+ if strict
+ raise "expected > (got #{scanner.rest.inspect} for #{content}, #{attributes.inspect})"
+ else
+ # throw away all text until we find what we're looking for
+ scanner.skip_until(/>/) or scanner.terminate
+ end
+ end
+
+ Tag.new(parent, line, pos, name, attributes, closing)
+ end
+ end
+ end
+ end
+
+ # A node that represents text, rather than markup.
+ class Text < Node #:nodoc:
+
+ attr_reader :content
+
+ # Creates a new text node as a child of the given parent, with the given
+ # content.
+ def initialize(parent, line, pos, content)
+ super(parent, line, pos)
+ @content = content
+ end
+
+ # Returns the content of this node.
+ def to_s
+ @content
+ end
+
+ # Returns +self+ if this node meets the given conditions. Text nodes support
+ # conditions of the following kinds:
+ #
+ # * if +conditions+ is a string, it must be a substring of the node's
+ # content
+ # * if +conditions+ is a regular expression, it must match the node's
+ # content
+ # * if +conditions+ is a hash, it must contain a <tt>:content</tt> key that
+ # is either a string or a regexp, and which is interpreted as described
+ # above.
+ def find(conditions)
+ match(conditions) && self
+ end
+
+ # Returns non-+nil+ if this node meets the given conditions, or +nil+
+ # otherwise. See the discussion of #find for the valid conditions.
+ def match(conditions)
+ case conditions
+ when String
+ @content == conditions
+ when Regexp
+ @content =~ conditions
+ when Hash
+ conditions = validate_conditions(conditions)
+
+ # Text nodes only have :content, :parent, :ancestor
+ unless (conditions.keys - [:content, :parent, :ancestor]).empty?
+ return false
+ end
+
+ match(conditions[:content])
+ else
+ nil
+ end
+ end
+
+ def ==(node)
+ return false unless super
+ content == node.content
+ end
+ end
+
+ # A CDATA node is simply a text node with a specialized way of displaying
+ # itself.
+ class CDATA < Text #:nodoc:
+ def to_s
+ "<![CDATA[#{super}]]>"
+ end
+ end
+
+ # A Tag is any node that represents markup. It may be an opening tag, a
+ # closing tag, or a self-closing tag. It has a name, and may have a hash of
+ # attributes.
+ class Tag < Node #:nodoc:
+
+ # Either +nil+, <tt>:close</tt>, or <tt>:self</tt>
+ attr_reader :closing
+
+ # Either +nil+, or a hash of attributes for this node.
+ attr_reader :attributes
+
+ # The name of this tag.
+ attr_reader :name
+
+ # Create a new node as a child of the given parent, using the given content
+ # to describe the node. It will be parsed and the node name, attributes and
+ # closing status extracted.
+ def initialize(parent, line, pos, name, attributes, closing)
+ super(parent, line, pos)
+ @name = name
+ @attributes = attributes
+ @closing = closing
+ end
+
+ # A convenience for obtaining an attribute of the node. Returns +nil+ if
+ # the node has no attributes.
+ def [](attr)
+ @attributes ? @attributes[attr] : nil
+ end
+
+ # Returns non-+nil+ if this tag can contain child nodes.
+ def childless?(xml = false)
+ return false if xml && @closing.nil?
+ !@closing.nil? ||
+ @name =~ /^(img|br|hr|link|meta|area|base|basefont|
+ col|frame|input|isindex|param)$/ox
+ end
+
+ # Returns a textual representation of the node
+ def to_s
+ if @closing == :close
+ "</#{@name}>"
+ else
+ s = "<#{@name}"
+ @attributes.each do |k,v|
+ s << " #{k}"
+ s << "=\"#{v}\"" if String === v
+ end
+ s << " /" if @closing == :self
+ s << ">"
+ @children.each { |child| s << child.to_s }
+ s << "</#{@name}>" if @closing != :self && !@children.empty?
+ s
+ end
+ end
+
+ # If either the node or any of its children meet the given conditions, the
+ # matching node is returned. Otherwise, +nil+ is returned. (See the
+ # description of the valid conditions in the +match+ method.)
+ def find(conditions)
+ match(conditions) && self || super
+ end
+
+ # Returns +true+, indicating that this node represents an HTML tag.
+ def tag?
+ true
+ end
+
+ # Returns +true+ if the node meets any of the given conditions. The
+ # +conditions+ parameter must be a hash of any of the following keys
+ # (all are optional):
+ #
+ # * <tt>:tag</tt>: the node name must match the corresponding value
+ # * <tt>:attributes</tt>: a hash. The node's values must match the
+ # corresponding values in the hash.
+ # * <tt>:parent</tt>: a hash. The node's parent must match the
+ # corresponding hash.
+ # * <tt>:child</tt>: a hash. At least one of the node's immediate children
+ # must meet the criteria described by the hash.
+ # * <tt>:ancestor</tt>: a hash. At least one of the node's ancestors must
+ # meet the criteria described by the hash.
+ # * <tt>:descendant</tt>: a hash. At least one of the node's descendants
+ # must meet the criteria described by the hash.
+ # * <tt>:sibling</tt>: a hash. At least one of the node's siblings must
+ # meet the criteria described by the hash.
+ # * <tt>:after</tt>: a hash. The node must be after any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:before</tt>: a hash. The node must be before any sibling meeting
+ # the criteria described by the hash, and at least one sibling must match.
+ # * <tt>:children</tt>: a hash, for counting children of a node. Accepts the
+ # keys:
+ # ** <tt>:count</tt>: either a number or a range which must equal (or
+ # include) the number of children that match.
+ # ** <tt>:less_than</tt>: the number of matching children must be less than
+ # this number.
+ # ** <tt>:greater_than</tt>: the number of matching children must be
+ # greater than this number.
+ # ** <tt>:only</tt>: another hash consisting of the keys to use
+ # to match on the children, and only matching children will be
+ # counted.
+ #
+ # Conditions are matched using the following algorithm:
+ #
+ # * if the condition is a string, it must be a substring of the value.
+ # * if the condition is a regexp, it must match the value.
+ # * if the condition is a number, the value must match number.to_s.
+ # * if the condition is +true+, the value must not be +nil+.
+ # * if the condition is +false+ or +nil+, the value must be +nil+.
+ #
+ # Usage:
+ #
+ # # test if the node is a "span" tag
+ # node.match tag: "span"
+ #
+ # # test if the node's parent is a "div"
+ # node.match parent: { tag: "div" }
+ #
+ # # test if any of the node's ancestors are "table" tags
+ # node.match ancestor: { tag: "table" }
+ #
+ # # test if any of the node's immediate children are "em" tags
+ # node.match child: { tag: "em" }
+ #
+ # # test if any of the node's descendants are "strong" tags
+ # node.match descendant: { tag: "strong" }
+ #
+ # # test if the node has between 2 and 4 span tags as immediate children
+ # node.match children: { count: 2..4, only: { tag: "span" } }
+ #
+ # # get funky: test to see if the node is a "div", has a "ul" ancestor
+ # # and an "li" parent (with "class" = "enum"), and whether or not it has
+ # # a "span" descendant that contains # text matching /hello world/:
+ # node.match tag: "div",
+ # ancestor: { tag: "ul" },
+ # parent: { tag: "li",
+ # attributes: { class: "enum" } },
+ # descendant: { tag: "span",
+ # child: /hello world/ }
+ def match(conditions)
+ conditions = validate_conditions(conditions)
+ # check content of child nodes
+ if conditions[:content]
+ if children.empty?
+ return false unless match_condition("", conditions[:content])
+ else
+ return false unless children.find { |child| child.match(conditions[:content]) }
+ end
+ end
+
+ # test the name
+ return false unless match_condition(@name, conditions[:tag]) if conditions[:tag]
+
+ # test attributes
+ (conditions[:attributes] || {}).each do |key, value|
+ return false unless match_condition(self[key], value)
+ end
+
+ # test parent
+ return false unless parent.match(conditions[:parent]) if conditions[:parent]
+
+ # test children
+ return false unless children.find { |child| child.match(conditions[:child]) } if conditions[:child]
+
+ # test ancestors
+ if conditions[:ancestor]
+ return false unless catch :found do
+ p = self
+ throw :found, true if p.match(conditions[:ancestor]) while p = p.parent
+ end
+ end
+
+ # test descendants
+ if conditions[:descendant]
+ return false unless children.find do |child|
+ # test the child
+ child.match(conditions[:descendant]) ||
+ # test the child's descendants
+ child.match(:descendant => conditions[:descendant])
+ end
+ end
+
+ # count children
+ if opts = conditions[:children]
+ matches = children.select do |c|
+ (c.kind_of?(HTML::Tag) and (c.closing == :self or ! c.childless?))
+ end
+
+ matches = matches.select { |c| c.match(opts[:only]) } if opts[:only]
+ opts.each do |key, value|
+ next if key == :only
+ case key
+ when :count
+ if Integer === value
+ return false if matches.length != value
+ else
+ return false unless value.include?(matches.length)
+ end
+ when :less_than
+ return false unless matches.length < value
+ when :greater_than
+ return false unless matches.length > value
+ else raise "unknown count condition #{key}"
+ end
+ end
+ end
+
+ # test siblings
+ if conditions[:sibling] || conditions[:before] || conditions[:after]
+ siblings = parent ? parent.children : []
+ self_index = siblings.index(self)
+
+ if conditions[:sibling]
+ return false unless siblings.detect do |s|
+ s != self && s.match(conditions[:sibling])
+ end
+ end
+
+ if conditions[:before]
+ return false unless siblings[self_index+1..-1].detect do |s|
+ s != self && s.match(conditions[:before])
+ end
+ end
+
+ if conditions[:after]
+ return false unless siblings[0,self_index].detect do |s|
+ s != self && s.match(conditions[:after])
+ end
+ end
+ end
+
+ true
+ end
+
+ def ==(node)
+ return false unless super
+ return false unless closing == node.closing && self.name == node.name
+ attributes == node.attributes
+ end
+
+ private
+ # Match the given value to the given condition.
+ def match_condition(value, condition)
+ case condition
+ when String
+ value && value == condition
+ when Regexp
+ value && value.match(condition)
+ when Numeric
+ value == condition.to_s
+ when true
+ !value.nil?
+ when false, nil
+ value.nil?
+ else
+ false
+ end
+ end
+ end
+end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
new file mode 100644
index 0000000000..30b6b8b141
--- /dev/null
+++ b/actionview/lib/action_view/vendor/html-scanner/html/sanitizer.rb
@@ -0,0 +1,188 @@
+require 'set'
+require 'cgi'
+require 'active_support/core_ext/class/attribute_accessors'
+
+module HTML
+ class Sanitizer
+ def sanitize(text, options = {})
+ validate_options(options)
+ return text unless sanitizeable?(text)
+ tokenize(text, options).join
+ end
+
+ def sanitizeable?(text)
+ !(text.nil? || text.empty? || !text.index("<"))
+ end
+
+ protected
+ def tokenize(text, options)
+ tokenizer = HTML::Tokenizer.new(text)
+ result = []
+ while token = tokenizer.next
+ node = Node.parse(nil, 0, 0, token, false)
+ process_node node, result, options
+ end
+ result
+ end
+
+ def process_node(node, result, options)
+ result << node.to_s
+ end
+
+ def validate_options(options)
+ if options[:tags] && !options[:tags].is_a?(Enumerable)
+ raise ArgumentError, "You should pass :tags as an Enumerable"
+ end
+
+ if options[:attributes] && !options[:attributes].is_a?(Enumerable)
+ raise ArgumentError, "You should pass :attributes as an Enumerable"
+ end
+ end
+ end
+
+ class FullSanitizer < Sanitizer
+ def sanitize(text, options = {})
+ result = super
+ # strip any comments, and if they have a newline at the end (ie. line with
+ # only a comment) strip that too
+ result = result.gsub(/<!--(.*?)-->[\n]?/m, "") if (result && result =~ /<!--(.*?)-->[\n]?/m)
+ # Recurse - handle all dirty nested tags
+ result == text ? result : sanitize(result, options)
+ end
+
+ def process_node(node, result, options)
+ result << node.to_s if node.class == HTML::Text
+ end
+ end
+
+ class LinkSanitizer < FullSanitizer
+ cattr_accessor :included_tags, :instance_writer => false
+ self.included_tags = Set.new(%w(a href))
+
+ def sanitizeable?(text)
+ !(text.nil? || text.empty? || !((text.index("<a") || text.index("<href")) && text.index(">")))
+ end
+
+ protected
+ def process_node(node, result, options)
+ result << node.to_s unless node.is_a?(HTML::Tag) && included_tags.include?(node.name)
+ end
+ end
+
+ class WhiteListSanitizer < Sanitizer
+ [:protocol_separator, :uri_attributes, :allowed_attributes, :allowed_tags, :allowed_protocols, :bad_tags,
+ :allowed_css_properties, :allowed_css_keywords, :shorthand_css_properties].each do |attr|
+ class_attribute attr, :instance_writer => false
+ end
+
+ # A regular expression of the valid characters used to separate protocols like
+ # the ':' in 'http://foo.com'
+ self.protocol_separator = /:|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i
+
+ # Specifies a Set of HTML attributes that can have URIs.
+ self.uri_attributes = Set.new(%w(href src cite action longdesc xlink:href lowsrc))
+
+ # Specifies a Set of 'bad' tags that the #sanitize helper will remove completely, as opposed
+ # to just escaping harmless tags like &lt;font&gt;
+ self.bad_tags = Set.new(%w(script))
+
+ # Specifies the default Set of tags that the #sanitize helper will allow unscathed.
+ self.allowed_tags = Set.new(%w(strong em b i p code pre tt samp kbd var sub
+ sup dfn cite big small address hr br div span h1 h2 h3 h4 h5 h6 ul ol li dl dt dd abbr
+ acronym a img blockquote del ins))
+
+ # Specifies the default Set of html attributes that the #sanitize helper will leave
+ # in the allowed tag.
+ self.allowed_attributes = Set.new(%w(href src width height alt cite datetime title class name xml:lang abbr))
+
+ # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
+ self.allowed_protocols = Set.new(%w(ed2k ftp http https irc mailto news gopher nntp telnet webcal xmpp callto
+ feed svn urn aim rsync tag ssh sftp rtsp afs))
+
+ # Specifies the default Set of acceptable css properties that #sanitize and #sanitize_css will accept.
+ self.allowed_css_properties = Set.new(%w(azimuth background-color border-bottom-color border-collapse
+ border-color border-left-color border-right-color border-top-color clear color cursor direction display
+ elevation float font font-family font-size font-style font-variant font-weight height letter-spacing line-height
+ overflow pause pause-after pause-before pitch pitch-range richness speak speak-header speak-numeral speak-punctuation
+ speech-rate stress text-align text-decoration text-indent unicode-bidi vertical-align voice-family volume white-space
+ width))
+
+ # Specifies the default Set of acceptable css keywords that #sanitize and #sanitize_css will accept.
+ self.allowed_css_keywords = Set.new(%w(auto aqua black block blue bold both bottom brown center
+ collapse dashed dotted fuchsia gray green !important italic left lime maroon medium none navy normal
+ nowrap olive pointer purple red right solid silver teal top transparent underline white yellow))
+
+ # Specifies the default Set of allowed shorthand css properties for the #sanitize and #sanitize_css helpers.
+ self.shorthand_css_properties = Set.new(%w(background border margin padding))
+
+ # Sanitizes a block of css code. Used by #sanitize when it comes across a style attribute
+ def sanitize_css(style)
+ # disallow urls
+ style = style.to_s.gsub(/url\s*\(\s*[^\s)]+?\s*\)\s*/, ' ')
+
+ # gauntlet
+ if style !~ /\A([:,;#%.\sa-zA-Z0-9!]|\w-\w|\'[\s\w]+\'|\"[\s\w]+\"|\([\d,\s]+\))*\z/ ||
+ style !~ /\A(\s*[-\w]+\s*:\s*[^:;]*(;|$)\s*)*\z/
+ return ''
+ end
+
+ clean = []
+ style.scan(/([-\w]+)\s*:\s*([^:;]*)/) do |prop,val|
+ if allowed_css_properties.include?(prop.downcase)
+ clean << prop + ': ' + val + ';'
+ elsif shorthand_css_properties.include?(prop.split('-')[0].downcase)
+ unless val.split().any? do |keyword|
+ !allowed_css_keywords.include?(keyword) &&
+ keyword !~ /\A(#[0-9a-f]+|rgb\(\d+%?,\d*%?,?\d*%?\)?|\d{0,2}\.?\d{0,2}(cm|em|ex|in|mm|pc|pt|px|%|,|\))?)\z/
+ end
+ clean << prop + ': ' + val + ';'
+ end
+ end
+ end
+ clean.join(' ')
+ end
+
+ protected
+ def tokenize(text, options)
+ options[:parent] = []
+ options[:attributes] ||= allowed_attributes
+ options[:tags] ||= allowed_tags
+ super
+ end
+
+ def process_node(node, result, options)
+ result << case node
+ when HTML::Tag
+ if node.closing == :close
+ options[:parent].shift
+ else
+ options[:parent].unshift node.name
+ end
+
+ process_attributes_for node, options
+
+ options[:tags].include?(node.name) ? node : nil
+ else
+ bad_tags.include?(options[:parent].first) ? nil : node.to_s.gsub(/</, "&lt;")
+ end
+ end
+
+ def process_attributes_for(node, options)
+ return unless node.attributes
+ node.attributes.keys.each do |attr_name|
+ value = node.attributes[attr_name].to_s
+
+ if !options[:attributes].include?(attr_name) || contains_bad_protocols?(attr_name, value)
+ node.attributes.delete(attr_name)
+ else
+ node.attributes[attr_name] = attr_name == 'style' ? sanitize_css(value) : CGI::escapeHTML(CGI::unescapeHTML(value))
+ end
+ end
+ end
+
+ def contains_bad_protocols?(attr_name, value)
+ uri_attributes.include?(attr_name) &&
+ (value =~ /(^[^\/:]*):|(&#0*58)|(&#x70)|(&#x0*3a)|(%|&#37;)3A/i && !allowed_protocols.include?(value.split(protocol_separator).first.downcase.strip))
+ end
+ end
+end
diff --git a/actionview/lib/action_view/vendor/html-scanner/html/selector.rb b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
new file mode 100644
index 0000000000..7f8609c408
--- /dev/null
+++ b/actionview/lib/action_view/vendor/html-scanner/html/selector.rb
@@ -0,0 +1,830 @@
+#--
+# Copyright (c) 2006 Assaf Arkin (http://labnotes.org)
+# Under MIT and/or CC By license.
+#++
+
+module HTML
+
+ # Selects HTML elements using CSS 2 selectors.
+ #
+ # The +Selector+ class uses CSS selector expressions to match and select
+ # HTML elements.
+ #
+ # For example:
+ # selector = HTML::Selector.new "form.login[action=/login]"
+ # creates a new selector that matches any +form+ element with the class
+ # +login+ and an attribute +action+ with the value <tt>/login</tt>.
+ #
+ # === Matching Elements
+ #
+ # Use the #match method to determine if an element matches the selector.
+ #
+ # For simple selectors, the method returns an array with that element,
+ # or +nil+ if the element does not match. For complex selectors (see below)
+ # the method returns an array with all matched elements, of +nil+ if no
+ # match found.
+ #
+ # For example:
+ # if selector.match(element)
+ # puts "Element is a login form"
+ # end
+ #
+ # === Selecting Elements
+ #
+ # Use the #select method to select all matching elements starting with
+ # one element and going through all children in depth-first order.
+ #
+ # This method returns an array of all matching elements, an empty array
+ # if no match is found
+ #
+ # For example:
+ # selector = HTML::Selector.new "input[type=text]"
+ # matches = selector.select(element)
+ # matches.each do |match|
+ # puts "Found text field with name #{match.attributes['name']}"
+ # end
+ #
+ # === Expressions
+ #
+ # Selectors can match elements using any of the following criteria:
+ # * <tt>name</tt> -- Match an element based on its name (tag name).
+ # For example, <tt>p</tt> to match a paragraph. You can use <tt>*</tt>
+ # to match any element.
+ # * <tt>#</tt><tt>id</tt> -- Match an element based on its identifier (the
+ # <tt>id</tt> attribute). For example, <tt>#</tt><tt>page</tt>.
+ # * <tt>.class</tt> -- Match an element based on its class name, all
+ # class names if more than one specified.
+ # * <tt>[attr]</tt> -- Match an element that has the specified attribute.
+ # * <tt>[attr=value]</tt> -- Match an element that has the specified
+ # attribute and value. (More operators are supported see below)
+ # * <tt>:pseudo-class</tt> -- Match an element based on a pseudo class,
+ # such as <tt>:nth-child</tt> and <tt>:empty</tt>.
+ # * <tt>:not(expr)</tt> -- Match an element that does not match the
+ # negation expression.
+ #
+ # When using a combination of the above, the element name comes first
+ # followed by identifier, class names, attributes, pseudo classes and
+ # negation in any order. Do not separate these parts with spaces!
+ # Space separation is used for descendant selectors.
+ #
+ # For example:
+ # selector = HTML::Selector.new "form.login[action=/login]"
+ # The matched element must be of type +form+ and have the class +login+.
+ # It may have other classes, but the class +login+ is required to match.
+ # It must also have an attribute called +action+ with the value
+ # <tt>/login</tt>.
+ #
+ # This selector will match the following element:
+ # <form class="login form" method="post" action="/login">
+ # but will not match the element:
+ # <form method="post" action="/logout">
+ #
+ # === Attribute Values
+ #
+ # Several operators are supported for matching attributes:
+ # * <tt>name</tt> -- The element must have an attribute with that name.
+ # * <tt>name=value</tt> -- The element must have an attribute with that
+ # name and value.
+ # * <tt>name^=value</tt> -- The attribute value must start with the
+ # specified value.
+ # * <tt>name$=value</tt> -- The attribute value must end with the
+ # specified value.
+ # * <tt>name*=value</tt> -- The attribute value must contain the
+ # specified value.
+ # * <tt>name~=word</tt> -- The attribute value must contain the specified
+ # word (space separated).
+ # * <tt>name|=word</tt> -- The attribute value must start with specified
+ # word.
+ #
+ # For example, the following two selectors match the same element:
+ # #my_id
+ # [id=my_id]
+ # and so do the following two selectors:
+ # .my_class
+ # [class~=my_class]
+ #
+ # === Alternatives, siblings, children
+ #
+ # Complex selectors use a combination of expressions to match elements:
+ # * <tt>expr1 expr2</tt> -- Match any element against the second expression
+ # if it has some parent element that matches the first expression.
+ # * <tt>expr1 > expr2</tt> -- Match any element against the second expression
+ # if it is the child of an element that matches the first expression.
+ # * <tt>expr1 + expr2</tt> -- Match any element against the second expression
+ # if it immediately follows an element that matches the first expression.
+ # * <tt>expr1 ~ expr2</tt> -- Match any element against the second expression
+ # that comes after an element that matches the first expression.
+ # * <tt>expr1, expr2</tt> -- Match any element against the first expression,
+ # or against the second expression.
+ #
+ # Since children and sibling selectors may match more than one element given
+ # the first element, the #match method may return more than one match.
+ #
+ # === Pseudo classes
+ #
+ # Pseudo classes were introduced in CSS 3. They are most often used to select
+ # elements in a given position:
+ # * <tt>:root</tt> -- Match the element only if it is the root element
+ # (no parent element).
+ # * <tt>:empty</tt> -- Match the element only if it has no child elements,
+ # and no text content.
+ # * <tt>:content(string)</tt> -- Match the element only if it has <tt>string</tt>
+ # as its text content (ignoring leading and trailing whitespace).
+ # * <tt>:only-child</tt> -- Match the element if it is the only child (element)
+ # of its parent element.
+ # * <tt>:only-of-type</tt> -- Match the element if it is the only child (element)
+ # of its parent element and its type.
+ # * <tt>:first-child</tt> -- Match the element if it is the first child (element)
+ # of its parent element.
+ # * <tt>:first-of-type</tt> -- Match the element if it is the first child (element)
+ # of its parent element of its type.
+ # * <tt>:last-child</tt> -- Match the element if it is the last child (element)
+ # of its parent element.
+ # * <tt>:last-of-type</tt> -- Match the element if it is the last child (element)
+ # of its parent element of its type.
+ # * <tt>:nth-child(b)</tt> -- Match the element if it is the b-th child (element)
+ # of its parent element. The value <tt>b</tt> specifies its index, starting with 1.
+ # * <tt>:nth-child(an+b)</tt> -- Match the element if it is the b-th child (element)
+ # in each group of <tt>a</tt> child elements of its parent element.
+ # * <tt>:nth-child(-an+b)</tt> -- Match the element if it is the first child (element)
+ # in each group of <tt>a</tt> child elements, up to the first <tt>b</tt> child
+ # elements of its parent element.
+ # * <tt>:nth-child(odd)</tt> -- Match element in the odd position (i.e. first, third).
+ # Same as <tt>:nth-child(2n+1)</tt>.
+ # * <tt>:nth-child(even)</tt> -- Match element in the even position (i.e. second,
+ # fourth). Same as <tt>:nth-child(2n+2)</tt>.
+ # * <tt>:nth-of-type(..)</tt> -- As above, but only counts elements of its type.
+ # * <tt>:nth-last-child(..)</tt> -- As above, but counts from the last child.
+ # * <tt>:nth-last-of-type(..)</tt> -- As above, but counts from the last child and
+ # only elements of its type.
+ # * <tt>:not(selector)</tt> -- Match the element only if the element does not
+ # match the simple selector.
+ #
+ # As you can see, <tt>:nth-child</tt> pseudo class and its variant can get quite
+ # tricky and the CSS specification doesn't do a much better job explaining it.
+ # But after reading the examples and trying a few combinations, it's easy to
+ # figure out.
+ #
+ # For example:
+ # table tr:nth-child(odd)
+ # Selects every second row in the table starting with the first one.
+ #
+ # div p:nth-child(4)
+ # Selects the fourth paragraph in the +div+, but not if the +div+ contains
+ # other elements, since those are also counted.
+ #
+ # div p:nth-of-type(4)
+ # Selects the fourth paragraph in the +div+, counting only paragraphs, and
+ # ignoring all other elements.
+ #
+ # div p:nth-of-type(-n+4)
+ # Selects the first four paragraphs, ignoring all others.
+ #
+ # And you can always select an element that matches one set of rules but
+ # not another using <tt>:not</tt>. For example:
+ # p:not(.post)
+ # Matches all paragraphs that do not have the class <tt>.post</tt>.
+ #
+ # === Substitution Values
+ #
+ # You can use substitution with identifiers, class names and element values.
+ # A substitution takes the form of a question mark (<tt>?</tt>) and uses the
+ # next value in the argument list following the CSS expression.
+ #
+ # The substitution value may be a string or a regular expression. All other
+ # values are converted to strings.
+ #
+ # For example:
+ # selector = HTML::Selector.new "#?", /^\d+$/
+ # matches any element whose identifier consists of one or more digits.
+ #
+ # See http://www.w3.org/TR/css3-selectors/
+ class Selector
+
+
+ # An invalid selector.
+ class InvalidSelectorError < StandardError #:nodoc:
+ end
+
+
+ class << self
+
+ # :call-seq:
+ # Selector.for_class(cls) => selector
+ #
+ # Creates a new selector for the given class name.
+ def for_class(cls)
+ self.new([".?", cls])
+ end
+
+
+ # :call-seq:
+ # Selector.for_id(id) => selector
+ #
+ # Creates a new selector for the given id.
+ def for_id(id)
+ self.new(["#?", id])
+ end
+
+ end
+
+
+ # :call-seq:
+ # Selector.new(string, [values ...]) => selector
+ #
+ # Creates a new selector from a CSS 2 selector expression.
+ #
+ # The first argument is the selector expression. All other arguments
+ # are used for value substitution.
+ #
+ # Throws InvalidSelectorError is the selector expression is invalid.
+ def initialize(selector, *values)
+ raise ArgumentError, "CSS expression cannot be empty" if selector.empty?
+ @source = ""
+ values = values[0] if values.size == 1 && values[0].is_a?(Array)
+
+ # We need a copy to determine if we failed to parse, and also
+ # preserve the original pass by-ref statement.
+ statement = selector.strip.dup
+
+ # Create a simple selector, along with negation.
+ simple_selector(statement, values).each { |name, value| instance_variable_set("@#{name}", value) }
+
+ @alternates = []
+ @depends = nil
+
+ # Alternative selector.
+ if statement.sub!(/^\s*,\s*/, "")
+ second = Selector.new(statement, values)
+ @alternates << second
+ # If there are alternate selectors, we group them in the top selector.
+ if alternates = second.instance_variable_get(:@alternates)
+ second.instance_variable_set(:@alternates, [])
+ @alternates.concat alternates
+ end
+ @source << " , " << second.to_s
+ # Sibling selector: create a dependency into second selector that will
+ # match element immediately following this one.
+ elsif statement.sub!(/^\s*\+\s*/, "")
+ second = next_selector(statement, values)
+ @depends = lambda do |element, first|
+ if element = next_element(element)
+ second.match(element, first)
+ end
+ end
+ @source << " + " << second.to_s
+ # Adjacent selector: create a dependency into second selector that will
+ # match all elements following this one.
+ elsif statement.sub!(/^\s*~\s*/, "")
+ second = next_selector(statement, values)
+ @depends = lambda do |element, first|
+ matches = []
+ while element = next_element(element)
+ if subset = second.match(element, first)
+ if first && !subset.empty?
+ matches << subset.first
+ break
+ else
+ matches.concat subset
+ end
+ end
+ end
+ matches.empty? ? nil : matches
+ end
+ @source << " ~ " << second.to_s
+ # Child selector: create a dependency into second selector that will
+ # match a child element of this one.
+ elsif statement.sub!(/^\s*>\s*/, "")
+ second = next_selector(statement, values)
+ @depends = lambda do |element, first|
+ matches = []
+ element.children.each do |child|
+ if child.tag? && subset = second.match(child, first)
+ if first && !subset.empty?
+ matches << subset.first
+ break
+ else
+ matches.concat subset
+ end
+ end
+ end
+ matches.empty? ? nil : matches
+ end
+ @source << " > " << second.to_s
+ # Descendant selector: create a dependency into second selector that
+ # will match all descendant elements of this one. Note,
+ elsif statement =~ /^\s+\S+/ && statement != selector
+ second = next_selector(statement, values)
+ @depends = lambda do |element, first|
+ matches = []
+ stack = element.children.reverse
+ while node = stack.pop
+ next unless node.tag?
+ if subset = second.match(node, first)
+ if first && !subset.empty?
+ matches << subset.first
+ break
+ else
+ matches.concat subset
+ end
+ elsif children = node.children
+ stack.concat children.reverse
+ end
+ end
+ matches.empty? ? nil : matches
+ end
+ @source << " " << second.to_s
+ else
+ # The last selector is where we check that we parsed
+ # all the parts.
+ unless statement.empty? || statement.strip.empty?
+ raise ArgumentError, "Invalid selector: #{statement}"
+ end
+ end
+ end
+
+
+ # :call-seq:
+ # match(element, first?) => array or nil
+ #
+ # Matches an element against the selector.
+ #
+ # For a simple selector this method returns an array with the
+ # element if the element matches, nil otherwise.
+ #
+ # For a complex selector (sibling and descendant) this method
+ # returns an array with all matching elements, nil if no match is
+ # found.
+ #
+ # Use +first_only=true+ if you are only interested in the first element.
+ #
+ # For example:
+ # if selector.match(element)
+ # puts "Element is a login form"
+ # end
+ def match(element, first_only = false)
+ # Match element if no element name or element name same as element name
+ if matched = (!@tag_name || @tag_name == element.name)
+ # No match if one of the attribute matches failed
+ for attr in @attributes
+ if element.attributes[attr[0]] !~ attr[1]
+ matched = false
+ break
+ end
+ end
+ end
+
+ # Pseudo class matches (nth-child, empty, etc).
+ if matched
+ for pseudo in @pseudo
+ unless pseudo.call(element)
+ matched = false
+ break
+ end
+ end
+ end
+
+ # Negation. Same rules as above, but we fail if a match is made.
+ if matched && @negation
+ for negation in @negation
+ if negation[:tag_name] == element.name
+ matched = false
+ else
+ for attr in negation[:attributes]
+ if element.attributes[attr[0]] =~ attr[1]
+ matched = false
+ break
+ end
+ end
+ end
+ if matched
+ for pseudo in negation[:pseudo]
+ if pseudo.call(element)
+ matched = false
+ break
+ end
+ end
+ end
+ break unless matched
+ end
+ end
+
+ # If element matched but depends on another element (child,
+ # sibling, etc), apply the dependent matches instead.
+ if matched && @depends
+ matches = @depends.call(element, first_only)
+ else
+ matches = matched ? [element] : nil
+ end
+
+ # If this selector is part of the group, try all the alternative
+ # selectors (unless first_only).
+ if !first_only || !matches
+ @alternates.each do |alternate|
+ break if matches && first_only
+ if subset = alternate.match(element, first_only)
+ if matches
+ matches.concat subset
+ else
+ matches = subset
+ end
+ end
+ end
+ end
+
+ matches
+ end
+
+
+ # :call-seq:
+ # select(root) => array
+ #
+ # Selects and returns an array with all matching elements, beginning
+ # with one node and traversing through all children depth-first.
+ # Returns an empty array if no match is found.
+ #
+ # The root node may be any element in the document, or the document
+ # itself.
+ #
+ # For example:
+ # selector = HTML::Selector.new "input[type=text]"
+ # matches = selector.select(element)
+ # matches.each do |match|
+ # puts "Found text field with name #{match.attributes['name']}"
+ # end
+ def select(root)
+ matches = []
+ stack = [root]
+ while node = stack.pop
+ if node.tag? && subset = match(node, false)
+ subset.each do |match|
+ matches << match unless matches.any? { |item| item.equal?(match) }
+ end
+ elsif children = node.children
+ stack.concat children.reverse
+ end
+ end
+ matches
+ end
+
+
+ # Similar to #select but returns the first matching element. Returns +nil+
+ # if no element matches the selector.
+ def select_first(root)
+ stack = [root]
+ while node = stack.pop
+ if node.tag? && subset = match(node, true)
+ return subset.first if !subset.empty?
+ elsif children = node.children
+ stack.concat children.reverse
+ end
+ end
+ nil
+ end
+
+
+ def to_s #:nodoc:
+ @source
+ end
+
+
+ # Return the next element after this one. Skips sibling text nodes.
+ #
+ # With the +name+ argument, returns the next element with that name,
+ # skipping other sibling elements.
+ def next_element(element, name = nil)
+ if siblings = element.parent.children
+ found = false
+ siblings.each do |node|
+ if node.equal?(element)
+ found = true
+ elsif found && node.tag?
+ return node if (name.nil? || node.name == name)
+ end
+ end
+ end
+ nil
+ end
+
+
+ protected
+
+
+ # Creates a simple selector given the statement and array of
+ # substitution values.
+ #
+ # Returns a hash with the values +tag_name+, +attributes+,
+ # +pseudo+ (classes) and +negation+.
+ #
+ # Called the first time with +can_negate+ true to allow
+ # negation. Called a second time with false since negation
+ # cannot be negated.
+ def simple_selector(statement, values, can_negate = true)
+ tag_name = nil
+ attributes = []
+ pseudo = []
+ negation = []
+
+ # Element name. (Note that in negation, this can come at
+ # any order, but for simplicity we allow if only first).
+ statement.sub!(/^(\*|[[:alpha:]][\w\-]*)/) do |match|
+ match.strip!
+ tag_name = match.downcase unless match == "*"
+ @source << match
+ "" # Remove
+ end
+
+ # Get identifier, class, attribute name, pseudo or negation.
+ while true
+ # Element identifier.
+ next if statement.sub!(/^#(\?|[\w\-]+)/) do
+ id = $1
+ if id == "?"
+ id = values.shift
+ end
+ @source << "##{id}"
+ id = Regexp.new("^#{Regexp.escape(id.to_s)}$") unless id.is_a?(Regexp)
+ attributes << ["id", id]
+ "" # Remove
+ end
+
+ # Class name.
+ next if statement.sub!(/^\.([\w\-]+)/) do
+ class_name = $1
+ @source << ".#{class_name}"
+ class_name = Regexp.new("(^|\s)#{Regexp.escape(class_name)}($|\s)") unless class_name.is_a?(Regexp)
+ attributes << ["class", class_name]
+ "" # Remove
+ end
+
+ # Attribute value.
+ next if statement.sub!(/^\[\s*([[:alpha:]][\w\-:]*)\s*((?:[~|^$*])?=)?\s*('[^']*'|"[^*]"|[^\]]*)\s*\]/) do
+ name, equality, value = $1, $2, $3
+ if value == "?"
+ value = values.shift
+ else
+ # Handle single and double quotes.
+ value.strip!
+ if (value[0] == ?" || value[0] == ?') && value[0] == value[-1]
+ value = value[1..-2]
+ end
+ end
+ @source << "[#{name}#{equality}'#{value}']"
+ attributes << [name.downcase.strip, attribute_match(equality, value)]
+ "" # Remove
+ end
+
+ # Root element only.
+ next if statement.sub!(/^:root/) do
+ pseudo << lambda do |element|
+ element.parent.nil? || !element.parent.tag?
+ end
+ @source << ":root"
+ "" # Remove
+ end
+
+ # Nth-child including last and of-type.
+ next if statement.sub!(/^:nth-(last-)?(child|of-type)\((odd|even|(\d+|\?)|(-?\d*|\?)?n([+\-]\d+|\?)?)\)/) do |match|
+ reverse = $1 == "last-"
+ of_type = $2 == "of-type"
+ @source << ":nth-#{$1}#{$2}("
+ case $3
+ when "odd"
+ pseudo << nth_child(2, 1, of_type, reverse)
+ @source << "odd)"
+ when "even"
+ pseudo << nth_child(2, 2, of_type, reverse)
+ @source << "even)"
+ when /^(\d+|\?)$/ # b only
+ b = ($1 == "?" ? values.shift : $1).to_i
+ pseudo << nth_child(0, b, of_type, reverse)
+ @source << "#{b})"
+ when /^(-?\d*|\?)?n([+\-]\d+|\?)?$/
+ a = ($1 == "?" ? values.shift :
+ $1 == "" ? 1 : $1 == "-" ? -1 : $1).to_i
+ b = ($2 == "?" ? values.shift : $2).to_i
+ pseudo << nth_child(a, b, of_type, reverse)
+ @source << (b >= 0 ? "#{a}n+#{b})" : "#{a}n#{b})")
+ else
+ raise ArgumentError, "Invalid nth-child #{match}"
+ end
+ "" # Remove
+ end
+ # First/last child (of type).
+ next if statement.sub!(/^:(first|last)-(child|of-type)/) do
+ reverse = $1 == "last"
+ of_type = $2 == "of-type"
+ pseudo << nth_child(0, 1, of_type, reverse)
+ @source << ":#{$1}-#{$2}"
+ "" # Remove
+ end
+ # Only child (of type).
+ next if statement.sub!(/^:only-(child|of-type)/) do
+ of_type = $1 == "of-type"
+ pseudo << only_child(of_type)
+ @source << ":only-#{$1}"
+ "" # Remove
+ end
+
+ # Empty: no child elements or meaningful content (whitespaces
+ # are ignored).
+ next if statement.sub!(/^:empty/) do
+ pseudo << lambda do |element|
+ empty = true
+ for child in element.children
+ if child.tag? || !child.content.strip.empty?
+ empty = false
+ break
+ end
+ end
+ empty
+ end
+ @source << ":empty"
+ "" # Remove
+ end
+ # Content: match the text content of the element, stripping
+ # leading and trailing spaces.
+ next if statement.sub!(/^:content\(\s*(\?|'[^']*'|"[^"]*"|[^)]*)\s*\)/) do
+ content = $1
+ if content == "?"
+ content = values.shift
+ elsif (content[0] == ?" || content[0] == ?') && content[0] == content[-1]
+ content = content[1..-2]
+ end
+ @source << ":content('#{content}')"
+ content = Regexp.new("^#{Regexp.escape(content.to_s)}$") unless content.is_a?(Regexp)
+ pseudo << lambda do |element|
+ text = ""
+ for child in element.children
+ unless child.tag?
+ text << child.content
+ end
+ end
+ text.strip =~ content
+ end
+ "" # Remove
+ end
+
+ # Negation. Create another simple selector to handle it.
+ if statement.sub!(/^:not\(\s*/, "")
+ raise ArgumentError, "Double negatives are not missing feature" unless can_negate
+ @source << ":not("
+ negation << simple_selector(statement, values, false)
+ raise ArgumentError, "Negation not closed" unless statement.sub!(/^\s*\)/, "")
+ @source << ")"
+ next
+ end
+
+ # No match: moving on.
+ break
+ end
+
+ # Return hash. The keys are mapped to instance variables.
+ {:tag_name=>tag_name, :attributes=>attributes, :pseudo=>pseudo, :negation=>negation}
+ end
+
+
+ # Create a regular expression to match an attribute value based
+ # on the equality operator (=, ^=, |=, etc).
+ def attribute_match(equality, value)
+ regexp = value.is_a?(Regexp) ? value : Regexp.escape(value.to_s)
+ case equality
+ when "=" then
+ # Match the attribute value in full
+ Regexp.new("^#{regexp}$")
+ when "~=" then
+ # Match a space-separated word within the attribute value
+ Regexp.new("(^|\s)#{regexp}($|\s)")
+ when "^="
+ # Match the beginning of the attribute value
+ Regexp.new("^#{regexp}")
+ when "$="
+ # Match the end of the attribute value
+ Regexp.new("#{regexp}$")
+ when "*="
+ # Match substring of the attribute value
+ regexp.is_a?(Regexp) ? regexp : Regexp.new(regexp)
+ when "|=" then
+ # Match the first space-separated item of the attribute value
+ Regexp.new("^#{regexp}($|\s)")
+ else
+ raise InvalidSelectorError, "Invalid operation/value" unless value.empty?
+ # Match all attributes values (existence check)
+ //
+ end
+ end
+
+
+ # Returns a lambda that can match an element against the nth-child
+ # pseudo class, given the following arguments:
+ # * +a+ -- Value of a part.
+ # * +b+ -- Value of b part.
+ # * +of_type+ -- True to test only elements of this type (of-type).
+ # * +reverse+ -- True to count in reverse order (last-).
+ def nth_child(a, b, of_type, reverse)
+ # a = 0 means select at index b, if b = 0 nothing selected
+ return lambda { |element| false } if a == 0 && b == 0
+ # a < 0 and b < 0 will never match against an index
+ return lambda { |element| false } if a < 0 && b < 0
+ b = a + b + 1 if b < 0 # b < 0 just picks last element from each group
+ b -= 1 unless b == 0 # b == 0 is same as b == 1, otherwise zero based
+ lambda do |element|
+ # Element must be inside parent element.
+ return false unless element.parent && element.parent.tag?
+ index = 0
+ # Get siblings, reverse if counting from last.
+ siblings = element.parent.children
+ siblings = siblings.reverse if reverse
+ # Match element name if of-type, otherwise ignore name.
+ name = of_type ? element.name : nil
+ found = false
+ for child in siblings
+ # Skip text nodes/comments.
+ if child.tag? && (name == nil || child.name == name)
+ if a == 0
+ # Shortcut when a == 0 no need to go past count
+ if index == b
+ found = child.equal?(element)
+ break
+ end
+ elsif a < 0
+ # Only look for first b elements
+ break if index > b
+ if child.equal?(element)
+ found = (index % a) == 0
+ break
+ end
+ else
+ # Otherwise, break if child found and count == an+b
+ if child.equal?(element)
+ found = (index % a) == b
+ break
+ end
+ end
+ index += 1
+ end
+ end
+ found
+ end
+ end
+
+
+ # Creates a only child lambda. Pass +of-type+ to only look at
+ # elements of its type.
+ def only_child(of_type)
+ lambda do |element|
+ # Element must be inside parent element.
+ return false unless element.parent && element.parent.tag?
+ name = of_type ? element.name : nil
+ other = false
+ for child in element.parent.children
+ # Skip text nodes/comments.
+ if child.tag? && (name == nil || child.name == name)
+ unless child.equal?(element)
+ other = true
+ break
+ end
+ end
+ end
+ !other
+ end
+ end
+
+
+ # Called to create a dependent selector (sibling, descendant, etc).
+ # Passes the remainder of the statement that will be reduced to zero
+ # eventually, and array of substitution values.
+ #
+ # This method is called from four places, so it helps to put it here
+ # for reuse. The only logic deals with the need to detect comma
+ # separators (alternate) and apply them to the selector group of the
+ # top selector.
+ def next_selector(statement, values)
+ second = Selector.new(statement, values)
+ # If there are alternate selectors, we group them in the top selector.
+ if alternates = second.instance_variable_get(:@alternates)
+ second.instance_variable_set(:@alternates, [])
+ @alternates.concat alternates
+ end
+ second
+ end
+
+ end
+
+
+ # See HTML::Selector.new
+ def self.selector(statement, *values)
+ Selector.new(statement, *values)
+ end
+
+
+ class Tag
+
+ def select(selector, *values)
+ selector = HTML::Selector.new(selector, values)
+ selector.select(self)
+ end
+
+ end
+
+end
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/tokenizer.rb b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
index 8ac8d34430..8ac8d34430 100644
--- a/actionpack/lib/action_view/vendor/html-scanner/html/tokenizer.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/tokenizer.rb
diff --git a/actionpack/lib/action_view/vendor/html-scanner/html/version.rb b/actionview/lib/action_view/vendor/html-scanner/html/version.rb
index 6d645c3e14..6d645c3e14 100644
--- a/actionpack/lib/action_view/vendor/html-scanner/html/version.rb
+++ b/actionview/lib/action_view/vendor/html-scanner/html/version.rb
diff --git a/actionview/lib/action_view/version.rb b/actionview/lib/action_view/version.rb
new file mode 100644
index 0000000000..094dd474df
--- /dev/null
+++ b/actionview/lib/action_view/version.rb
@@ -0,0 +1,11 @@
+module ActionView
+ # Returns the version of the currently loaded ActionView as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
+
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActionView.version.segments
+ STRING = ActionView.version.to_s
+ end
+end
diff --git a/actionview/lib/action_view/view_paths.rb b/actionview/lib/action_view/view_paths.rb
new file mode 100644
index 0000000000..6c349feb1d
--- /dev/null
+++ b/actionview/lib/action_view/view_paths.rb
@@ -0,0 +1,96 @@
+require 'action_view/base'
+
+module ActionView
+ module ViewPaths
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :_view_paths
+ self._view_paths = ActionView::PathSet.new
+ self._view_paths.freeze
+ end
+
+ delegate :template_exists?, :view_paths, :formats, :formats=,
+ :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
+
+ prefixes
+ end
+ 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
+ end
+
+ # LookupContext is the object responsible to hold all information required to lookup
+ # templates, i.e. view paths and details. Check ActionView::LookupContext for more
+ # information.
+ def lookup_context
+ @_lookup_context ||=
+ ActionView::LookupContext.new(self.class._view_paths, details_for_lookup, _prefixes)
+ end
+
+ def details_for_lookup
+ { }
+ end
+
+ def append_view_path(path)
+ lookup_context.view_paths.push(*path)
+ end
+
+ def prepend_view_path(path)
+ lookup_context.view_paths.unshift(*path)
+ end
+
+ module ClassMethods
+ # Append a path to the list of view paths for this controller.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
+ def append_view_path(path)
+ self._view_paths = view_paths + Array(path)
+ end
+
+ # Prepend a path to the list of view paths for this controller.
+ #
+ # ==== Parameters
+ # * <tt>path</tt> - If a String is provided, it gets converted into
+ # the default view path. You may also provide a custom view path
+ # (see ActionView::PathSet for more information)
+ def prepend_view_path(path)
+ self._view_paths = ActionView::PathSet.new(Array(path) + view_paths)
+ end
+
+ # A list of all of the default view paths for this controller.
+ def view_paths
+ _view_paths
+ end
+
+ # Set the view paths.
+ #
+ # ==== Parameters
+ # * <tt>paths</tt> - If a PathSet is provided, use that;
+ # otherwise, process the parameter into a PathSet.
+ def view_paths=(paths)
+ self._view_paths = ActionView::PathSet.new(Array(paths))
+ end
+ end
+ end
+end
diff --git a/actionview/test/abstract_unit.rb b/actionview/test/abstract_unit.rb
new file mode 100644
index 0000000000..6623b47e83
--- /dev/null
+++ b/actionview/test/abstract_unit.rb
@@ -0,0 +1,332 @@
+require File.expand_path('../../../load_paths', __FILE__)
+
+$:.unshift(File.dirname(__FILE__) + '/lib')
+$:.unshift(File.dirname(__FILE__) + '/fixtures/helpers')
+$:.unshift(File.dirname(__FILE__) + '/fixtures/alternate_helpers')
+
+ENV['TMPDIR'] = File.join(File.dirname(__FILE__), 'tmp')
+
+require 'active_support/core_ext/kernel/reporting'
+
+# These are the normal settings that will be set up by Railties
+# TODO: Have these tests support other combinations of these values
+silence_warnings do
+ Encoding.default_internal = "UTF-8"
+ Encoding.default_external = "UTF-8"
+end
+
+require 'active_support/testing/autorun'
+require 'abstract_controller'
+require 'action_controller'
+require 'action_view'
+require 'action_view/testing/resolvers'
+require 'action_dispatch'
+require 'active_support/dependencies'
+require 'active_model'
+require 'active_record'
+
+require 'pp' # require 'pp' early to prevent hidden_methods from not picking up the pretty-print methods until too late
+
+module Rails
+ class << self
+ def env
+ @_env ||= ActiveSupport::StringInquirer.new(ENV["RAILS_ENV"] || ENV["RACK_ENV"] || "test")
+ end
+ end
+end
+
+ActiveSupport::Dependencies.hook!
+
+Thread.abort_on_exception = true
+
+# Show backtraces for deprecated behavior for quicker cleanup.
+ActiveSupport::Deprecation.debug = true
+
+# Register danish language for testing
+I18n.backend.store_translations 'da', {}
+I18n.backend.store_translations 'pt-BR', {}
+ORIGINAL_LOCALES = I18n.available_locales.map {|locale| locale.to_s }.sort
+
+FIXTURE_LOAD_PATH = File.join(File.dirname(__FILE__), 'fixtures')
+FIXTURES = Pathname.new(FIXTURE_LOAD_PATH)
+
+module RackTestUtils
+ def body_to_string(body)
+ if body.respond_to?(:each)
+ str = ""
+ body.each {|s| str << s }
+ str
+ else
+ body
+ end
+ end
+ extend self
+end
+
+module RenderERBUtils
+ def view
+ @view ||= begin
+ path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
+ view_paths = ActionView::PathSet.new([path])
+ ActionView::Base.new(view_paths)
+ end
+ end
+
+ def render_erb(string)
+ @virtual_path = nil
+
+ template = ActionView::Template.new(
+ string.strip,
+ "test template",
+ ActionView::Template::Handlers::ERB,
+ {})
+
+ template.render(self, {}).strip
+ end
+end
+
+SharedTestRoutes = ActionDispatch::Routing::RouteSet.new
+
+module ActionDispatch
+ module SharedRoutes
+ def before_setup
+ @routes = SharedTestRoutes
+ super
+ end
+ end
+
+ # Hold off drawing routes until all the possible controller classes
+ # have been loaded.
+ module DrawOnce
+ class << self
+ attr_accessor :drew
+ end
+ self.drew = false
+
+ def before_setup
+ super
+ return if DrawOnce.drew
+
+ SharedTestRoutes.draw do
+ get ':controller(/:action)'
+ end
+
+ ActionDispatch::IntegrationTest.app.routes.draw do
+ get ':controller(/:action)'
+ end
+
+ DrawOnce.drew = true
+ end
+ end
+end
+
+module ActiveSupport
+ class TestCase
+ include ActionDispatch::DrawOnce
+ end
+end
+
+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
+
+class BasicController
+ attr_accessor :request
+
+ def config
+ @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config).tap do |config|
+ # VIEW TODO: View tests should not require a controller
+ public_dir = File.expand_path("../fixtures/public", __FILE__)
+ config.assets_dir = public_dir
+ config.javascripts_dir = "#{public_dir}/javascripts"
+ config.stylesheets_dir = "#{public_dir}/stylesheets"
+ config.assets = ActiveSupport::InheritableOptions.new({ :prefix => "assets" })
+ config
+ end
+ end
+end
+
+class ActionDispatch::IntegrationTest < ActiveSupport::TestCase
+ include ActionDispatch::SharedRoutes
+
+ def self.build_app(routes = nil)
+ RoutedRackApp.new(routes || ActionDispatch::Routing::RouteSet.new) do |middleware|
+ middleware.use "ActionDispatch::ShowExceptions", ActionDispatch::PublicExceptions.new("#{FIXTURE_LOAD_PATH}/public")
+ middleware.use "ActionDispatch::DebugExceptions"
+ middleware.use "ActionDispatch::Callbacks"
+ middleware.use "ActionDispatch::ParamsParser"
+ middleware.use "ActionDispatch::Cookies"
+ middleware.use "ActionDispatch::Flash"
+ middleware.use "Rack::Head"
+ yield(middleware) if block_given?
+ end
+ end
+
+ self.app = build_app
+
+ # Stub Rails dispatcher so it does not get controller references and
+ # simply return the controller#action as Rack::Body.
+ class StubDispatcher < ::ActionDispatch::Routing::RouteSet::Dispatcher
+ protected
+ def controller_reference(controller_param)
+ controller_param
+ end
+
+ def dispatch(controller, action, env)
+ [200, {'Content-Type' => 'text/html'}, ["#{controller}##{action}"]]
+ end
+ end
+
+ def self.stub_controllers
+ old_dispatcher = ActionDispatch::Routing::RouteSet::Dispatcher
+ ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
+ ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, StubDispatcher }
+ yield ActionDispatch::Routing::RouteSet.new
+ ensure
+ ActionDispatch::Routing::RouteSet.module_eval { remove_const :Dispatcher }
+ ActionDispatch::Routing::RouteSet.module_eval { const_set :Dispatcher, old_dispatcher }
+ end
+
+ def with_routing(&block)
+ temporary_routes = ActionDispatch::Routing::RouteSet.new
+ old_app, self.class.app = self.class.app, self.class.build_app(temporary_routes)
+ old_routes = SharedTestRoutes
+ silence_warnings { Object.const_set(:SharedTestRoutes, temporary_routes) }
+
+ yield temporary_routes
+ ensure
+ self.class.app = old_app
+ silence_warnings { Object.const_set(:SharedTestRoutes, old_routes) }
+ end
+
+ def with_autoload_path(path)
+ path = File.join(File.dirname(__FILE__), "fixtures", path)
+ if ActiveSupport::Dependencies.autoload_paths.include?(path)
+ yield
+ else
+ begin
+ ActiveSupport::Dependencies.autoload_paths << path
+ yield
+ ensure
+ ActiveSupport::Dependencies.autoload_paths.reject! {|p| p == path}
+ ActiveSupport::Dependencies.clear
+ end
+ end
+ end
+end
+
+# Temporary base class
+class Rack::TestCase < ActionDispatch::IntegrationTest
+ def self.testing(klass = nil)
+ if klass
+ @testing = "/#{klass.name.underscore}".sub!(/_controller$/, '')
+ else
+ @testing
+ end
+ end
+
+ def get(thing, *args)
+ if thing.is_a?(Symbol)
+ super("#{self.class.testing}/#{thing}", *args)
+ else
+ super
+ end
+ end
+
+ def assert_body(body)
+ assert_equal body, Array(response.body).join
+ end
+
+ def assert_status(code)
+ assert_equal code, response.status
+ end
+
+ def assert_response(body, status = 200, headers = {})
+ assert_body body
+ assert_status status
+ headers.each do |header, value|
+ assert_header header, value
+ end
+ end
+
+ def assert_content_type(type)
+ assert_equal type, response.headers["Content-Type"]
+ end
+
+ def assert_header(name, value)
+ assert_equal value, response.headers[name]
+ end
+end
+
+# Emulate AV railtie.
+ActionController::Base.superclass.send(:include, ActionView::Layouts)
+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
+
+ self.view_paths = FIXTURE_LOAD_PATH
+
+ def self.test_routes(&block)
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw(&block)
+ include routes.url_helpers
+ end
+ end
+
+ class TestCase
+ include ActionDispatch::TestProcess
+ include ActionDispatch::SharedRoutes
+ end
+end
+
+module ActionView
+ class TestCase
+ # Must repeat the setup because AV::TestCase is a duplication
+ # of AC::TestCase
+ include ActionDispatch::SharedRoutes
+ end
+end
+
+class Workshop
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ attr_accessor :id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def persisted?
+ id.present?
+ end
+
+ def to_s
+ id.to_s
+ end
+end
+
+module ActionDispatch
+ class DebugExceptions
+ private
+ remove_method :stderr_logger
+ # Silence logger
+ def stderr_logger
+ nil
+ end
+ end
+end
+
diff --git a/actionview/test/actionpack/abstract/abstract_controller_test.rb b/actionview/test/actionpack/abstract/abstract_controller_test.rb
new file mode 100644
index 0000000000..40d3b17131
--- /dev/null
+++ b/actionview/test/actionpack/abstract/abstract_controller_test.rb
@@ -0,0 +1,262 @@
+require 'abstract_unit'
+require 'set'
+
+module AbstractController
+ module Testing
+
+ # Test basic dispatching.
+ # ====
+ # * Call process
+ # * Test that the response_body is set correctly
+ class SimpleController < AbstractController::Base
+ end
+
+ class Me < SimpleController
+ def index
+ self.response_body = "Hello world"
+ "Something else"
+ end
+ end
+
+ class TestBasic < ActiveSupport::TestCase
+ test "dispatching works" do
+ controller = Me.new
+ controller.process(:index)
+ assert_equal "Hello world", controller.response_body
+ end
+ end
+
+ # Test Render mixin
+ # ====
+ class RenderingController < AbstractController::Base
+ include AbstractController::Rendering
+ include ActionView::Rendering
+
+ def _prefixes
+ []
+ end
+
+ def render(options = {})
+ if options.is_a?(String)
+ options = {:_template_name => options}
+ end
+ super
+ end
+
+ append_view_path File.expand_path(File.join(File.dirname(__FILE__), "views"))
+ end
+
+ class Me2 < RenderingController
+ def index
+ render "index.erb"
+ end
+
+ def index_to_string
+ self.response_body = render_to_string "index"
+ end
+
+ def action_with_ivars
+ @my_ivar = "Hello"
+ render "action_with_ivars.erb"
+ end
+
+ def naked_render
+ render
+ end
+
+ def rendering_to_body
+ self.response_body = render_to_body :template => "naked_render"
+ end
+
+ def rendering_to_string
+ self.response_body = render_to_string :template => "naked_render"
+ end
+ end
+
+ class TestRenderingController < ActiveSupport::TestCase
+ def setup
+ @controller = Me2.new
+ end
+
+ test "rendering templates works" do
+ @controller.process(:index)
+ assert_equal "Hello from index.erb", @controller.response_body
+ end
+
+ test "render_to_string works with a String as an argument" do
+ @controller.process(:index_to_string)
+ assert_equal "Hello from index.erb", @controller.response_body
+ end
+
+ test "rendering passes ivars to the view" do
+ @controller.process(:action_with_ivars)
+ assert_equal "Hello from index_with_ivars.erb", @controller.response_body
+ end
+
+ test "rendering with no template name" do
+ @controller.process(:naked_render)
+ assert_equal "Hello from naked_render.erb", @controller.response_body
+ end
+
+ test "rendering to a rack body" do
+ @controller.process(:rendering_to_body)
+ assert_equal "Hello from naked_render.erb", @controller.response_body
+ end
+
+ test "rendering to a string" do
+ @controller.process(:rendering_to_string)
+ assert_equal "Hello from naked_render.erb", @controller.response_body
+ end
+ end
+
+ # Test rendering with prefixes
+ # ====
+ # * self._prefix is used when defined
+ class PrefixedViews < RenderingController
+ private
+ def self.prefix
+ name.underscore
+ end
+
+ def _prefixes
+ [self.class.prefix]
+ end
+ end
+
+ class Me3 < PrefixedViews
+ def index
+ render
+ end
+
+ def formatted
+ self.formats = [:html]
+ render
+ end
+ end
+
+ class TestPrefixedViews < ActiveSupport::TestCase
+ def setup
+ @controller = Me3.new
+ end
+
+ test "templates are located inside their 'prefix' folder" do
+ @controller.process(:index)
+ assert_equal "Hello from me3/index.erb", @controller.response_body
+ end
+
+ test "templates included their format" do
+ @controller.process(:formatted)
+ assert_equal "Hello from me3/formatted.html.erb", @controller.response_body
+ end
+ end
+
+ # Test rendering with layouts
+ # ====
+ # self._layout is used when defined
+ class WithLayouts < PrefixedViews
+ include ActionView::Layouts
+
+ private
+ def self.layout(formats)
+ find_template(name.underscore, {:formats => formats}, :_prefixes => ["layouts"])
+ rescue ActionView::MissingTemplate
+ begin
+ find_template("application", {:formats => formats}, :_prefixes => ["layouts"])
+ rescue ActionView::MissingTemplate
+ end
+ end
+
+ def render_to_body(options = {})
+ options[:_layout] = options[:layout] || _default_layout({})
+ super
+ end
+ end
+
+ class Me4 < WithLayouts
+ def index
+ render
+ end
+ end
+
+ class TestLayouts < ActiveSupport::TestCase
+ test "layouts are included" do
+ controller = Me4.new
+ controller.process(:index)
+ assert_equal "Me4 Enter : Hello from me4/index.erb : Exit", controller.response_body
+ end
+ end
+
+ # respond_to_action?(action_name)
+ # ====
+ # * A method can be used as an action only if this method
+ # returns true when passed the method name as an argument
+ # * Defaults to true in AbstractController
+ class DefaultRespondToActionController < AbstractController::Base
+ def index() self.response_body = "success" end
+ end
+
+ class ActionMissingRespondToActionController < AbstractController::Base
+ # No actions
+ private
+ def action_missing(action_name)
+ self.response_body = "success"
+ end
+ end
+
+ class RespondToActionController < AbstractController::Base;
+ def index() self.response_body = "success" end
+
+ def fail() self.response_body = "fail" end
+
+ private
+
+ def method_for_action(action_name)
+ action_name.to_s != "fail" && action_name
+ end
+ end
+
+ class TestRespondToAction < ActiveSupport::TestCase
+
+ def assert_dispatch(klass, body = "success", action = :index)
+ controller = klass.new
+ controller.process(action)
+ assert_equal body, controller.response_body
+ end
+
+ test "an arbitrary method is available as an action by default" do
+ assert_dispatch DefaultRespondToActionController, "success", :index
+ end
+
+ test "raises ActionNotFound when method does not exist and action_missing is not defined" do
+ assert_raise(ActionNotFound) { DefaultRespondToActionController.new.process(:fail) }
+ end
+
+ test "dispatches to action_missing when method does not exist and action_missing is defined" do
+ assert_dispatch ActionMissingRespondToActionController, "success", :ohai
+ end
+
+ test "a method is available as an action if method_for_action returns true" do
+ assert_dispatch RespondToActionController, "success", :index
+ end
+
+ test "raises ActionNotFound if method is defined but method_for_action returns false" do
+ assert_raise(ActionNotFound) { RespondToActionController.new.process(:fail) }
+ end
+ end
+
+ class Me6 < AbstractController::Base
+ self.action_methods
+
+ def index
+ end
+ end
+
+ class TestActionMethodsReloading < ActiveSupport::TestCase
+
+ test "action_methods should be reloaded after defining a new method" do
+ assert_equal Set.new(["index"]), Me6.action_methods
+ end
+ end
+
+ end
+end
diff --git a/actionview/test/actionpack/abstract/helper_test.rb b/actionview/test/actionpack/abstract/helper_test.rb
new file mode 100644
index 0000000000..4e05245584
--- /dev/null
+++ b/actionview/test/actionpack/abstract/helper_test.rb
@@ -0,0 +1,127 @@
+require 'abstract_unit'
+
+ActionController::Base.helpers_path = File.expand_path('../../../fixtures/helpers', __FILE__)
+
+module AbstractController
+ module Testing
+
+ class ControllerWithHelpers < AbstractController::Base
+ include AbstractController::Helpers
+ include AbstractController::Rendering
+ include ActionView::Rendering
+
+ def with_module
+ render :inline => "Module <%= included_method %>"
+ end
+ end
+
+ module HelperyTest
+ def included_method
+ "Included"
+ end
+ end
+
+ class AbstractHelpers < ControllerWithHelpers
+ helper(HelperyTest) do
+ def helpery_test
+ "World"
+ end
+ end
+
+ helper :abc
+
+ def with_block
+ render :inline => "Hello <%= helpery_test %>"
+ end
+
+ def with_symbol
+ render :inline => "I respond to bare_a: <%= respond_to?(:bare_a) %>"
+ end
+ end
+
+ class ::HelperyTestController < AbstractHelpers
+ clear_helpers
+ end
+
+ class AbstractHelpersBlock < ControllerWithHelpers
+ helper do
+ include AbstractController::Testing::HelperyTest
+ end
+ end
+
+ class AbstractInvalidHelpers < AbstractHelpers
+ include ActionController::Helpers
+
+ path = File.join(File.expand_path('../../../fixtures', __FILE__), "helpers_missing")
+ $:.unshift(path)
+ self.helpers_path = path
+ end
+
+ class TestHelpers < ActiveSupport::TestCase
+ def setup
+ @controller = AbstractHelpers.new
+ end
+
+ def test_helpers_with_block
+ @controller.process(:with_block)
+ assert_equal "Hello World", @controller.response_body
+ end
+
+ def test_helpers_with_module
+ @controller.process(:with_module)
+ assert_equal "Module Included", @controller.response_body
+ end
+
+ def test_helpers_with_symbol
+ @controller.process(:with_symbol)
+ assert_equal "I respond to bare_a: true", @controller.response_body
+ end
+
+ def test_declare_missing_helper
+ AbstractHelpers.helper :missing
+ flunk "should have raised an exception"
+ rescue LoadError => e
+ assert_equal "helpers/missing_helper.rb", e.path
+ end
+
+ def test_helpers_with_module_through_block
+ @controller = AbstractHelpersBlock.new
+ @controller.process(:with_module)
+ assert_equal "Module Included", @controller.response_body
+ end
+ end
+
+ class ClearHelpersTest < ActiveSupport::TestCase
+ def setup
+ @controller = HelperyTestController.new
+ end
+
+ def test_clears_up_previous_helpers
+ @controller.process(:with_symbol)
+ assert_equal "I respond to bare_a: false", @controller.response_body
+ end
+
+ def test_includes_controller_default_helper
+ @controller.process(:with_block)
+ assert_equal "Hello Default", @controller.response_body
+ end
+ end
+
+ class InvalidHelpersTest < ActiveSupport::TestCase
+ def test_controller_raise_error_about_real_require_problem
+ e = assert_raise(LoadError) { AbstractInvalidHelpers.helper(:invalid_require) }
+ assert_equal "No such file to load -- very_invalid_file_name", e.message
+ end
+
+ def test_controller_raise_error_about_missing_helper
+ e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) }
+ assert_equal "Missing helper file helpers/missing_helper.rb", e.message
+ end
+
+ def test_missing_helper_error_has_the_right_path
+ e = assert_raise(AbstractController::Helpers::MissingHelperError) { AbstractInvalidHelpers.helper(:missing) }
+ assert_equal "helpers/missing_helper.rb", e.path
+ end
+ end
+ end
+end
diff --git a/actionview/test/actionpack/abstract/layouts_test.rb b/actionview/test/actionpack/abstract/layouts_test.rb
new file mode 100644
index 0000000000..c79cb50fd7
--- /dev/null
+++ b/actionview/test/actionpack/abstract/layouts_test.rb
@@ -0,0 +1,384 @@
+require 'abstract_unit'
+
+module AbstractControllerTests
+ module Layouts
+
+ # Base controller for these tests
+ class Base < AbstractController::Base
+ include AbstractController::Rendering
+ include ActionView::Rendering
+ include ActionView::Layouts
+
+ abstract!
+
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "layouts/hello.erb" => "With String <%= yield %>",
+ "layouts/hello_override.erb" => "With Override <%= yield %>",
+ "layouts/overwrite.erb" => "Overwrite <%= yield %>",
+ "layouts/with_false_layout.erb" => "False Layout <%= yield %>",
+ "abstract_controller_tests/layouts/with_string_implied_child.erb" =>
+ "With Implied <%= yield %>",
+ "abstract_controller_tests/layouts/with_grand_child_of_implied.erb" =>
+ "With Grand Child <%= yield %>"
+
+ )]
+ end
+
+ class Blank < Base
+ self.view_paths = []
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello blank!")
+ end
+ end
+
+ class WithString < Base
+ layout "hello"
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello string!")
+ end
+
+ def overwrite_default
+ render :template => ActionView::Template::Text.new("Hello string!"), :layout => :default
+ end
+
+ def overwrite_false
+ render :template => ActionView::Template::Text.new("Hello string!"), :layout => false
+ end
+
+ def overwrite_string
+ render :template => ActionView::Template::Text.new("Hello string!"), :layout => "overwrite"
+ end
+
+ def overwrite_skip
+ render :text => "Hello text!"
+ end
+ end
+
+ class WithStringChild < WithString
+ end
+
+ class WithStringOverriddenChild < WithString
+ layout "hello_override"
+ end
+
+ class WithStringImpliedChild < WithString
+ layout nil
+ end
+
+ class WithChildOfImplied < WithStringImpliedChild
+ end
+
+ class WithGrandChildOfImplied < WithStringImpliedChild
+ layout nil
+ end
+
+ class WithProc < Base
+ layout proc { "overwrite" }
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello proc!")
+ end
+ end
+
+ class WithProcReturningNil < Base
+ layout proc { nil }
+
+ def index
+ render template: ActionView::Template::Text.new("Hello nil!")
+ end
+ end
+
+ class WithZeroArityProc < Base
+ layout proc { "overwrite" }
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello zero arity proc!")
+ end
+ end
+
+ class WithProcInContextOfInstance < Base
+ def an_instance_method; end
+
+ layout proc {
+ break unless respond_to? :an_instance_method
+ "overwrite"
+ }
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello again zero arity proc!")
+ end
+ end
+
+ class WithSymbol < Base
+ layout :hello
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello symbol!")
+ end
+ private
+ def hello
+ "overwrite"
+ end
+ end
+
+ class WithSymbolReturningNil < Base
+ layout :nilz
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello nilz!")
+ end
+
+ def nilz() end
+ end
+
+ class WithSymbolReturningObj < Base
+ layout :objekt
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello nilz!")
+ end
+
+ def objekt
+ Object.new
+ end
+ end
+
+ class WithSymbolAndNoMethod < Base
+ layout :no_method
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello boom!")
+ end
+ end
+
+ class WithMissingLayout < Base
+ layout "missing"
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello missing!")
+ end
+ end
+
+ class WithFalseLayout < Base
+ layout false
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello false!")
+ end
+ end
+
+ class WithNilLayout < Base
+ layout nil
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello nil!")
+ end
+ end
+
+ class WithOnlyConditional < WithStringImpliedChild
+ layout "overwrite", :only => :show
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello index!")
+ end
+
+ def show
+ render :template => ActionView::Template::Text.new("Hello show!")
+ end
+ end
+
+ class WithExceptConditional < WithStringImpliedChild
+ layout "overwrite", :except => :show
+
+ def index
+ render :template => ActionView::Template::Text.new("Hello index!")
+ end
+
+ def show
+ render :template => ActionView::Template::Text.new("Hello show!")
+ end
+ end
+
+ class TestBase < ActiveSupport::TestCase
+ test "when no layout is specified, and no default is available, render without a layout" do
+ controller = Blank.new
+ controller.process(:index)
+ assert_equal "Hello blank!", controller.response_body
+ end
+
+ test "when layout is specified as a string, render with that layout" do
+ controller = WithString.new
+ controller.process(:index)
+ assert_equal "With String Hello string!", controller.response_body
+ end
+
+ test "when layout is overwriten by :default in render, render default layout" do
+ controller = WithString.new
+ controller.process(:overwrite_default)
+ assert_equal "With String Hello string!", controller.response_body
+ end
+
+ test "when layout is overwriten by string in render, render new layout" do
+ controller = WithString.new
+ controller.process(:overwrite_string)
+ assert_equal "Overwrite Hello string!", controller.response_body
+ end
+
+ test "when layout is overwriten by false in render, render no layout" do
+ controller = WithString.new
+ controller.process(:overwrite_false)
+ assert_equal "Hello string!", controller.response_body
+ end
+
+ test "when text is rendered, render no layout" do
+ controller = WithString.new
+ controller.process(:overwrite_skip)
+ assert_equal "Hello text!", controller.response_body
+ end
+
+ test "when layout is specified as a string, but the layout is missing, raise an exception" do
+ assert_raises(ActionView::MissingTemplate) { WithMissingLayout.new.process(:index) }
+ end
+
+ test "when layout is specified as false, do not use a layout" do
+ controller = WithFalseLayout.new
+ controller.process(:index)
+ assert_equal "Hello false!", controller.response_body
+ end
+
+ test "when layout is specified as nil, do not use a layout" do
+ controller = WithNilLayout.new
+ controller.process(:index)
+ assert_equal "Hello nil!", controller.response_body
+ end
+
+ test "when layout is specified as a proc, do not leak any methods into controller's action_methods" do
+ assert_equal Set.new(['index']), WithProc.action_methods
+ end
+
+ test "when layout is specified as a proc, call it and use the layout returned" do
+ controller = WithProc.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello proc!", controller.response_body
+ end
+
+ test "when layout is specified as a proc and the proc retuns nil, don't use a layout" do
+ controller = WithProcReturningNil.new
+ controller.process(:index)
+ assert_equal "Hello nil!", controller.response_body
+ end
+
+ test "when layout is specified as a proc without parameters it works just the same" do
+ controller = WithZeroArityProc.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello zero arity proc!", controller.response_body
+ end
+
+ test "when layout is specified as a proc without parameters the block is evaluated in the context of an instance" do
+ controller = WithProcInContextOfInstance.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello again zero arity proc!", controller.response_body
+ end
+
+ test "when layout is specified as a symbol, call the requested method and use the layout returned" do
+ controller = WithSymbol.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello symbol!", controller.response_body
+ end
+
+ test "when layout is specified as a symbol and the method returns nil, don't use a layout" do
+ controller = WithSymbolReturningNil.new
+ controller.process(:index)
+ assert_equal "Hello nilz!", controller.response_body
+ end
+
+ test "when the layout is specified as a symbol and the method doesn't exist, raise an exception" do
+ assert_raises(NameError) { WithSymbolAndNoMethod.new.process(:index) }
+ end
+
+ test "when the layout is specified as a symbol and the method returns something besides a string/false/nil, raise an exception" do
+ assert_raises(ArgumentError) { WithSymbolReturningObj.new.process(:index) }
+ end
+
+ test "when a child controller does not have a layout, use the parent controller layout" do
+ controller = WithStringChild.new
+ controller.process(:index)
+ assert_equal "With String Hello string!", controller.response_body
+ end
+
+ test "when a child controller has specified a layout, use that layout and not the parent controller layout" do
+ controller = WithStringOverriddenChild.new
+ controller.process(:index)
+ assert_equal "With Override Hello string!", controller.response_body
+ end
+
+ test "when a child controller has an implied layout, use that layout and not the parent controller layout" do
+ controller = WithStringImpliedChild.new
+ controller.process(:index)
+ assert_equal "With Implied Hello string!", controller.response_body
+ end
+
+ test "when a grandchild has no layout specified, the child has an implied layout, and the " \
+ "parent has specified a layout, use the child controller layout" do
+ controller = WithChildOfImplied.new
+ controller.process(:index)
+ assert_equal "With Implied Hello string!", controller.response_body
+ end
+
+ test "when a grandchild has nil layout specified, the child has an implied layout, and the " \
+ "parent has specified a layout, use the child controller layout" do
+ controller = WithGrandChildOfImplied.new
+ controller.process(:index)
+ assert_equal "With Grand Child Hello string!", controller.response_body
+ end
+
+ test "raises an exception when specifying layout true" do
+ assert_raises ArgumentError do
+ Object.class_eval do
+ class ::BadFailLayout < AbstractControllerTests::Layouts::Base
+ layout true
+ end
+ end
+ end
+ end
+
+ test "when specify an :only option which match current action name" do
+ controller = WithOnlyConditional.new
+ controller.process(:show)
+ assert_equal "Overwrite Hello show!", controller.response_body
+ end
+
+ test "when specify an :only option which does not match current action name" do
+ controller = WithOnlyConditional.new
+ controller.process(:index)
+ assert_equal "With Implied Hello index!", controller.response_body
+ end
+
+ test "when specify an :except option which match current action name" do
+ controller = WithExceptConditional.new
+ controller.process(:show)
+ assert_equal "With Implied Hello show!", controller.response_body
+ end
+
+ test "when specify an :except option which does not match current action name" do
+ controller = WithExceptConditional.new
+ controller.process(:index)
+ assert_equal "Overwrite Hello index!", controller.response_body
+ end
+
+ test "layout for anonymous controller" do
+ klass = Class.new(WithString) do
+ def index
+ render :text => 'index', :layout => true
+ end
+ end
+
+ controller = klass.new
+ controller.process(:index)
+ assert_equal "With String index", controller.response_body
+ end
+ end
+ end
+end
diff --git a/actionview/test/actionpack/abstract/render_test.rb b/actionview/test/actionpack/abstract/render_test.rb
new file mode 100644
index 0000000000..f9d8c916d9
--- /dev/null
+++ b/actionview/test/actionpack/abstract/render_test.rb
@@ -0,0 +1,103 @@
+require 'abstract_unit'
+
+module AbstractController
+ module Testing
+
+ class ControllerRenderer < AbstractController::Base
+ include AbstractController::Rendering
+ include ActionView::Rendering
+
+ def _prefixes
+ %w[renderer]
+ end
+
+ self.view_paths = [ActionView::FixtureResolver.new(
+ "template.erb" => "With Template",
+ "renderer/default.erb" => "With Default",
+ "renderer/string.erb" => "With String",
+ "renderer/symbol.erb" => "With Symbol",
+ "string/with_path.erb" => "With String With Path",
+ "some/file.erb" => "With File"
+ )]
+
+ def template
+ render :template => "template"
+ end
+
+ def file
+ render :file => "some/file"
+ end
+
+ def inline
+ render :inline => "With <%= :Inline %>"
+ end
+
+ def text
+ render :text => "With Text"
+ end
+
+ def default
+ render
+ end
+
+ def string
+ render "string"
+ end
+
+ def string_with_path
+ render "string/with_path"
+ end
+
+ def symbol
+ render :symbol
+ end
+ end
+
+ class TestRenderer < ActiveSupport::TestCase
+
+ def setup
+ @controller = ControllerRenderer.new
+ end
+
+ def test_render_template
+ @controller.process(:template)
+ assert_equal "With Template", @controller.response_body
+ end
+
+ def test_render_file
+ @controller.process(:file)
+ assert_equal "With File", @controller.response_body
+ end
+
+ def test_render_inline
+ @controller.process(:inline)
+ assert_equal "With Inline", @controller.response_body
+ end
+
+ def test_render_text
+ @controller.process(:text)
+ assert_equal "With Text", @controller.response_body
+ end
+
+ def test_render_default
+ @controller.process(:default)
+ assert_equal "With Default", @controller.response_body
+ end
+
+ def test_render_string
+ @controller.process(:string)
+ assert_equal "With String", @controller.response_body
+ end
+
+ def test_render_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.response_body
+ end
+ end
+ end
+end
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb
index 785bf69191..785bf69191 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me3/formatted.html.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/formatted.html.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb
index f079ad8204..f079ad8204 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me3/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me3/index.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb
index 89dce12bdc..89dce12bdc 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me4/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me4/index.erb
diff --git a/actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
index 84d0b7417e..84d0b7417e 100644
--- a/actionpack/test/abstract/views/abstract_controller/testing/me5/index.erb
+++ b/actionview/test/actionpack/abstract/views/abstract_controller/testing/me5/index.erb
diff --git a/actionpack/test/abstract/views/action_with_ivars.erb b/actionview/test/actionpack/abstract/views/action_with_ivars.erb
index 8d8ae22fd7..8d8ae22fd7 100644
--- a/actionpack/test/abstract/views/action_with_ivars.erb
+++ b/actionview/test/actionpack/abstract/views/action_with_ivars.erb
diff --git a/actionpack/test/abstract/views/helper_test.erb b/actionview/test/actionpack/abstract/views/helper_test.erb
index 8ae45cc195..8ae45cc195 100644
--- a/actionpack/test/abstract/views/helper_test.erb
+++ b/actionview/test/actionpack/abstract/views/helper_test.erb
diff --git a/actionpack/test/abstract/views/index.erb b/actionview/test/actionpack/abstract/views/index.erb
index cc1a8b8c85..cc1a8b8c85 100644
--- a/actionpack/test/abstract/views/index.erb
+++ b/actionview/test/actionpack/abstract/views/index.erb
diff --git a/actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb b/actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb
index 172dd56569..172dd56569 100644
--- a/actionpack/test/abstract/views/layouts/abstract_controller/testing/me4.erb
+++ b/actionview/test/actionpack/abstract/views/layouts/abstract_controller/testing/me4.erb
diff --git a/actionpack/test/abstract/views/layouts/application.erb b/actionview/test/actionpack/abstract/views/layouts/application.erb
index 27317140ad..27317140ad 100644
--- a/actionpack/test/abstract/views/layouts/application.erb
+++ b/actionview/test/actionpack/abstract/views/layouts/application.erb
diff --git a/actionpack/test/abstract/views/naked_render.erb b/actionview/test/actionpack/abstract/views/naked_render.erb
index 1b3d03878b..1b3d03878b 100644
--- a/actionpack/test/abstract/views/naked_render.erb
+++ b/actionview/test/actionpack/abstract/views/naked_render.erb
diff --git a/actionview/test/actionpack/controller/capture_test.rb b/actionview/test/actionpack/controller/capture_test.rb
new file mode 100644
index 0000000000..f8387b27b0
--- /dev/null
+++ b/actionview/test/actionpack/controller/capture_test.rb
@@ -0,0 +1,81 @@
+require 'abstract_unit'
+require 'active_support/logger'
+
+class CaptureController < ActionController::Base
+ self.view_paths = [ File.dirname(__FILE__) + '/../../fixtures/actionpack' ]
+
+ def self.controller_name; "test"; end
+ def self.controller_path; "test"; end
+
+ def content_for
+ @title = nil
+ render :layout => "talk_from_action"
+ end
+
+ def content_for_with_parameter
+ @title = nil
+ render :layout => "talk_from_action"
+ end
+
+ def content_for_concatenated
+ @title = nil
+ render :layout => "talk_from_action"
+ end
+
+ def non_erb_block_content_for
+ @title = nil
+ render :layout => "talk_from_action"
+ end
+
+ def proper_block_detection
+ @todo = "some todo"
+ end
+end
+
+class CaptureTest < ActionController::TestCase
+ tests CaptureController
+
+ def setup
+ super
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ @controller.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_simple_capture
+ get :capturing
+ assert_equal "Dreamy days", @response.body.strip
+ end
+
+ def test_content_for
+ get :content_for
+ assert_equal expected_content_for_output, @response.body
+ end
+
+ def test_should_concatentate_content_for
+ get :content_for_concatenated
+ assert_equal expected_content_for_output, @response.body
+ end
+
+ def test_should_set_content_for_with_parameter
+ get :content_for_with_parameter
+ assert_equal expected_content_for_output, @response.body
+ end
+
+ def test_non_erb_block_content_for
+ get :non_erb_block_content_for
+ assert_equal expected_content_for_output, @response.body
+ end
+
+ def test_proper_block_detection
+ get :proper_block_detection
+ assert_equal "some todo", @response.body
+ end
+
+ private
+ def expected_content_for_output
+ "<title>Putting stuff in the title!</title>\nGreat stuff!"
+ end
+end
diff --git a/actionview/test/actionpack/controller/layout_test.rb b/actionview/test/actionpack/controller/layout_test.rb
new file mode 100644
index 0000000000..5dd23c4b31
--- /dev/null
+++ b/actionview/test/actionpack/controller/layout_test.rb
@@ -0,0 +1,262 @@
+require 'abstract_unit'
+require 'rbconfig'
+require 'active_support/core_ext/array/extract_options'
+
+# The view_paths array must be set on Base and not LayoutTest so that LayoutTest's inherited
+# 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
+ def self.controller_path; 'views' end
+ def self._implied_layout_name; to_s.underscore.gsub(/_controller$/, '') ; end
+ self.view_paths = ActionController::Base.view_paths.dup
+end
+
+# Restore view_paths to previous value
+ActionController::Base.view_paths = old_load_paths
+
+class ProductController < LayoutTest
+end
+
+class ItemController < LayoutTest
+end
+
+class ThirdPartyTemplateLibraryController < LayoutTest
+end
+
+module ControllerNameSpace
+end
+
+class ControllerNameSpace::NestedController < LayoutTest
+end
+
+class MultipleExtensions < LayoutTest
+end
+
+class LayoutAutoDiscoveryTest < ActionController::TestCase
+ def setup
+ super
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_application_layout_is_default_when_no_controller_match
+ @controller = ProductController.new
+ get :hello
+ assert_equal 'layout_test.erb hello.erb', @response.body
+ end
+
+ def test_controller_name_layout_name_match
+ @controller = ItemController.new
+ get :hello
+ assert_equal 'item.erb hello.erb', @response.body
+ 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
+ end
+
+ def test_namespaced_controllers_auto_detect_layouts1
+ @controller = ControllerNameSpace::NestedController.new
+ get :hello
+ assert_equal 'controller_name_space/nested.erb hello.erb', @response.body
+ end
+
+ def test_namespaced_controllers_auto_detect_layouts2
+ @controller = MultipleExtensions.new
+ get :hello
+ assert_equal 'multiple_extensions.html.erb hello.erb', @response.body.strip
+ end
+end
+
+class DefaultLayoutController < LayoutTest
+end
+
+class StreamingLayoutController < LayoutTest
+ def render(*args)
+ options = args.extract_options!
+ super(*args, options.merge(:stream => true))
+ end
+end
+
+class AbsolutePathLayoutController < LayoutTest
+ layout File.expand_path(File.expand_path(__FILE__) + '/../../../fixtures/actionpack/layout_tests/layouts/layout_test')
+end
+
+class HasOwnLayoutController < LayoutTest
+ layout 'item'
+end
+
+class HasNilLayoutSymbol < LayoutTest
+ layout :nilz
+
+ def nilz
+ nil
+ end
+end
+
+class HasNilLayoutProc < LayoutTest
+ layout proc { nil }
+end
+
+class PrependsViewPathController < LayoutTest
+ def hello
+ prepend_view_path File.dirname(__FILE__) + '/../../fixtures/actionpack/layout_tests/alt/'
+ render :layout => 'alt'
+ end
+end
+
+class OnlyLayoutController < LayoutTest
+ layout 'item', :only => "hello"
+end
+
+class ExceptLayoutController < LayoutTest
+ layout 'item', :except => "goodbye"
+end
+
+class SetsLayoutInRenderController < LayoutTest
+ def hello
+ render :layout => 'third_party_template_library'
+ end
+end
+
+class RendersNoLayoutController < LayoutTest
+ def hello
+ render :layout => false
+ end
+end
+
+class LayoutSetInResponseTest < ActionController::TestCase
+ include ActionView::Template::Handlers
+
+ def test_layout_set_when_using_default_layout
+ @controller = DefaultLayoutController.new
+ get :hello
+ assert_template :layout => "layouts/layout_test"
+ end
+
+ def test_layout_set_when_using_streaming_layout
+ @controller = StreamingLayoutController.new
+ get :hello
+ assert_template :hello
+ end
+
+ def test_layout_set_when_set_in_controller
+ @controller = HasOwnLayoutController.new
+ get :hello
+ assert_template :layout => "layouts/item"
+ end
+
+ def test_layout_symbol_set_in_controller_returning_nil_falls_back_to_default
+ @controller = HasNilLayoutSymbol.new
+ get :hello
+ assert_template layout: "layouts/layout_test"
+ end
+
+ def test_layout_proc_set_in_controller_returning_nil_falls_back_to_default
+ @controller = HasNilLayoutProc.new
+ get :hello
+ assert_template layout: "layouts/layout_test"
+ end
+
+ def test_layout_only_exception_when_included
+ @controller = OnlyLayoutController.new
+ get :hello
+ assert_template :layout => "layouts/item"
+ end
+
+ def test_layout_only_exception_when_excepted
+ @controller = OnlyLayoutController.new
+ get :goodbye
+ assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
+ end
+
+ def test_layout_except_exception_when_included
+ @controller = ExceptLayoutController.new
+ get :hello
+ assert_template :layout => "layouts/item"
+ end
+
+ def test_layout_except_exception_when_excepted
+ @controller = ExceptLayoutController.new
+ get :goodbye
+ assert !@response.body.include?("item.erb"), "#{@response.body.inspect} included 'item.erb'"
+ end
+
+ def test_layout_set_when_using_render
+ @controller = SetsLayoutInRenderController.new
+ get :hello
+ assert_template :layout => "layouts/third_party_template_library"
+ end
+
+ def test_layout_is_not_set_when_none_rendered
+ @controller = RendersNoLayoutController.new
+ get :hello
+ assert_template :layout => nil
+ end
+
+ def test_layout_is_picked_from_the_controller_instances_view_path
+ @controller = PrependsViewPathController.new
+ get :hello
+ assert_template :layout => /layouts\/alt/
+ end
+
+ def test_absolute_pathed_layout
+ @controller = AbsolutePathLayoutController.new
+ get :hello
+ assert_equal "layout_test.erb hello.erb", @response.body.strip
+ end
+end
+
+class RenderWithTemplateOptionController < LayoutTest
+ def hello
+ render :template => 'alt/hello'
+ end
+end
+
+class SetsNonExistentLayoutFile < LayoutTest
+ layout "nofile"
+end
+
+class LayoutExceptionRaisedTest < ActionController::TestCase
+ def test_exception_raised_when_layout_file_not_found
+ @controller = SetsNonExistentLayoutFile.new
+ assert_raise(ActionView::MissingTemplate) { get :hello }
+ end
+end
+
+class LayoutStatusIsRendered < LayoutTest
+ def hello
+ render :status => 401
+ end
+end
+
+class LayoutStatusIsRenderedTest < ActionController::TestCase
+ def test_layout_status_is_rendered
+ @controller = LayoutStatusIsRendered.new
+ get :hello
+ assert_response 401
+ end
+end
+
+unless RbConfig::CONFIG['host_os'] =~ /mswin|mingw/
+ class LayoutSymlinkedTest < LayoutTest
+ layout "symlinked/symlinked_layout"
+ end
+
+ class LayoutSymlinkedIsRenderedTest < ActionController::TestCase
+ def test_symlinked_layout_is_rendered
+ @controller = LayoutSymlinkedTest.new
+ get :hello
+ assert_response 200
+ assert_template :layout => "layouts/symlinked/symlinked_layout"
+ end
+ end
+end
diff --git a/actionview/test/actionpack/controller/render_test.rb b/actionview/test/actionpack/controller/render_test.rb
new file mode 100644
index 0000000000..964dccbffb
--- /dev/null
+++ b/actionview/test/actionpack/controller/render_test.rb
@@ -0,0 +1,1337 @@
+require 'abstract_unit'
+require "active_model"
+
+class ApplicationController < ActionController::Base
+ self.view_paths = File.join(FIXTURE_LOAD_PATH, "actionpack")
+end
+
+class Customer < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ undef_method :to_json
+
+ def to_xml(options={})
+ if options[:builder]
+ options[:builder].name name
+ else
+ "<name>#{name}</name>"
+ end
+ end
+
+ def to_js(options={})
+ "name: #{name.inspect}"
+ end
+ alias :to_text :to_js
+
+ def errors
+ []
+ end
+
+ def persisted?
+ id.present?
+ end
+end
+
+module Quiz
+ #Models
+ class Question < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?
+ id.present?
+ end
+ end
+
+ class Store < Question; end
+
+ # Controller
+ class QuestionsController < ApplicationController
+ def new
+ render :partial => Quiz::Question.new("Namespaced Partial")
+ end
+ end
+end
+
+class BadCustomer < Customer; end
+class GoodCustomer < Customer; end
+
+module Fun
+ class GamesController < ApplicationController
+ def hello_world; end
+
+ def nested_partial_with_form_builder
+ render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ end
+ end
+end
+
+class TestController < ApplicationController
+ protect_from_forgery
+
+ before_action :set_variable_for_layout
+
+ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
+ end
+
+ layout :determine_layout
+
+ def name
+ nil
+ end
+
+ private :name
+ helper_method :name
+
+ def hello_world
+ end
+
+ def hello_world_file
+ render :file => File.expand_path("../../../fixtures/actionpack/hello", __FILE__), :formats => [:html]
+ end
+
+ # :ported:
+ def render_hello_world
+ render :template => "test/hello_world"
+ end
+
+ def render_hello_world_with_last_modified_set
+ response.last_modified = Date.new(2008, 10, 10).to_time
+ render :template => "test/hello_world"
+ end
+
+ # :ported: compatibility
+ def render_hello_world_with_forward_slash
+ render :template => "/test/hello_world"
+ end
+
+ # :ported:
+ def render_template_in_top_directory
+ render :template => 'shared'
+ end
+
+ # :deprecated:
+ def render_template_in_top_directory_with_slash
+ render :template => '/shared'
+ end
+
+ # :ported:
+ def render_hello_world_from_variable
+ @person = "david"
+ render :text => "hello #{@person}"
+ end
+
+ # :ported:
+ def render_action_hello_world
+ render :action => "hello_world"
+ end
+
+ def render_action_upcased_hello_world
+ render :action => "Hello_world"
+ end
+
+ def render_action_hello_world_as_string
+ render "hello_world"
+ end
+
+ def render_action_hello_world_with_symbol
+ render :action => :hello_world
+ end
+
+ # :ported:
+ def render_text_hello_world
+ render :text => "hello world"
+ end
+
+ # :ported:
+ def render_text_hello_world_with_layout
+ @variable_for_layout = ", I am here!"
+ render :text => "hello world", :layout => true
+ end
+
+ def hello_world_with_layout_false
+ render :layout => false
+ end
+
+ # :ported:
+ def render_file_with_instance_variables
+ @secret = 'in the sauce'
+ path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar')
+ render :file => path
+ end
+
+ # :ported:
+ def render_file_as_string_with_instance_variables
+ @secret = 'in the sauce'
+ path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar'))
+ render path
+ end
+
+ # :ported:
+ def render_file_not_using_full_path
+ @secret = 'in the sauce'
+ render :file => 'test/render_file_with_ivar'
+ end
+
+ def render_file_not_using_full_path_with_dot_in_path
+ @secret = 'in the sauce'
+ render :file => 'test/dot.directory/render_file_with_ivar'
+ end
+
+ def render_file_using_pathname
+ @secret = 'in the sauce'
+ render :file => Pathname.new(File.dirname(__FILE__)).join('..', '..', 'fixtures', 'test', 'dot.directory', 'render_file_with_ivar')
+ end
+
+ def render_file_from_template
+ @secret = 'in the sauce'
+ @path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_ivar'))
+ end
+
+ def render_file_with_locals
+ path = File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals')
+ render :file => path, :locals => {:secret => 'in the sauce'}
+ end
+
+ def render_file_as_string_with_locals
+ path = File.expand_path(File.join(File.dirname(__FILE__), '../../fixtures/test/render_file_with_locals'))
+ render path, :locals => {:secret => 'in the sauce'}
+ end
+
+ def accessing_request_in_template
+ render :inline => "Hello: <%= request.host %>"
+ end
+
+ def accessing_logger_in_template
+ render :inline => "<%= logger.class %>"
+ end
+
+ def accessing_action_name_in_template
+ render :inline => "<%= action_name %>"
+ end
+
+ def accessing_controller_name_in_template
+ render :inline => "<%= controller_name %>"
+ end
+
+ # :ported:
+ def render_custom_code
+ render :text => "hello world", :status => 404
+ end
+
+ # :ported:
+ def render_text_with_nil
+ render :text => nil
+ end
+
+ # :ported:
+ def render_text_with_false
+ render :text => false
+ end
+
+ def render_text_with_resource
+ render :text => Customer.new("David")
+ end
+
+ # :ported:
+ def render_nothing_with_appendix
+ render :text => "appended"
+ end
+
+ # This test is testing 3 things:
+ # render :file in AV :ported:
+ # render :template in AC :ported:
+ # setting content type
+ def render_xml_hello
+ @name = "David"
+ render :template => "test/hello"
+ end
+
+ def render_xml_hello_as_string_template
+ @name = "David"
+ render "test/hello"
+ end
+
+ def render_line_offset
+ render :inline => '<% raise %>', :locals => {:foo => 'bar'}
+ end
+
+ def heading
+ head :ok
+ end
+
+ def greeting
+ # let's just rely on the template
+ end
+
+ # :ported:
+ def blank_response
+ render :text => ' '
+ end
+
+ # :ported:
+ def layout_test
+ render :action => "hello_world"
+ end
+
+ # :ported:
+ def builder_layout_test
+ @name = nil
+ render :action => "hello", :layout => "layouts/builder"
+ end
+
+ # :move: test this in Action View
+ def builder_partial_test
+ render :action => "hello_world_container"
+ end
+
+ # :ported:
+ def partials_list
+ @test_unchanged = 'hello'
+ @customers = [ Customer.new("david"), Customer.new("mary") ]
+ render :action => "list"
+ end
+
+ def partial_only
+ render :partial => true
+ end
+
+ def hello_in_a_string
+ @customers = [ Customer.new("david"), Customer.new("mary") ]
+ render :text => "How's there? " + render_to_string(:template => "test/list")
+ end
+
+ def accessing_params_in_template
+ render :inline => "Hello: <%= params[:name] %>"
+ end
+
+ def accessing_local_assigns_in_inline_template
+ name = params[:local_name]
+ render :inline => "<%= 'Goodbye, ' + local_name %>",
+ :locals => { :local_name => name }
+ end
+
+ def render_implicit_html_template_from_xhr_request
+ end
+
+ def render_implicit_js_template_without_layout
+ end
+
+ def formatted_html_erb
+ end
+
+ def formatted_xml_erb
+ end
+
+ def render_to_string_test
+ @foo = render_to_string :inline => "this is a test"
+ end
+
+ def default_render
+ @alternate_default_render ||= nil
+ if @alternate_default_render
+ @alternate_default_render.call
+ else
+ super
+ end
+ end
+
+ def render_action_hello_world_as_symbol
+ render :action => :hello_world
+ end
+
+ def layout_test_with_different_layout
+ render :action => "hello_world", :layout => "standard"
+ end
+
+ def layout_test_with_different_layout_and_string_action
+ render "hello_world", :layout => "standard"
+ end
+
+ def layout_test_with_different_layout_and_symbol_action
+ render :hello_world, :layout => "standard"
+ end
+
+ def rendering_without_layout
+ render :action => "hello_world", :layout => false
+ end
+
+ def layout_overriding_layout
+ render :action => "hello_world", :layout => "standard"
+ end
+
+ def rendering_nothing_on_layout
+ render :nothing => true
+ end
+
+ def render_to_string_with_assigns
+ @before = "i'm before the render"
+ render_to_string :text => "foo"
+ @after = "i'm after the render"
+ render :template => "test/hello_world"
+ end
+
+ def render_to_string_with_exception
+ render_to_string :file => "exception that will not be caught - this will certainly not work"
+ end
+
+ def render_to_string_with_caught_exception
+ @before = "i'm before the render"
+ begin
+ render_to_string :file => "exception that will be caught- hope my future instance vars still work!"
+ rescue
+ end
+ @after = "i'm after the render"
+ render :template => "test/hello_world"
+ end
+
+ def accessing_params_in_template_with_layout
+ render :layout => true, :inline => "Hello: <%= params[:name] %>"
+ end
+
+ # :ported:
+ def render_with_explicit_template
+ render :template => "test/hello_world"
+ end
+
+ def render_with_explicit_unescaped_template
+ render :template => "test/h*llo_world"
+ end
+
+ def render_with_explicit_escaped_template
+ render :template => "test/hello,world"
+ end
+
+ def render_with_explicit_string_template
+ render "test/hello_world"
+ end
+
+ # :ported:
+ def render_with_explicit_template_with_locals
+ render :template => "test/render_file_with_locals", :locals => { :secret => 'area51' }
+ end
+
+ # :ported:
+ def double_render
+ render :text => "hello"
+ render :text => "world"
+ end
+
+ def double_redirect
+ redirect_to :action => "double_render"
+ redirect_to :action => "double_render"
+ end
+
+ def render_and_redirect
+ render :text => "hello"
+ redirect_to :action => "double_render"
+ end
+
+ def render_to_string_and_render
+ @stuff = render_to_string :text => "here is some cached stuff"
+ render :text => "Hi web users! #{@stuff}"
+ end
+
+ def render_to_string_with_inline_and_render
+ render_to_string :inline => "<%= 'dlrow olleh'.reverse %>"
+ render :template => "test/hello_world"
+ end
+
+ def rendering_with_conflicting_local_vars
+ @name = "David"
+ render :action => "potential_conflicts"
+ end
+
+ def hello_world_from_rxml_using_action
+ render :action => "hello_world_from_rxml", :handlers => [:builder]
+ end
+
+ # :deprecated:
+ def hello_world_from_rxml_using_template
+ render :template => "test/hello_world_from_rxml", :handlers => [:builder]
+ end
+
+ def action_talk_to_layout
+ # Action template sets variable that's picked up by layout
+ end
+
+ # :addressed:
+ def render_text_with_assigns
+ @hello = "world"
+ render :text => "foo"
+ end
+
+ def yield_content_for
+ render :action => "content_for", :layout => "yield"
+ end
+
+ def render_content_type_from_body
+ response.content_type = Mime::RSS
+ render :text => "hello world!"
+ end
+
+ def render_using_layout_around_block
+ render :action => "using_layout_around_block"
+ end
+
+ def render_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ render :action => "using_layout_around_block", :layout => "layouts/block_with_layout"
+ end
+
+ def partial_formats_html
+ render :partial => 'partial', :formats => [:html]
+ end
+
+ def partial
+ render :partial => 'partial'
+ end
+
+ def partial_html_erb
+ render :partial => 'partial_html_erb'
+ end
+
+ def render_to_string_with_partial
+ @partial_only = render_to_string :partial => "partial_only"
+ @partial_with_locals = render_to_string :partial => "customer", :locals => { :customer => Customer.new("david") }
+ render :template => "test/hello_world"
+ end
+
+ def render_to_string_with_template_and_html_partial
+ @text = render_to_string :template => "test/with_partial", :formats => [:text]
+ @html = render_to_string :template => "test/with_partial", :formats => [:html]
+ render :template => "test/with_html_partial"
+ end
+
+ def render_to_string_and_render_with_different_formats
+ @html = render_to_string :template => "test/with_partial", :formats => [:html]
+ render :template => "test/with_partial", :formats => [:text]
+ end
+
+ def render_template_within_a_template_with_other_format
+ render :template => "test/with_xml_template",
+ :formats => [:html],
+ :layout => "with_html_partial"
+ end
+
+ def partial_with_counter
+ render :partial => "counter", :locals => { :counter_counter => 5 }
+ end
+
+ def partial_with_locals
+ render :partial => "customer", :locals => { :customer => Customer.new("david") }
+ end
+
+ def partial_with_form_builder
+ render :partial => ActionView::Helpers::FormBuilder.new(:post, nil, view_context, {})
+ end
+
+ def partial_with_form_builder_subclass
+ render :partial => LabellingFormBuilder.new(:post, nil, view_context, {})
+ end
+
+ def partial_collection
+ render :partial => "customer", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_as
+ render :partial => "customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer
+ end
+
+ def partial_collection_with_counter
+ render :partial => "customer_counter", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_as_and_counter
+ render :partial => "customer_counter_with_as", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :client
+ end
+
+ def partial_collection_with_locals
+ render :partial => "customer_greeting", :collection => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_with_spacer
+ render :partial => "customer", :spacer_template => "partial_only", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_with_spacer_which_uses_render
+ render :partial => "customer", :spacer_template => "partial_with_partial", :collection => [ Customer.new("david"), Customer.new("mary") ]
+ end
+
+ def partial_collection_shorthand_with_locals
+ render :partial => [ Customer.new("david"), Customer.new("mary") ], :locals => { :greeting => "Bonjour" }
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records
+ render :partial => [
+ BadCustomer.new("mark"),
+ GoodCustomer.new("craig"),
+ BadCustomer.new("john"),
+ GoodCustomer.new("zach"),
+ GoodCustomer.new("brandon"),
+ BadCustomer.new("dan") ],
+ :locals => { :greeting => "Bonjour" }
+ end
+
+ def empty_partial_collection
+ render :partial => "customer", :collection => []
+ end
+
+ def partial_collection_shorthand_with_different_types_of_records_with_counter
+ partial_collection_shorthand_with_different_types_of_records
+ end
+
+ def missing_partial
+ render :partial => 'thisFileIsntHere'
+ end
+
+ def partial_with_hash_object
+ render :partial => "hash_object", :object => {:first_name => "Sam"}
+ end
+
+ def partial_with_nested_object
+ render :partial => "quiz/questions/question", :object => Quiz::Question.new("first")
+ end
+
+ def partial_with_nested_object_shorthand
+ render Quiz::Question.new("first")
+ end
+
+ def partial_hash_collection
+ render :partial => "hash_object", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ]
+ end
+
+ def partial_hash_collection_with_locals
+ render :partial => "hash_greeting", :collection => [ {:first_name => "Pratik"}, {:first_name => "Amy"} ], :locals => { :greeting => "Hola" }
+ end
+
+ def partial_with_implicit_local_assignment
+ @customer = Customer.new("Marcel")
+ render :partial => "customer"
+ end
+
+ def render_call_to_partial_with_layout
+ render :action => "calling_partial_with_layout"
+ end
+
+ def render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ render :action => "calling_partial_with_layout", :layout => "layouts/partial_with_layout"
+ end
+
+ before_action only: :render_with_filters do
+ request.format = :xml
+ end
+
+ # Ensure that the before filter is executed *before* self.formats is set.
+ def render_with_filters
+ render :action => :formatted_xml_erb
+ end
+
+ private
+
+ def set_variable_for_layout
+ @variable_for_layout = nil
+ end
+
+ def determine_layout
+ case action_name
+ when "hello_world", "layout_test", "rendering_without_layout",
+ "rendering_nothing_on_layout", "render_text_hello_world",
+ "render_text_hello_world_with_layout",
+ "hello_world_with_layout_false",
+ "partial_only", "accessing_params_in_template",
+ "accessing_params_in_template_with_layout",
+ "render_with_explicit_template",
+ "render_with_explicit_string_template",
+ "update_page", "update_page_with_instance_variables"
+
+ "layouts/standard"
+ when "action_talk_to_layout", "layout_overriding_layout"
+ "layouts/talk_from_action"
+ when "render_implicit_html_template_from_xhr_request"
+ (request.xhr? ? 'layouts/xhr' : 'layouts/standard')
+ end
+ end
+end
+
+class RenderTest < ActionController::TestCase
+ tests TestController
+
+ def setup
+ # enable a logger so that (e.g.) the benchmarking stuff runs, so we can get
+ # a more accurate simulation of what happens in "real life".
+ super
+ @controller.logger = ActiveSupport::Logger.new(nil)
+ ActionView::Base.logger = ActiveSupport::Logger.new(nil)
+
+ @request.host = "www.nextangle.com"
+ end
+
+ def teardown
+ ActionView::Base.logger = nil
+ end
+
+ # :ported:
+ def test_simple_show
+ get :hello_world
+ assert_response 200
+ assert_response :success
+ assert_template "test/hello_world"
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ # :ported:
+ def test_renders_default_template_for_missing_action
+ get :'hyphen-ated'
+ assert_template 'test/hyphen-ated'
+ end
+
+ # :ported:
+ def test_render
+ get :render_hello_world
+ assert_template "test/hello_world"
+ end
+
+ def test_line_offset
+ get :render_line_offset
+ flunk "the action should have raised an exception"
+ rescue StandardError => exc
+ line = exc.backtrace.first
+ assert(line =~ %r{:(\d+):})
+ assert_equal "1", $1,
+ "The line offset is wrong, perhaps the wrong exception has been raised, exception was: #{exc.inspect}"
+ end
+
+ # :ported: compatibility
+ def test_render_with_forward_slash
+ get :render_hello_world_with_forward_slash
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_in_top_directory
+ get :render_template_in_top_directory
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ # :ported:
+ def test_render_in_top_directory_with_slash
+ get :render_template_in_top_directory_with_slash
+ assert_template "shared"
+ assert_equal "Elastica", @response.body
+ end
+
+ # :ported:
+ def test_render_from_variable
+ get :render_hello_world_from_variable
+ assert_equal "hello david", @response.body
+ end
+
+ # :ported:
+ def test_render_action
+ get :render_action_hello_world
+ assert_template "test/hello_world"
+ end
+
+ def test_render_action_upcased
+ assert_raise ActionView::MissingTemplate do
+ get :render_action_upcased_hello_world
+ end
+ end
+
+ # :ported:
+ def test_render_action_hello_world_as_string
+ get :render_action_hello_world_as_string
+ assert_equal "Hello world!", @response.body
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_action_with_symbol
+ get :render_action_hello_world_with_symbol
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_render_text
+ get :render_text_hello_world
+ assert_equal "hello world", @response.body
+ end
+
+ # :ported:
+ def test_do_with_render_text_and_layout
+ get :render_text_hello_world_with_layout
+ assert_equal "<html>hello world, I am here!</html>", @response.body
+ end
+
+ # :ported:
+ def test_do_with_render_action_and_layout_false
+ get :hello_world_with_layout_false
+ assert_equal 'Hello world!', @response.body
+ end
+
+ # :ported:
+ def test_render_file_with_instance_variables
+ get :render_file_with_instance_variables
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ def test_render_file
+ get :hello_world_file
+ assert_equal "Hello world!", @response.body
+ end
+
+ # :ported:
+ def test_render_file_as_string_with_instance_variables
+ get :render_file_as_string_with_instance_variables
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_not_using_full_path
+ get :render_file_not_using_full_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_not_using_full_path_with_dot_in_path
+ get :render_file_not_using_full_path_with_dot_in_path
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_using_pathname
+ get :render_file_using_pathname
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_with_locals
+ get :render_file_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_file_as_string_with_locals
+ get :render_file_as_string_with_locals
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :assessed:
+ def test_render_file_from_template
+ get :render_file_from_template
+ assert_equal "The secret is in the sauce\n", @response.body
+ end
+
+ # :ported:
+ def test_render_custom_code
+ get :render_custom_code
+ assert_response 404
+ assert_response :missing
+ assert_equal 'hello world', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_nil
+ get :render_text_with_nil
+ assert_response 200
+ assert_equal ' ', @response.body
+ end
+
+ # :ported:
+ def test_render_text_with_false
+ get :render_text_with_false
+ assert_equal 'false', @response.body
+ end
+
+ # :ported:
+ def test_render_nothing_with_appendix
+ get :render_nothing_with_appendix
+ assert_response 200
+ assert_equal 'appended', @response.body
+ end
+
+ def test_render_text_with_resource
+ get :render_text_with_resource
+ assert_equal 'name: "David"', @response.body
+ end
+
+ # :ported:
+ def test_attempt_to_access_object_method
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [clone]") { get :clone }
+ end
+
+ # :ported:
+ def test_private_methods
+ assert_raise(AbstractController::ActionNotFound, "No action responded to [determine_layout]") { get :determine_layout }
+ end
+
+ # :ported:
+ def test_access_to_request_in_view
+ get :accessing_request_in_template
+ assert_equal "Hello: www.nextangle.com", @response.body
+ end
+
+ def test_access_to_logger_in_view
+ get :accessing_logger_in_template
+ assert_equal "ActiveSupport::Logger", @response.body
+ end
+
+ # :ported:
+ def test_access_to_action_name_in_view
+ get :accessing_action_name_in_template
+ assert_equal "accessing_action_name_in_template", @response.body
+ end
+
+ # :ported:
+ def test_access_to_controller_name_in_view
+ get :accessing_controller_name_in_template
+ assert_equal "test", @response.body # name is explicitly set in the controller.
+ end
+
+ # :ported:
+ def test_render_xml
+ get :render_xml_hello
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_as_string_template
+ get :render_xml_hello_as_string_template
+ assert_equal "<html>\n <p>Hello David</p>\n<p>This is grand!</p>\n</html>\n", @response.body
+ assert_equal "application/xml", @response.content_type
+ end
+
+ # :ported:
+ def test_render_xml_with_default
+ get :greeting
+ assert_equal "<p>This is grand!</p>\n", @response.body
+ end
+
+ # :move: test in AV
+ def test_render_xml_with_partial
+ get :builder_partial_test
+ assert_equal "<test>\n <hello/>\n</test>\n", @response.body
+ end
+
+ # :ported:
+ def test_layout_rendering
+ get :layout_test
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_xml_with_layouts
+ get :builder_layout_test
+ assert_equal "<wrapper>\n<html>\n <p>Hello </p>\n<p>This is grand!</p>\n</html>\n</wrapper>\n", @response.body
+ end
+
+ def test_partials_list
+ get :partials_list
+ assert_equal "goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string
+ get :hello_in_a_string
+ assert_equal "How's there? goodbyeHello: davidHello: marygoodbye\n", @response.body
+ end
+
+ def test_render_to_string_resets_assigns
+ get :render_to_string_test
+ assert_equal "The value of foo is: ::this is a test::\n", @response.body
+ end
+
+ def test_render_to_string_inline
+ get :render_to_string_with_inline_and_render
+ assert_template "test/hello_world"
+ end
+
+ # :ported:
+ def test_nested_rendering
+ @controller = Fun::GamesController.new
+ get :hello_world
+ assert_equal "Living in a nested world", @response.body
+ end
+
+ def test_accessing_params_in_template
+ get :accessing_params_in_template, :name => "David"
+ assert_equal "Hello: David", @response.body
+ end
+
+ def test_accessing_local_assigns_in_inline_template
+ get :accessing_local_assigns_in_inline_template, :local_name => "Local David"
+ assert_equal "Goodbye, Local David", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_implicitly_render_html_template_from_xhr_request
+ xhr :get, :render_implicit_html_template_from_xhr_request
+ assert_equal "XHR!\nHello HTML!", @response.body
+ end
+
+ def test_should_implicitly_render_js_template_without_layout
+ get :render_implicit_js_template_without_layout, :format => :js
+ assert_no_match %r{<html>}, @response.body
+ end
+
+ def test_should_render_formatted_template
+ get :formatted_html_erb
+ assert_equal 'formatted html erb', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_bad_accepts_header
+ @request.env["HTTP_ACCEPT"] = "; a=dsf"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_should_render_formatted_html_erb_template_with_faulty_accepts_header
+ @request.accept = "image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, appliction/x-shockwave-flash, */*"
+ get :formatted_xml_erb
+ assert_equal '<test>passed formatted html erb</test>', @response.body
+ end
+
+ def test_layout_test_with_different_layout
+ get :layout_test_with_different_layout
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_string_action
+ get :layout_test_with_different_layout_and_string_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_layout_test_with_different_layout_and_symbol_action
+ get :layout_test_with_different_layout_and_symbol_action
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_rendering_without_layout
+ get :rendering_without_layout
+ assert_equal "Hello world!", @response.body
+ end
+
+ def test_layout_overriding_layout
+ get :layout_overriding_layout
+ assert_no_match %r{<title>}, @response.body
+ end
+
+ def test_rendering_nothing_on_layout
+ get :rendering_nothing_on_layout
+ assert_equal " ", @response.body
+ end
+
+ def test_render_to_string_doesnt_break_assigns
+ get :render_to_string_with_assigns
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_bad_render_to_string_still_throws_exception
+ assert_raise(ActionView::MissingTemplate) { get :render_to_string_with_exception }
+ end
+
+ def test_render_to_string_that_throws_caught_exception_doesnt_break_assigns
+ assert_nothing_raised { get :render_to_string_with_caught_exception }
+ assert_equal "i'm before the render", assigns(:before)
+ assert_equal "i'm after the render", assigns(:after)
+ end
+
+ def test_accessing_params_in_template_with_layout
+ get :accessing_params_in_template_with_layout, :name => "David"
+ assert_equal "<html>Hello: David</html>", @response.body
+ end
+
+ def test_render_with_explicit_template
+ get :render_with_explicit_template
+ assert_response :success
+ end
+
+ def test_render_with_explicit_unescaped_template
+ assert_raise(ActionView::MissingTemplate) { get :render_with_explicit_unescaped_template }
+ get :render_with_explicit_escaped_template
+ assert_equal "Hello w*rld!", @response.body
+ end
+
+ def test_render_with_explicit_string_template
+ get :render_with_explicit_string_template
+ assert_equal "<html>Hello world!</html>", @response.body
+ end
+
+ def test_render_with_filters
+ get :render_with_filters
+ assert_equal "<test>passed formatted xml erb</test>", @response.body
+ end
+
+ # :ported:
+ def test_double_render
+ assert_raise(AbstractController::DoubleRenderError) { get :double_render }
+ end
+
+ def test_double_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :double_redirect }
+ end
+
+ def test_render_and_redirect
+ assert_raise(AbstractController::DoubleRenderError) { get :render_and_redirect }
+ end
+
+ # specify the one exception to double render rule - render_to_string followed by render
+ def test_render_to_string_and_render
+ get :render_to_string_and_render
+ assert_equal("Hi web users! here is some cached stuff", @response.body)
+ end
+
+ def test_rendering_with_conflicting_local_vars
+ get :rendering_with_conflicting_local_vars
+ assert_equal("First: David\nSecond: Stephan\nThird: David\nFourth: David\nFifth: ", @response.body)
+ end
+
+ def test_action_talk_to_layout
+ get :action_talk_to_layout
+ assert_equal "<title>Talking to the layout</title>\nAction was here!", @response.body
+ end
+
+ # :addressed:
+ def test_render_text_with_assigns
+ get :render_text_with_assigns
+ assert_equal "world", assigns["hello"]
+ end
+
+ # :ported:
+ def test_template_with_locals
+ get :render_with_explicit_template_with_locals
+ assert_equal "The secret is area51\n", @response.body
+ end
+
+ def test_yield_content_for
+ get :yield_content_for
+ assert_equal "<title>Putting stuff in the title!</title>\nGreat stuff!\n", @response.body
+ end
+
+ def test_overwritting_rendering_relative_file_with_extension
+ get :hello_world_from_rxml_using_template
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+
+ get :hello_world_from_rxml_using_action
+ assert_equal "<html>\n <p>Hello</p>\n</html>\n", @response.body
+ end
+
+ def test_using_layout_around_block
+ get :render_using_layout_around_block
+ assert_equal "Before (David)\nInside from block\nAfter", @response.body
+ end
+
+ def test_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ get :render_using_layout_around_block_in_main_layout_and_within_content_for_layout
+ assert_equal "Before (Anthony)\nInside from first block in layout\nAfter\nBefore (David)\nInside from block\nAfter\nBefore (Ramm)\nInside from second block in layout\nAfter\n", @response.body
+ end
+
+ def test_partial_only
+ get :partial_only
+ assert_equal "only partial", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_formatted_partial
+ get :partial
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_html_formatted_partial_even_with_other_mime_time_in_accept
+ @request.accept = "text/javascript, text/html"
+
+ get :partial_html_erb
+
+ assert_equal "partial.html.erb", @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_should_render_html_partial_with_formats
+ get :partial_formats_html
+ assert_equal "partial html", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_partial
+ get :render_to_string_with_partial
+ assert_equal "only partial", assigns(:partial_only)
+ assert_equal "Hello: david", assigns(:partial_with_locals)
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_with_template_and_html_partial
+ get :render_to_string_with_template_and_html_partial
+ assert_equal "**only partial**\n", assigns(:text)
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "<strong>only html partial</strong>\n", @response.body
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_render_to_string_and_render_with_different_formats
+ get :render_to_string_and_render_with_different_formats
+ assert_equal "<strong>only partial</strong>\n", assigns(:html)
+ assert_equal "**only partial**\n", @response.body
+ assert_equal "text/plain", @response.content_type
+ end
+
+ def test_render_template_within_a_template_with_other_format
+ get :render_template_within_a_template_with_other_format
+ expected = "only html partial<p>This is grand!</p>"
+ assert_equal expected, @response.body.strip
+ assert_equal "text/html", @response.content_type
+ end
+
+ def test_partial_with_counter
+ get :partial_with_counter
+ assert_equal "5", @response.body
+ end
+
+ def test_partial_with_locals
+ get :partial_with_locals
+ assert_equal "Hello: david", @response.body
+ end
+
+ def test_partial_with_form_builder
+ get :partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('test/_form')
+ end
+
+ def test_partial_with_form_builder_subclass
+ get :partial_with_form_builder_subclass
+ assert_match(/<label/, @response.body)
+ assert_template('test/_labelling_form')
+ end
+
+ def test_nested_partial_with_form_builder
+ @controller = Fun::GamesController.new
+ get :nested_partial_with_form_builder
+ assert_match(/<label/, @response.body)
+ assert_template('fun/games/_form')
+ end
+
+ def test_namespaced_object_partial
+ @controller = Quiz::QuestionsController.new
+ get :new
+ assert_equal "Namespaced Partial", @response.body
+ end
+
+ def test_partial_collection
+ get :partial_collection
+ assert_equal "Hello: davidHello: mary", @response.body
+ end
+
+ def test_partial_collection_with_as
+ get :partial_collection_with_as
+ assert_equal "david david davidmary mary mary", @response.body
+ end
+
+ def test_partial_collection_with_counter
+ get :partial_collection_with_counter
+ assert_equal "david0mary1", @response.body
+ end
+
+ def test_partial_collection_with_as_and_counter
+ get :partial_collection_with_as_and_counter
+ assert_equal "david0mary1", @response.body
+ end
+
+ def test_partial_collection_with_locals
+ get :partial_collection_with_locals
+ assert_equal "Bonjour: davidBonjour: mary", @response.body
+ end
+
+ def test_locals_option_to_assert_template_is_not_supported
+ get :partial_collection_with_locals
+
+ warning_buffer = StringIO.new
+ $stderr = warning_buffer
+
+ assert_template partial: 'customer_greeting', locals: { greeting: 'Bonjour' }
+ assert_equal "the :locals option to #assert_template is only supported in a ActionView::TestCase\n", warning_buffer.string
+ ensure
+ $stderr = STDERR
+ end
+
+ def test_partial_collection_with_spacer
+ get :partial_collection_with_spacer
+ assert_equal "Hello: davidonly partialHello: mary", @response.body
+ assert_template :partial => '_customer'
+ end
+
+ def test_partial_collection_with_spacer_which_uses_render
+ get :partial_collection_with_spacer_which_uses_render
+ assert_equal "Hello: davidpartial html\npartial with partial\nHello: mary", @response.body
+ assert_template :partial => '_customer'
+ end
+
+ def test_partial_collection_shorthand_with_locals
+ get :partial_collection_shorthand_with_locals
+ assert_equal "Bonjour: davidBonjour: mary", @response.body
+ assert_template :partial => 'customers/_customer', :count => 2
+ assert_template :partial => '_completely_fake_and_made_up_template_that_cannot_possibly_be_rendered', :count => 0
+ end
+
+ def test_partial_collection_shorthand_with_different_types_of_records
+ get :partial_collection_shorthand_with_different_types_of_records
+ assert_equal "Bonjour bad customer: mark0Bonjour good customer: craig1Bonjour bad customer: john2Bonjour good customer: zach3Bonjour good customer: brandon4Bonjour bad customer: dan5", @response.body
+ assert_template :partial => 'good_customers/_good_customer', :count => 3
+ assert_template :partial => 'bad_customers/_bad_customer', :count => 3
+ end
+
+ def test_empty_partial_collection
+ get :empty_partial_collection
+ assert_equal " ", @response.body
+ assert_template :partial => false
+ end
+
+ def test_partial_with_hash_object
+ get :partial_with_hash_object
+ assert_equal "Sam\nmaS\n", @response.body
+ end
+
+ def test_partial_with_nested_object
+ get :partial_with_nested_object
+ assert_equal "first", @response.body
+ end
+
+ def test_partial_with_nested_object_shorthand
+ get :partial_with_nested_object_shorthand
+ assert_equal "first", @response.body
+ end
+
+ def test_hash_partial_collection
+ get :partial_hash_collection
+ assert_equal "Pratik\nkitarP\nAmy\nymA\n", @response.body
+ end
+
+ def test_partial_hash_collection_with_locals
+ get :partial_hash_collection_with_locals
+ assert_equal "Hola: PratikHola: Amy", @response.body
+ end
+
+ def test_render_missing_partial_template
+ assert_raise(ActionView::MissingTemplate) do
+ get :missing_partial
+ end
+ end
+
+ def test_render_call_to_partial_with_layout
+ get :render_call_to_partial_with_layout
+ assert_equal "Before (David)\nInside from partial (David)\nAfter", @response.body
+ end
+
+ def test_render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ get :render_call_to_partial_with_layout_in_main_layout_and_within_content_for_layout
+ assert_equal "Before (Anthony)\nInside from partial (Anthony)\nAfter\nBefore (David)\nInside from partial (David)\nAfter\nBefore (Ramm)\nInside from partial (Ramm)\nAfter", @response.body
+ end
+end
+
diff --git a/actionview/test/actionpack/controller/view_paths_test.rb b/actionview/test/actionpack/controller/view_paths_test.rb
new file mode 100644
index 0000000000..c6e7a523b9
--- /dev/null
+++ b/actionview/test/actionpack/controller/view_paths_test.rb
@@ -0,0 +1,174 @@
+require 'abstract_unit'
+
+class ViewLoadPathsTest < ActionController::TestCase
+ class TestController < ActionController::Base
+ def self.controller_path() "test" end
+
+ before_action :add_view_path, only: :hello_world_at_request_time
+
+ def hello_world() end
+ def hello_world_at_request_time() render(:action => 'hello_world') end
+
+ private
+ def add_view_path
+ prepend_view_path "#{FIXTURE_LOAD_PATH}/override"
+ end
+ end
+
+ module Test
+ class SubController < ActionController::Base
+ layout 'test/sub'
+ def hello_world; render(:template => 'test/hello_world'); end
+ end
+ end
+
+ def setup
+ @request = ActionController::TestRequest.new
+ @response = ActionController::TestResponse.new
+ @controller = TestController.new
+ @paths = TestController.view_paths
+ end
+
+ def teardown
+ TestController.view_paths = @paths
+ end
+
+ def expand(array)
+ array.map {|x| File.expand_path(x.to_s)}
+ end
+
+ def assert_paths(*paths)
+ controller = paths.first.is_a?(Class) ? paths.shift : @controller
+ assert_equal expand(paths), controller.view_paths.map { |p| p.to_s }
+ end
+
+ def test_template_load_path_was_set_correctly
+ assert_paths FIXTURE_LOAD_PATH
+ end
+
+ def test_controller_appends_view_path_correctly
+ @controller.append_view_path 'foo'
+ assert_paths(FIXTURE_LOAD_PATH, "foo")
+
+ @controller.append_view_path(%w(bar baz))
+ assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz")
+
+ @controller.append_view_path(FIXTURE_LOAD_PATH)
+ assert_paths(FIXTURE_LOAD_PATH, "foo", "bar", "baz", FIXTURE_LOAD_PATH)
+ end
+
+ def test_controller_prepends_view_path_correctly
+ @controller.prepend_view_path 'baz'
+ assert_paths("baz", FIXTURE_LOAD_PATH)
+
+ @controller.prepend_view_path(%w(foo bar))
+ assert_paths "foo", "bar", "baz", FIXTURE_LOAD_PATH
+
+ @controller.prepend_view_path(FIXTURE_LOAD_PATH)
+ assert_paths FIXTURE_LOAD_PATH, "foo", "bar", "baz", FIXTURE_LOAD_PATH
+ end
+
+ def test_template_appends_view_path_correctly
+ @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ class_view_paths = TestController.view_paths
+
+ @controller.append_view_path 'foo'
+ assert_paths FIXTURE_LOAD_PATH, "foo"
+
+ @controller.append_view_path(%w(bar baz))
+ assert_paths FIXTURE_LOAD_PATH, "foo", "bar", "baz"
+ assert_paths TestController, *class_view_paths
+ end
+
+ def test_template_prepends_view_path_correctly
+ @controller.instance_variable_set :@template, ActionView::Base.new(TestController.view_paths, {}, @controller)
+ class_view_paths = TestController.view_paths
+
+ @controller.prepend_view_path 'baz'
+ assert_paths "baz", FIXTURE_LOAD_PATH
+
+ @controller.prepend_view_path(%w(foo bar))
+ assert_paths "foo", "bar", "baz", FIXTURE_LOAD_PATH
+ assert_paths TestController, *class_view_paths
+ end
+
+ def test_view_paths
+ get :hello_world
+ assert_response :success
+ assert_equal "Hello world!", @response.body
+ end
+
+ def test_view_paths_override
+ TestController.prepend_view_path "#{FIXTURE_LOAD_PATH}/override"
+ get :hello_world
+ assert_response :success
+ assert_equal "Hello overridden world!", @response.body
+ end
+
+ def test_view_paths_override_for_layouts_in_controllers_with_a_module
+ @controller = Test::SubController.new
+ Test::SubController.view_paths = [ "#{FIXTURE_LOAD_PATH}/override", FIXTURE_LOAD_PATH, "#{FIXTURE_LOAD_PATH}/override2" ]
+ get :hello_world
+ assert_response :success
+ assert_equal "layout: Hello overridden world!", @response.body
+ end
+
+ def test_view_paths_override_at_request_time
+ get :hello_world_at_request_time
+ assert_response :success
+ assert_equal "Hello overridden world!", @response.body
+ end
+
+ def test_decorate_view_paths_with_custom_resolver
+ decorator_class = Class.new(ActionView::PathResolver) do
+ def initialize(path_set)
+ @path_set = path_set
+ end
+
+ def find_all(*args)
+ @path_set.find_all(*args).collect do |template|
+ ::ActionView::Template.new(
+ "Decorated body",
+ template.identifier,
+ template.handler,
+ {
+ :virtual_path => template.virtual_path,
+ :format => template.formats
+ }
+ )
+ end
+ end
+ end
+
+ decorator = decorator_class.new(TestController.view_paths)
+ TestController.view_paths = ActionView::PathSet.new.push(decorator)
+
+ get :hello_world
+ assert_response :success
+ assert_equal "Decorated body", @response.body
+ end
+
+ def test_inheritance
+ original_load_paths = ActionController::Base.view_paths
+
+ self.class.class_eval %{
+ class A < ActionController::Base; end
+ class B < A; end
+ class C < ActionController::Base; end
+ }
+
+ A.view_paths = ['a/path']
+
+ assert_paths A, "a/path"
+ assert_paths A, *B.view_paths
+ assert_paths C, *original_load_paths
+
+ C.view_paths = []
+ assert_nothing_raised { C.append_view_path 'c/path' }
+ assert_paths C, "c/path"
+ end
+
+ def test_lookup_context_accessor
+ assert_equal ["test"], TestController.new.lookup_context.prefixes
+ end
+end
diff --git a/actionview/test/active_record_unit.rb b/actionview/test/active_record_unit.rb
new file mode 100644
index 0000000000..95fbb112c0
--- /dev/null
+++ b/actionview/test/active_record_unit.rb
@@ -0,0 +1,91 @@
+require 'abstract_unit'
+
+# Define the essentials
+class ActiveRecordTestConnector
+ cattr_accessor :able_to_connect
+ cattr_accessor :connected
+
+ # Set our defaults
+ self.connected = false
+ self.able_to_connect = true
+end
+
+# Try to grab AR
+unless defined?(ActiveRecord) && defined?(FixtureSet)
+ begin
+ PATH_TO_AR = "#{File.dirname(__FILE__)}/../../activerecord/lib"
+ raise LoadError, "#{PATH_TO_AR} doesn't exist" unless File.directory?(PATH_TO_AR)
+ $LOAD_PATH.unshift PATH_TO_AR
+ require 'active_record'
+ rescue LoadError => e
+ $stderr.print "Failed to load Active Record. Skipping Active Record assertion tests: #{e}"
+ ActiveRecordTestConnector.able_to_connect = false
+ end
+end
+$stderr.flush
+
+
+# Define the rest of the connector
+class ActiveRecordTestConnector
+ class << self
+ def setup
+ unless self.connected || !self.able_to_connect
+ setup_connection
+ load_schema
+ require_fixture_models
+ self.connected = true
+ end
+ rescue Exception => e # errors from ActiveRecord setup
+ $stderr.puts "\nSkipping ActiveRecord assertion tests: #{e}"
+ #$stderr.puts " #{e.backtrace.join("\n ")}\n"
+ self.able_to_connect = false
+ end
+
+ private
+ def setup_connection
+ if Object.const_defined?(:ActiveRecord)
+ defaults = { :database => ':memory:' }
+ adapter = defined?(JRUBY_VERSION) ? 'jdbcsqlite3' : 'sqlite3'
+ options = defaults.merge :adapter => adapter, :timeout => 500
+ ActiveRecord::Base.establish_connection(options)
+ ActiveRecord::Base.configurations = { 'sqlite3_ar_integration' => options }
+ ActiveRecord::Base.connection
+
+ Object.send(:const_set, :QUOTED_TYPE, ActiveRecord::Base.connection.quote_column_name('type')) unless Object.const_defined?(:QUOTED_TYPE)
+ else
+ raise "Can't setup connection since ActiveRecord isn't loaded."
+ end
+ end
+
+ # Load actionpack sqlite tables
+ def load_schema
+ File.read(File.dirname(__FILE__) + "/fixtures/db_definitions/sqlite.sql").split(';').each do |sql|
+ ActiveRecord::Base.connection.execute(sql) unless sql.blank?
+ end
+ end
+
+ def require_fixture_models
+ Dir.glob(File.dirname(__FILE__) + "/fixtures/*.rb").each {|f| require f}
+ end
+ end
+end
+
+class ActiveRecordTestCase < ActionController::TestCase
+ include ActiveRecord::TestFixtures
+
+ # Set our fixture path
+ if ActiveRecordTestConnector.able_to_connect
+ self.fixture_path = [FIXTURE_LOAD_PATH]
+ self.use_transactional_fixtures = false
+ end
+
+ def self.fixtures(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+
+ def run(*args)
+ super if ActiveRecordTestConnector.connected
+ end
+end
+
+ActiveRecordTestConnector.setup
diff --git a/actionview/test/activerecord/controller_runtime_test.rb b/actionview/test/activerecord/controller_runtime_test.rb
new file mode 100644
index 0000000000..368bec1c70
--- /dev/null
+++ b/actionview/test/activerecord/controller_runtime_test.rb
@@ -0,0 +1,95 @@
+require 'active_record_unit'
+require 'active_record/railties/controller_runtime'
+require 'fixtures/project'
+require 'active_support/log_subscriber/test_helper'
+require 'action_controller/log_subscriber'
+
+ActionController::Base.send :include, ActiveRecord::Railties::ControllerRuntime
+
+class ControllerRuntimeLogSubscriberTest < ActionController::TestCase
+ class LogSubscriberController < ActionController::Base
+ respond_to :html
+
+ def show
+ render :inline => "<%= Project.all %>"
+ end
+
+ def zero
+ render :inline => "Zero DB runtime"
+ end
+
+ def create
+ ActiveRecord::LogSubscriber.runtime += 100
+ project = Project.last
+ respond_with(project, location: url_for(action: :show))
+ end
+
+ def redirect
+ Project.all
+ redirect_to :action => 'show'
+ end
+
+ def db_after_render
+ render :inline => "Hello world"
+ Project.all
+ ActiveRecord::LogSubscriber.runtime += 100
+ end
+ end
+
+ include ActiveSupport::LogSubscriber::TestHelper
+ tests LogSubscriberController
+
+ def setup
+ super
+ @old_logger = ActionController::Base.logger
+ ActionController::LogSubscriber.attach_to :action_controller
+ end
+
+ def teardown
+ super
+ ActiveSupport::LogSubscriber.log_subscribers.clear
+ ActionController::Base.logger = @old_logger
+ end
+
+ def set_logger(logger)
+ ActionController::Base.logger = logger
+ end
+
+ def test_log_with_active_record
+ get :show
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[1])
+ end
+
+ def test_runtime_reset_before_requests
+ ActiveRecord::LogSubscriber.runtime += 12345
+ get :zero
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: 0.0ms\)/, @logger.logged(:info)[1])
+ end
+
+ def test_log_with_active_record_when_post
+ post :create
+ wait
+ assert_match(/ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[2])
+ end
+
+ def test_log_with_active_record_when_redirecting
+ get :redirect
+ wait
+ assert_equal 3, @logger.logged(:info).size
+ assert_match(/\(ActiveRecord: [\d.]+ms\)/, @logger.logged(:info)[2])
+ end
+
+ def test_include_time_query_time_after_rendering
+ get :db_after_render
+ wait
+
+ assert_equal 2, @logger.logged(:info).size
+ assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms\)/, @logger.logged(:info)[1])
+ end
+end
diff --git a/actionview/test/activerecord/form_helper_activerecord_test.rb b/actionview/test/activerecord/form_helper_activerecord_test.rb
new file mode 100644
index 0000000000..2e302c65a7
--- /dev/null
+++ b/actionview/test/activerecord/form_helper_activerecord_test.rb
@@ -0,0 +1,91 @@
+require 'active_record_unit'
+require 'fixtures/project'
+require 'fixtures/developer'
+
+class FormHelperActiveRecordTest < ActionView::TestCase
+ tests ActionView::Helpers::FormHelper
+
+ def form_for(*)
+ @output_buffer = super
+ end
+
+ def setup
+ @developer = Developer.new
+ @developer.id = 123
+ @developer.name = "developer #123"
+
+ @project = Project.new
+ @project.id = 321
+ @project.name = "project #321"
+ @project.save
+
+ @developer.projects << @project
+ @developer.save
+ end
+
+ def teardown
+ Project.delete(321)
+ Developer.delete(123)
+ end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ resources :developers do
+ resources :projects
+ end
+ end
+
+ def _routes
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association
+ form_for(@developer) do |f|
+ concat f.fields_for(:projects, @developer.projects.first, :child_index => 'abc') { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/developers/123', 'edit_developer_123', 'edit_developer', :method => 'patch') do
+ '<input id="developer_projects_attributes_abc_name" name="developer[projects_attributes][abc][name]" type="text" value="project #321" />' +
+ '<input id="developer_projects_attributes_abc_id" name="developer[projects_attributes][abc][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ protected
+
+ def hidden_fields(method = nil)
+ txt = %{<div style="margin:0;padding:0;display:inline">}
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+ txt << %{</div>}
+ end
+
+ def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
+ txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if multipart
+ txt << %{ data-remote="true"} if remote
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ method = method.to_s == "get" ? "get" : "post"
+ txt << %{ method="#{method}">}
+ end
+
+ def whole_form(action = "/", id = nil, html_class = nil, options = nil)
+ contents = block_given? ? yield : ""
+
+ if options.is_a?(Hash)
+ method, remote, multipart = options.values_at(:method, :remote, :multipart)
+ else
+ method = options
+ end
+
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
+ end
+end \ No newline at end of file
diff --git a/actionpack/test/activerecord/polymorphic_routes_test.rb b/actionview/test/activerecord/polymorphic_routes_test.rb
index afb714484b..afb714484b 100644
--- a/actionpack/test/activerecord/polymorphic_routes_test.rb
+++ b/actionview/test/activerecord/polymorphic_routes_test.rb
diff --git a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
index 409370104d..409370104d 100644
--- a/actionpack/test/activerecord/render_partial_with_record_identification_test.rb
+++ b/actionview/test/activerecord/render_partial_with_record_identification_test.rb
diff --git a/actionpack/test/fixtures/_top_level_partial.html.erb b/actionview/test/fixtures/_top_level_partial.html.erb
index 0b1c2e46e0..0b1c2e46e0 100644
--- a/actionpack/test/fixtures/_top_level_partial.html.erb
+++ b/actionview/test/fixtures/_top_level_partial.html.erb
diff --git a/actionview/test/fixtures/_top_level_partial_only.erb b/actionview/test/fixtures/_top_level_partial_only.erb
new file mode 100644
index 0000000000..44f25b61d0
--- /dev/null
+++ b/actionview/test/fixtures/_top_level_partial_only.erb
@@ -0,0 +1 @@
+top level partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb b/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb
new file mode 100644
index 0000000000..d22af431ec
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/bad_customers/_bad_customer.html.erb
@@ -0,0 +1 @@
+<%= greeting %> bad customer: <%= bad_customer.name %><%= bad_customer_counter %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/customers/_customer.html.erb b/actionview/test/fixtures/actionpack/customers/_customer.html.erb
index 483571e22a..483571e22a 100644
--- a/actionpack/test/fixtures/customers/_customer.html.erb
+++ b/actionview/test/fixtures/actionpack/customers/_customer.html.erb
diff --git a/actionpack/test/fixtures/fun/games/_form.erb b/actionview/test/fixtures/actionpack/fun/games/_form.erb
index 01107f1cb2..01107f1cb2 100644
--- a/actionpack/test/fixtures/fun/games/_form.erb
+++ b/actionview/test/fixtures/actionpack/fun/games/_form.erb
diff --git a/actionpack/test/fixtures/fun/games/hello_world.erb b/actionview/test/fixtures/actionpack/fun/games/hello_world.erb
index 1ebfbe2539..1ebfbe2539 100644
--- a/actionpack/test/fixtures/fun/games/hello_world.erb
+++ b/actionview/test/fixtures/actionpack/fun/games/hello_world.erb
diff --git a/actionpack/test/fixtures/good_customers/_good_customer.html.erb b/actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb
index a2d97ebc6d..a2d97ebc6d 100644
--- a/actionpack/test/fixtures/good_customers/_good_customer.html.erb
+++ b/actionview/test/fixtures/actionpack/good_customers/_good_customer.html.erb
diff --git a/actionpack/test/fixtures/hello.html b/actionview/test/fixtures/actionpack/hello.html
index 6769dd60bd..6769dd60bd 100644
--- a/actionpack/test/fixtures/hello.html
+++ b/actionview/test/fixtures/actionpack/hello.html
diff --git a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/layout_tests/alt/layouts/alt.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/alt/layouts/alt.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb
index 121bc079a1..121bc079a1 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/controller_name_space/nested.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/controller_name_space/nested.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/item.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb
index 60f04d77d5..60f04d77d5 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/item.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/item.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb
index b74ac0840d..b74ac0840d 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/layout_test.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/layout_test.erb
diff --git a/actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb
index 3b65e54f9c..3b65e54f9c 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/multiple_extensions.html.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/multiple_extensions.html.erb
diff --git a/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb b/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb
new file mode 100644
index 0000000000..bda57d0fae
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/symlinked/symlinked_layout.erb
@@ -0,0 +1,5 @@
+This is my layout
+
+<%= yield %>
+
+End.
diff --git a/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab b/actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab
index fcee620d82..fcee620d82 100644
--- a/actionpack/test/fixtures/layout_tests/layouts/third_party_template_library.mab
+++ b/actionview/test/fixtures/actionpack/layout_tests/layouts/third_party_template_library.mab
diff --git a/actionpack/test/fixtures/layout_tests/views/goodbye.erb b/actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb
index 4ee911188e..4ee911188e 100644
--- a/actionpack/test/fixtures/layout_tests/views/goodbye.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/views/goodbye.erb
diff --git a/actionpack/test/fixtures/layout_tests/views/hello.erb b/actionview/test/fixtures/actionpack/layout_tests/views/hello.erb
index 4ee911188e..4ee911188e 100644
--- a/actionpack/test/fixtures/layout_tests/views/hello.erb
+++ b/actionview/test/fixtures/actionpack/layout_tests/views/hello.erb
diff --git a/actionpack/test/fixtures/layouts/_column.html.erb b/actionview/test/fixtures/actionpack/layouts/_column.html.erb
index 96db002b8a..96db002b8a 100644
--- a/actionpack/test/fixtures/layouts/_column.html.erb
+++ b/actionview/test/fixtures/actionpack/layouts/_column.html.erb
diff --git a/actionview/test/fixtures/actionpack/layouts/_customers.erb b/actionview/test/fixtures/actionpack/layouts/_customers.erb
new file mode 100644
index 0000000000..ae63f13cd3
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/_customers.erb
@@ -0,0 +1 @@
+<title><%= yield Struct.new(:name).new("David") %></title> \ No newline at end of file
diff --git a/actionpack/test/fixtures/layouts/_partial_and_yield.erb b/actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb
index 74cc428ffa..74cc428ffa 100644
--- a/actionpack/test/fixtures/layouts/_partial_and_yield.erb
+++ b/actionview/test/fixtures/actionpack/layouts/_partial_and_yield.erb
diff --git a/actionpack/test/fixtures/layouts/_yield_only.erb b/actionview/test/fixtures/actionpack/layouts/_yield_only.erb
index 37f0bddbd7..37f0bddbd7 100644
--- a/actionpack/test/fixtures/layouts/_yield_only.erb
+++ b/actionview/test/fixtures/actionpack/layouts/_yield_only.erb
diff --git a/actionpack/test/fixtures/layouts/_yield_with_params.erb b/actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb
index 68e6557fb8..68e6557fb8 100644
--- a/actionpack/test/fixtures/layouts/_yield_with_params.erb
+++ b/actionview/test/fixtures/actionpack/layouts/_yield_with_params.erb
diff --git a/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb b/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb
new file mode 100644
index 0000000000..73ac833e52
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/block_with_layout.erb
@@ -0,0 +1,3 @@
+<%= render(:layout => "layout_for_partial", :locals => { :name => "Anthony" }) do %>Inside from first block in layout<% "Return value should be discarded" %><% end %>
+<%= yield %>
+<%= render(:layout => "layout_for_partial", :locals => { :name => "Ramm" }) do %>Inside from second block in layout<% end %>
diff --git a/actionview/test/fixtures/actionpack/layouts/builder.builder b/actionview/test/fixtures/actionpack/layouts/builder.builder
new file mode 100644
index 0000000000..7c7d4b2dd1
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/builder.builder
@@ -0,0 +1,3 @@
+xml.wrapper do
+ xml << yield
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb b/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb
new file mode 100644
index 0000000000..a0349d731e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/partial_with_layout.erb
@@ -0,0 +1,3 @@
+<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Anthony' } ) %>
+<%= yield %>
+<%= render( :layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => {:name => 'Ramm' } ) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/standard.html.erb b/actionview/test/fixtures/actionpack/layouts/standard.html.erb
new file mode 100644
index 0000000000..5e6c24fe39
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/standard.html.erb
@@ -0,0 +1 @@
+<html><%= yield %><%= @variable_for_layout %></html> \ No newline at end of file
diff --git a/actionpack/test/fixtures/layouts/streaming.erb b/actionview/test/fixtures/actionpack/layouts/streaming.erb
index d3f896a6ca..d3f896a6ca 100644
--- a/actionpack/test/fixtures/layouts/streaming.erb
+++ b/actionview/test/fixtures/actionpack/layouts/streaming.erb
diff --git a/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb b/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb
new file mode 100644
index 0000000000..bf53fdb785
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/talk_from_action.erb
@@ -0,0 +1,2 @@
+<title><%= @title || yield(:title) %></title>
+<%= yield -%> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb b/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb
new file mode 100644
index 0000000000..fd2896aeaa
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/with_html_partial.html.erb
@@ -0,0 +1 @@
+<%= render :partial => "partial_only_html" %><%= yield %>
diff --git a/actionview/test/fixtures/actionpack/layouts/xhr.html.erb b/actionview/test/fixtures/actionpack/layouts/xhr.html.erb
new file mode 100644
index 0000000000..85285324ec
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/xhr.html.erb
@@ -0,0 +1,2 @@
+XHR!
+<%= yield %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/layouts/yield.erb b/actionview/test/fixtures/actionpack/layouts/yield.erb
new file mode 100644
index 0000000000..482dc9022e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/layouts/yield.erb
@@ -0,0 +1,2 @@
+<title><%= yield :title %></title>
+<%= yield %>
diff --git a/actionpack/test/fixtures/layouts/yield_with_render_inline_inside.erb b/actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb
index 7298d79690..7298d79690 100644
--- a/actionpack/test/fixtures/layouts/yield_with_render_inline_inside.erb
+++ b/actionview/test/fixtures/actionpack/layouts/yield_with_render_inline_inside.erb
diff --git a/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb b/actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb
index 74cc428ffa..74cc428ffa 100644
--- a/actionpack/test/fixtures/layouts/yield_with_render_partial_inside.erb
+++ b/actionview/test/fixtures/actionpack/layouts/yield_with_render_partial_inside.erb
diff --git a/actionpack/test/fixtures/quiz/questions/_question.html.erb b/actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb
index fb4dcfee64..fb4dcfee64 100644
--- a/actionpack/test/fixtures/quiz/questions/_question.html.erb
+++ b/actionview/test/fixtures/actionpack/quiz/questions/_question.html.erb
diff --git a/actionview/test/fixtures/actionpack/shared.html.erb b/actionview/test/fixtures/actionpack/shared.html.erb
new file mode 100644
index 0000000000..af262fc9f8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/shared.html.erb
@@ -0,0 +1 @@
+Elastica \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
new file mode 100644
index 0000000000..3225efc49a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.html.erb
@@ -0,0 +1 @@
+HTML \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
new file mode 100644
index 0000000000..7fa41dce66
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_changing_priority.json.erb
@@ -0,0 +1 @@
+JSON \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_counter.html.erb b/actionview/test/fixtures/actionpack/test/_counter.html.erb
new file mode 100644
index 0000000000..fd245bfc70
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_counter.html.erb
@@ -0,0 +1 @@
+<%= counter_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer.erb b/actionview/test/fixtures/actionpack/test/_customer.erb
new file mode 100644
index 0000000000..d8220afeda
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer.erb
@@ -0,0 +1 @@
+Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_counter.erb b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
new file mode 100644
index 0000000000..3435979dba
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter.erb
@@ -0,0 +1 @@
+<%= customer_counter.name %><%= customer_counter_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
new file mode 100644
index 0000000000..1241eb604d
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_counter_with_as.erb
@@ -0,0 +1 @@
+<%= client.name %><%= client_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_greeting.erb b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
new file mode 100644
index 0000000000..6acbcb20c4
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_greeting.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= customer_greeting.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_customer_with_var.erb b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
new file mode 100644
index 0000000000..00047dd20e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_customer_with_var.erb
@@ -0,0 +1 @@
+<%= customer.name %> <%= customer.name %> <%= customer.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb b/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb
new file mode 100644
index 0000000000..1cc8d41475
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_directory/_partial_with_locales.html.erb
@@ -0,0 +1 @@
+Hello <%= name %>
diff --git a/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
new file mode 100644
index 0000000000..790ee896db
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_first_json_partial.json.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/second_json_partial" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_form.erb b/actionview/test/fixtures/actionpack/test/_form.erb
new file mode 100644
index 0000000000..01107f1cb2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_form.erb
@@ -0,0 +1 @@
+<%= form.label :title %>
diff --git a/actionview/test/fixtures/actionpack/test/_hash_greeting.erb b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
new file mode 100644
index 0000000000..fc54a36f2a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hash_greeting.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= hash_greeting[:first_name] %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_hash_object.erb b/actionview/test/fixtures/actionpack/test/_hash_object.erb
new file mode 100644
index 0000000000..34a92c6a56
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hash_object.erb
@@ -0,0 +1,2 @@
+<%= hash_object[:first_name] %>
+<%= hash_object[:first_name].reverse %>
diff --git a/actionview/test/fixtures/actionpack/test/_hello.builder b/actionview/test/fixtures/actionpack/test/_hello.builder
new file mode 100644
index 0000000000..ef52f632d1
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_hello.builder
@@ -0,0 +1 @@
+xm.hello \ No newline at end of file
diff --git a/actionpack/test/fixtures/digestor/level/below/_header.html.erb b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/level/below/_header.html.erb
+++ b/actionview/test/fixtures/actionpack/test/_json_change_priority.json.erb
diff --git a/actionview/test/fixtures/actionpack/test/_labelling_form.erb b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
new file mode 100644
index 0000000000..1b95763165
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_labelling_form.erb
@@ -0,0 +1 @@
+<%= labelling_form.label :title %>
diff --git a/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
new file mode 100644
index 0000000000..666efadbb6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_layout_for_partial.html.erb
@@ -0,0 +1,3 @@
+Before (<%= name %>)
+<%= yield %>
+After \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.erb b/actionview/test/fixtures/actionpack/test/_partial.erb
new file mode 100644
index 0000000000..e466dcbd8e
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.erb
@@ -0,0 +1 @@
+invalid \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.html.erb b/actionview/test/fixtures/actionpack/test/_partial.html.erb
new file mode 100644
index 0000000000..e39f6c9827
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.html.erb
@@ -0,0 +1 @@
+partial html \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial.js.erb b/actionview/test/fixtures/actionpack/test/_partial.js.erb
new file mode 100644
index 0000000000..b350cdd7ef
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial.js.erb
@@ -0,0 +1 @@
+partial js \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
new file mode 100644
index 0000000000..3a03a64e31
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_for_use_in_layout.html.erb
@@ -0,0 +1 @@
+Inside from partial (<%= name %>) \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb
new file mode 100644
index 0000000000..4b54875782
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_html_erb.html.erb
@@ -0,0 +1 @@
+<%= "partial.html.erb" %>
diff --git a/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb b/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb
new file mode 100644
index 0000000000..cc3a91c89f
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_name_local_variable.erb
@@ -0,0 +1 @@
+<%= partial_name_local_variable %>
diff --git a/actionview/test/fixtures/actionpack/test/_partial_only.erb b/actionview/test/fixtures/actionpack/test/_partial_only.erb
new file mode 100644
index 0000000000..a44b3eed40
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_only.erb
@@ -0,0 +1 @@
+only partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_only_html.html b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
new file mode 100644
index 0000000000..d2d630bd40
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_only_html.html
@@ -0,0 +1 @@
+only html partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
new file mode 100644
index 0000000000..ee0d5037b6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_partial_with_partial.erb
@@ -0,0 +1,2 @@
+<%= render 'test/partial' %>
+partial with partial
diff --git a/actionview/test/fixtures/actionpack/test/_person.erb b/actionview/test/fixtures/actionpack/test/_person.erb
new file mode 100644
index 0000000000..b2e5688956
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_person.erb
@@ -0,0 +1,2 @@
+Second: <%= name %>
+Third: <%= @name %>
diff --git a/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
new file mode 100644
index 0000000000..f9a93728fe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_raise_indentation.html.erb
@@ -0,0 +1,13 @@
+<p>First paragraph</p>
+<p>Second paragraph</p>
+<p>Third paragraph</p>
+<p>Fourth paragraph</p>
+<p>Fifth paragraph</p>
+<p>Sixth paragraph</p>
+<p>Seventh paragraph</p>
+<p>Eight paragraph</p>
+<p>Ninth paragraph</p>
+<p>Tenth paragraph</p>
+<%= raise "error here!" %>
+<p>Eleventh paragraph</p>
+<p>Twelfth paragraph</p> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
new file mode 100644
index 0000000000..5ebb7f1afd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/_second_json_partial.json.erb
@@ -0,0 +1 @@
+Third level \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
new file mode 100644
index 0000000000..36e896daa8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/action_talk_to_layout.erb
@@ -0,0 +1,2 @@
+<% @title = "Talking to the layout" -%>
+Action was here! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
new file mode 100644
index 0000000000..ac44bc0d81
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/calling_partial_with_layout.html.erb
@@ -0,0 +1 @@
+<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/capturing.erb b/actionview/test/fixtures/actionpack/test/capturing.erb
new file mode 100644
index 0000000000..1addaa40d9
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/capturing.erb
@@ -0,0 +1,4 @@
+<% days = capture do %>
+ Dreamy days
+<% end %>
+<%= days %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/change_priority.html.erb b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
new file mode 100644
index 0000000000..5618977d05
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/change_priority.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => "test/json_change_priority", formats: :json %>
+HTML Template, but <%= render :partial => "test/changing_priority" %> partial \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for.erb b/actionview/test/fixtures/actionpack/test/content_for.erb
new file mode 100644
index 0000000000..1fb829f54c
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for.erb
@@ -0,0 +1 @@
+<% content_for :title do -%>Putting stuff in the title!<% end -%>Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
new file mode 100644
index 0000000000..e65f629574
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for_concatenated.erb
@@ -0,0 +1,3 @@
+<% content_for :title, "Putting stuff "
+ content_for :title, "in the title!" -%>
+Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
new file mode 100644
index 0000000000..aeb6f73ce0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/content_for_with_parameter.erb
@@ -0,0 +1,2 @@
+<% content_for :title, "Putting stuff in the title!" -%>
+Great stuff! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb b/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/dot.directory/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
new file mode 100644
index 0000000000..1c64efabd8
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_html_erb.html.erb
@@ -0,0 +1 @@
+formatted html erb \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
new file mode 100644
index 0000000000..14fd3549fb
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.builder
@@ -0,0 +1 @@
+xml.test 'failed' \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb
new file mode 100644
index 0000000000..0c855a604b
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.html.erb
@@ -0,0 +1 @@
+<test>passed formatted html erb</test> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb
new file mode 100644
index 0000000000..6ca09d5304
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/formatted_xml_erb.xml.erb
@@ -0,0 +1 @@
+<test>passed formatted xml erb</test> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/greeting.html.erb b/actionview/test/fixtures/actionpack/test/greeting.html.erb
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/greeting.html.erb
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionview/test/fixtures/actionpack/test/greeting.xml.erb b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/greeting.xml.erb
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionview/test/fixtures/actionpack/test/hello,world.erb b/actionview/test/fixtures/actionpack/test/hello,world.erb
new file mode 100644
index 0000000000..bc8fa5e0ca
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello,world.erb
@@ -0,0 +1 @@
+Hello w*rld! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello.builder b/actionview/test/fixtures/actionpack/test/hello.builder
new file mode 100644
index 0000000000..a471553941
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello.builder
@@ -0,0 +1,4 @@
+xml.html do
+ xml.p "Hello #{@name}"
+ xml << render(:file => "test/greeting")
+end \ No newline at end of file
diff --git a/actionpack/test/fixtures/happy_path/render_action/hello_world.erb b/actionview/test/fixtures/actionpack/test/hello/hello.erb
index 6769dd60bd..6769dd60bd 100644
--- a/actionpack/test/fixtures/happy_path/render_action/hello_world.erb
+++ b/actionview/test/fixtures/actionpack/test/hello/hello.erb
diff --git a/actionview/test/fixtures/actionpack/test/hello_world.erb b/actionview/test/fixtures/actionpack/test/hello_world.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_container.builder b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
new file mode 100644
index 0000000000..e48d75c405
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_container.builder
@@ -0,0 +1,3 @@
+xml.test do
+ render :partial => 'hello', :locals => { :xm => xml }
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
new file mode 100644
index 0000000000..619a97ba96
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_from_rxml.builder
@@ -0,0 +1,3 @@
+xml.html do
+ xml.p "Hello"
+end
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_with_layout_false.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb b/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb
new file mode 100644
index 0000000000..ec31545356
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_world_with_partial.html.erb
@@ -0,0 +1,2 @@
+Hello world!
+<%= render '/test/partial' %>
diff --git a/actionview/test/fixtures/actionpack/test/hello_xml_world.builder b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
new file mode 100644
index 0000000000..e7081b89fe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hello_xml_world.builder
@@ -0,0 +1,11 @@
+xml.html do
+ xml.head do
+ xml.title "Hello World"
+ end
+
+ xml.body do
+ xml.p "abes"
+ xml.p "monks"
+ xml.p "wiseguys"
+ end
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/html_template.html.erb b/actionview/test/fixtures/actionpack/test/html_template.html.erb
new file mode 100644
index 0000000000..1bbc2b7f09
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/html_template.html.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/first_json_partial", formats: :json %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/hyphen-ated.erb b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
new file mode 100644
index 0000000000..cd0875583a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/hyphen-ated.erb
@@ -0,0 +1 @@
+Hello world!
diff --git a/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder b/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder
new file mode 100644
index 0000000000..2fcb32d247
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/implicit_content_type.atom.builder
@@ -0,0 +1,2 @@
+xml.atom do
+end
diff --git a/actionview/test/fixtures/actionpack/test/list.erb b/actionview/test/fixtures/actionpack/test/list.erb
new file mode 100644
index 0000000000..0a4bda58ee
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/list.erb
@@ -0,0 +1 @@
+<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %>
diff --git a/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
new file mode 100644
index 0000000000..d539a425a4
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/non_erb_block_content_for.builder
@@ -0,0 +1,4 @@
+content_for :title do
+ 'Putting stuff in the title!'
+end
+xml << "Great stuff!"
diff --git a/actionview/test/fixtures/actionpack/test/potential_conflicts.erb b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
new file mode 100644
index 0000000000..a5e964e359
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/potential_conflicts.erb
@@ -0,0 +1,4 @@
+First: <%= @name %>
+<%= render :partial => "person", :locals => { :name => "Stephan" } -%>
+Fourth: <%= @name %>
+Fifth: <%= name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/proper_block_detection.erb b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
new file mode 100644
index 0000000000..b55efbb25d
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/proper_block_detection.erb
@@ -0,0 +1 @@
+<%= @todo %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
new file mode 100644
index 0000000000..fde9f4bb64
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_from_template.html.erb
@@ -0,0 +1 @@
+<%= render :file => @path %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb b/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb b/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb
new file mode 100644
index 0000000000..ebe09faee6
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_locals.erb
@@ -0,0 +1 @@
+The secret is <%= secret %>
diff --git a/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
new file mode 100644
index 0000000000..9b4900acc5
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_file_with_locals_and_default.erb
@@ -0,0 +1 @@
+<%= secret ||= 'one' %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb
new file mode 100644
index 0000000000..0740b2d07c
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.da.html.erb
@@ -0,0 +1 @@
+Hey HTML!
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb
new file mode 100644
index 0000000000..4a11845cfe
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_html_template_from_xhr_request.html.erb
@@ -0,0 +1 @@
+Hello HTML! \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
new file mode 100644
index 0000000000..892ae5eca2
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_implicit_js_template_without_layout.js.erb
@@ -0,0 +1 @@
+alert('hello'); \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb b/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb
new file mode 100644
index 0000000000..1461b95186
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_partial_inside_directory.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %>
diff --git a/actionview/test/fixtures/actionpack/test/render_to_string_test.erb b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
new file mode 100644
index 0000000000..6e267e8634
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_to_string_test.erb
@@ -0,0 +1 @@
+The value of foo is: ::<%= @foo %>::
diff --git a/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
new file mode 100644
index 0000000000..3db6025860
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/render_two_partials.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'partial', :locals => {'first' => '1'} %>
+<%= render :partial => 'partial', :locals => {'second' => '2'} %>
diff --git a/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
new file mode 100644
index 0000000000..3d6661df9a
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/using_layout_around_block.html.erb
@@ -0,0 +1 @@
+<%= render(:layout => "layout_for_partial", :locals => { :name => "David" }) do %>Inside from block<% end %> \ No newline at end of file
diff --git a/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
new file mode 100644
index 0000000000..d84d909d64
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_html_partial.html.erb
@@ -0,0 +1 @@
+<strong><%= render :partial => "partial_only_html" %></strong>
diff --git a/actionview/test/fixtures/actionpack/test/with_partial.html.erb b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
new file mode 100644
index 0000000000..7502364cf5
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_partial.html.erb
@@ -0,0 +1 @@
+<strong><%= render :partial => "partial_only" %></strong>
diff --git a/actionview/test/fixtures/actionpack/test/with_partial.text.erb b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
new file mode 100644
index 0000000000..5f068ebf27
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_partial.text.erb
@@ -0,0 +1 @@
+**<%= render :partial => "partial_only" %>**
diff --git a/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
new file mode 100644
index 0000000000..e54a7cd001
--- /dev/null
+++ b/actionview/test/fixtures/actionpack/test/with_xml_template.html.erb
@@ -0,0 +1 @@
+<%= render :template => "test/greeting", :formats => :xml %>
diff --git a/actionpack/test/fixtures/blog_public/.gitignore b/actionview/test/fixtures/blog_public/.gitignore
index 312e635ee6..312e635ee6 100644
--- a/actionpack/test/fixtures/blog_public/.gitignore
+++ b/actionview/test/fixtures/blog_public/.gitignore
diff --git a/actionpack/test/fixtures/blog_public/blog.html b/actionview/test/fixtures/blog_public/blog.html
index 79ad44c010..79ad44c010 100644
--- a/actionpack/test/fixtures/blog_public/blog.html
+++ b/actionview/test/fixtures/blog_public/blog.html
diff --git a/actionpack/test/fixtures/blog_public/index.html b/actionview/test/fixtures/blog_public/index.html
index 2de3825481..2de3825481 100644
--- a/actionpack/test/fixtures/blog_public/index.html
+++ b/actionview/test/fixtures/blog_public/index.html
diff --git a/actionpack/test/fixtures/blog_public/subdir/index.html b/actionview/test/fixtures/blog_public/subdir/index.html
index 517bded335..517bded335 100644
--- a/actionpack/test/fixtures/blog_public/subdir/index.html
+++ b/actionview/test/fixtures/blog_public/subdir/index.html
diff --git a/actionpack/test/fixtures/comments/empty.de.html.erb b/actionview/test/fixtures/comments/empty.de.html.erb
index cffd90dd26..cffd90dd26 100644
--- a/actionpack/test/fixtures/comments/empty.de.html.erb
+++ b/actionview/test/fixtures/comments/empty.de.html.erb
diff --git a/actionpack/test/fixtures/comments/empty.html.builder b/actionview/test/fixtures/comments/empty.html.builder
index 2b0c7207a3..2b0c7207a3 100644
--- a/actionpack/test/fixtures/comments/empty.html.builder
+++ b/actionview/test/fixtures/comments/empty.html.builder
diff --git a/actionpack/test/fixtures/comments/empty.html.erb b/actionview/test/fixtures/comments/empty.html.erb
index 827f3861de..827f3861de 100644
--- a/actionpack/test/fixtures/comments/empty.html.erb
+++ b/actionview/test/fixtures/comments/empty.html.erb
diff --git a/actionpack/test/fixtures/comments/empty.xml.erb b/actionview/test/fixtures/comments/empty.xml.erb
index db1027cd7d..db1027cd7d 100644
--- a/actionpack/test/fixtures/comments/empty.xml.erb
+++ b/actionview/test/fixtures/comments/empty.xml.erb
diff --git a/actionpack/test/fixtures/companies.yml b/actionview/test/fixtures/companies.yml
index ed2992e0b1..ed2992e0b1 100644
--- a/actionpack/test/fixtures/companies.yml
+++ b/actionview/test/fixtures/companies.yml
diff --git a/actionview/test/fixtures/company.rb b/actionview/test/fixtures/company.rb
new file mode 100644
index 0000000000..f3ac3642fa
--- /dev/null
+++ b/actionview/test/fixtures/company.rb
@@ -0,0 +1,9 @@
+class Company < ActiveRecord::Base
+ has_one :mascot
+ self.sequence_name = :companies_nonstd_seq
+
+ validates_presence_of :name
+ def validate
+ errors.add('rating', 'rating should not be 2') if rating == 2
+ end
+end
diff --git a/actionpack/test/fixtures/custom_pattern/another.html.erb b/actionview/test/fixtures/custom_pattern/another.html.erb
index 6d7f3bafbb..6d7f3bafbb 100644
--- a/actionpack/test/fixtures/custom_pattern/another.html.erb
+++ b/actionview/test/fixtures/custom_pattern/another.html.erb
diff --git a/actionpack/test/fixtures/custom_pattern/html/another.erb b/actionview/test/fixtures/custom_pattern/html/another.erb
index dbd7e96ab6..dbd7e96ab6 100644
--- a/actionpack/test/fixtures/custom_pattern/html/another.erb
+++ b/actionview/test/fixtures/custom_pattern/html/another.erb
diff --git a/actionpack/test/fixtures/custom_pattern/html/path.erb b/actionview/test/fixtures/custom_pattern/html/path.erb
index 6d7f3bafbb..6d7f3bafbb 100644
--- a/actionpack/test/fixtures/custom_pattern/html/path.erb
+++ b/actionview/test/fixtures/custom_pattern/html/path.erb
diff --git a/actionview/test/fixtures/customers/_customer.html.erb b/actionview/test/fixtures/customers/_customer.html.erb
new file mode 100644
index 0000000000..483571e22a
--- /dev/null
+++ b/actionview/test/fixtures/customers/_customer.html.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= customer.name %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/db_definitions/sqlite.sql b/actionview/test/fixtures/db_definitions/sqlite.sql
index 99df4b3e61..99df4b3e61 100644
--- a/actionpack/test/fixtures/db_definitions/sqlite.sql
+++ b/actionview/test/fixtures/db_definitions/sqlite.sql
diff --git a/actionview/test/fixtures/developer.rb b/actionview/test/fixtures/developer.rb
new file mode 100644
index 0000000000..4941463015
--- /dev/null
+++ b/actionview/test/fixtures/developer.rb
@@ -0,0 +1,10 @@
+class Developer < ActiveRecord::Base
+ has_and_belongs_to_many :projects
+ has_many :replies
+ has_many :topics, :through => :replies
+ accepts_nested_attributes_for :projects
+end
+
+class DeVeLoPeR < ActiveRecord::Base
+ self.table_name = "developers"
+end
diff --git a/actionpack/test/fixtures/developers.yml b/actionview/test/fixtures/developers.yml
index 3656564f63..3656564f63 100644
--- a/actionpack/test/fixtures/developers.yml
+++ b/actionview/test/fixtures/developers.yml
diff --git a/actionpack/test/fixtures/developers/_developer.erb b/actionview/test/fixtures/developers/_developer.erb
index 904a3137e7..904a3137e7 100644
--- a/actionpack/test/fixtures/developers/_developer.erb
+++ b/actionview/test/fixtures/developers/_developer.erb
diff --git a/actionpack/test/fixtures/developers_projects.yml b/actionview/test/fixtures/developers_projects.yml
index cee359c7cf..cee359c7cf 100644
--- a/actionpack/test/fixtures/developers_projects.yml
+++ b/actionview/test/fixtures/developers_projects.yml
diff --git a/actionpack/test/fixtures/digestor/comments/_comment.html.erb b/actionview/test/fixtures/digestor/comments/_comment.html.erb
index f172e749da..f172e749da 100644
--- a/actionpack/test/fixtures/digestor/comments/_comment.html.erb
+++ b/actionview/test/fixtures/digestor/comments/_comment.html.erb
diff --git a/actionpack/test/fixtures/digestor/comments/_comments.html.erb b/actionview/test/fixtures/digestor/comments/_comments.html.erb
index c28646a283..c28646a283 100644
--- a/actionpack/test/fixtures/digestor/comments/_comments.html.erb
+++ b/actionview/test/fixtures/digestor/comments/_comments.html.erb
diff --git a/actionpack/test/fixtures/digestor/messages/_form.html.erb b/actionview/test/fixtures/digestor/events/_event.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/_form.html.erb
+++ b/actionview/test/fixtures/digestor/events/_event.html.erb
diff --git a/actionview/test/fixtures/digestor/level/_recursion.html.erb b/actionview/test/fixtures/digestor/level/_recursion.html.erb
new file mode 100644
index 0000000000..ee5aaf09c3
--- /dev/null
+++ b/actionview/test/fixtures/digestor/level/_recursion.html.erb
@@ -0,0 +1 @@
+<%= render 'recursion' %>
diff --git a/actionpack/test/fixtures/digestor/messages/_header.html.erb b/actionview/test/fixtures/digestor/level/below/_header.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/_header.html.erb
+++ b/actionview/test/fixtures/digestor/level/below/_header.html.erb
diff --git a/actionpack/test/fixtures/digestor/level/below/index.html.erb b/actionview/test/fixtures/digestor/level/below/index.html.erb
index b92f49a8f8..b92f49a8f8 100644
--- a/actionpack/test/fixtures/digestor/level/below/index.html.erb
+++ b/actionview/test/fixtures/digestor/level/below/index.html.erb
diff --git a/actionview/test/fixtures/digestor/level/recursion.html.erb b/actionview/test/fixtures/digestor/level/recursion.html.erb
new file mode 100644
index 0000000000..ee5aaf09c3
--- /dev/null
+++ b/actionview/test/fixtures/digestor/level/recursion.html.erb
@@ -0,0 +1 @@
+<%= render 'recursion' %>
diff --git a/actionpack/test/fixtures/digestor/messages/actions/_move.html.erb b/actionview/test/fixtures/digestor/messages/_form.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/actionpack/test/fixtures/digestor/messages/actions/_move.html.erb
+++ b/actionview/test/fixtures/digestor/messages/_form.html.erb
diff --git a/guides/code/getting_started/app/mailers/.gitkeep b/actionview/test/fixtures/digestor/messages/_header.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/app/mailers/.gitkeep
+++ b/actionview/test/fixtures/digestor/messages/_header.html.erb
diff --git a/actionpack/test/fixtures/digestor/messages/_message.html.erb b/actionview/test/fixtures/digestor/messages/_message.html.erb
index 406a0fb848..406a0fb848 100644
--- a/actionpack/test/fixtures/digestor/messages/_message.html.erb
+++ b/actionview/test/fixtures/digestor/messages/_message.html.erb
diff --git a/guides/code/getting_started/app/models/.gitkeep b/actionview/test/fixtures/digestor/messages/actions/_move.html.erb
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/app/models/.gitkeep
+++ b/actionview/test/fixtures/digestor/messages/actions/_move.html.erb
diff --git a/actionpack/test/fixtures/digestor/messages/edit.html.erb b/actionview/test/fixtures/digestor/messages/edit.html.erb
index a9e0a88e32..a9e0a88e32 100644
--- a/actionpack/test/fixtures/digestor/messages/edit.html.erb
+++ b/actionview/test/fixtures/digestor/messages/edit.html.erb
diff --git a/actionpack/test/fixtures/digestor/messages/index.html.erb b/actionview/test/fixtures/digestor/messages/index.html.erb
index 1937b652e4..1937b652e4 100644
--- a/actionpack/test/fixtures/digestor/messages/index.html.erb
+++ b/actionview/test/fixtures/digestor/messages/index.html.erb
diff --git a/actionview/test/fixtures/digestor/messages/show.html.erb b/actionview/test/fixtures/digestor/messages/show.html.erb
new file mode 100644
index 0000000000..42aa2363dd
--- /dev/null
+++ b/actionview/test/fixtures/digestor/messages/show.html.erb
@@ -0,0 +1,14 @@
+<%# 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/actionpack/test/fixtures/fun/games/_game.erb b/actionview/test/fixtures/fun/games/_game.erb
index f0f542ff92..f0f542ff92 100644
--- a/actionpack/test/fixtures/fun/games/_game.erb
+++ b/actionview/test/fixtures/fun/games/_game.erb
diff --git a/actionview/test/fixtures/fun/games/hello_world.erb b/actionview/test/fixtures/fun/games/hello_world.erb
new file mode 100644
index 0000000000..1ebfbe2539
--- /dev/null
+++ b/actionview/test/fixtures/fun/games/hello_world.erb
@@ -0,0 +1 @@
+Living in a nested world \ No newline at end of file
diff --git a/actionpack/test/fixtures/fun/serious/games/_game.erb b/actionview/test/fixtures/fun/serious/games/_game.erb
index 523bc55bd7..523bc55bd7 100644
--- a/actionpack/test/fixtures/fun/serious/games/_game.erb
+++ b/actionview/test/fixtures/fun/serious/games/_game.erb
diff --git a/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb b/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
new file mode 100644
index 0000000000..3125583a28
--- /dev/null
+++ b/actionview/test/fixtures/functional_caching/fragment_cached_without_digest.html.erb
@@ -0,0 +1,3 @@
+<body>
+<%= cache 'nodigest', skip_digest: true do %><p>ERB</p><% end %>
+</body>
diff --git a/actionpack/test/fixtures/games/_game.erb b/actionview/test/fixtures/games/_game.erb
index 1aeb81fcba..1aeb81fcba 100644
--- a/actionpack/test/fixtures/games/_game.erb
+++ b/actionview/test/fixtures/games/_game.erb
diff --git a/actionview/test/fixtures/good_customers/_good_customer.html.erb b/actionview/test/fixtures/good_customers/_good_customer.html.erb
new file mode 100644
index 0000000000..a2d97ebc6d
--- /dev/null
+++ b/actionview/test/fixtures/good_customers/_good_customer.html.erb
@@ -0,0 +1 @@
+<%= greeting %> good customer: <%= good_customer.name %><%= good_customer_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/happy_path/render_action/hello_world.erb b/actionview/test/fixtures/happy_path/render_action/hello_world.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/happy_path/render_action/hello_world.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionview/test/fixtures/helpers/abc_helper.rb b/actionview/test/fixtures/helpers/abc_helper.rb
new file mode 100644
index 0000000000..cf2774bb5f
--- /dev/null
+++ b/actionview/test/fixtures/helpers/abc_helper.rb
@@ -0,0 +1,3 @@
+module AbcHelper
+ def bare_a() end
+end
diff --git a/actionpack/test/fixtures/helpers/helpery_test_helper.rb b/actionview/test/fixtures/helpers/helpery_test_helper.rb
index a4f2951efa..a4f2951efa 100644
--- a/actionpack/test/fixtures/helpers/helpery_test_helper.rb
+++ b/actionview/test/fixtures/helpers/helpery_test_helper.rb
diff --git a/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
new file mode 100644
index 0000000000..d8801e54d5
--- /dev/null
+++ b/actionview/test/fixtures/helpers_missing/invalid_require_helper.rb
@@ -0,0 +1,5 @@
+require 'very_invalid_file_name'
+
+module InvalidRequireHelper
+end
+
diff --git a/actionpack/test/fixtures/layout_tests/alt/hello.erb b/actionview/test/fixtures/layout_tests/alt/hello.erb
index 1055c36659..1055c36659 100644
--- a/actionpack/test/fixtures/layout_tests/alt/hello.erb
+++ b/actionview/test/fixtures/layout_tests/alt/hello.erb
diff --git a/actionview/test/fixtures/layouts/_column.html.erb b/actionview/test/fixtures/layouts/_column.html.erb
new file mode 100644
index 0000000000..96db002b8a
--- /dev/null
+++ b/actionview/test/fixtures/layouts/_column.html.erb
@@ -0,0 +1,2 @@
+<div id="column"><%= yield :column %></div>
+<div id="content"><%= yield %></div> \ No newline at end of file
diff --git a/actionview/test/fixtures/layouts/_customers.erb b/actionview/test/fixtures/layouts/_customers.erb
new file mode 100644
index 0000000000..ae63f13cd3
--- /dev/null
+++ b/actionview/test/fixtures/layouts/_customers.erb
@@ -0,0 +1 @@
+<title><%= yield Struct.new(:name).new("David") %></title> \ No newline at end of file
diff --git a/actionview/test/fixtures/layouts/_partial_and_yield.erb b/actionview/test/fixtures/layouts/_partial_and_yield.erb
new file mode 100644
index 0000000000..74cc428ffa
--- /dev/null
+++ b/actionview/test/fixtures/layouts/_partial_and_yield.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'test/partial' %>
+<%= yield %>
diff --git a/actionview/test/fixtures/layouts/_yield_only.erb b/actionview/test/fixtures/layouts/_yield_only.erb
new file mode 100644
index 0000000000..37f0bddbd7
--- /dev/null
+++ b/actionview/test/fixtures/layouts/_yield_only.erb
@@ -0,0 +1 @@
+<%= yield %>
diff --git a/actionview/test/fixtures/layouts/_yield_with_params.erb b/actionview/test/fixtures/layouts/_yield_with_params.erb
new file mode 100644
index 0000000000..68e6557fb8
--- /dev/null
+++ b/actionview/test/fixtures/layouts/_yield_with_params.erb
@@ -0,0 +1 @@
+<%= yield 'Yield!' %>
diff --git a/actionview/test/fixtures/layouts/streaming.erb b/actionview/test/fixtures/layouts/streaming.erb
new file mode 100644
index 0000000000..d3f896a6ca
--- /dev/null
+++ b/actionview/test/fixtures/layouts/streaming.erb
@@ -0,0 +1,4 @@
+<%= yield :header -%>
+<%= yield -%>
+<%= yield :footer -%>
+<%= yield(:unknown).presence || "." -%> \ No newline at end of file
diff --git a/actionview/test/fixtures/layouts/yield.erb b/actionview/test/fixtures/layouts/yield.erb
new file mode 100644
index 0000000000..482dc9022e
--- /dev/null
+++ b/actionview/test/fixtures/layouts/yield.erb
@@ -0,0 +1,2 @@
+<title><%= yield :title %></title>
+<%= yield %>
diff --git a/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb b/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb
new file mode 100644
index 0000000000..7298d79690
--- /dev/null
+++ b/actionview/test/fixtures/layouts/yield_with_render_inline_inside.erb
@@ -0,0 +1,2 @@
+<%= render :inline => 'welcome' %>
+<%= yield %>
diff --git a/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb b/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb
new file mode 100644
index 0000000000..74cc428ffa
--- /dev/null
+++ b/actionview/test/fixtures/layouts/yield_with_render_partial_inside.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'test/partial' %>
+<%= yield %>
diff --git a/actionpack/test/fixtures/mascot.rb b/actionview/test/fixtures/mascot.rb
index f9f1448b8f..f9f1448b8f 100644
--- a/actionpack/test/fixtures/mascot.rb
+++ b/actionview/test/fixtures/mascot.rb
diff --git a/actionpack/test/fixtures/mascots.yml b/actionview/test/fixtures/mascots.yml
index 17b7dff454..17b7dff454 100644
--- a/actionpack/test/fixtures/mascots.yml
+++ b/actionview/test/fixtures/mascots.yml
diff --git a/actionpack/test/fixtures/mascots/_mascot.html.erb b/actionview/test/fixtures/mascots/_mascot.html.erb
index 432773a1da..432773a1da 100644
--- a/actionpack/test/fixtures/mascots/_mascot.html.erb
+++ b/actionview/test/fixtures/mascots/_mascot.html.erb
diff --git a/actionview/test/fixtures/multipart/bracketed_utf8_param b/actionview/test/fixtures/multipart/bracketed_utf8_param
new file mode 100644
index 0000000000..df9cecea08
--- /dev/null
+++ b/actionview/test/fixtures/multipart/bracketed_utf8_param
@@ -0,0 +1,5 @@
+--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name[Iñtërnâtiônàlizætiøn_nested_name]"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionview/test/fixtures/multipart/single_utf8_param b/actionview/test/fixtures/multipart/single_utf8_param
new file mode 100644
index 0000000000..1d9fae7b17
--- /dev/null
+++ b/actionview/test/fixtures/multipart/single_utf8_param
@@ -0,0 +1,5 @@
+--AaB03x
+Content-Disposition: form-data; name="Iñtërnâtiônàlizætiøn_name"
+
+Iñtërnâtiônàlizætiøn_value
+--AaB03x--
diff --git a/actionpack/test/fixtures/override/test/hello_world.erb b/actionview/test/fixtures/override/test/hello_world.erb
index 3e308d3d86..3e308d3d86 100644
--- a/actionpack/test/fixtures/override/test/hello_world.erb
+++ b/actionview/test/fixtures/override/test/hello_world.erb
diff --git a/actionpack/test/fixtures/override2/layouts/test/sub.erb b/actionview/test/fixtures/override2/layouts/test/sub.erb
index 3863d5a8ef..3863d5a8ef 100644
--- a/actionpack/test/fixtures/override2/layouts/test/sub.erb
+++ b/actionview/test/fixtures/override2/layouts/test/sub.erb
diff --git a/actionpack/test/fixtures/plain_text.raw b/actionview/test/fixtures/plain_text.raw
index b13985337f..b13985337f 100644
--- a/actionpack/test/fixtures/plain_text.raw
+++ b/actionview/test/fixtures/plain_text.raw
diff --git a/actionpack/test/fixtures/plain_text_with_characters.raw b/actionview/test/fixtures/plain_text_with_characters.raw
index 1e86e44fb4..1e86e44fb4 100644
--- a/actionpack/test/fixtures/plain_text_with_characters.raw
+++ b/actionview/test/fixtures/plain_text_with_characters.raw
diff --git a/actionpack/test/fixtures/project.rb b/actionview/test/fixtures/project.rb
index c124a9e605..c124a9e605 100644
--- a/actionpack/test/fixtures/project.rb
+++ b/actionview/test/fixtures/project.rb
diff --git a/actionpack/test/fixtures/projects.yml b/actionview/test/fixtures/projects.yml
index 02800c7824..02800c7824 100644
--- a/actionpack/test/fixtures/projects.yml
+++ b/actionview/test/fixtures/projects.yml
diff --git a/actionpack/test/fixtures/projects/_project.erb b/actionview/test/fixtures/projects/_project.erb
index 480c4c2af3..480c4c2af3 100644
--- a/actionpack/test/fixtures/projects/_project.erb
+++ b/actionview/test/fixtures/projects/_project.erb
diff --git a/actionpack/test/fixtures/public/.gitignore b/actionview/test/fixtures/public/.gitignore
index 312e635ee6..312e635ee6 100644
--- a/actionpack/test/fixtures/public/.gitignore
+++ b/actionview/test/fixtures/public/.gitignore
diff --git a/actionpack/test/fixtures/public/elsewhere/cools.js b/actionview/test/fixtures/public/elsewhere/cools.js
index 6e12fe29c4..6e12fe29c4 100644
--- a/actionpack/test/fixtures/public/elsewhere/cools.js
+++ b/actionview/test/fixtures/public/elsewhere/cools.js
diff --git a/actionpack/test/fixtures/public/elsewhere/file.css b/actionview/test/fixtures/public/elsewhere/file.css
index 6aea0733b1..6aea0733b1 100644
--- a/actionpack/test/fixtures/public/elsewhere/file.css
+++ b/actionview/test/fixtures/public/elsewhere/file.css
diff --git a/actionview/test/fixtures/public/foo/baz.css b/actionview/test/fixtures/public/foo/baz.css
new file mode 100644
index 0000000000..b5173fbef2
--- /dev/null
+++ b/actionview/test/fixtures/public/foo/baz.css
@@ -0,0 +1,3 @@
+body {
+background: #000;
+}
diff --git a/actionpack/test/fixtures/public/javascripts/application.js b/actionview/test/fixtures/public/javascripts/application.js
index 9702692980..9702692980 100644
--- a/actionpack/test/fixtures/public/javascripts/application.js
+++ b/actionview/test/fixtures/public/javascripts/application.js
diff --git a/actionpack/test/fixtures/public/javascripts/bank.js b/actionview/test/fixtures/public/javascripts/bank.js
index 4a1bee7182..4a1bee7182 100644
--- a/actionpack/test/fixtures/public/javascripts/bank.js
+++ b/actionview/test/fixtures/public/javascripts/bank.js
diff --git a/actionpack/test/fixtures/public/javascripts/common.javascript b/actionview/test/fixtures/public/javascripts/common.javascript
index 2ae1929056..2ae1929056 100644
--- a/actionpack/test/fixtures/public/javascripts/common.javascript
+++ b/actionview/test/fixtures/public/javascripts/common.javascript
diff --git a/actionpack/test/fixtures/public/javascripts/controls.js b/actionview/test/fixtures/public/javascripts/controls.js
index 88168d9f13..88168d9f13 100644
--- a/actionpack/test/fixtures/public/javascripts/controls.js
+++ b/actionview/test/fixtures/public/javascripts/controls.js
diff --git a/actionpack/test/fixtures/public/javascripts/dragdrop.js b/actionview/test/fixtures/public/javascripts/dragdrop.js
index c07061ac0c..c07061ac0c 100644
--- a/actionpack/test/fixtures/public/javascripts/dragdrop.js
+++ b/actionview/test/fixtures/public/javascripts/dragdrop.js
diff --git a/actionpack/test/fixtures/public/javascripts/effects.js b/actionview/test/fixtures/public/javascripts/effects.js
index b555d63034..b555d63034 100644
--- a/actionpack/test/fixtures/public/javascripts/effects.js
+++ b/actionview/test/fixtures/public/javascripts/effects.js
diff --git a/actionpack/test/fixtures/public/javascripts/prototype.js b/actionview/test/fixtures/public/javascripts/prototype.js
index 9780064a0e..9780064a0e 100644
--- a/actionpack/test/fixtures/public/javascripts/prototype.js
+++ b/actionview/test/fixtures/public/javascripts/prototype.js
diff --git a/actionpack/test/fixtures/public/javascripts/robber.js b/actionview/test/fixtures/public/javascripts/robber.js
index eb82fcbdf4..eb82fcbdf4 100644
--- a/actionpack/test/fixtures/public/javascripts/robber.js
+++ b/actionview/test/fixtures/public/javascripts/robber.js
diff --git a/actionpack/test/fixtures/public/javascripts/subdir/subdir.js b/actionview/test/fixtures/public/javascripts/subdir/subdir.js
index 9d23a67aa1..9d23a67aa1 100644
--- a/actionpack/test/fixtures/public/javascripts/subdir/subdir.js
+++ b/actionview/test/fixtures/public/javascripts/subdir/subdir.js
diff --git a/actionpack/test/fixtures/public/javascripts/version.1.0.js b/actionview/test/fixtures/public/javascripts/version.1.0.js
index cfd5fce70e..cfd5fce70e 100644
--- a/actionpack/test/fixtures/public/javascripts/version.1.0.js
+++ b/actionview/test/fixtures/public/javascripts/version.1.0.js
diff --git a/actionpack/test/fixtures/public/stylesheets/bank.css b/actionview/test/fixtures/public/stylesheets/bank.css
index ea161b12b2..ea161b12b2 100644
--- a/actionpack/test/fixtures/public/stylesheets/bank.css
+++ b/actionview/test/fixtures/public/stylesheets/bank.css
diff --git a/actionpack/test/fixtures/public/stylesheets/random.styles b/actionview/test/fixtures/public/stylesheets/random.styles
index d4eeead95c..d4eeead95c 100644
--- a/actionpack/test/fixtures/public/stylesheets/random.styles
+++ b/actionview/test/fixtures/public/stylesheets/random.styles
diff --git a/actionpack/test/fixtures/public/stylesheets/robber.css b/actionview/test/fixtures/public/stylesheets/robber.css
index 0fdd00a6a5..0fdd00a6a5 100644
--- a/actionpack/test/fixtures/public/stylesheets/robber.css
+++ b/actionview/test/fixtures/public/stylesheets/robber.css
diff --git a/actionpack/test/fixtures/public/stylesheets/subdir/subdir.css b/actionview/test/fixtures/public/stylesheets/subdir/subdir.css
index 241152a905..241152a905 100644
--- a/actionpack/test/fixtures/public/stylesheets/subdir/subdir.css
+++ b/actionview/test/fixtures/public/stylesheets/subdir/subdir.css
diff --git a/actionpack/test/fixtures/public/stylesheets/version.1.0.css b/actionview/test/fixtures/public/stylesheets/version.1.0.css
index 30f5f9ba6e..30f5f9ba6e 100644
--- a/actionpack/test/fixtures/public/stylesheets/version.1.0.css
+++ b/actionview/test/fixtures/public/stylesheets/version.1.0.css
diff --git a/actionpack/test/fixtures/replies.yml b/actionview/test/fixtures/replies.yml
index 2a3454b8bf..2a3454b8bf 100644
--- a/actionpack/test/fixtures/replies.yml
+++ b/actionview/test/fixtures/replies.yml
diff --git a/actionpack/test/fixtures/replies/_reply.erb b/actionview/test/fixtures/replies/_reply.erb
index 68baf548d8..68baf548d8 100644
--- a/actionpack/test/fixtures/replies/_reply.erb
+++ b/actionview/test/fixtures/replies/_reply.erb
diff --git a/actionpack/test/fixtures/reply.rb b/actionview/test/fixtures/reply.rb
index 047522c55b..047522c55b 100644
--- a/actionpack/test/fixtures/reply.rb
+++ b/actionview/test/fixtures/reply.rb
diff --git a/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb b/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb
new file mode 100644
index 0000000000..9f1f855269
--- /dev/null
+++ b/actionview/test/fixtures/respond_to/using_defaults_with_all.html.erb
@@ -0,0 +1 @@
+HTML!
diff --git a/actionview/test/fixtures/ruby_template.ruby b/actionview/test/fixtures/ruby_template.ruby
new file mode 100644
index 0000000000..5097bce47c
--- /dev/null
+++ b/actionview/test/fixtures/ruby_template.ruby
@@ -0,0 +1,2 @@
+body = ""
+body << ["Hello", "from", "Ruby", "code"].join(" ")
diff --git a/actionpack/test/fixtures/scope/test/modgreet.erb b/actionview/test/fixtures/scope/test/modgreet.erb
index 8947726e89..8947726e89 100644
--- a/actionpack/test/fixtures/scope/test/modgreet.erb
+++ b/actionview/test/fixtures/scope/test/modgreet.erb
diff --git a/actionview/test/fixtures/shared.html.erb b/actionview/test/fixtures/shared.html.erb
new file mode 100644
index 0000000000..af262fc9f8
--- /dev/null
+++ b/actionview/test/fixtures/shared.html.erb
@@ -0,0 +1 @@
+Elastica \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_200.html.erb b/actionview/test/fixtures/test/_200.html.erb
index c9f45675dc..c9f45675dc 100644
--- a/actionpack/test/fixtures/test/_200.html.erb
+++ b/actionview/test/fixtures/test/_200.html.erb
diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial.html.erb
index e918ba8f83..e918ba8f83 100644
--- a/actionpack/test/fixtures/test/_b_layout_for_partial.html.erb
+++ b/actionview/test/fixtures/test/_b_layout_for_partial.html.erb
diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb
index bdd53014cd..bdd53014cd 100644
--- a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object.html.erb
+++ b/actionview/test/fixtures/test/_b_layout_for_partial_with_object.html.erb
diff --git a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb b/actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb
index 44d6121297..44d6121297 100644
--- a/actionpack/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb
+++ b/actionview/test/fixtures/test/_b_layout_for_partial_with_object_counter.html.erb
diff --git a/actionview/test/fixtures/test/_changing_priority.html.erb b/actionview/test/fixtures/test/_changing_priority.html.erb
new file mode 100644
index 0000000000..3225efc49a
--- /dev/null
+++ b/actionview/test/fixtures/test/_changing_priority.html.erb
@@ -0,0 +1 @@
+HTML \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_changing_priority.json.erb b/actionview/test/fixtures/test/_changing_priority.json.erb
new file mode 100644
index 0000000000..7fa41dce66
--- /dev/null
+++ b/actionview/test/fixtures/test/_changing_priority.json.erb
@@ -0,0 +1 @@
+JSON \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb b/actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb
index 2f21a75dd9..2f21a75dd9 100644
--- a/actionpack/test/fixtures/test/_content_tag_nested_in_content_tag.erb
+++ b/actionview/test/fixtures/test/_content_tag_nested_in_content_tag.erb
diff --git a/actionview/test/fixtures/test/_counter.html.erb b/actionview/test/fixtures/test/_counter.html.erb
new file mode 100644
index 0000000000..fd245bfc70
--- /dev/null
+++ b/actionview/test/fixtures/test/_counter.html.erb
@@ -0,0 +1 @@
+<%= counter_counter %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_customer.erb b/actionview/test/fixtures/test/_customer.erb
new file mode 100644
index 0000000000..d8220afeda
--- /dev/null
+++ b/actionview/test/fixtures/test/_customer.erb
@@ -0,0 +1 @@
+Hello: <%= customer.name rescue "Anonymous" %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_customer_greeting.erb b/actionview/test/fixtures/test/_customer_greeting.erb
new file mode 100644
index 0000000000..6acbcb20c4
--- /dev/null
+++ b/actionview/test/fixtures/test/_customer_greeting.erb
@@ -0,0 +1 @@
+<%= greeting %>: <%= customer_greeting.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_customer_with_var.erb b/actionview/test/fixtures/test/_customer_with_var.erb
new file mode 100644
index 0000000000..00047dd20e
--- /dev/null
+++ b/actionview/test/fixtures/test/_customer_with_var.erb
@@ -0,0 +1 @@
+<%= customer.name %> <%= customer.name %> <%= customer.name %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb b/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb
new file mode 100644
index 0000000000..1cc8d41475
--- /dev/null
+++ b/actionview/test/fixtures/test/_directory/_partial_with_locales.html.erb
@@ -0,0 +1 @@
+Hello <%= name %>
diff --git a/actionview/test/fixtures/test/_first_json_partial.json.erb b/actionview/test/fixtures/test/_first_json_partial.json.erb
new file mode 100644
index 0000000000..790ee896db
--- /dev/null
+++ b/actionview/test/fixtures/test/_first_json_partial.json.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/second_json_partial" %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_from_helper.erb b/actionview/test/fixtures/test/_from_helper.erb
index 16de7c0f8a..16de7c0f8a 100644
--- a/actionpack/test/fixtures/test/_from_helper.erb
+++ b/actionview/test/fixtures/test/_from_helper.erb
diff --git a/guides/code/getting_started/lib/assets/.gitkeep b/actionview/test/fixtures/test/_json_change_priority.json.erb
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/lib/assets/.gitkeep
+++ b/actionview/test/fixtures/test/_json_change_priority.json.erb
diff --git a/actionpack/test/fixtures/test/_label_with_block.erb b/actionview/test/fixtures/test/_label_with_block.erb
index 40117e594e..40117e594e 100644
--- a/actionpack/test/fixtures/test/_label_with_block.erb
+++ b/actionview/test/fixtures/test/_label_with_block.erb
diff --git a/actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb b/actionview/test/fixtures/test/_layout_for_block_with_args.html.erb
index 307533208d..307533208d 100644
--- a/actionpack/test/fixtures/test/_layout_for_block_with_args.html.erb
+++ b/actionview/test/fixtures/test/_layout_for_block_with_args.html.erb
diff --git a/actionview/test/fixtures/test/_layout_for_partial.html.erb b/actionview/test/fixtures/test/_layout_for_partial.html.erb
new file mode 100644
index 0000000000..666efadbb6
--- /dev/null
+++ b/actionview/test/fixtures/test/_layout_for_partial.html.erb
@@ -0,0 +1,3 @@
+Before (<%= name %>)
+<%= yield %>
+After \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb b/actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb
index 820e7db789..820e7db789 100644
--- a/actionpack/test/fixtures/test/_layout_with_partial_and_yield.html.erb
+++ b/actionview/test/fixtures/test/_layout_with_partial_and_yield.html.erb
diff --git a/actionpack/test/fixtures/test/_local_inspector.html.erb b/actionview/test/fixtures/test/_local_inspector.html.erb
index e6765c0882..e6765c0882 100644
--- a/actionpack/test/fixtures/test/_local_inspector.html.erb
+++ b/actionview/test/fixtures/test/_local_inspector.html.erb
diff --git a/actionpack/test/fixtures/test/_object_inspector.erb b/actionview/test/fixtures/test/_object_inspector.erb
index 53af593821..53af593821 100644
--- a/actionpack/test/fixtures/test/_object_inspector.erb
+++ b/actionview/test/fixtures/test/_object_inspector.erb
diff --git a/actionpack/test/fixtures/test/_one.html.erb b/actionview/test/fixtures/test/_one.html.erb
index f796291cb4..f796291cb4 100644
--- a/actionpack/test/fixtures/test/_one.html.erb
+++ b/actionview/test/fixtures/test/_one.html.erb
diff --git a/actionview/test/fixtures/test/_partial.erb b/actionview/test/fixtures/test/_partial.erb
new file mode 100644
index 0000000000..e466dcbd8e
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial.erb
@@ -0,0 +1 @@
+invalid \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_partial.html.erb b/actionview/test/fixtures/test/_partial.html.erb
new file mode 100644
index 0000000000..e39f6c9827
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial.html.erb
@@ -0,0 +1 @@
+partial html \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_partial.js.erb b/actionview/test/fixtures/test/_partial.js.erb
new file mode 100644
index 0000000000..b350cdd7ef
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial.js.erb
@@ -0,0 +1 @@
+partial js \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb b/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb
new file mode 100644
index 0000000000..3a03a64e31
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_for_use_in_layout.html.erb
@@ -0,0 +1 @@
+Inside from partial (<%= name %>) \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_partial_name_local_variable.erb b/actionview/test/fixtures/test/_partial_name_local_variable.erb
new file mode 100644
index 0000000000..cc3a91c89f
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_name_local_variable.erb
@@ -0,0 +1 @@
+<%= partial_name_local_variable %>
diff --git a/actionview/test/fixtures/test/_partial_only.erb b/actionview/test/fixtures/test/_partial_only.erb
new file mode 100644
index 0000000000..a44b3eed40
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_only.erb
@@ -0,0 +1 @@
+only partial \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_partial_with_layout.erb b/actionview/test/fixtures/test/_partial_with_layout.erb
index 2a50c834fe..2a50c834fe 100644
--- a/actionpack/test/fixtures/test/_partial_with_layout.erb
+++ b/actionview/test/fixtures/test/_partial_with_layout.erb
diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb b/actionview/test/fixtures/test/_partial_with_layout_block_content.erb
index 65dafd93a8..65dafd93a8 100644
--- a/actionpack/test/fixtures/test/_partial_with_layout_block_content.erb
+++ b/actionview/test/fixtures/test/_partial_with_layout_block_content.erb
diff --git a/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb b/actionview/test/fixtures/test/_partial_with_layout_block_partial.erb
index 444197a7d0..444197a7d0 100644
--- a/actionpack/test/fixtures/test/_partial_with_layout_block_partial.erb
+++ b/actionview/test/fixtures/test/_partial_with_layout_block_partial.erb
diff --git a/actionpack/test/fixtures/test/_partial_with_only_html_version.html.erb b/actionview/test/fixtures/test/_partial_with_only_html_version.html.erb
index 00e6b6d6da..00e6b6d6da 100644
--- a/actionpack/test/fixtures/test/_partial_with_only_html_version.html.erb
+++ b/actionview/test/fixtures/test/_partial_with_only_html_version.html.erb
diff --git a/actionview/test/fixtures/test/_partial_with_partial.erb b/actionview/test/fixtures/test/_partial_with_partial.erb
new file mode 100644
index 0000000000..ee0d5037b6
--- /dev/null
+++ b/actionview/test/fixtures/test/_partial_with_partial.erb
@@ -0,0 +1,2 @@
+<%= render 'test/partial' %>
+partial with partial
diff --git a/actionpack/test/fixtures/test/_raise.html.erb b/actionview/test/fixtures/test/_raise.html.erb
index 68b08181d3..68b08181d3 100644
--- a/actionpack/test/fixtures/test/_raise.html.erb
+++ b/actionview/test/fixtures/test/_raise.html.erb
diff --git a/actionview/test/fixtures/test/_raise_indentation.html.erb b/actionview/test/fixtures/test/_raise_indentation.html.erb
new file mode 100644
index 0000000000..f9a93728fe
--- /dev/null
+++ b/actionview/test/fixtures/test/_raise_indentation.html.erb
@@ -0,0 +1,13 @@
+<p>First paragraph</p>
+<p>Second paragraph</p>
+<p>Third paragraph</p>
+<p>Fourth paragraph</p>
+<p>Fifth paragraph</p>
+<p>Sixth paragraph</p>
+<p>Seventh paragraph</p>
+<p>Eight paragraph</p>
+<p>Ninth paragraph</p>
+<p>Tenth paragraph</p>
+<%= raise "error here!" %>
+<p>Eleventh paragraph</p>
+<p>Twelfth paragraph</p> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/_second_json_partial.json.erb b/actionview/test/fixtures/test/_second_json_partial.json.erb
new file mode 100644
index 0000000000..5ebb7f1afd
--- /dev/null
+++ b/actionview/test/fixtures/test/_second_json_partial.json.erb
@@ -0,0 +1 @@
+Third level \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/_two.html.erb b/actionview/test/fixtures/test/_two.html.erb
index 5ab2f8a432..5ab2f8a432 100644
--- a/actionpack/test/fixtures/test/_two.html.erb
+++ b/actionview/test/fixtures/test/_two.html.erb
diff --git a/actionpack/test/fixtures/test/_utf8_partial.html.erb b/actionview/test/fixtures/test/_utf8_partial.html.erb
index 8d717fd427..8d717fd427 100644
--- a/actionpack/test/fixtures/test/_utf8_partial.html.erb
+++ b/actionview/test/fixtures/test/_utf8_partial.html.erb
diff --git a/actionpack/test/fixtures/test/_utf8_partial_magic.html.erb b/actionview/test/fixtures/test/_utf8_partial_magic.html.erb
index 4e2224610a..4e2224610a 100644
--- a/actionpack/test/fixtures/test/_utf8_partial_magic.html.erb
+++ b/actionview/test/fixtures/test/_utf8_partial_magic.html.erb
diff --git a/actionpack/test/fixtures/test/basic.html.erb b/actionview/test/fixtures/test/basic.html.erb
index ea696d7e01..ea696d7e01 100644
--- a/actionpack/test/fixtures/test/basic.html.erb
+++ b/actionview/test/fixtures/test/basic.html.erb
diff --git a/actionview/test/fixtures/test/calling_partial_with_layout.html.erb b/actionview/test/fixtures/test/calling_partial_with_layout.html.erb
new file mode 100644
index 0000000000..ac44bc0d81
--- /dev/null
+++ b/actionview/test/fixtures/test/calling_partial_with_layout.html.erb
@@ -0,0 +1 @@
+<%= render(:layout => "layout_for_partial", :partial => "partial_for_use_in_layout", :locals => { :name => "David" }) %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/change_priority.html.erb b/actionview/test/fixtures/test/change_priority.html.erb
new file mode 100644
index 0000000000..5618977d05
--- /dev/null
+++ b/actionview/test/fixtures/test/change_priority.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => "test/json_change_priority", formats: :json %>
+HTML Template, but <%= render :partial => "test/changing_priority" %> partial \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/dont_pick_me b/actionview/test/fixtures/test/dont_pick_me
index 0157c9e503..0157c9e503 100644
--- a/actionpack/test/fixtures/test/dont_pick_me
+++ b/actionview/test/fixtures/test/dont_pick_me
diff --git a/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb b/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/test/dot.directory/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/test/greeting.xml.erb b/actionview/test/fixtures/test/greeting.xml.erb
new file mode 100644
index 0000000000..62fb0293f0
--- /dev/null
+++ b/actionview/test/fixtures/test/greeting.xml.erb
@@ -0,0 +1 @@
+<p>This is grand!</p>
diff --git a/actionview/test/fixtures/test/hello.builder b/actionview/test/fixtures/test/hello.builder
new file mode 100644
index 0000000000..a471553941
--- /dev/null
+++ b/actionview/test/fixtures/test/hello.builder
@@ -0,0 +1,4 @@
+xml.html do
+ xml.p "Hello #{@name}"
+ xml << render(:file => "test/greeting")
+end \ No newline at end of file
diff --git a/actionview/test/fixtures/test/hello/hello.erb b/actionview/test/fixtures/test/hello/hello.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/test/hello/hello.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_world.da.html.erb b/actionview/test/fixtures/test/hello_world.da.html.erb
index 10ec443291..10ec443291 100644
--- a/actionpack/test/fixtures/test/hello_world.da.html.erb
+++ b/actionview/test/fixtures/test/hello_world.da.html.erb
diff --git a/actionview/test/fixtures/test/hello_world.erb b/actionview/test/fixtures/test/hello_world.erb
new file mode 100644
index 0000000000..6769dd60bd
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world.erb
@@ -0,0 +1 @@
+Hello world! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/hello_world.erb~ b/actionview/test/fixtures/test/hello_world.erb~
index 21934a1c95..21934a1c95 100644
--- a/actionpack/test/fixtures/test/hello_world.erb~
+++ b/actionview/test/fixtures/test/hello_world.erb~
diff --git a/actionpack/test/fixtures/test/hello_world.pt-BR.html.erb b/actionview/test/fixtures/test/hello_world.pt-BR.html.erb
index 773b3c8c6e..773b3c8c6e 100644
--- a/actionpack/test/fixtures/test/hello_world.pt-BR.html.erb
+++ b/actionview/test/fixtures/test/hello_world.pt-BR.html.erb
diff --git a/actionview/test/fixtures/test/hello_world_with_partial.html.erb b/actionview/test/fixtures/test/hello_world_with_partial.html.erb
new file mode 100644
index 0000000000..ec31545356
--- /dev/null
+++ b/actionview/test/fixtures/test/hello_world_with_partial.html.erb
@@ -0,0 +1,2 @@
+Hello world!
+<%= render '/test/partial' %>
diff --git a/actionview/test/fixtures/test/html_template.html.erb b/actionview/test/fixtures/test/html_template.html.erb
new file mode 100644
index 0000000000..1bbc2b7f09
--- /dev/null
+++ b/actionview/test/fixtures/test/html_template.html.erb
@@ -0,0 +1 @@
+<%= render :partial => "test/first_json_partial", formats: :json %> \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/layout_render_file.erb b/actionview/test/fixtures/test/layout_render_file.erb
index 2f8e921c5f..2f8e921c5f 100644
--- a/actionpack/test/fixtures/test/layout_render_file.erb
+++ b/actionview/test/fixtures/test/layout_render_file.erb
diff --git a/actionpack/test/fixtures/test/layout_render_object.erb b/actionview/test/fixtures/test/layout_render_object.erb
index acc4453c08..acc4453c08 100644
--- a/actionpack/test/fixtures/test/layout_render_object.erb
+++ b/actionview/test/fixtures/test/layout_render_object.erb
diff --git a/actionview/test/fixtures/test/list.erb b/actionview/test/fixtures/test/list.erb
new file mode 100644
index 0000000000..0a4bda58ee
--- /dev/null
+++ b/actionview/test/fixtures/test/list.erb
@@ -0,0 +1 @@
+<%= @test_unchanged = 'goodbye' %><%= render :partial => 'customer', :collection => @customers %><%= @test_unchanged %>
diff --git a/actionpack/test/fixtures/test/malformed/malformed.en.html.erb~ b/actionview/test/fixtures/test/malformed/malformed.en.html.erb~
index d009950384..d009950384 100644
--- a/actionpack/test/fixtures/test/malformed/malformed.en.html.erb~
+++ b/actionview/test/fixtures/test/malformed/malformed.en.html.erb~
diff --git a/actionpack/test/fixtures/test/malformed/malformed.erb~ b/actionview/test/fixtures/test/malformed/malformed.erb~
index d009950384..d009950384 100644
--- a/actionpack/test/fixtures/test/malformed/malformed.erb~
+++ b/actionview/test/fixtures/test/malformed/malformed.erb~
diff --git a/actionpack/test/fixtures/test/malformed/malformed.html.erb~ b/actionview/test/fixtures/test/malformed/malformed.html.erb~
index d009950384..d009950384 100644
--- a/actionpack/test/fixtures/test/malformed/malformed.html.erb~
+++ b/actionview/test/fixtures/test/malformed/malformed.html.erb~
diff --git a/actionview/test/fixtures/test/malformed/malformed~ b/actionview/test/fixtures/test/malformed/malformed~
new file mode 100644
index 0000000000..d009950384
--- /dev/null
+++ b/actionview/test/fixtures/test/malformed/malformed~
@@ -0,0 +1 @@
+Don't render me! \ No newline at end of file
diff --git a/actionpack/test/fixtures/test/nested_layout.erb b/actionview/test/fixtures/test/nested_layout.erb
index 6078f74b4c..6078f74b4c 100644
--- a/actionpack/test/fixtures/test/nested_layout.erb
+++ b/actionview/test/fixtures/test/nested_layout.erb
diff --git a/actionpack/test/fixtures/test/nested_streaming.erb b/actionview/test/fixtures/test/nested_streaming.erb
index 55525e0c92..55525e0c92 100644
--- a/actionpack/test/fixtures/test/nested_streaming.erb
+++ b/actionview/test/fixtures/test/nested_streaming.erb
diff --git a/actionpack/test/fixtures/test/one.html.erb b/actionview/test/fixtures/test/one.html.erb
index 0151874809..0151874809 100644
--- a/actionpack/test/fixtures/test/one.html.erb
+++ b/actionview/test/fixtures/test/one.html.erb
diff --git a/actionview/test/fixtures/test/render_file_with_ivar.erb b/actionview/test/fixtures/test/render_file_with_ivar.erb
new file mode 100644
index 0000000000..8b8a449236
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_with_ivar.erb
@@ -0,0 +1 @@
+The secret is <%= @secret %>
diff --git a/actionview/test/fixtures/test/render_file_with_locals.erb b/actionview/test/fixtures/test/render_file_with_locals.erb
new file mode 100644
index 0000000000..ebe09faee6
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_with_locals.erb
@@ -0,0 +1 @@
+The secret is <%= secret %>
diff --git a/actionview/test/fixtures/test/render_file_with_locals_and_default.erb b/actionview/test/fixtures/test/render_file_with_locals_and_default.erb
new file mode 100644
index 0000000000..9b4900acc5
--- /dev/null
+++ b/actionview/test/fixtures/test/render_file_with_locals_and_default.erb
@@ -0,0 +1 @@
+<%= secret ||= 'one' %> \ No newline at end of file
diff --git a/actionview/test/fixtures/test/render_partial_inside_directory.html.erb b/actionview/test/fixtures/test/render_partial_inside_directory.html.erb
new file mode 100644
index 0000000000..1461b95186
--- /dev/null
+++ b/actionview/test/fixtures/test/render_partial_inside_directory.html.erb
@@ -0,0 +1 @@
+<%= render partial: 'test/_directory/partial_with_locales', locals: {'name' => 'Jane'} %>
diff --git a/actionview/test/fixtures/test/render_two_partials.html.erb b/actionview/test/fixtures/test/render_two_partials.html.erb
new file mode 100644
index 0000000000..3db6025860
--- /dev/null
+++ b/actionview/test/fixtures/test/render_two_partials.html.erb
@@ -0,0 +1,2 @@
+<%= render :partial => 'partial', :locals => {'first' => '1'} %>
+<%= render :partial => 'partial', :locals => {'second' => '2'} %>
diff --git a/actionpack/test/fixtures/test/streaming.erb b/actionview/test/fixtures/test/streaming.erb
index fb9b8b1ade..fb9b8b1ade 100644
--- a/actionpack/test/fixtures/test/streaming.erb
+++ b/actionview/test/fixtures/test/streaming.erb
diff --git a/actionpack/test/fixtures/test/streaming_buster.erb b/actionview/test/fixtures/test/streaming_buster.erb
index 4221d56dcc..4221d56dcc 100644
--- a/actionpack/test/fixtures/test/streaming_buster.erb
+++ b/actionview/test/fixtures/test/streaming_buster.erb
diff --git a/actionpack/test/fixtures/test/sub_template_raise.html.erb b/actionview/test/fixtures/test/sub_template_raise.html.erb
index f38c0bda90..f38c0bda90 100644
--- a/actionpack/test/fixtures/test/sub_template_raise.html.erb
+++ b/actionview/test/fixtures/test/sub_template_raise.html.erb
diff --git a/actionpack/test/fixtures/test/template.erb b/actionview/test/fixtures/test/template.erb
index 785afa8f6a..785afa8f6a 100644
--- a/actionpack/test/fixtures/test/template.erb
+++ b/actionview/test/fixtures/test/template.erb
diff --git a/actionpack/test/fixtures/test/update_element_with_capture.erb b/actionview/test/fixtures/test/update_element_with_capture.erb
index fa3ef200f9..fa3ef200f9 100644
--- a/actionpack/test/fixtures/test/update_element_with_capture.erb
+++ b/actionview/test/fixtures/test/update_element_with_capture.erb
diff --git a/actionpack/test/fixtures/test/utf8.html.erb b/actionview/test/fixtures/test/utf8.html.erb
index ac98c2f012..ac98c2f012 100644
--- a/actionpack/test/fixtures/test/utf8.html.erb
+++ b/actionview/test/fixtures/test/utf8.html.erb
diff --git a/actionpack/test/fixtures/test/utf8_magic.html.erb b/actionview/test/fixtures/test/utf8_magic.html.erb
index 257279c29f..257279c29f 100644
--- a/actionpack/test/fixtures/test/utf8_magic.html.erb
+++ b/actionview/test/fixtures/test/utf8_magic.html.erb
diff --git a/actionpack/test/fixtures/test/utf8_magic_with_bare_partial.html.erb b/actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb
index cb22692f9a..cb22692f9a 100644
--- a/actionpack/test/fixtures/test/utf8_magic_with_bare_partial.html.erb
+++ b/actionview/test/fixtures/test/utf8_magic_with_bare_partial.html.erb
diff --git a/actionpack/test/fixtures/topic.rb b/actionview/test/fixtures/topic.rb
index 9fa9746535..9fa9746535 100644
--- a/actionpack/test/fixtures/topic.rb
+++ b/actionview/test/fixtures/topic.rb
diff --git a/actionpack/test/fixtures/topics.yml b/actionview/test/fixtures/topics.yml
index 7fdd49d54e..7fdd49d54e 100644
--- a/actionpack/test/fixtures/topics.yml
+++ b/actionview/test/fixtures/topics.yml
diff --git a/actionpack/test/fixtures/topics/_topic.html.erb b/actionview/test/fixtures/topics/_topic.html.erb
index 98659ca098..98659ca098 100644
--- a/actionpack/test/fixtures/topics/_topic.html.erb
+++ b/actionview/test/fixtures/topics/_topic.html.erb
diff --git a/actionpack/test/fixtures/translations/templates/array.erb b/actionview/test/fixtures/translations/templates/array.erb
index d86045a172..d86045a172 100644
--- a/actionpack/test/fixtures/translations/templates/array.erb
+++ b/actionview/test/fixtures/translations/templates/array.erb
diff --git a/actionpack/test/fixtures/translations/templates/default.erb b/actionview/test/fixtures/translations/templates/default.erb
index 8b70031071..8b70031071 100644
--- a/actionpack/test/fixtures/translations/templates/default.erb
+++ b/actionview/test/fixtures/translations/templates/default.erb
diff --git a/actionpack/test/fixtures/translations/templates/found.erb b/actionview/test/fixtures/translations/templates/found.erb
index 080c9c0aee..080c9c0aee 100644
--- a/actionpack/test/fixtures/translations/templates/found.erb
+++ b/actionview/test/fixtures/translations/templates/found.erb
diff --git a/actionpack/test/fixtures/translations/templates/missing.erb b/actionview/test/fixtures/translations/templates/missing.erb
index 0f3f17f8ef..0f3f17f8ef 100644
--- a/actionpack/test/fixtures/translations/templates/missing.erb
+++ b/actionview/test/fixtures/translations/templates/missing.erb
diff --git a/actionpack/test/fixtures/with_format.json.erb b/actionview/test/fixtures/with_format.json.erb
index a7f480ab1d..a7f480ab1d 100644
--- a/actionpack/test/fixtures/with_format.json.erb
+++ b/actionview/test/fixtures/with_format.json.erb
diff --git a/actionview/test/lib/controller/fake_models.rb b/actionview/test/lib/controller/fake_models.rb
new file mode 100644
index 0000000000..a463a08bb6
--- /dev/null
+++ b/actionview/test/lib/controller/fake_models.rb
@@ -0,0 +1,185 @@
+require "active_model"
+
+class Customer < Struct.new(:name, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ undef_method :to_json
+
+ def to_xml(options={})
+ if options[:builder]
+ options[:builder].name name
+ else
+ "<name>#{name}</name>"
+ end
+ end
+
+ def to_js(options={})
+ "name: #{name.inspect}"
+ end
+ alias :to_text :to_js
+
+ def errors
+ []
+ end
+
+ def persisted?
+ id.present?
+ end
+end
+
+class GoodCustomer < Customer
+end
+
+class Post < Struct.new(:title, :author_name, :body, :secret, :persisted, :written_on, :cost)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ extend ActiveModel::Translation
+
+ alias_method :secret?, :secret
+ alias_method :persisted?, :persisted
+
+ def initialize(*args)
+ super
+ @persisted = false
+ end
+
+ attr_accessor :author
+ def author_attributes=(attributes); end
+
+ attr_accessor :comments, :comment_ids
+ def comments_attributes=(attributes); end
+
+ attr_accessor :tags
+ def tags_attributes=(attributes); end
+end
+
+class Comment
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+ attr_reader :post_id
+ def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end
+ def to_key; id ? [id] : nil end
+ def save; @id = 1; @post_id = 1 end
+ def persisted?; @id.present? end
+ def to_param; @id.to_s; end
+ def name
+ @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
+ end
+
+ attr_accessor :relevances
+ def relevances_attributes=(attributes); end
+
+ attr_accessor :body
+end
+
+class Tag
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+ attr_reader :post_id
+ def initialize(id = nil, post_id = nil); @id, @post_id = id, post_id end
+ def to_key; id ? [id] : nil end
+ def save; @id = 1; @post_id = 1 end
+ def persisted?; @id.present? end
+ def to_param; @id; end
+ def value
+ @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
+ end
+
+ attr_accessor :relevances
+ def relevances_attributes=(attributes); end
+
+end
+
+class CommentRelevance
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+ attr_reader :comment_id
+ def initialize(id = nil, comment_id = nil); @id, @comment_id = id, comment_id end
+ def to_key; id ? [id] : nil end
+ def save; @id = 1; @comment_id = 1 end
+ def persisted?; @id.present? end
+ def to_param; @id; end
+ def value
+ @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
+ end
+end
+
+class Sheep
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+ def to_key; id ? [id] : nil end
+ def save; @id = 1 end
+ def new_record?; @id.nil? end
+ def name
+ @id.nil? ? 'new sheep' : "sheep ##{@id}"
+ end
+end
+
+class TagRelevance
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ attr_reader :id
+ attr_reader :tag_id
+ def initialize(id = nil, tag_id = nil); @id, @tag_id = id, tag_id end
+ def to_key; id ? [id] : nil end
+ def save; @id = 1; @tag_id = 1 end
+ def persisted?; @id.present? end
+ def to_param; @id; end
+ def value
+ @id.nil? ? "new #{self.class.name.downcase}" : "#{self.class.name.downcase} ##{@id}"
+ end
+end
+
+class Author < Comment
+ attr_accessor :post
+ def post_attributes=(attributes); end
+end
+
+class HashBackedAuthor < Hash
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?; false; end
+
+ def name
+ "hash backed author"
+ end
+end
+
+module Blog
+ def self.use_relative_model_naming?
+ true
+ end
+
+ class Post < Struct.new(:title, :id)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?
+ id.present?
+ end
+ end
+end
+
+class ArelLike
+ def to_ary
+ true
+ end
+ def each
+ a = Array.new(2) { |id| Comment.new(id + 1) }
+ a.each { |i| yield i }
+ end
+end
+
+class Car < Struct.new(:color)
+end
diff --git a/actionpack/test/template/active_model_helper_test.rb b/actionview/test/template/active_model_helper_test.rb
index 86bccdfade..86bccdfade 100644
--- a/actionpack/test/template/active_model_helper_test.rb
+++ b/actionview/test/template/active_model_helper_test.rb
diff --git a/actionview/test/template/asset_tag_helper_test.rb b/actionview/test/template/asset_tag_helper_test.rb
new file mode 100644
index 0000000000..214a13654e
--- /dev/null
+++ b/actionview/test/template/asset_tag_helper_test.rb
@@ -0,0 +1,762 @@
+require 'zlib'
+require 'abstract_unit'
+require 'active_support/ordered_options'
+
+class FakeController
+ attr_accessor :request
+
+ def config
+ @config ||= ActiveSupport::InheritableOptions.new(ActionController::Base.config)
+ end
+end
+
+class AssetTagHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ attr_reader :request
+
+ def setup
+ super
+
+ @controller = BasicController.new
+
+ @request = Class.new do
+ attr_accessor :script_name
+ def protocol() 'http://' end
+ def ssl?() false end
+ def host_with_port() 'localhost' end
+ def base_url() 'http://www.example.com' end
+ end.new
+
+ @controller.request = @request
+ end
+
+ def url_for(*args)
+ "http://www.example.com"
+ end
+
+ AssetPathToTag = {
+ %(asset_path("foo")) => %(/foo),
+ %(asset_path("style.css")) => %(/style.css),
+ %(asset_path("xmlhr.js")) => %(/xmlhr.js),
+ %(asset_path("xml.png")) => %(/xml.png),
+ %(asset_path("dir/xml.png")) => %(/dir/xml.png),
+ %(asset_path("/dir/xml.png")) => %(/dir/xml.png),
+
+ %(asset_path("script.min")) => %(/script.min),
+ %(asset_path("script.min.js")) => %(/script.min.js),
+ %(asset_path("style.min")) => %(/style.min),
+ %(asset_path("style.min.css")) => %(/style.min.css),
+
+ %(asset_path("http://www.outside.com/image.jpg")) => %(http://www.outside.com/image.jpg),
+ %(asset_path("HTTP://www.outside.com/image.jpg")) => %(HTTP://www.outside.com/image.jpg),
+
+ %(asset_path("style", type: :stylesheet)) => %(/stylesheets/style.css),
+ %(asset_path("xmlhr", type: :javascript)) => %(/javascripts/xmlhr.js),
+ %(asset_path("xml.png", type: :image)) => %(/images/xml.png)
+ }
+
+ AutoDiscoveryToTag = {
+ %(auto_discovery_link_tag) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:rss)) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:atom)) => %(<link href="http://www.example.com" rel="alternate" title="ATOM" type="application/atom+xml" />),
+ %(auto_discovery_link_tag(:rss, :action => "feed")) => %(<link href="http://www.example.com" rel="alternate" title="RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:rss, "http://localhost/feed")) => %(<link href="http://localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:rss, "//localhost/feed")) => %(<link href="//localhost/feed" rel="alternate" title="RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:rss, {:action => "feed"}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="application/rss+xml" />),
+ %(auto_discovery_link_tag(nil, {}, {:type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="" type="text/html" />),
+ %(auto_discovery_link_tag(nil, {}, {:title => "No stream.. really", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="No stream.. really" type="text/html" />),
+ %(auto_discovery_link_tag(:rss, {}, {:title => "My RSS", :type => "text/html"})) => %(<link href="http://www.example.com" rel="alternate" title="My RSS" type="text/html" />),
+ %(auto_discovery_link_tag(:atom, {}, {:rel => "Not so alternate"})) => %(<link href="http://www.example.com" rel="Not so alternate" title="ATOM" type="application/atom+xml" />),
+ }
+
+ JavascriptPathToTag = {
+ %(javascript_path("xmlhr")) => %(/javascripts/xmlhr.js),
+ %(javascript_path("super/xmlhr")) => %(/javascripts/super/xmlhr.js),
+ %(javascript_path("/super/xmlhr.js")) => %(/super/xmlhr.js),
+ %(javascript_path("xmlhr.min")) => %(/javascripts/xmlhr.min.js),
+ %(javascript_path("xmlhr.min.js")) => %(/javascripts/xmlhr.min.js),
+
+ %(javascript_path("xmlhr.js?123")) => %(/javascripts/xmlhr.js?123),
+ %(javascript_path("xmlhr.js?body=1")) => %(/javascripts/xmlhr.js?body=1),
+ %(javascript_path("xmlhr.js#hash")) => %(/javascripts/xmlhr.js#hash),
+ %(javascript_path("xmlhr.js?123#hash")) => %(/javascripts/xmlhr.js?123#hash)
+ }
+
+ PathToJavascriptToTag = {
+ %(path_to_javascript("xmlhr")) => %(/javascripts/xmlhr.js),
+ %(path_to_javascript("super/xmlhr")) => %(/javascripts/super/xmlhr.js),
+ %(path_to_javascript("/super/xmlhr.js")) => %(/super/xmlhr.js)
+ }
+
+ JavascriptUrlToTag = {
+ %(javascript_url("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js),
+ %(javascript_url("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js),
+ %(javascript_url("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js)
+ }
+
+ UrlToJavascriptToTag = {
+ %(url_to_javascript("xmlhr")) => %(http://www.example.com/javascripts/xmlhr.js),
+ %(url_to_javascript("super/xmlhr")) => %(http://www.example.com/javascripts/super/xmlhr.js),
+ %(url_to_javascript("/super/xmlhr.js")) => %(http://www.example.com/super/xmlhr.js)
+ }
+
+ JavascriptIncludeToTag = {
+ %(javascript_include_tag("bank")) => %(<script src="/javascripts/bank.js" ></script>),
+ %(javascript_include_tag("bank.js")) => %(<script src="/javascripts/bank.js" ></script>),
+ %(javascript_include_tag("bank", :lang => "vbscript")) => %(<script lang="vbscript" src="/javascripts/bank.js" ></script>),
+
+ %(javascript_include_tag("http://example.com/all")) => %(<script src="http://example.com/all"></script>),
+ %(javascript_include_tag("http://example.com/all.js")) => %(<script src="http://example.com/all.js"></script>),
+ %(javascript_include_tag("//example.com/all.js")) => %(<script src="//example.com/all.js"></script>),
+ }
+
+ StylePathToTag = {
+ %(stylesheet_path("bank")) => %(/stylesheets/bank.css),
+ %(stylesheet_path("bank.css")) => %(/stylesheets/bank.css),
+ %(stylesheet_path('subdir/subdir')) => %(/stylesheets/subdir/subdir.css),
+ %(stylesheet_path('/subdir/subdir.css')) => %(/subdir/subdir.css),
+ %(stylesheet_path("style.min")) => %(/stylesheets/style.min.css),
+ %(stylesheet_path("style.min.css")) => %(/stylesheets/style.min.css)
+ }
+
+ PathToStyleToTag = {
+ %(path_to_stylesheet("style")) => %(/stylesheets/style.css),
+ %(path_to_stylesheet("style.css")) => %(/stylesheets/style.css),
+ %(path_to_stylesheet('dir/file')) => %(/stylesheets/dir/file.css),
+ %(path_to_stylesheet('/dir/file.rcss', :extname => false)) => %(/dir/file.rcss),
+ %(path_to_stylesheet('/dir/file', :extname => '.rcss')) => %(/dir/file.rcss)
+ }
+
+ StyleUrlToTag = {
+ %(stylesheet_url("bank")) => %(http://www.example.com/stylesheets/bank.css),
+ %(stylesheet_url("bank.css")) => %(http://www.example.com/stylesheets/bank.css),
+ %(stylesheet_url('subdir/subdir')) => %(http://www.example.com/stylesheets/subdir/subdir.css),
+ %(stylesheet_url('/subdir/subdir.css')) => %(http://www.example.com/subdir/subdir.css)
+ }
+
+ UrlToStyleToTag = {
+ %(url_to_stylesheet("style")) => %(http://www.example.com/stylesheets/style.css),
+ %(url_to_stylesheet("style.css")) => %(http://www.example.com/stylesheets/style.css),
+ %(url_to_stylesheet('dir/file')) => %(http://www.example.com/stylesheets/dir/file.css),
+ %(url_to_stylesheet('/dir/file.rcss', :extname => false)) => %(http://www.example.com/dir/file.rcss),
+ %(url_to_stylesheet('/dir/file', :extname => '.rcss')) => %(http://www.example.com/dir/file.rcss)
+ }
+
+ StyleLinkToTag = {
+ %(stylesheet_link_tag("bank")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("bank.css")) => %(<link href="/stylesheets/bank.css" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("/elsewhere/file")) => %(<link href="/elsewhere/file.css" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("subdir/subdir")) => %(<link href="/stylesheets/subdir/subdir.css" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("bank", :media => "all")) => %(<link href="/stylesheets/bank.css" media="all" rel="stylesheet" />),
+
+ %(stylesheet_link_tag("http://www.example.com/styles/style")) => %(<link href="http://www.example.com/styles/style" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("http://www.example.com/styles/style.css")) => %(<link href="http://www.example.com/styles/style.css" media="screen" rel="stylesheet" />),
+ %(stylesheet_link_tag("//www.example.com/styles/style.css")) => %(<link href="//www.example.com/styles/style.css" media="screen" rel="stylesheet" />),
+ }
+
+ ImagePathToTag = {
+ %(image_path("xml")) => %(/images/xml),
+ %(image_path("xml.png")) => %(/images/xml.png),
+ %(image_path("dir/xml.png")) => %(/images/dir/xml.png),
+ %(image_path("/dir/xml.png")) => %(/dir/xml.png)
+ }
+
+ PathToImageToTag = {
+ %(path_to_image("xml")) => %(/images/xml),
+ %(path_to_image("xml.png")) => %(/images/xml.png),
+ %(path_to_image("dir/xml.png")) => %(/images/dir/xml.png),
+ %(path_to_image("/dir/xml.png")) => %(/dir/xml.png)
+ }
+
+ ImageUrlToTag = {
+ %(image_url("xml")) => %(http://www.example.com/images/xml),
+ %(image_url("xml.png")) => %(http://www.example.com/images/xml.png),
+ %(image_url("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png),
+ %(image_url("/dir/xml.png")) => %(http://www.example.com/dir/xml.png)
+ }
+
+ UrlToImageToTag = {
+ %(url_to_image("xml")) => %(http://www.example.com/images/xml),
+ %(url_to_image("xml.png")) => %(http://www.example.com/images/xml.png),
+ %(url_to_image("dir/xml.png")) => %(http://www.example.com/images/dir/xml.png),
+ %(url_to_image("/dir/xml.png")) => %(http://www.example.com/dir/xml.png)
+ }
+
+ ImageLinkToTag = {
+ %(image_tag("xml.png")) => %(<img alt="Xml" src="/images/xml.png" />),
+ %(image_tag("rss.gif", :alt => "rss syndication")) => %(<img alt="rss syndication" src="/images/rss.gif" />),
+ %(image_tag("gold.png", :size => "20")) => %(<img alt="Gold" height="20" src="/images/gold.png" width="20" />),
+ %(image_tag("gold.png", :size => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
+ %(image_tag("gold.png", "size" => "45x70")) => %(<img alt="Gold" height="70" src="/images/gold.png" width="45" />),
+ %(image_tag("error.png", "size" => "45 x 70")) => %(<img alt="Error" src="/images/error.png" />),
+ %(image_tag("error.png", "size" => "x")) => %(<img alt="Error" src="/images/error.png" />),
+ %(image_tag("google.com.png")) => %(<img alt="Google.com" src="/images/google.com.png" />),
+ %(image_tag("slash..png")) => %(<img alt="Slash." src="/images/slash..png" />),
+ %(image_tag(".pdf.png")) => %(<img alt=".pdf" src="/images/.pdf.png" />),
+ %(image_tag("http://www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="http://www.rubyonrails.com/images/rails.png" />),
+ %(image_tag("//www.rubyonrails.com/images/rails.png")) => %(<img alt="Rails" src="//www.rubyonrails.com/images/rails.png" />),
+ %(image_tag("mouse.png", :alt => nil)) => %(<img src="/images/mouse.png" />),
+ %(image_tag("data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", :alt => nil)) => %(<img src="data:image/gif;base64,R0lGODlhAQABAID/AMDAwAAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==" />),
+ %(image_tag("")) => %(<img src="" />)
+ }
+
+ 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 '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" />)
+ }
+
+ VideoPathToTag = {
+ %(video_path("xml")) => %(/videos/xml),
+ %(video_path("xml.ogg")) => %(/videos/xml.ogg),
+ %(video_path("dir/xml.ogg")) => %(/videos/dir/xml.ogg),
+ %(video_path("/dir/xml.ogg")) => %(/dir/xml.ogg)
+ }
+
+ PathToVideoToTag = {
+ %(path_to_video("xml")) => %(/videos/xml),
+ %(path_to_video("xml.ogg")) => %(/videos/xml.ogg),
+ %(path_to_video("dir/xml.ogg")) => %(/videos/dir/xml.ogg),
+ %(path_to_video("/dir/xml.ogg")) => %(/dir/xml.ogg)
+ }
+
+ VideoUrlToTag = {
+ %(video_url("xml")) => %(http://www.example.com/videos/xml),
+ %(video_url("xml.ogg")) => %(http://www.example.com/videos/xml.ogg),
+ %(video_url("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg),
+ %(video_url("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg)
+ }
+
+ UrlToVideoToTag = {
+ %(url_to_video("xml")) => %(http://www.example.com/videos/xml),
+ %(url_to_video("xml.ogg")) => %(http://www.example.com/videos/xml.ogg),
+ %(url_to_video("dir/xml.ogg")) => %(http://www.example.com/videos/dir/xml.ogg),
+ %(url_to_video("/dir/xml.ogg")) => %(http://www.example.com/dir/xml.ogg)
+ }
+
+ VideoLinkToTag = {
+ %(video_tag("xml.ogg")) => %(<video src="/videos/xml.ogg"></video>),
+ %(video_tag("rss.m4v", :autoplay => true, :controls => true)) => %(<video autoplay="autoplay" controls="controls" src="/videos/rss.m4v"></video>),
+ %(video_tag("rss.m4v", :autobuffer => true)) => %(<video autobuffer="autobuffer" src="/videos/rss.m4v"></video>),
+ %(video_tag("gold.m4v", :size => "160x120")) => %(<video height="120" src="/videos/gold.m4v" width="160"></video>),
+ %(video_tag("gold.m4v", "size" => "320x240")) => %(<video height="240" src="/videos/gold.m4v" width="320"></video>),
+ %(video_tag("trailer.ogg", :poster => "screenshot.png")) => %(<video poster="/images/screenshot.png" src="/videos/trailer.ogg"></video>),
+ %(video_tag("error.avi", "size" => "100")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("error.avi", "size" => "100 x 100")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("error.avi", "size" => "x")) => %(<video src="/videos/error.avi"></video>),
+ %(video_tag("http://media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="http://media.rubyonrails.org/video/rails_blog_2.mov"></video>),
+ %(video_tag("//media.rubyonrails.org/video/rails_blog_2.mov")) => %(<video src="//media.rubyonrails.org/video/rails_blog_2.mov"></video>),
+ %(video_tag("multiple.ogg", "multiple.avi")) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>),
+ %(video_tag(["multiple.ogg", "multiple.avi"])) => %(<video><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>),
+ %(video_tag(["multiple.ogg", "multiple.avi"], :size => "160x120", :controls => true)) => %(<video controls="controls" height="120" width="160"><source src="/videos/multiple.ogg" /><source src="/videos/multiple.avi" /></video>)
+ }
+
+ AudioPathToTag = {
+ %(audio_path("xml")) => %(/audios/xml),
+ %(audio_path("xml.wav")) => %(/audios/xml.wav),
+ %(audio_path("dir/xml.wav")) => %(/audios/dir/xml.wav),
+ %(audio_path("/dir/xml.wav")) => %(/dir/xml.wav)
+ }
+
+ PathToAudioToTag = {
+ %(path_to_audio("xml")) => %(/audios/xml),
+ %(path_to_audio("xml.wav")) => %(/audios/xml.wav),
+ %(path_to_audio("dir/xml.wav")) => %(/audios/dir/xml.wav),
+ %(path_to_audio("/dir/xml.wav")) => %(/dir/xml.wav)
+ }
+
+ AudioUrlToTag = {
+ %(audio_url("xml")) => %(http://www.example.com/audios/xml),
+ %(audio_url("xml.wav")) => %(http://www.example.com/audios/xml.wav),
+ %(audio_url("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav),
+ %(audio_url("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav)
+ }
+
+ UrlToAudioToTag = {
+ %(url_to_audio("xml")) => %(http://www.example.com/audios/xml),
+ %(url_to_audio("xml.wav")) => %(http://www.example.com/audios/xml.wav),
+ %(url_to_audio("dir/xml.wav")) => %(http://www.example.com/audios/dir/xml.wav),
+ %(url_to_audio("/dir/xml.wav")) => %(http://www.example.com/dir/xml.wav)
+ }
+
+ AudioLinkToTag = {
+ %(audio_tag("xml.wav")) => %(<audio src="/audios/xml.wav"></audio>),
+ %(audio_tag("rss.wav", :autoplay => true, :controls => true)) => %(<audio autoplay="autoplay" controls="controls" src="/audios/rss.wav"></audio>),
+ %(audio_tag("http://media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="http://media.rubyonrails.org/audio/rails_blog_2.mov"></audio>),
+ %(audio_tag("//media.rubyonrails.org/audio/rails_blog_2.mov")) => %(<audio src="//media.rubyonrails.org/audio/rails_blog_2.mov"></audio>),
+ %(audio_tag("audio.mp3", "audio.ogg")) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
+ %(audio_tag(["audio.mp3", "audio.ogg"])) => %(<audio><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>),
+ %(audio_tag(["audio.mp3", "audio.ogg"], :autobuffer => true, :controls => true)) => %(<audio autobuffer="autobuffer" controls="controls"><source src="/audios/audio.mp3" /><source src="/audios/audio.ogg" /></audio>)
+ }
+
+ FontPathToTag = {
+ %(font_path("font.eot")) => %(/fonts/font.eot),
+ %(font_path("font.eot#iefix")) => %(/fonts/font.eot#iefix),
+ %(font_path("font.woff")) => %(/fonts/font.woff),
+ %(font_path("font.ttf")) => %(/fonts/font.ttf),
+ %(font_path("font.ttf?123")) => %(/fonts/font.ttf?123)
+ }
+
+ def test_autodiscovery_link_tag_with_unknown_type_but_not_pass_type_option_key
+ assert_raise(ArgumentError) do
+ auto_discovery_link_tag(:xml)
+ end
+ end
+
+ def test_autodiscovery_link_tag_with_unknown_type
+ result = auto_discovery_link_tag(:xml, '/feed.xml', :type => 'application/xml')
+ expected = %(<link href="/feed.xml" rel="alternate" title="XML" type="application/xml" />)
+ assert_equal expected, result
+ end
+
+ def test_asset_path_tag
+ AssetPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ 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")
+ assert_equal "/javascripts/foo.js", compute_asset_path("foo.js", :type => :javascript)
+ assert_equal "/javascripts/foo.js", compute_asset_path("/foo.js", :type => :javascript)
+ assert_equal "/stylesheets/foo.css", compute_asset_path("foo.css", :type => :stylesheet)
+ end
+
+ def test_auto_discovery_link_tag
+ AutoDiscoveryToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_javascript_path
+ JavascriptPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_javascript_alias_for_javascript_path
+ PathToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_javascript_url
+ JavascriptUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_javascript_alias_for_javascript_url
+ UrlToJavascriptToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_javascript_include_tag
+ JavascriptIncludeToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_javascript_include_tag_with_missing_source
+ assert_nothing_raised {
+ javascript_include_tag('missing_security_guard')
+ }
+
+ assert_nothing_raised {
+ javascript_include_tag('http://example.com/css/missing_security_guard')
+ }
+ end
+
+ def test_javascript_include_tag_is_html_safe
+ assert javascript_include_tag("prototype").html_safe?
+ end
+
+ def test_javascript_include_tag_relative_protocol
+ @controller.config.asset_host = "assets.example.com"
+ assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype', protocol: :relative)
+ end
+
+ def test_javascript_include_tag_default_protocol
+ @controller.config.asset_host = "assets.example.com"
+ @controller.config.default_asset_host_protocol = :relative
+ assert_dom_equal %(<script src="//assets.example.com/javascripts/prototype.js"></script>), javascript_include_tag('prototype')
+ end
+
+ def test_stylesheet_path
+ StylePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_stylesheet_alias_for_stylesheet_path
+ PathToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_stylesheet_url
+ StyleUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_stylesheet_alias_for_stylesheet_url
+ UrlToStyleToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_stylesheet_link_tag
+ StyleLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_stylesheet_link_tag_with_missing_source
+ assert_nothing_raised {
+ stylesheet_link_tag('missing_security_guard')
+ }
+
+ assert_nothing_raised {
+ stylesheet_link_tag('http://example.com/css/missing_security_guard')
+ }
+ end
+
+ def test_stylesheet_link_tag_is_html_safe
+ assert stylesheet_link_tag('dir/file').html_safe?
+ assert stylesheet_link_tag('dir/other/file', 'dir/file2').html_safe?
+ end
+
+ def test_stylesheet_link_tag_escapes_options
+ assert_dom_equal %(<link href="/file.css" media="&lt;script&gt;" rel="stylesheet" />), stylesheet_link_tag('/file', :media => '<script>')
+ end
+
+ def test_stylesheet_link_tag_should_not_output_the_same_asset_twice
+ assert_dom_equal %(<link href="/stylesheets/wellington.css" media="screen" rel="stylesheet" />\n<link href="/stylesheets/amsterdam.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', 'wellington', 'amsterdam')
+ end
+
+ def test_stylesheet_link_tag_with_relative_protocol
+ @controller.config.asset_host = "assets.example.com"
+ assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington', protocol: :relative)
+ end
+
+ def test_stylesheet_link_tag_with_default_protocol
+ @controller.config.asset_host = "assets.example.com"
+ @controller.config.default_asset_host_protocol = :relative
+ assert_dom_equal %(<link href="//assets.example.com/stylesheets/wellington.css" media="screen" rel="stylesheet" />), stylesheet_link_tag('wellington')
+ end
+
+ def test_image_path
+ ImagePathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_image_alias_for_image_path
+ PathToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_image_url
+ ImageUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_image_alias_for_image_url
+ UrlToImageToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_image_alt
+ [nil, '/', '/foo/bar/', 'foo/bar/'].each do |prefix|
+ assert_equal 'Rails', image_alt("#{prefix}rails.png")
+ assert_equal 'Rails', image_alt("#{prefix}rails-9c0a079bdd7701d7e729bd956823d153.png")
+ assert_equal 'Long file name with hyphens', image_alt("#{prefix}long-file-name-with-hyphens.png")
+ assert_equal 'Long file name with underscores', image_alt("#{prefix}long_file_name_with_underscores.png")
+ end
+ end
+
+ def test_image_tag
+ ImageLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_image_tag_does_not_modify_options
+ options = {:size => '16x10'}
+ image_tag('icon', options)
+ assert_equal({:size => '16x10'}, options)
+ end
+
+ def test_favicon_link_tag
+ FaviconLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_video_path
+ VideoPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_video_alias_for_video_path
+ PathToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_video_url
+ VideoUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_video_alias_for_video_url
+ UrlToVideoToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_video_tag
+ VideoLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_audio_path
+ AudioPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_path_to_audio_alias_for_audio_path
+ PathToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_audio_url
+ AudioUrlToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_url_to_audio_alias_for_audio_url
+ UrlToAudioToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_audio_tag
+ AudioLinkToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_font_path
+ FontPathToTag.each { |method, tag| assert_dom_equal(tag, eval(method)) }
+ end
+
+ def test_video_audio_tag_does_not_modify_options
+ options = {:autoplay => true}
+ video_tag('video', options)
+ assert_equal({:autoplay => true}, options)
+ audio_tag('audio', options)
+ assert_equal({:autoplay => true}, options)
+ end
+
+ def test_image_tag_interpreting_email_cid_correctly
+ # An inline image has no need for an alt tag to be automatically generated from the cid:
+ assert_equal '<img src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid")
+ end
+
+ def test_image_tag_interpreting_email_adding_optional_alt_tag
+ assert_equal '<img alt="Image" src="cid:thi%25%25sis@acontentid" />', image_tag("cid:thi%25%25sis@acontentid", :alt => "Image")
+ end
+
+ def test_should_not_modify_source_string
+ source = '/images/rails.png'
+ copy = source.dup
+ image_tag(source)
+ assert_equal copy, source
+ end
+
+ def test_image_path_with_asset_host_proc_returning_nil
+ @controller.config.asset_host = Proc.new do |source|
+ unless source.end_with?("tiff")
+ "cdn.example.com"
+ end
+ end
+
+ assert_equal "/images/file.tiff", image_path("file.tiff")
+ assert_equal "http://cdn.example.com/images/file.png", image_path("file.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?
+ "#{request.protocol}#{request.host_with_port}"
+ else
+ "#{request.protocol}assets#{source.length}.example.com"
+ end
+ end
+
+ @controller.request.stubs(:ssl?).returns(false)
+ assert_equal "http://assets15.example.com/images/xml.png", image_path("xml.png")
+
+ @controller.request.stubs(:ssl?).returns(true)
+ assert_equal "http://localhost/images/xml.png", image_path("xml.png")
+ end
+end
+
+class AssetTagHelperNonVhostTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetTagHelper
+
+ attr_reader :request
+
+ def setup
+ super
+ @controller = BasicController.new
+ @controller.config.relative_url_root = "/collaboration/hieraki"
+
+ @request = Struct.new(:protocol, :base_url).new("gopher://", "gopher://www.example.com")
+ @controller.request = @request
+ end
+
+ def url_for(options)
+ "http://www.example.com/collaboration/hieraki"
+ end
+
+ def test_should_compute_proper_path
+ assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
+ assert_dom_equal(%(/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
+ assert_dom_equal(%(/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
+ assert_dom_equal(%(/collaboration/hieraki/images/xml.png), image_path("xml.png"))
+ end
+
+ def test_should_return_nothing_if_asset_host_isnt_configured
+ assert_equal nil, compute_asset_host("foo")
+ end
+
+ def test_should_current_request_host_is_always_returned_for_request
+ assert_equal "gopher://www.example.com", compute_asset_host("foo", :protocol => :request)
+ 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
+
+ def test_should_return_simple_string_asset_host
+ @controller.config.asset_host = "assets.example.com"
+ assert_equal "gopher://assets.example.com", compute_asset_host("foo")
+ end
+
+ def test_should_return_relative_asset_host
+ @controller.config.asset_host = "assets.example.com"
+ assert_equal "//assets.example.com", compute_asset_host("foo", :protocol => :relative)
+ end
+
+ def test_should_return_custom_protocol_asset_host
+ @controller.config.asset_host = "assets.example.com"
+ assert_equal "ftp://assets.example.com", compute_asset_host("foo", :protocol => "ftp")
+ end
+
+ def test_should_compute_proper_path_with_asset_host
+ @controller.config.asset_host = "assets.example.com"
+ assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png"))
+ end
+
+ def test_should_compute_proper_path_with_asset_host_and_default_protocol
+ @controller.config.asset_host = "assets.example.com"
+ @controller.config.default_asset_host_protocol = :request
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_path("xmlhr"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_path("style"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_path("xml.png"))
+ end
+
+ def test_should_compute_proper_url_with_asset_host
+ @controller.config.asset_host = "assets.example.com"
+ assert_dom_equal(%(<link href="http://www.example.com/collaboration/hieraki" rel="alternate" title="RSS" type="application/rss+xml" />), auto_discovery_link_tag)
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png"))
+ end
+
+ def test_should_compute_proper_url_with_asset_host_and_default_protocol
+ @controller.config.asset_host = "assets.example.com"
+ @controller.config.default_asset_host_protocol = :request
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/javascripts/xmlhr.js), javascript_url("xmlhr"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/stylesheets/style.css), stylesheet_url("style"))
+ assert_dom_equal(%(gopher://assets.example.com/collaboration/hieraki/images/xml.png), image_url("xml.png"))
+ end
+
+ def test_should_return_asset_host_with_protocol
+ @controller.config.asset_host = "http://assets.example.com"
+ assert_equal "http://assets.example.com", compute_asset_host("foo")
+ end
+
+ def test_should_ignore_asset_host_on_complete_url
+ @controller.config.asset_host = "http://assets.example.com"
+ assert_dom_equal(%(<link href="http://bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("http://bar.example.com/stylesheets/style.css"))
+ end
+
+ def test_should_ignore_asset_host_on_scheme_relative_url
+ @controller.config.asset_host = "http://assets.example.com"
+ assert_dom_equal(%(<link href="//bar.example.com/stylesheets/style.css" media="screen" rel="stylesheet" />), stylesheet_link_tag("//bar.example.com/stylesheets/style.css"))
+ end
+
+ def test_should_wildcard_asset_host
+ @controller.config.asset_host = 'http://a%d.example.com'
+ assert_match(%r(http://a[0123].example.com), compute_asset_host("foo"))
+ end
+
+ def test_should_wildcard_asset_host_between_zero_and_four
+ @controller.config.asset_host = 'http://a%d.example.com'
+ assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_path('xml.png'))
+ assert_match(%r(http://a[0123].example.com/collaboration/hieraki/images/xml.png), image_url('xml.png'))
+ end
+
+ def test_asset_host_without_protocol_should_be_protocol_relative
+ @controller.config.asset_host = 'a.example.com'
+ assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_path('xml.png')
+ assert_equal 'gopher://a.example.com/collaboration/hieraki/images/xml.png', image_url('xml.png')
+ end
+
+ def test_asset_host_without_protocol_should_be_protocol_relative_even_if_path_present
+ @controller.config.asset_host = 'a.example.com/files/go/here'
+ assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_path('xml.png')
+ assert_equal 'gopher://a.example.com/files/go/here/collaboration/hieraki/images/xml.png', image_url('xml.png')
+ end
+
+ def test_assert_css_and_js_of_the_same_name_return_correct_extension
+ assert_dom_equal(%(/collaboration/hieraki/javascripts/foo.js), javascript_path("foo"))
+ assert_dom_equal(%(/collaboration/hieraki/stylesheets/foo.css), stylesheet_path("foo"))
+ end
+end
+
+class AssetUrlHelperControllerTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetUrlHelper
+
+ def setup
+ super
+
+ @controller = BasicController.new
+ @controller.extend ActionView::Helpers::AssetUrlHelper
+
+ @request = Class.new do
+ attr_accessor :script_name
+ def protocol() 'http://' end
+ def ssl?() false end
+ def host_with_port() 'www.example.com' end
+ def base_url() 'http://www.example.com' end
+ end.new
+
+ @controller.request = @request
+ end
+
+ def test_asset_path
+ assert_equal "/foo", @controller.asset_path("foo")
+ end
+
+ def test_asset_url
+ assert_equal "http://www.example.com/foo", @controller.asset_url("foo")
+ end
+end
+
+class AssetUrlHelperEmptyModuleTest < ActionView::TestCase
+ tests ActionView::Helpers::AssetUrlHelper
+
+ def setup
+ super
+
+ @module = Module.new
+ @module.extend ActionView::Helpers::AssetUrlHelper
+ end
+
+ def test_asset_path
+ assert_equal "/foo", @module.asset_path("foo")
+ end
+
+ def test_asset_url
+ assert_equal "/foo", @module.asset_url("foo")
+ end
+
+ def test_asset_url_with_request
+ @module.instance_eval do
+ def request
+ Struct.new(:base_url, :script_name).new("http://www.example.com", nil)
+ end
+ end
+
+ assert @module.request
+ assert_equal "http://www.example.com/foo", @module.asset_url("foo")
+ end
+
+ def test_asset_url_with_config_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://www.example.com/foo", @module.asset_url("foo")
+ end
+end
diff --git a/actionview/test/template/atom_feed_helper_test.rb b/actionview/test/template/atom_feed_helper_test.rb
new file mode 100644
index 0000000000..63b5ac0fab
--- /dev/null
+++ b/actionview/test/template/atom_feed_helper_test.rb
@@ -0,0 +1,349 @@
+require 'abstract_unit'
+
+class Scroll < Struct.new(:id, :to_param, :title, :body, :updated_at, :created_at)
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+
+ def persisted?
+ false
+ end
+end
+
+class ScrollsController < ActionController::Base
+ FEEDS = {}
+ FEEDS["defaults"] = <<-EOT
+ atom_feed(:schema_date => '2008') do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["entry_options"] = <<-EOT
+ atom_feed do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["entry_type_options"] = <<-EOT
+ atom_feed(:schema_date => '2008') do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll, :type => 'text/xml') do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["xml_block"] = <<-EOT
+ atom_feed do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ feed.author do |author|
+ author.name("DHH")
+ end
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll, :url => "/otherstuff/" + scroll.to_param.to_s, :updated => Time.utc(2007, 1, scroll.id)) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+ end
+ end
+ end
+ EOT
+ FEEDS["feed_with_atomPub_namespace"] = <<-EOT
+ atom_feed({'xmlns:app' => 'http://www.w3.org/2007/app',
+ 'xmlns:openSearch' => 'http://a9.com/-/spec/opensearch/1.1/'}) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+ entry.tag!('app:edited', Time.now)
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["feed_with_overridden_ids"] = <<-EOT
+ atom_feed({:id => 'tag:test.rubyonrails.org,2008:test/'}) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll, :id => "tag:test.rubyonrails.org,2008:"+scroll.id.to_s) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+ entry.tag!('app:edited', Time.now)
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["feed_with_xml_processing_instructions"] = <<-EOT
+ atom_feed(:schema_date => '2008',
+ :instruct => {'xml-stylesheet' => { :href=> 't.css', :type => 'text/css' }}) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["feed_with_xml_processing_instructions_duplicate_targets"] = <<-EOT
+ atom_feed(:schema_date => '2008',
+ :instruct => {'target1' => [{ :a => '1', :b => '2' }, { :c => '3', :d => '4' }]}) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["feed_with_xhtml_content"] = <<-'EOT'
+ atom_feed do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.summary(:type => 'xhtml') do |xhtml|
+ xhtml.p "before #{scroll.id}"
+ xhtml.p {xhtml << scroll.body}
+ xhtml.p "after #{scroll.id}"
+ end
+ entry.tag!('app:edited', Time.now)
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ FEEDS["provide_builder"] = <<-'EOT'
+ # we pass in the new_xml to the helper so it doesn't
+ # call anything on the original builder
+ new_xml = Builder::XmlMarkup.new(:target=>'')
+ atom_feed(:xml => new_xml) do |feed|
+ feed.title("My great blog!")
+ feed.updated((@scrolls.first.created_at))
+
+ @scrolls.each do |scroll|
+ feed.entry(scroll) do |entry|
+ entry.title(scroll.title)
+ entry.content(scroll.body, :type => 'html')
+
+ entry.author do |author|
+ author.name("DHH")
+ end
+ end
+ end
+ end
+ EOT
+ def index
+ @scrolls = [
+ Scroll.new(1, "1", "Hello One", "Something <i>COOL!</i>", Time.utc(2007, 12, 12, 15), Time.utc(2007, 12, 12, 15)),
+ Scroll.new(2, "2", "Hello Two", "Something Boring", Time.utc(2007, 12, 12, 15)),
+ ]
+
+ render :inline => FEEDS[params[:id]], :type => :builder
+ end
+end
+
+class AtomFeedTest < ActionController::TestCase
+ tests ScrollsController
+
+ def setup
+ super
+ @request.host = "www.nextangle.com"
+ end
+
+ def test_feed_should_use_default_language_if_none_is_given
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_match(%r{xml:lang="en-US"}, @response.body)
+ end
+ end
+
+ def test_feed_should_include_two_entries
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_select "entry", 2
+ end
+ end
+
+ def test_entry_should_only_use_published_if_created_at_is_present
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_select "published", 1
+ end
+ end
+
+ def test_providing_builder_to_atom_feed
+ with_restful_routing(:scrolls) do
+ get :index, :id=>"provide_builder"
+ # because we pass in the non-default builder, the content generated by the
+ # helper should go 'nowhere'. Leaving the response body blank.
+ assert @response.body.blank?
+ end
+ end
+
+ def test_entry_with_prefilled_options_should_use_those_instead_of_querying_the_record
+ with_restful_routing(:scrolls) do
+ get :index, :id => "entry_options"
+
+ assert_select "updated", Time.utc(2007, 1, 1).xmlschema
+ assert_select "updated", Time.utc(2007, 1, 2).xmlschema
+ end
+ end
+
+ def test_self_url_should_default_to_current_request_url
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_select "link[rel=self][href=http://www.nextangle.com/scrolls?id=defaults]"
+ end
+ end
+
+ def test_feed_id_should_be_a_valid_tag
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_select "id", :text => "tag:www.nextangle.com,2008:/scrolls?id=defaults"
+ end
+ end
+
+ def test_entry_id_should_be_a_valid_tag
+ with_restful_routing(:scrolls) do
+ get :index, :id => "defaults"
+ assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/1"
+ assert_select "entry id", :text => "tag:www.nextangle.com,2008:Scroll/2"
+ end
+ end
+
+ def test_feed_should_allow_nested_xml_blocks
+ with_restful_routing(:scrolls) do
+ get :index, :id => "xml_block"
+ assert_select "author name", :text => "DHH"
+ end
+ end
+
+ def test_feed_should_include_atomPub_namespace
+ with_restful_routing(:scrolls) do
+ get :index, :id => "feed_with_atomPub_namespace"
+ assert_match %r{xml:lang="en-US"}, @response.body
+ assert_match %r{xmlns="http://www.w3.org/2005/Atom"}, @response.body
+ assert_match %r{xmlns:app="http://www.w3.org/2007/app"}, @response.body
+ end
+ end
+
+ def test_feed_should_allow_overriding_ids
+ with_restful_routing(:scrolls) do
+ get :index, :id => "feed_with_overridden_ids"
+ assert_select "id", :text => "tag:test.rubyonrails.org,2008:test/"
+ assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:1"
+ assert_select "entry id", :text => "tag:test.rubyonrails.org,2008:2"
+ end
+ end
+
+ def test_feed_xml_processing_instructions
+ with_restful_routing(:scrolls) do
+ get :index, :id => 'feed_with_xml_processing_instructions'
+ assert_match %r{<\?xml-stylesheet [^\?]*type="text/css"}, @response.body
+ assert_match %r{<\?xml-stylesheet [^\?]*href="t.css"}, @response.body
+ end
+ end
+
+ def test_feed_xml_processing_instructions_duplicate_targets
+ with_restful_routing(:scrolls) do
+ get :index, :id => 'feed_with_xml_processing_instructions_duplicate_targets'
+ assert_match %r{<\?target1 (a="1" b="2"|b="2" a="1")\?>}, @response.body
+ assert_match %r{<\?target1 (c="3" d="4"|d="4" c="3")\?>}, @response.body
+ end
+ end
+
+ def test_feed_xhtml
+ with_restful_routing(:scrolls) do
+ get :index, :id => "feed_with_xhtml_content"
+ assert_match %r{xmlns="http://www.w3.org/1999/xhtml"}, @response.body
+ assert_select "summary div p", :text => "Something Boring"
+ assert_select "summary div p", :text => "after 2"
+ end
+ end
+
+ def test_feed_entry_type_option_default_to_text_html
+ with_restful_routing(:scrolls) do
+ get :index, :id => 'defaults'
+ assert_select "entry link[rel=alternate][type=text/html]"
+ end
+ end
+
+ def test_feed_entry_type_option_specified
+ with_restful_routing(:scrolls) do
+ get :index, :id => 'entry_type_options'
+ assert_select "entry link[rel=alternate][type=text/xml]"
+ end
+ end
+
+ private
+ def with_restful_routing(resources)
+ with_routing do |set|
+ set.draw do
+ resources(resources)
+ end
+ yield
+ end
+ end
+end
diff --git a/actionview/test/template/capture_helper_test.rb b/actionview/test/template/capture_helper_test.rb
new file mode 100644
index 0000000000..938f1c3e54
--- /dev/null
+++ b/actionview/test/template/capture_helper_test.rb
@@ -0,0 +1,242 @@
+require 'abstract_unit'
+
+class CaptureHelperTest < ActionView::TestCase
+ def setup
+ super
+ @av = ActionView::Base.new
+ @view_flow = ActionView::OutputFlow.new
+ end
+
+ def test_capture_captures_the_temporary_output_buffer_in_its_block
+ assert_nil @av.output_buffer
+ string = @av.capture do
+ @av.output_buffer << 'foo'
+ @av.output_buffer << 'bar'
+ end
+ assert_nil @av.output_buffer
+ assert_equal 'foobar', string
+ end
+
+ def test_capture_captures_the_value_returned_by_the_block_if_the_temporary_buffer_is_blank
+ string = @av.capture('foo', 'bar') do |a, b|
+ a + b
+ end
+ assert_equal 'foobar', string
+ end
+
+ def test_capture_returns_nil_if_the_returned_value_is_not_a_string
+ assert_nil @av.capture { 1 }
+ end
+
+ def test_capture_escapes_html
+ string = @av.capture { '<em>bar</em>' }
+ assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
+ end
+
+ def test_capture_doesnt_escape_twice
+ string = @av.capture { '&lt;em&gt;bar&lt;/em&gt;'.html_safe }
+ assert_equal '&lt;em&gt;bar&lt;/em&gt;', string
+ end
+
+ def test_capture_used_for_read
+ content_for :foo, "foo"
+ assert_equal "foo", content_for(:foo)
+
+ content_for(:bar){ "bar" }
+ assert_equal "bar", content_for(:bar)
+ end
+
+ def test_content_for_with_multiple_calls
+ assert ! content_for?(:title)
+ content_for :title, 'foo'
+ content_for :title, 'bar'
+ assert_equal 'foobar', content_for(:title)
+ end
+
+ def test_content_for_with_multiple_calls_and_flush
+ assert ! content_for?(:title)
+ content_for :title, 'foo'
+ content_for :title, 'bar', flush: true
+ assert_equal 'bar', content_for(:title)
+ end
+
+ def test_content_for_with_block
+ assert ! content_for?(:title)
+ content_for :title do
+ output_buffer << 'foo'
+ output_buffer << 'bar'
+ nil
+ end
+ assert_equal 'foobar', content_for(:title)
+ end
+
+ def test_content_for_with_block_and_multiple_calls_with_flush
+ assert ! content_for?(:title)
+ content_for :title do
+ 'foo'
+ end
+ content_for :title, flush: true do
+ 'bar'
+ end
+ assert_equal 'bar', content_for(:title)
+ end
+
+ def test_content_for_with_block_and_multiple_calls_with_flush_nil_content
+ assert ! content_for?(:title)
+ content_for :title do
+ 'foo'
+ end
+ content_for :title, nil, flush: true do
+ 'bar'
+ end
+ assert_equal 'bar', content_for(:title)
+ end
+
+ def test_content_for_with_block_and_multiple_calls_without_flush
+ assert ! content_for?(:title)
+ content_for :title do
+ 'foo'
+ end
+ content_for :title, flush: false do
+ 'bar'
+ end
+ assert_equal 'foobar', content_for(:title)
+ end
+
+ def test_content_for_with_whitespace_block
+ assert ! content_for?(:title)
+ content_for :title, 'foo'
+ content_for :title do
+ output_buffer << " \n "
+ nil
+ end
+ content_for :title, 'bar'
+ assert_equal 'foobar', content_for(:title)
+ end
+
+ def test_content_for_with_whitespace_block_and_flush
+ assert ! content_for?(:title)
+ content_for :title, 'foo'
+ content_for :title, flush: true do
+ output_buffer << " \n "
+ nil
+ end
+ content_for :title, 'bar', flush: true
+ assert_equal 'bar', content_for(:title)
+ end
+
+ def test_content_for_returns_nil_when_writing
+ assert ! content_for?(:title)
+ assert_equal nil, content_for(:title, 'foo')
+ assert_equal nil, content_for(:title) { output_buffer << 'bar'; nil }
+ assert_equal nil, content_for(:title) { output_buffer << " \n "; nil }
+ assert_equal 'foobar', content_for(:title)
+ assert_equal nil, content_for(:title, 'foo', flush: true)
+ assert_equal nil, content_for(:title, flush: true) { output_buffer << 'bar'; nil }
+ assert_equal nil, content_for(:title, flush: true) { output_buffer << " \n "; nil }
+ assert_equal 'bar', content_for(:title)
+ end
+
+ def test_content_for_returns_nil_when_content_missing
+ assert_equal nil, content_for(:some_missing_key)
+ end
+
+ def test_content_for_question_mark
+ assert ! content_for?(:title)
+ content_for :title, 'title'
+ assert content_for?(:title)
+ assert ! content_for?(:something_else)
+ end
+
+ def test_provide
+ assert !content_for?(:title)
+ provide :title, "hi"
+ assert content_for?(:title)
+ assert_equal "hi", content_for(:title)
+ provide :title, "<p>title</p>"
+ assert_equal "hi&lt;p&gt;title&lt;/p&gt;", content_for(:title)
+
+ @view_flow = ActionView::OutputFlow.new
+ provide :title, "hi"
+ provide :title, "<p>title</p>".html_safe
+ assert_equal "hi<p>title</p>", content_for(:title)
+ end
+
+ def test_with_output_buffer_swaps_the_output_buffer_given_no_argument
+ assert_nil @av.output_buffer
+ buffer = @av.with_output_buffer do
+ @av.output_buffer << '.'
+ end
+ assert_equal '.', buffer
+ assert_nil @av.output_buffer
+ end
+
+ def test_with_output_buffer_swaps_the_output_buffer_with_an_argument
+ assert_nil @av.output_buffer
+ buffer = ActionView::OutputBuffer.new('.')
+ @av.with_output_buffer(buffer) do
+ @av.output_buffer << '.'
+ end
+ assert_equal '..', buffer
+ assert_nil @av.output_buffer
+ end
+
+ def test_with_output_buffer_restores_the_output_buffer
+ buffer = ActionView::OutputBuffer.new
+ @av.output_buffer = buffer
+ @av.with_output_buffer do
+ @av.output_buffer << '.'
+ end
+ assert buffer.equal?(@av.output_buffer)
+ end
+
+ def test_with_output_buffer_sets_proper_encoding
+ @av.output_buffer = ActionView::OutputBuffer.new
+
+ # Ensure we set the output buffer to an encoding different than the default one.
+ alt_encoding = alt_encoding(@av.output_buffer)
+ @av.output_buffer.force_encoding(alt_encoding)
+
+ @av.with_output_buffer do
+ assert_equal alt_encoding, @av.output_buffer.encoding
+ end
+ end
+
+ def test_with_output_buffer_does_not_assume_there_is_an_output_buffer
+ assert_nil @av.output_buffer
+ 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
+
+ def view_with_controller
+ TestController.new.view_context.tap do |view|
+ view.output_buffer = ActionView::OutputBuffer.new
+ end
+ end
+end
diff --git a/actionview/test/template/compiled_templates_test.rb b/actionview/test/template/compiled_templates_test.rb
new file mode 100644
index 0000000000..2336321f3e
--- /dev/null
+++ b/actionview/test/template/compiled_templates_test.rb
@@ -0,0 +1,62 @@
+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
+ 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
+ assert_equal "one", render(:file => "test/render_file_with_locals_and_default")
+ assert_equal "two", render(:file => "test/render_file_with_locals_and_default", :locals => { :secret => "two" })
+ end
+
+ def test_template_changes_are_not_reflected_with_cached_templates
+ assert_equal "Hello world!", render(:file => "test/hello_world")
+ modify_template "test/hello_world.erb", "Goodbye world!" do
+ assert_equal "Hello world!", render(:file => "test/hello_world")
+ end
+ assert_equal "Hello world!", render(:file => "test/hello_world")
+ end
+
+ def test_template_changes_are_reflected_with_uncached_templates
+ assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
+ modify_template "test/hello_world.erb", "Goodbye world!" do
+ assert_equal "Goodbye world!", render_without_cache(:file => "test/hello_world")
+ end
+ assert_equal "Hello world!", render_without_cache(:file => "test/hello_world")
+ end
+
+ private
+ def render(*args)
+ render_with_cache(*args)
+ end
+
+ def render_with_cache(*args)
+ view_paths = ActionController::Base.view_paths
+ ActionView::Base.new(view_paths, {}).render(*args)
+ end
+
+ def render_without_cache(*args)
+ path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
+ view_paths = ActionView::PathSet.new([path])
+ ActionView::Base.new(view_paths, {}).render(*args)
+ end
+
+ def modify_template(template, content)
+ filename = "#{FIXTURE_LOAD_PATH}/#{template}"
+ old_content = File.read(filename)
+ begin
+ File.open(filename, "wb+") { |f| f.write(content) }
+ yield
+ ensure
+ File.open(filename, "wb+") { |f| f.write(old_content) }
+ end
+ end
+end
diff --git a/actionview/test/template/date_helper_i18n_test.rb b/actionview/test/template/date_helper_i18n_test.rb
new file mode 100644
index 0000000000..21fca35185
--- /dev/null
+++ b/actionview/test/template/date_helper_i18n_test.rb
@@ -0,0 +1,148 @@
+require 'abstract_unit'
+
+class DateHelperDistanceOfTimeInWordsI18nTests < ActiveSupport::TestCase
+ include ActionView::Helpers::DateHelper
+ attr_reader :request
+
+ def setup
+ @from = Time.utc(2004, 6, 6, 21, 45, 0)
+ end
+
+ # distance_of_time_in_words
+
+ def test_distance_of_time_in_words_calls_i18n
+ { # with include_seconds
+ [2.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 5],
+ [9.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 10],
+ [19.seconds, { :include_seconds => true }] => [:'less_than_x_seconds', 20],
+ [30.seconds, { :include_seconds => true }] => [:'half_a_minute', nil],
+ [59.seconds, { :include_seconds => true }] => [:'less_than_x_minutes', 1],
+ [60.seconds, { :include_seconds => true }] => [:'x_minutes', 1],
+
+ # without include_seconds
+ [29.seconds, { :include_seconds => false }] => [:'less_than_x_minutes', 1],
+ [60.seconds, { :include_seconds => false }] => [:'x_minutes', 1],
+ [44.minutes, { :include_seconds => false }] => [:'x_minutes', 44],
+ [61.minutes, { :include_seconds => false }] => [:'about_x_hours', 1],
+ [24.hours, { :include_seconds => false }] => [:'x_days', 1],
+ [30.days, { :include_seconds => false }] => [:'about_x_months', 1],
+ [60.days, { :include_seconds => false }] => [:'x_months', 2],
+ [1.year, { :include_seconds => false }] => [:'about_x_years', 1],
+ [3.years + 6.months, { :include_seconds => false }] => [:'over_x_years', 3],
+ [3.years + 10.months, { :include_seconds => false }] => [:'almost_x_years', 4]
+
+ }.each do |passed, expected|
+ assert_distance_of_time_in_words_translates_key passed, expected
+ end
+ end
+
+ def test_distance_of_time_in_words_calls_i18n_with_custom_scope
+ {
+ [30.days, { scope: :'datetime.distance_in_words_ago' }] => [:'about_x_months', 1],
+ [60.days, { scope: :'datetime.distance_in_words_ago' }] => [:'x_months', 2],
+ }.each do |passed, expected|
+ assert_distance_of_time_in_words_translates_key(passed, expected, scope: :'datetime.distance_in_words_ago')
+ end
+ end
+
+ def test_time_ago_in_words_passes_locale
+ I18n.expects(:t).with(:less_than_x_minutes, :scope => :'datetime.distance_in_words', :count => 1, :locale => 'ru')
+ time_ago_in_words(15.seconds.ago, :locale => 'ru')
+ end
+
+ def test_distance_of_time_pluralizations
+ { [:'less_than_x_seconds', 1] => 'less than 1 second',
+ [:'less_than_x_seconds', 2] => 'less than 2 seconds',
+ [:'less_than_x_minutes', 1] => 'less than a minute',
+ [:'less_than_x_minutes', 2] => 'less than 2 minutes',
+ [:'x_minutes', 1] => '1 minute',
+ [:'x_minutes', 2] => '2 minutes',
+ [:'about_x_hours', 1] => 'about 1 hour',
+ [:'about_x_hours', 2] => 'about 2 hours',
+ [:'x_days', 1] => '1 day',
+ [:'x_days', 2] => '2 days',
+ [:'about_x_years', 1] => 'about 1 year',
+ [:'about_x_years', 2] => 'about 2 years',
+ [:'over_x_years', 1] => 'over 1 year',
+ [:'over_x_years', 2] => 'over 2 years'
+
+ }.each do |args, expected|
+ key, count = *args
+ assert_equal expected, I18n.t(key, :count => count, :scope => 'datetime.distance_in_words')
+ end
+ end
+
+ def assert_distance_of_time_in_words_translates_key(passed, expected, expected_options = {})
+ diff, passed_options = *passed
+ key, count = *expected
+ to = @from + diff
+
+ options = { locale: 'en', scope: :'datetime.distance_in_words' }.merge!(expected_options)
+ options[:count] = count if count
+
+ I18n.expects(:t).with(key, options)
+ distance_of_time_in_words(@from, to, passed_options.merge(locale: 'en'))
+ end
+end
+
+class DateHelperSelectTagsI18nTests < ActiveSupport::TestCase
+ include ActionView::Helpers::DateHelper
+ attr_reader :request
+
+ def setup
+ @prompt_defaults = {:year => 'Year', :month => 'Month', :day => 'Day', :hour => 'Hour', :minute => 'Minute', :second => 'Seconds'}
+
+ I18n.stubs(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
+ end
+
+ # select_month
+
+ def test_select_month_given_use_month_names_option_does_not_translate_monthnames
+ I18n.expects(:translate).never
+ select_month(8, :locale => 'en', :use_month_names => Date::MONTHNAMES)
+ end
+
+ def test_select_month_translates_monthnames
+ I18n.expects(:translate).with(:'date.month_names', :locale => 'en').returns Date::MONTHNAMES
+ select_month(8, :locale => 'en')
+ end
+
+ def test_select_month_given_use_short_month_option_translates_abbr_monthnames
+ I18n.expects(:translate).with(:'date.abbr_month_names', :locale => 'en').returns Date::ABBR_MONTHNAMES
+ select_month(8, :locale => 'en', :use_short_month => true)
+ end
+
+ def test_date_or_time_select_translates_prompts
+ @prompt_defaults.each do |key, prompt|
+ I18n.expects(:translate).with(('datetime.prompts.' + key.to_s).to_sym, :locale => 'en').returns prompt
+ end
+
+ I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day)
+ datetime_select('post', 'updated_at', :locale => 'en', :include_seconds => true, :prompt => true)
+ end
+
+ # date_or_time_select
+
+ def test_date_or_time_select_given_an_order_options_does_not_translate_order
+ I18n.expects(:translate).never
+ datetime_select('post', 'updated_at', :order => [:year, :month, :day], :locale => 'en')
+ end
+
+ def test_date_or_time_select_given_no_order_options_translates_order
+ I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(year month day)
+ datetime_select('post', 'updated_at', :locale => 'en')
+ end
+
+ def test_date_or_time_select_given_invalid_order
+ I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns %w(invalid month day)
+
+ assert_raise StandardError do
+ datetime_select('post', 'updated_at', :locale => 'en')
+ end
+ end
+
+ def test_date_or_time_select_given_symbol_keys
+ I18n.expects(:translate).with(:'date.order', :locale => 'en', :default => []).returns [:year, :month, :day]
+ datetime_select('post', 'updated_at', :locale => 'en')
+ end
+end
diff --git a/actionview/test/template/date_helper_test.rb b/actionview/test/template/date_helper_test.rb
new file mode 100644
index 0000000000..5f09aef249
--- /dev/null
+++ b/actionview/test/template/date_helper_test.rb
@@ -0,0 +1,3201 @@
+require 'abstract_unit'
+
+class DateHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::DateHelper
+
+ silence_warnings do
+ Post = Struct.new("Post", :id, :written_on, :updated_at)
+ Post.class_eval do
+ def id
+ 123
+ end
+ def id_before_type_cast
+ 123
+ end
+ def to_param
+ '123'
+ end
+ end
+ end
+
+ def assert_distance_of_time_in_words(from, to=nil)
+ to ||= from
+
+ # 0..1 minute with :include_seconds => true
+ assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => true)
+ assert_equal "less than 5 seconds", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => true)
+ assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => true)
+ assert_equal "less than 10 seconds", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => true)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => true)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => true)
+ assert_equal "half a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => true)
+ assert_equal "half a minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => true)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => true)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => true)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => true)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => true)
+
+ # 0..1 minute with :include_seconds => false
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 4.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 5.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 9.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 10.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 19.seconds, :include_seconds => false)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 20.seconds, :include_seconds => false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 39.seconds, :include_seconds => false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 40.seconds, :include_seconds => false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 59.seconds, :include_seconds => false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 60.seconds, :include_seconds => false)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 89.seconds, :include_seconds => false)
+
+ # Note that we are including a 30-second boundary around the interval we
+ # want to test. For instance, "1 minute" is actually 30s to 1m29s. The
+ # reason for doing this is simple -- in `distance_of_time_to_words`, when we
+ # take the distance between our two Time objects in seconds and convert it
+ # to minutes, we round the number. So 29s gets rounded down to 0m, 30s gets
+ # rounded up to 1m, and 1m29s gets rounded down to 1m. A similar thing
+ # happens with the other cases.
+
+ # First case 0..1 minute
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 0.seconds)
+ assert_equal "less than a minute", distance_of_time_in_words(from, to + 29.seconds)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 30.seconds)
+ assert_equal "1 minute", distance_of_time_in_words(from, to + 1.minutes + 29.seconds)
+
+ # 2 minutes up to 45 minutes
+ assert_equal "2 minutes", distance_of_time_in_words(from, to + 1.minutes + 30.seconds)
+ assert_equal "44 minutes", distance_of_time_in_words(from, to + 44.minutes + 29.seconds)
+
+ # 45 minutes up to 90 minutes
+ assert_equal "about 1 hour", distance_of_time_in_words(from, to + 44.minutes + 30.seconds)
+ assert_equal "about 1 hour", distance_of_time_in_words(from, to + 89.minutes + 29.seconds)
+
+ # 90 minutes up to 24 hours
+ assert_equal "about 2 hours", distance_of_time_in_words(from, to + 89.minutes + 30.seconds)
+ assert_equal "about 24 hours", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 29.seconds)
+
+ # 24 hours up to 42 hours
+ assert_equal "1 day", distance_of_time_in_words(from, to + 23.hours + 59.minutes + 30.seconds)
+ assert_equal "1 day", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 29.seconds)
+
+ # 42 hours up to 30 days
+ assert_equal "2 days", distance_of_time_in_words(from, to + 41.hours + 59.minutes + 30.seconds)
+ assert_equal "3 days", distance_of_time_in_words(from, to + 2.days + 12.hours)
+ assert_equal "30 days", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 29.seconds)
+
+ # 30 days up to 60 days
+ assert_equal "about 1 month", distance_of_time_in_words(from, to + 29.days + 23.hours + 59.minutes + 30.seconds)
+ assert_equal "about 1 month", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 29.seconds)
+ assert_equal "about 2 months", distance_of_time_in_words(from, to + 44.days + 23.hours + 59.minutes + 30.seconds)
+ assert_equal "about 2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 29.seconds)
+
+ # 60 days up to 365 days
+ assert_equal "2 months", distance_of_time_in_words(from, to + 59.days + 23.hours + 59.minutes + 30.seconds)
+ assert_equal "12 months", distance_of_time_in_words(from, to + 1.years - 31.seconds)
+
+ # >= 365 days
+ assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years - 30.seconds)
+ assert_equal "about 1 year", distance_of_time_in_words(from, to + 1.years + 3.months - 1.day)
+ assert_equal "over 1 year", distance_of_time_in_words(from, to + 1.years + 6.months)
+
+ assert_equal "almost 2 years", distance_of_time_in_words(from, to + 2.years - 3.months + 1.day)
+ assert_equal "about 2 years", distance_of_time_in_words(from, to + 2.years + 3.months - 1.day)
+ assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 3.months + 1.day)
+ assert_equal "over 2 years", distance_of_time_in_words(from, to + 2.years + 9.months - 1.day)
+ assert_equal "almost 3 years", distance_of_time_in_words(from, to + 2.years + 9.months + 1.day)
+
+ assert_equal "almost 5 years", distance_of_time_in_words(from, to + 5.years - 3.months + 1.day)
+ assert_equal "about 5 years", distance_of_time_in_words(from, to + 5.years + 3.months - 1.day)
+ assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 3.months + 1.day)
+ assert_equal "over 5 years", distance_of_time_in_words(from, to + 5.years + 9.months - 1.day)
+ assert_equal "almost 6 years", distance_of_time_in_words(from, to + 5.years + 9.months + 1.day)
+
+ assert_equal "almost 10 years", distance_of_time_in_words(from, to + 10.years - 3.months + 1.day)
+ assert_equal "about 10 years", distance_of_time_in_words(from, to + 10.years + 3.months - 1.day)
+ assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 3.months + 1.day)
+ assert_equal "over 10 years", distance_of_time_in_words(from, to + 10.years + 9.months - 1.day)
+ assert_equal "almost 11 years", distance_of_time_in_words(from, to + 10.years + 9.months + 1.day)
+
+ # test to < from
+ assert_equal "about 4 hours", distance_of_time_in_words(from + 4.hours, to)
+ assert_equal "less than 20 seconds", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => true)
+ assert_equal "less than a minute", distance_of_time_in_words(from + 19.seconds, to, :include_seconds => false)
+ end
+
+ def test_distance_in_words
+ from = Time.utc(2004, 6, 6, 21, 45, 0)
+ assert_distance_of_time_in_words(from)
+ end
+
+ def test_distance_in_words_with_mathn_required
+ # test we avoid Integer#/ (redefined by mathn)
+ require 'mathn'
+ from = Time.utc(2004, 6, 6, 21, 45, 0)
+ assert_distance_of_time_in_words(from)
+ end
+
+ def test_time_ago_in_words_passes_include_seconds
+ assert_equal "less than 20 seconds", time_ago_in_words(15.seconds.ago, :include_seconds => true)
+ assert_equal "less than a minute", time_ago_in_words(15.seconds.ago, :include_seconds => false)
+ end
+
+ def test_distance_in_words_with_time_zones
+ from = Time.mktime(2004, 6, 6, 21, 45, 0)
+ assert_distance_of_time_in_words(from.in_time_zone('Alaska'))
+ assert_distance_of_time_in_words(from.in_time_zone('Hawaii'))
+ end
+
+ def test_distance_in_words_with_different_time_zones
+ from = Time.mktime(2004, 6, 6, 21, 45, 0)
+ assert_distance_of_time_in_words(
+ from.in_time_zone('Alaska'),
+ from.in_time_zone('Hawaii')
+ )
+ end
+
+ def test_distance_in_words_with_dates
+ start_date = Date.new 1975, 1, 31
+ end_date = Date.new 1977, 1, 31
+ assert_equal("about 2 years", distance_of_time_in_words(start_date, end_date))
+
+ start_date = Date.new 1982, 12, 3
+ end_date = Date.new 2010, 11, 30
+ assert_equal("almost 28 years", distance_of_time_in_words(start_date, end_date))
+ assert_equal("almost 28 years", distance_of_time_in_words(end_date, start_date))
+ end
+
+ def test_distance_in_words_with_integers
+ assert_equal "1 minute", distance_of_time_in_words(59)
+ assert_equal "about 1 hour", distance_of_time_in_words(60*60)
+ assert_equal "1 minute", distance_of_time_in_words(0, 59)
+ assert_equal "about 1 hour", distance_of_time_in_words(60*60, 0)
+ assert_equal "about 3 years", distance_of_time_in_words(10**8)
+ assert_equal "about 3 years", distance_of_time_in_words(0, 10**8)
+ end
+
+ def test_distance_in_words_with_times
+ assert_equal "1 minute", distance_of_time_in_words(30.seconds)
+ assert_equal "1 minute", distance_of_time_in_words(59.seconds)
+ assert_equal "2 minutes", distance_of_time_in_words(119.seconds)
+ assert_equal "2 minutes", distance_of_time_in_words(1.minute + 59.seconds)
+ assert_equal "3 minutes", distance_of_time_in_words(2.minute + 30.seconds)
+ assert_equal "44 minutes", distance_of_time_in_words(44.minutes + 29.seconds)
+ assert_equal "about 1 hour", distance_of_time_in_words(44.minutes + 30.seconds)
+ assert_equal "about 1 hour", distance_of_time_in_words(60.minutes)
+
+ # include seconds
+ assert_equal "half a minute", distance_of_time_in_words(39.seconds, 0, :include_seconds => true)
+ assert_equal "less than a minute", distance_of_time_in_words(40.seconds, 0, :include_seconds => true)
+ assert_equal "less than a minute", distance_of_time_in_words(59.seconds, 0, :include_seconds => true)
+ assert_equal "1 minute", distance_of_time_in_words(60.seconds, 0, :include_seconds => true)
+ end
+
+ def test_time_ago_in_words
+ assert_equal "about 1 year", time_ago_in_words(1.year.ago - 1.day)
+ end
+
+ def test_select_day
+ expected = %(<select id="date_day" name="date[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_day(Time.mktime(2003, 8, 16))
+ assert_dom_equal expected, select_day(16)
+ end
+
+ def test_select_day_with_blank
+ expected = %(<select id="date_day" name="date[day]">\n)
+ expected << %(<option value=""></option>\n<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_day(Time.mktime(2003, 8, 16), :include_blank => true)
+ assert_dom_equal expected, select_day(16, :include_blank => true)
+ end
+
+ def test_select_day_nil_with_blank
+ expected = %(<select id="date_day" name="date[day]">\n)
+ expected << %(<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_day(nil, :include_blank => true)
+ end
+
+ def test_select_day_with_two_digit_numbers
+ expected = %(<select id="date_day" name="date[day]">\n)
+ expected << %(<option value="1">01</option>\n<option selected="selected" value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_day(Time.mktime(2011, 8, 2), :use_two_digit_numbers => true)
+ assert_dom_equal expected, select_day(2, :use_two_digit_numbers => true)
+ end
+
+ def test_select_day_with_html_options
+ expected = %(<select id="date_day" name="date[day]" class="selector">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_day(Time.mktime(2003, 8, 16), {}, :class => 'selector')
+ assert_dom_equal expected, select_day(16, {}, :class => 'selector')
+ end
+
+ def test_select_day_with_default_prompt
+ expected = %(<select id="date_day" name="date[day]">\n)
+ expected << %(<option value="">Day</option>\n<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_day(16, :prompt => true)
+ end
+
+ def test_select_day_with_custom_prompt
+ expected = %(<select id="date_day" name="date[day]">\n)
+ expected << %(<option value="">Choose day</option>\n<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_day(16, :prompt => 'Choose day')
+ end
+
+ def test_select_month
+ expected = %(<select id="date_month" name="date[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"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16))
+ assert_dom_equal expected, select_month(8)
+ end
+
+ def test_select_month_with_two_digit_numbers
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">01</option>\n<option value="2">02</option>\n<option value="3">03</option>\n<option value="4">04</option>\n<option value="5">05</option>\n<option value="6">06</option>\n<option value="7">07</option>\n<option value="8" selected="selected">08</option>\n<option value="9">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2011, 8, 16), :use_two_digit_numbers => true)
+ assert_dom_equal expected, select_month(8, :use_two_digit_numbers => true)
+ end
+
+ def test_select_month_with_disabled
+ expected = %(<select id="date_month" name="date[month]" disabled="disabled">\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"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :disabled => true)
+ assert_dom_equal expected, select_month(8, :disabled => true)
+ end
+
+ def test_select_month_with_field_name_override
+ expected = %(<select id="date_mois" name="date[mois]">\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"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :field_name => 'mois')
+ assert_dom_equal expected, select_month(8, :field_name => 'mois')
+ end
+
+ def test_select_month_with_blank
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value=""></option>\n<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"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :include_blank => true)
+ assert_dom_equal expected, select_month(8, :include_blank => true)
+ end
+
+ def test_select_month_nil_with_blank
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value=""></option>\n<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">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"
+
+ assert_dom_equal expected, select_month(nil, :include_blank => true)
+ end
+
+ def test_select_month_with_numbers
+ expected = %(<select id="date_month" name="date[month]">\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" selected="selected">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)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_numbers => true)
+ assert_dom_equal expected, select_month(8, :use_month_numbers => true)
+ end
+
+ def test_select_month_with_numbers_and_names
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">1 - January</option>\n<option value="2">2 - February</option>\n<option value="3">3 - March</option>\n<option value="4">4 - April</option>\n<option value="5">5 - May</option>\n<option value="6">6 - June</option>\n<option value="7">7 - July</option>\n<option value="8" selected="selected">8 - August</option>\n<option value="9">9 - September</option>\n<option value="10">10 - October</option>\n<option value="11">11 - November</option>\n<option value="12">12 - December</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true)
+ assert_dom_equal expected, select_month(8, :add_month_numbers => true)
+ end
+
+ def test_select_month_with_numbers_and_names_with_abbv
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">1 - Jan</option>\n<option value="2">2 - Feb</option>\n<option value="3">3 - Mar</option>\n<option value="4">4 - Apr</option>\n<option value="5">5 - May</option>\n<option value="6">6 - Jun</option>\n<option value="7">7 - Jul</option>\n<option value="8" selected="selected">8 - Aug</option>\n<option value="9">9 - Sep</option>\n<option value="10">10 - Oct</option>\n<option value="11">11 - Nov</option>\n<option value="12">12 - Dec</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :add_month_numbers => true, :use_short_month => true)
+ assert_dom_equal expected, select_month(8, :add_month_numbers => true, :use_short_month => true)
+ end
+
+ def test_select_month_with_abbv
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="1">Jan</option>\n<option value="2">Feb</option>\n<option value="3">Mar</option>\n<option value="4">Apr</option>\n<option value="5">May</option>\n<option value="6">Jun</option>\n<option value="7">Jul</option>\n<option value="8" selected="selected">Aug</option>\n<option value="9">Sep</option>\n<option value="10">Oct</option>\n<option value="11">Nov</option>\n<option value="12">Dec</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_short_month => true)
+ assert_dom_equal expected, select_month(8, :use_short_month => true)
+ end
+
+ def test_select_month_with_custom_names
+ month_names = %w(nil Januar Februar Marts April Maj Juni Juli August September Oktober November December)
+
+ expected = %(<select id="date_month" name="date[month]">\n)
+ 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month]}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
+ assert_dom_equal expected, select_month(8, :use_month_names => month_names)
+ end
+
+ def test_select_month_with_zero_indexed_custom_names
+ month_names = %w(Januar Februar Marts April Maj Juni Juli August September Oktober November December)
+
+ expected = %(<select id="date_month" name="date[month]">\n)
+ 1.upto(12) { |month| expected << %(<option value="#{month}"#{' selected="selected"' if month == 8}>#{month_names[month-1]}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), :use_month_names => month_names)
+ assert_dom_equal expected, select_month(8, :use_month_names => month_names)
+ end
+
+ def test_select_month_with_hidden
+ assert_dom_equal "<input type=\"hidden\" id=\"date_month\" name=\"date[month]\" value=\"8\" />\n", select_month(8, :use_hidden => true)
+ end
+
+ def test_select_month_with_hidden_and_field_name
+ assert_dom_equal "<input type=\"hidden\" id=\"date_mois\" name=\"date[mois]\" value=\"8\" />\n", select_month(8, :use_hidden => true, :field_name => 'mois')
+ end
+
+ def test_select_month_with_html_options
+ expected = %(<select id="date_month" name="date[month]" class="selector" accesskey="M">\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"
+
+ assert_dom_equal expected, select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
+ #result = select_month(Time.mktime(2003, 8, 16), {}, :class => 'selector', :accesskey => 'M')
+ #assert result.include?('<select id="date_month" name="date[month]"')
+ #assert result.include?('class="selector"')
+ #assert result.include?('accesskey="M"')
+ #assert result.include?('<option value="1">January')
+ end
+
+ def test_select_month_with_default_prompt
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="">Month</option>\n<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"
+
+ assert_dom_equal expected, select_month(8, :prompt => true)
+ end
+
+ def test_select_month_with_custom_prompt
+ expected = %(<select id="date_month" name="date[month]">\n)
+ expected << %(<option value="">Choose month</option>\n<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"
+
+ assert_dom_equal expected, select_month(8, :prompt => 'Choose month')
+ end
+
+ def test_select_year
+ expected = %(<select id="date_year" name="date[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"
+
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005)
+ assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005)
+ end
+
+ def test_select_year_with_disabled
+ expected = %(<select id="date_year" name="date[year]" disabled="disabled">\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"
+
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :disabled => true, :start_year => 2003, :end_year => 2005)
+ assert_dom_equal expected, select_year(2003, :disabled => true, :start_year => 2003, :end_year => 2005)
+ end
+
+ def test_select_year_with_field_name_override
+ expected = %(<select id="date_annee" name="date[annee]">\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"
+
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :field_name => 'annee')
+ assert_dom_equal expected, select_year(2003, :start_year => 2003, :end_year => 2005, :field_name => 'annee')
+ end
+
+ def test_select_year_with_type_discarding
+ expected = %(<select id="date_year" name="date_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"
+
+ assert_dom_equal expected, select_year(
+ Time.mktime(2003, 8, 16), :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ assert_dom_equal expected, select_year(
+ 2003, :prefix => "date_year", :discard_type => true, :start_year => 2003, :end_year => 2005)
+ end
+
+ def test_select_year_descending
+ expected = %(<select id="date_year" name="date[year]">\n)
+ expected << %(<option value="2005" selected="selected">2005</option>\n<option value="2004">2004</option>\n<option value="2003">2003</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(Time.mktime(2005, 8, 16), :start_year => 2005, :end_year => 2003)
+ assert_dom_equal expected, select_year(2005, :start_year => 2005, :end_year => 2003)
+ end
+
+ def test_select_year_with_hidden
+ assert_dom_equal "<input type=\"hidden\" id=\"date_year\" name=\"date[year]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true)
+ end
+
+ def test_select_year_with_hidden_and_field_name
+ assert_dom_equal "<input type=\"hidden\" id=\"date_anno\" name=\"date[anno]\" value=\"2007\" />\n", select_year(2007, :use_hidden => true, :field_name => 'anno')
+ end
+
+ def test_select_year_with_html_options
+ expected = %(<select id="date_year" name="date[year]" class="selector" accesskey="M">\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"
+
+ assert_dom_equal expected, select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
+ #result = select_year(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005}, :class => 'selector', :accesskey => 'M')
+ #assert result.include?('<select id="date_year" name="date[year]"')
+ #assert result.include?('class="selector"')
+ #assert result.include?('accesskey="M"')
+ #assert result.include?('<option value="2003"')
+ end
+
+ def test_select_year_with_default_prompt
+ expected = %(<select id="date_year" name="date[year]">\n)
+ expected << %(<option value="">Year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => true)
+ end
+
+ def test_select_year_with_custom_prompt
+ expected = %(<select id="date_year" name="date[year]">\n)
+ expected << %(<option value="">Choose year</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_year(nil, :start_year => 2003, :end_year => 2005, :prompt => 'Choose year')
+ end
+
+ def test_select_hour
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18))
+ end
+
+ def test_select_hour_with_ampm
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :ampm => true)
+ end
+
+ def test_select_hour_with_disabled
+ expected = %(<select id="date_hour" name="date[hour]" disabled="disabled">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ end
+
+ def test_select_hour_with_field_name_override
+ expected = %(<select id="date_heure" name="date[heure]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'heure')
+ end
+
+ def test_select_hour_with_blank
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ end
+
+ def test_select_hour_nil_with_blank
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(nil, :include_blank => true)
+ end
+
+ def test_select_hour_with_html_options
+ expected = %(<select id="date_hour" name="date[hour]" class="selector" accesskey="M">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+ end
+
+ def test_select_hour_with_default_prompt
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ end
+
+ def test_select_hour_with_custom_prompt
+ expected = %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_hour(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose hour')
+ end
+
+ def test_select_minute
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18))
+ end
+
+ def test_select_minute_with_disabled
+ expected = %(<select id="date_minute" name="date[minute]" disabled="disabled">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ end
+
+ def test_select_minute_with_field_name_override
+ expected = %(<select id="date_minuto" name="date[minuto]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'minuto')
+ end
+
+ def test_select_minute_with_blank
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ end
+
+ def test_select_minute_with_blank_and_step
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), { :include_blank => true , :minute_step => 15 })
+ end
+
+ def test_select_minute_nil_with_blank
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(nil, :include_blank => true)
+ end
+
+ def test_select_minute_nil_with_blank_and_step
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="15">15</option>\n<option value="30">30</option>\n<option value="45">45</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(nil, { :include_blank => true , :minute_step => 15 })
+ end
+
+ def test_select_minute_with_hidden
+ assert_dom_equal "<input type=\"hidden\" id=\"date_minute\" name=\"date[minute]\" value=\"8\" />\n", select_minute(8, :use_hidden => true)
+ end
+
+ def test_select_minute_with_hidden_and_field_name
+ assert_dom_equal "<input type=\"hidden\" id=\"date_minuto\" name=\"date[minuto]\" value=\"8\" />\n", select_minute(8, :use_hidden => true, :field_name => 'minuto')
+ end
+
+ def test_select_minute_with_html_options
+ expected = %(<select id="date_minute" name="date[minute]" class="selector" accesskey="M">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+
+ #result = select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+ #assert result.include?('<select id="date_minute" name="date[minute]"')
+ #assert result.include?('class="selector"')
+ #assert result.include?('accesskey="M"')
+ #assert result.include?('<option value="00">00')
+ end
+
+ def test_select_minute_with_default_prompt
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ end
+
+ def test_select_minute_with_custom_prompt
+ expected = %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_minute(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose minute')
+ end
+
+ def test_select_second
+ expected = %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18))
+ end
+
+ def test_select_second_with_disabled
+ expected = %(<select id="date_second" name="date[second]" disabled="disabled">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :disabled => true)
+ end
+
+ def test_select_second_with_field_name_override
+ expected = %(<select id="date_segundo" name="date[segundo]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :field_name => 'segundo')
+ end
+
+ def test_select_second_with_blank
+ expected = %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :include_blank => true)
+ end
+
+ def test_select_second_nil_with_blank
+ expected = %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value=""></option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(nil, :include_blank => true)
+ end
+
+ def test_select_second_with_html_options
+ expected = %(<select id="date_second" name="date[second]" class="selector" accesskey="M">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+
+ #result = select_second(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector', :accesskey => 'M')
+ #assert result.include?('<select id="date_second" name="date[second]"')
+ #assert result.include?('class="selector"')
+ #assert result.include?('accesskey="M"')
+ #assert result.include?('<option value="00">00')
+ end
+
+ def test_select_second_with_default_prompt
+ expected = %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true)
+ end
+
+ def test_select_second_with_custom_prompt
+ expected = %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_second(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => 'Choose seconds')
+ end
+
+ def test_select_date
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_too_big_range_between_start_year_and_end_year
+ assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 20000, :prefix => "date[first]", :order => [:month, :day, :year]) }
+ assert_raise(ArgumentError) { select_date(Time.mktime(2003, 8, 16), :start_year => Date.today.year - 100.years, :end_year => 2000, :prefix => "date[first]", :order => [:month, :day, :year]) }
+ end
+
+ def test_select_date_can_have_more_then_1000_years_interval_if_forced_via_parameter
+ assert_nothing_raised { select_date(Time.mktime(2003, 8, 16), :start_year => 2000, :end_year => 3100, :max_years_allowed => 2000) }
+ end
+
+ def test_select_date_with_order
+ expected = %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:month, :day, :year])
+ end
+
+ def test_select_date_with_incomplete_order
+ # Since the order is incomplete nothing will be shown
+ expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="1" />\n)
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :order => [:day])
+ end
+
+ def test_select_date_with_disabled
+ expected = %(<select id="date_first_year" name="date[first][year]" disabled="disabled">\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]" disabled="disabled">\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]" disabled="disabled">\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]", :disabled => true)
+ end
+
+ def test_select_date_with_no_start_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+1) do |y|
+ if y == Date.today.year
+ expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
+ else
+ expected << %(<option value="#{y}">#{y}</option>\n)
+ end
+ end
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(
+ Time.mktime(Date.today.year, 8, 16), :end_year => Date.today.year+1, :prefix => "date[first]"
+ )
+ end
+
+ def test_select_date_with_no_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ 2003.upto(2008) do |y|
+ if y == 2003
+ expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
+ else
+ expected << %(<option value="#{y}">#{y}</option>\n)
+ end
+ end
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(
+ Time.mktime(2003, 8, 16), :start_year => 2003, :prefix => "date[first]"
+ )
+ end
+
+ def test_select_date_with_no_start_or_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) do |y|
+ if y == Date.today.year
+ expected << %(<option value="#{y}" selected="selected">#{y}</option>\n)
+ else
+ expected << %(<option value="#{y}">#{y}</option>\n)
+ end
+ end
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(
+ Time.mktime(Date.today.year, 8, 16), :prefix => "date[first]"
+ )
+ end
+
+ def test_select_date_with_zero_value
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003">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]">\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">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_zero_value_and_no_start_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_zero_value_and_no_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ last_year = Time.now.year + 5
+ 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_zero_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_nil_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
+ end
+
+ def test_select_date_with_html_options
+ expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => "selector")
+ end
+
+ def test_select_date_with_separator
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ end
+
+ def test_select_date_with_separator_and_discard_day
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ end
+
+ def test_select_date_with_separator_discard_month_and_day
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<input type="hidden" id="date_first_month" name="date[first][month]" value="8" />\n)
+ expected << %(<input type="hidden" id="date_first_day" name="date[first][day]" value="1" />\n)
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :discard_month => true, :discard_day => true, :start_year => 2003, :end_year => 2005, :prefix => "date[first]"})
+ end
+
+ def test_select_date_with_hidden
+ expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003"/>\n)
+ expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
+
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :prefix => "date[first]", :use_hidden => true })
+ assert_dom_equal expected, select_date(Time.mktime(2003, 8, 16), { :date_separator => " / ", :prefix => "date[first]", :use_hidden => true })
+ end
+
+ def test_select_date_with_css_classes_option
+ expected = %(<select id="date_first_year" name="date[first][year]" class="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="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="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})
+ 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)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]")
+ end
+
+ def test_select_datetime_with_ampm
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :ampm => true)
+ end
+
+ def test_select_datetime_with_separators
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]", :datetime_separator => ' &mdash; ', :time_separator => ' : ')
+ end
+
+ def test_select_datetime_with_nil_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
+ end
+
+ def test_select_datetime_with_html_options
+ expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), {:start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
+ end
+
+ def test_select_datetime_with_all_separators
+ expected = %(<select id="date_first_year" name="date[first][year]" class="selector">\n)
+ expected << %(<option value="2003" selected="selected">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n)
+ expected << "</select>\n"
+
+ expected << "/"
+
+ expected << %(<select id="date_first_month" name="date[first][month]" class="selector">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << "/"
+
+ expected << %(<select id="date_first_day" name="date[first][day]" class="selector">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << "&mdash;"
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << ":"
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), { :datetime_separator => "&mdash;", :date_separator => "/", :time_separator => ":", :start_year => 2003, :end_year => 2005, :prefix => "date[first]"}, :class => 'selector')
+ end
+
+ def test_select_datetime_should_work_with_date
+ assert_nothing_raised { select_datetime(Date.today) }
+ end
+
+ def test_select_datetime_with_default_prompt
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="">Year</option>\n<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]">\n)
+ expected << %(<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005,
+ :prefix => "date[first]", :prompt => true)
+ end
+
+ def test_select_datetime_with_custom_prompt
+
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="">Choose year</option>\n<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]">\n)
+ expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :prefix => "date[first]",
+ :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
+ end
+
+ def test_select_datetime_with_custom_hours
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ expected << %(<option value="">Choose year</option>\n<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]">\n)
+ expected << %(<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8" selected="selected">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" selected="selected">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="">Choose hour</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :start_year => 2003, :end_year => 2005, :start_hour => 1, :end_hour => 9, :prefix => "date[first]",
+ :prompt => {:day => 'Choose day', :month => 'Choose month', :year => 'Choose year', :hour => 'Choose hour', :minute => 'Choose minute'})
+ end
+
+ def test_select_datetime_with_hidden
+ expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
+ expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
+
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
+ assert_dom_equal expected, select_datetime(Time.mktime(2003, 8, 16, 8, 4, 18), :datetime_separator => "&mdash;", :date_separator => "/",
+ :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
+ end
+
+ def test_select_time
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18))
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false)
+ end
+
+ def test_select_time_with_ampm
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">12 AM</option>\n<option value="01">01 AM</option>\n<option value="02">02 AM</option>\n<option value="03">03 AM</option>\n<option value="04">04 AM</option>\n<option value="05">05 AM</option>\n<option value="06">06 AM</option>\n<option value="07">07 AM</option>\n<option value="08" selected="selected">08 AM</option>\n<option value="09">09 AM</option>\n<option value="10">10 AM</option>\n<option value="11">11 AM</option>\n<option value="12">12 PM</option>\n<option value="13">01 PM</option>\n<option value="14">02 PM</option>\n<option value="15">03 PM</option>\n<option value="16">04 PM</option>\n<option value="17">05 PM</option>\n<option value="18">06 PM</option>\n<option value="19">07 PM</option>\n<option value="20">08 PM</option>\n<option value="21">09 PM</option>\n<option value="22">10 PM</option>\n<option value="23">11 PM</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => false, :ampm => true)
+ end
+
+ def test_select_time_with_separator
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ')
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ' : ', :include_seconds => false)
+ end
+
+ def test_select_time_with_seconds
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << ' : '
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << ' : '
+
+ expected << %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true)
+ end
+
+ def test_select_time_with_seconds_and_separator
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :time_separator => ' : ')
+ end
+
+ def test_select_time_with_html_options
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]" class="selector">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector')
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {:include_seconds => false}, :class => 'selector')
+ end
+
+ def test_select_time_should_work_with_date
+ assert_nothing_raised { select_time(Date.today) }
+ end
+
+ def test_select_time_with_default_prompt
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="">Seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :include_seconds => true, :prompt => true)
+ end
+
+ def test_select_time_with_custom_prompt
+ expected = %(<input name="date[year]" id="date_year" value="2003" type="hidden" />\n)
+ expected << %(<input name="date[month]" id="date_month" value="8" type="hidden" />\n)
+ expected << %(<input name="date[day]" id="date_day" value="16" type="hidden" />\n)
+
+ expected << %(<select id="date_hour" name="date[hour]">\n)
+ expected << %(<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08" selected="selected">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_minute" name="date[minute]">\n)
+ expected << %(<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04" selected="selected">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_second" name="date[second]">\n)
+ expected << %(<option value="">Choose seconds</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18" selected="selected">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prompt => true, :include_seconds => true,
+ :prompt => {:hour => 'Choose hour', :minute => 'Choose minute', :second => 'Choose seconds'})
+ end
+
+ def test_select_time_with_hidden
+ expected = %(<input id="date_first_year" name="date[first][year]" type="hidden" value="2003" />\n)
+ expected << %(<input id="date_first_month" name="date[first][month]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_day" name="date[first][day]" type="hidden" value="16" />\n)
+ expected << %(<input id="date_first_hour" name="date[first][hour]" type="hidden" value="8" />\n)
+ expected << %(<input id="date_first_minute" name="date[first][minute]" type="hidden" value="4" />\n)
+
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :prefix => "date[first]", :use_hidden => true)
+ assert_dom_equal expected, select_time(Time.mktime(2003, 8, 16, 8, 4, 18), :time_separator => ":", :prefix => "date[first]", :use_hidden => true)
+ end
+
+ def test_date_select
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on")
+ end
+
+ def test_date_select_with_selected
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option selected="selected" value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7" selected="selected">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :selected => Date.new(2004, 07, 10))
+ end
+
+ def test_date_select_with_selected_nil
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1"/>' + "\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value=""></option>\n<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">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="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value=""></option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", include_blank: true, discard_year: true, selected: nil)
+ end
+
+ def test_date_select_without_day
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :order => [ :month, :year ])
+ end
+
+ def test_date_select_without_day_and_month
+ @post = Post.new
+ @post.written_on = Date.new(2004, 2, 29)
+
+ expected = "<input type=\"hidden\" id=\"post_written_on_2i\" name=\"post[written_on(2i)]\" value=\"2\" />\n"
+ expected << "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :order => [ :year ])
+ end
+
+ def test_date_select_without_day_with_separator
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = "<input type=\"hidden\" id=\"post_written_on_3i\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << "/"
+
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :date_separator => '/', :order => [ :month, :year ])
+ end
+
+ def test_date_select_without_day_and_with_disabled_html_option
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = "<input type=\"hidden\" id=\"post_written_on_3i\" disabled=\"disabled\" name=\"post[written_on(3i)]\" value=\"1\" />\n"
+
+ expected << %{<select id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", { :order => [ :month, :year ] }, :disabled => true)
+ end
+
+ def test_date_select_within_fields_for
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.date_select(:written_on)
+ end
+
+ expected = "<select id='post_written_on_1i' name='post[written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
+ expected << "<select id='post_written_on_2i' name='post[written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
+ expected << "<select id='post_written_on_3i' name='post[written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+
+ assert_dom_equal(expected, output_buffer)
+ end
+
+ def test_date_select_within_fields_for_with_index
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+ id = 27
+
+ output_buffer = fields_for :post, @post, :index => id do |f|
+ concat f.date_select(:written_on)
+ end
+
+ expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
+ expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
+ expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+
+ assert_dom_equal(expected, output_buffer)
+ end
+
+ def test_date_select_within_fields_for_with_blank_index
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+ id = nil
+
+ output_buffer = fields_for :post, @post, :index => id do |f|
+ concat f.date_select(:written_on)
+ end
+
+ expected = "<select id='post_#{id}_written_on_1i' name='post[#{id}][written_on(1i)]'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
+ expected << "<select id='post_#{id}_written_on_2i' name='post[#{id}][written_on(2i)]'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
+ expected << "<select id='post_#{id}_written_on_3i' name='post[#{id}][written_on(3i)]'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+
+ assert_dom_equal(expected, output_buffer)
+ end
+
+ def test_date_select_with_index
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+ id = 456
+
+ expected = %{<select id="post_456_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_written_on_2i" name="post[#{id}][written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_written_on_3i" name="post[#{id}][written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :index => id)
+ end
+
+ def test_date_select_with_auto_index
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+ id = 123
+
+ expected = %{<select id="post_123_written_on_1i" name="post[#{id}][written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_123_written_on_2i" name="post[#{id}][written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_123_written_on_3i" name="post[#{id}][written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post[]", "written_on")
+ end
+
+ def test_date_select_with_different_order
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :order => [:day, :month, :year])
+ end
+
+ def test_date_select_with_nil
+ @post = Post.new
+
+ start_year = Time.now.year-5
+ end_year = Time.now.year+5
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ start_year.upto(end_year) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.month}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on")
+ end
+
+ def test_date_select_with_nil_and_blank
+ @post = Post.new
+
+ start_year = Time.now.year-5
+ end_year = Time.now.year+5
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :include_blank => true)
+ end
+
+ def test_date_select_with_nil_and_blank_and_order
+ @post = Post.new
+
+ start_year = Time.now.year-5
+ end_year = Time.now.year+5
+
+ expected = '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :order=>[:year, :month], :include_blank=>true)
+ end
+
+ def test_date_select_with_nil_and_blank_and_discard_month
+ @post = Post.new
+
+ start_year = Time.now.year-5
+ end_year = Time.now.year+5
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ start_year.upto(end_year) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << '<input name="post[written_on(2i)]" type="hidden" id="post_written_on_2i" value="1"/>' + "\n"
+ expected << '<input name="post[written_on(3i)]" type="hidden" id="post_written_on_3i" value="1"/>' + "\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :discard_month => true, :include_blank=>true)
+ end
+
+ def test_date_select_with_nil_and_blank_and_discard_year
+ @post = Post.new
+
+ expected = '<input id="post_written_on_1i" name="post[written_on(1i)]" type="hidden" value="1" />' + "\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << "<option value=\"\"></option>\n"
+ 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :discard_year => true, :include_blank=>true)
+ end
+
+ def test_date_select_cant_override_discard_hour
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :discard_hour => false)
+ end
+
+ def test_date_select_with_html_options
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", {}, :class => 'selector')
+ end
+
+ def test_date_select_with_html_options_within_fields_for
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.date_select(:written_on, {}, :class => 'selector')
+ end
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]" class="selector">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]" class="selector">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]" class="selector">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_date_select_with_separator
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", { :date_separator => " / " })
+ end
+
+ def test_date_select_with_separator_and_order
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :date_separator => " / " })
+ end
+
+ def test_date_select_with_separator_and_order_and_year_discarded
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+ expected << %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+
+ assert_dom_equal expected, date_select("post", "written_on", { :order => [:day, :month, :year], :discard_year => true, :date_separator => " / " })
+ end
+
+ def test_date_select_with_default_prompt
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="">Month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :prompt => true)
+ end
+
+ def test_date_select_with_custom_prompt
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ expected = %{<select id="post_written_on_1i" name="post[written_on(1i)]">\n}
+ expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_2i" name="post[written_on(2i)]">\n}
+ expected << %{<option value="">Choose month</option>\n<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_written_on_3i" name="post[written_on(3i)]">\n}
+ expected << %{<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "written_on", :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day'})
+ end
+
+ def test_time_select
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on")
+ end
+
+ def test_time_select_with_selected
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 12}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 20}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", selected: Time.local(2004, 6, 15, 12, 20, 30))
+ end
+
+ def test_time_select_with_selected_nil
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="1" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="1" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="1" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}">#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", discard_year: true, discard_month: true, discard_day: true, selected: nil)
+ end
+
+ def test_time_select_without_date_hidden_fields
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", :ignore_date => true)
+ end
+
+ def test_time_select_with_seconds
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", :include_seconds => true)
+ end
+
+ def test_time_select_with_html_options
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", {}, :class => 'selector')
+ end
+
+ def test_time_select_with_html_options_within_fields_for
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.time_select(:written_on, {}, :class => 'selector')
+ end
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]" class="selector">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]" class="selector">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_time_select_with_separator
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_written_on_6i" name="post[written_on(6i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", { :time_separator => " - ", :include_seconds => true })
+ end
+
+ def test_time_select_with_default_prompt
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ expected << %(<option value="">Hour</option>\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ expected << %(<option value="">Minute</option>\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", :prompt => true)
+ end
+
+ def test_time_select_with_custom_prompt
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" name="post[written_on(4i)]">\n)
+ expected << %(<option value="">Choose hour</option>\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" name="post[written_on(5i)]">\n)
+ expected << %(<option value="">Choose minute</option>\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", :prompt => {:hour => 'Choose hour', :minute => 'Choose minute'})
+ end
+
+ def test_time_select_with_disabled_html_option
+ @post = Post.new
+ @post.written_on = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_written_on_1i" disabled="disabled" name="post[written_on(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_written_on_2i" disabled="disabled" name="post[written_on(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_written_on_3i" disabled="disabled" name="post[written_on(3i)]" value="15" />\n}
+
+ expected << %(<select id="post_written_on_4i" disabled="disabled" name="post[written_on(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %(<select id="post_written_on_5i" disabled="disabled" name="post[written_on(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, time_select("post", "written_on", {}, :disabled => true)
+ end
+
+ def test_datetime_select
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at")
+ end
+
+ def test_datetime_select_with_selected
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3" selected="selected">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">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="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10" selected="selected">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12" selected="selected">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30" selected="selected">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :selected => Time.local(2004, 3, 10, 12, 30))
+ end
+
+ def test_datetime_select_with_selected_nil
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+
+ expected = '<input id="post_updated_at_1i" name="post[updated_at(1i)]" type="hidden" value="1"/>' + "\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", discard_year: true, selected: nil)
+ end
+
+ def test_datetime_select_defaults_to_time_zone_now_when_config_time_zone_is_set
+ # The love zone is UTC+0
+ mytz = Class.new(ActiveSupport::TimeZone) {
+ attr_accessor :now
+ }.create('tenderlove', 0)
+
+ now = Time.mktime(2004, 6, 15, 16, 35, 0)
+ mytz.now = now
+ Time.zone = mytz
+
+ assert_equal mytz, Time.zone
+
+ @post = Post.new
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at")
+ ensure
+ Time.zone = nil
+ end
+
+ def test_datetime_select_with_html_options_within_fields_for
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.datetime_select(:updated_at, {}, :class => 'selector')
+ end
+
+ expected = "<select id='post_updated_at_1i' name='post[updated_at(1i)]' class='selector'>\n<option value='1999'>1999</option>\n<option value='2000'>2000</option>\n<option value='2001'>2001</option>\n<option value='2002'>2002</option>\n<option value='2003'>2003</option>\n<option selected='selected' value='2004'>2004</option>\n<option value='2005'>2005</option>\n<option value='2006'>2006</option>\n<option value='2007'>2007</option>\n<option value='2008'>2008</option>\n<option value='2009'>2009</option>\n</select>\n"
+ expected << "<select id='post_updated_at_2i' name='post[updated_at(2i)]' class='selector'>\n<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 selected='selected' value='6'>June</option>\n<option value='7'>July</option>\n<option value='8'>August</option>\n<option value='9'>September</option>\n<option value='10'>October</option>\n<option value='11'>November</option>\n<option value='12'>December</option>\n</select>\n"
+ expected << "<select id='post_updated_at_3i' name='post[updated_at(3i)]' class='selector'>\n<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 selected='selected' value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n</select>\n"
+ expected << " &mdash; <select id='post_updated_at_4i' name='post[updated_at(4i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option selected='selected' value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n</select>\n"
+ expected << " : <select id='post_updated_at_5i' name='post[updated_at(5i)]' class='selector'>\n<option value='00'>00</option>\n<option value='01'>01</option>\n<option value='02'>02</option>\n<option value='03'>03</option>\n<option value='04'>04</option>\n<option value='05'>05</option>\n<option value='06'>06</option>\n<option value='07'>07</option>\n<option value='08'>08</option>\n<option value='09'>09</option>\n<option value='10'>10</option>\n<option value='11'>11</option>\n<option value='12'>12</option>\n<option value='13'>13</option>\n<option value='14'>14</option>\n<option value='15'>15</option>\n<option value='16'>16</option>\n<option value='17'>17</option>\n<option value='18'>18</option>\n<option value='19'>19</option>\n<option value='20'>20</option>\n<option value='21'>21</option>\n<option value='22'>22</option>\n<option value='23'>23</option>\n<option value='24'>24</option>\n<option value='25'>25</option>\n<option value='26'>26</option>\n<option value='27'>27</option>\n<option value='28'>28</option>\n<option value='29'>29</option>\n<option value='30'>30</option>\n<option value='31'>31</option>\n<option value='32'>32</option>\n<option value='33'>33</option>\n<option value='34'>34</option>\n<option selected='selected' value='35'>35</option>\n<option value='36'>36</option>\n<option value='37'>37</option>\n<option value='38'>38</option>\n<option value='39'>39</option>\n<option value='40'>40</option>\n<option value='41'>41</option>\n<option value='42'>42</option>\n<option value='43'>43</option>\n<option value='44'>44</option>\n<option value='45'>45</option>\n<option value='46'>46</option>\n<option value='47'>47</option>\n<option value='48'>48</option>\n<option value='49'>49</option>\n<option value='50'>50</option>\n<option value='51'>51</option>\n<option value='52'>52</option>\n<option value='53'>53</option>\n<option value='54'>54</option>\n<option value='55'>55</option>\n<option value='56'>56</option>\n<option value='57'>57</option>\n<option value='58'>58</option>\n<option value='59'>59</option>\n</select>\n"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_datetime_select_with_separators
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << " / "
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " , "
+
+ expected << %(<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n)
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " - "
+
+ expected << %(<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n)
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", { :date_separator => " / ", :datetime_separator => " , ", :time_separator => " - ", :include_seconds => true })
+ end
+
+ def test_datetime_select_with_integer
+ @post = Post.new
+ @post.updated_at = 3
+ datetime_select("post", "updated_at")
+ end
+
+ def test_datetime_select_with_infinity # Float
+ @post = Post.new
+ @post.updated_at = (-1.0/0)
+ datetime_select("post", "updated_at")
+ end
+
+ def test_datetime_select_with_default_prompt
+ @post = Post.new
+ @post.updated_at = nil
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="">Year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="">Month</option>\n<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">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="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="">Day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="">Hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="">Minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => true)
+ end
+
+ def test_datetime_select_with_custom_prompt
+ @post = Post.new
+ @post.updated_at = nil
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %{<option value="">Choose year</option>\n<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %{<option value="">Choose month</option>\n<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">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="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %{<option value="">Choose day</option>\n<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ expected << %{<option value="">Choose hour</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ expected << %{<option value="">Choose minute</option>\n<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :start_year=>1999, :end_year=>2009, :prompt => {:year => 'Choose year', :month => 'Choose month', :day => 'Choose day', :hour => 'Choose hour', :minute => 'Choose minute'})
+ end
+
+ def test_date_select_with_zero_value_and_no_start_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+1) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :end_year => Date.today.year+1, :prefix => "date[first]")
+ end
+
+ def test_date_select_with_zero_value_and_no_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ last_year = Time.now.year + 5
+ 2003.upto(last_year) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :start_year => 2003, :prefix => "date[first]")
+ end
+
+ def test_date_select_with_zero_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(0, :prefix => "date[first]")
+ end
+
+ def test_date_select_with_nil_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_date(nil, :prefix => "date[first]")
+ end
+
+ def test_datetime_select_with_nil_value_and_no_start_and_end_year
+ expected = %(<select id="date_first_year" name="date[first][year]">\n)
+ (Date.today.year-5).upto(Date.today.year+5) { |y| expected << %(<option value="#{y}">#{y}</option>\n) }
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_month" name="date[first][month]">\n)
+ expected << %(<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n)
+ expected << "</select>\n"
+
+ expected << %(<select id="date_first_day" name="date[first][day]">\n)
+ expected << %(<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n)
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %(<select id="date_first_hour" name="date[first][hour]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n)
+ expected << "</select>\n"
+
+ expected << " : "
+
+ expected << %(<select id="date_first_minute" name="date[first][minute]">\n)
+ expected << %(<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n)
+ expected << "</select>\n"
+
+ assert_dom_equal expected, select_datetime(nil, :prefix => "date[first]")
+ end
+
+ def test_datetime_select_with_options_index
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+ id = 456
+
+ expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :index => id)
+ end
+
+ def test_datetime_select_within_fields_for_with_options_index
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+ id = 456
+
+ output_buffer = fields_for :post, @post, :index => id do |f|
+ concat f.datetime_select(:updated_at)
+ end
+
+ expected = %{<select id="post_456_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_456_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_456_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_456_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_datetime_select_with_auto_index
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+ id = @post.id
+
+ expected = %{<select id="post_123_updated_at_1i" name="post[#{id}][updated_at(1i)]">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_123_updated_at_2i" name="post[#{id}][updated_at(2i)]">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_123_updated_at_3i" name="post[#{id}][updated_at(3i)]">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_123_updated_at_4i" name="post[#{id}][updated_at(4i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_123_updated_at_5i" name="post[#{id}][updated_at(5i)]">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post[]", "updated_at")
+ end
+
+ def test_datetime_select_with_seconds
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_6i" name="post[updated_at(6i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 35}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :include_seconds => true)
+ end
+
+ def test_datetime_select_discard_year
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true)
+ end
+
+ def test_datetime_select_discard_month
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n}
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_month => true)
+ end
+
+ def test_datetime_select_discard_year_and_month
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_updated_at_2i" name="post[updated_at(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_updated_at_3i" name="post[updated_at(3i)]" value="1" />\n}
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_year => true, :discard_month => true)
+ end
+
+ def test_datetime_select_discard_year_and_month_with_disabled_html_option
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]" value="2004" />\n}
+ expected << %{<input type="hidden" id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]" value="6" />\n}
+ expected << %{<input type="hidden" id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]" value="1" />\n}
+
+ expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", { :discard_year => true, :discard_month => true }, :disabled => true)
+ end
+
+ def test_datetime_select_discard_hour
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_hour => true)
+ end
+
+ def test_datetime_select_discard_minute
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<input type="hidden" id="post_updated_at_5i" name="post[updated_at(5i)]" value="16" />\n}
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true)
+ end
+
+ def test_datetime_select_disabled_and_discard_minute
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" disabled="disabled" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" disabled="disabled" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" disabled="disabled" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" disabled="disabled" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<input type="hidden" id="post_updated_at_5i" disabled="disabled" name="post[updated_at(5i)]" value="16" />\n}
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :discard_minute => true, :disabled => true)
+ end
+
+ def test_datetime_select_invalid_order
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 1999.upto(2009) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2004}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:minute, :day, :hour, :month, :year, :second])
+ end
+
+ def test_datetime_select_discard_with_order
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 15, 16, 35)
+
+ expected = %{<input type="hidden" id="post_updated_at_1i" name="post[updated_at(1i)]" value="2004" />\n}
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 15}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 6}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :order => [:day, :month])
+ end
+
+ def test_datetime_select_with_default_value_as_time
+ @post = Post.new
+ @post.updated_at = nil
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ 2001.upto(2011) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 2006}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 9}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 19}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 15}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 16}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35))
+ end
+
+ def test_include_blank_overrides_default_option
+ @post = Post.new
+ @post.updated_at = nil
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ expected << %(<option value=""></option>\n)
+ (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ expected << %(<option value=""></option>\n)
+ 1.upto(12) { |i| expected << %(<option value="#{i}">#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ expected << %(<option value=""></option>\n)
+ 1.upto(31) { |i| expected << %(<option value="#{i}">#{i}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, date_select("post", "updated_at", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true)
+ end
+
+ def test_datetime_select_with_default_value_as_hash
+ @post = Post.new
+ @post.updated_at = nil
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]">\n}
+ (Time.now.year - 5).upto(Time.now.year + 5) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.year}>#{i}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]">\n}
+ 1.upto(12) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == 10}>#{Date::MONTHNAMES[i]}</option>\n) }
+ expected << "</select>\n"
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]">\n}
+ 1.upto(31) { |i| expected << %(<option value="#{i}"#{' selected="selected"' if i == Time.now.day}>#{i}</option>\n) }
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]">\n}
+ 0.upto(23) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 9}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]">\n}
+ 0.upto(59) { |i| expected << %(<option value="#{sprintf("%02d", i)}"#{' selected="selected"' if i == 42}>#{sprintf("%02d", i)}</option>\n) }
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", :default => { :month => 10, :minute => 42, :hour => 9 })
+ end
+
+ def test_datetime_select_with_html_options
+ @post = Post.new
+ @post.updated_at = Time.local(2004, 6, 15, 16, 35)
+
+ expected = %{<select id="post_updated_at_1i" name="post[updated_at(1i)]" class="selector">\n}
+ expected << %{<option value="1999">1999</option>\n<option value="2000">2000</option>\n<option value="2001">2001</option>\n<option value="2002">2002</option>\n<option value="2003">2003</option>\n<option value="2004" selected="selected">2004</option>\n<option value="2005">2005</option>\n<option value="2006">2006</option>\n<option value="2007">2007</option>\n<option value="2008">2008</option>\n<option value="2009">2009</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_2i" name="post[updated_at(2i)]" class="selector">\n}
+ expected << %{<option value="1">January</option>\n<option value="2">February</option>\n<option value="3">March</option>\n<option value="4">April</option>\n<option value="5">May</option>\n<option value="6" selected="selected">June</option>\n<option value="7">July</option>\n<option value="8">August</option>\n<option value="9">September</option>\n<option value="10">October</option>\n<option value="11">November</option>\n<option value="12">December</option>\n}
+ expected << "</select>\n"
+
+ expected << %{<select id="post_updated_at_3i" name="post[updated_at(3i)]" class="selector">\n}
+ expected << %{<option value="1">1</option>\n<option value="2">2</option>\n<option value="3">3</option>\n<option value="4">4</option>\n<option value="5">5</option>\n<option value="6">6</option>\n<option value="7">7</option>\n<option value="8">8</option>\n<option value="9">9</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15" selected="selected">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n}
+ expected << "</select>\n"
+
+ expected << " &mdash; "
+
+ expected << %{<select id="post_updated_at_4i" name="post[updated_at(4i)]" class="selector">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16" 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}
+ expected << "</select>\n"
+ expected << " : "
+ expected << %{<select id="post_updated_at_5i" name="post[updated_at(5i)]" class="selector">\n}
+ expected << %{<option value="00">00</option>\n<option value="01">01</option>\n<option value="02">02</option>\n<option value="03">03</option>\n<option value="04">04</option>\n<option value="05">05</option>\n<option value="06">06</option>\n<option value="07">07</option>\n<option value="08">08</option>\n<option value="09">09</option>\n<option value="10">10</option>\n<option value="11">11</option>\n<option value="12">12</option>\n<option value="13">13</option>\n<option value="14">14</option>\n<option value="15">15</option>\n<option value="16">16</option>\n<option value="17">17</option>\n<option value="18">18</option>\n<option value="19">19</option>\n<option value="20">20</option>\n<option value="21">21</option>\n<option value="22">22</option>\n<option value="23">23</option>\n<option value="24">24</option>\n<option value="25">25</option>\n<option value="26">26</option>\n<option value="27">27</option>\n<option value="28">28</option>\n<option value="29">29</option>\n<option value="30">30</option>\n<option value="31">31</option>\n<option value="32">32</option>\n<option value="33">33</option>\n<option value="34">34</option>\n<option value="35" selected="selected">35</option>\n<option value="36">36</option>\n<option value="37">37</option>\n<option value="38">38</option>\n<option value="39">39</option>\n<option value="40">40</option>\n<option value="41">41</option>\n<option value="42">42</option>\n<option value="43">43</option>\n<option value="44">44</option>\n<option value="45">45</option>\n<option value="46">46</option>\n<option value="47">47</option>\n<option value="48">48</option>\n<option value="49">49</option>\n<option value="50">50</option>\n<option value="51">51</option>\n<option value="52">52</option>\n<option value="53">53</option>\n<option value="54">54</option>\n<option value="55">55</option>\n<option value="56">56</option>\n<option value="57">57</option>\n<option value="58">58</option>\n<option value="59">59</option>\n}
+ expected << "</select>\n"
+
+ assert_dom_equal expected, datetime_select("post", "updated_at", {}, :class => 'selector')
+ end
+
+ def test_date_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ date_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_datetime_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ datetime_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_time_select_should_not_change_passed_options_hash
+ @post = Post.new
+ @post.updated_at = Time.local(2008, 7, 16, 23, 30)
+
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ time_select(@post, :updated_at, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_date_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_date(Date.today, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_datetime_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_datetime(Time.now, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_time_should_not_change_passed_options_hash
+ options = {
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }
+ select_time(Time.now, options)
+
+ # note: the literal hash is intentional to show that the actual options hash isn't modified
+ # don't change this!
+ assert_equal({
+ :order => [ :year, :month, :day ],
+ :default => { :year => 2008, :month => 7, :day => 16, :hour => 23, :minute => 30, :second => 1 },
+ :discard_type => false,
+ :include_blank => false,
+ :ignore_date => false,
+ :include_seconds => true
+ }, options)
+ end
+
+ def test_select_html_safety
+ assert select_day(16).html_safe?
+ assert select_month(8).html_safe?
+ assert select_year(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
+ assert select_minute(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
+ assert select_second(Time.mktime(2003, 8, 16, 8, 4, 18)).html_safe?
+
+ assert select_minute(8, :use_hidden => true).html_safe?
+ assert select_month(8, :prompt => 'Choose month').html_safe?
+
+ assert select_time(Time.mktime(2003, 8, 16, 8, 4, 18), {}, :class => 'selector').html_safe?
+ assert select_date(Time.mktime(2003, 8, 16), :date_separator => " / ", :start_year => 2003, :end_year => 2005, :prefix => "date[first]").html_safe?
+ end
+
+ def test_object_select_html_safety
+ @post = Post.new
+ @post.written_on = Date.new(2004, 6, 15)
+
+ assert date_select("post", "written_on", :default => Time.local(2006, 9, 19, 15, 16, 35), :include_blank => true).html_safe?
+ assert time_select("post", "written_on", :ignore_date => true).html_safe?
+ end
+
+ def test_time_tag_with_date
+ date = Date.new(2013, 2, 20)
+ expected = '<time datetime="2013-02-20">February 20, 2013</time>'
+ assert_equal expected, time_tag(date)
+ end
+
+ def test_time_tag_with_time
+ time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00')
+ expected = '<time datetime="2013-02-20T00:00:00+00:00">February 20, 2013 00:00</time>'
+ assert_equal expected, time_tag(time)
+ end
+
+ def test_time_tag_pubdate_option
+ assert_match(/<time.*pubdate="pubdate">.*<\/time>/, time_tag(Time.now, :pubdate => true))
+ end
+
+ def test_time_tag_with_given_text
+ assert_match(/<time.*>Right now<\/time>/, time_tag(Time.now, 'Right now'))
+ end
+
+ def test_time_tag_with_given_block
+ assert_match(/<time.*><span>Right now<\/span><\/time>/, time_tag(Time.now){ '<span>Right now</span>'.html_safe })
+ end
+
+ def test_time_tag_with_different_format
+ time = Time.new(2013, 2, 20, 0, 0, 0, '+00:00')
+ expected = '<time datetime="2013-02-20T00:00:00+00:00">20 Feb 00:00</time>'
+ assert_equal expected, time_tag(time, :format => :short)
+ 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/actionview/test/template/debug_helper_test.rb b/actionview/test/template/debug_helper_test.rb
new file mode 100644
index 0000000000..42d06bd9ff
--- /dev/null
+++ b/actionview/test/template/debug_helper_test.rb
@@ -0,0 +1,8 @@
+require 'active_record_unit'
+
+class DebugHelperTest < ActionView::TestCase
+ def test_debug
+ company = Company.new(name: "firebase")
+ assert_match "&nbsp; name: firebase", debug(company)
+ end
+end
diff --git a/actionview/test/template/dependency_tracker_test.rb b/actionview/test/template/dependency_tracker_test.rb
new file mode 100644
index 0000000000..7a9b4b26ac
--- /dev/null
+++ b/actionview/test/template/dependency_tracker_test.rb
@@ -0,0 +1,74 @@
+require 'abstract_unit'
+require 'action_view/dependency_tracker'
+
+class NeckbeardTracker
+ def self.call(name, template)
+ ["foo/#{name}"]
+ end
+end
+
+class FakeTemplate
+ attr_reader :source, :handler
+
+ def initialize(source, handler = Neckbeard)
+ @source, @handler = source, handler
+ end
+end
+
+Neckbeard = lambda {|template| template.source }
+Bowtie = lambda {|template| template.source }
+
+class DependencyTrackerTest < ActionView::TestCase
+ def tracker
+ ActionView::DependencyTracker
+ end
+
+ def setup
+ ActionView::Template.register_template_handler :neckbeard, Neckbeard
+ tracker.register_tracker(:neckbeard, NeckbeardTracker)
+ end
+
+ def teardown
+ tracker.remove_tracker(:neckbeard)
+ end
+
+ def test_finds_tracker_by_template_handler
+ template = FakeTemplate.new("boo/hoo")
+ dependencies = tracker.find_dependencies("boo/hoo", template)
+ assert_equal ["foo/boo/hoo"], dependencies
+ end
+
+ def test_returns_empty_array_if_no_tracker_is_found
+ template = FakeTemplate.new("boo/hoo", Bowtie)
+ dependencies = tracker.find_dependencies("boo/hoo", template)
+ assert_equal [], dependencies
+ end
+end
+
+class ERBTrackerTest < Minitest::Test
+ def make_tracker(name, template)
+ ActionView::DependencyTracker::ERBTracker.new(name, template)
+ end
+
+ def test_dependency_of_erb_template_with_number_in_filename
+ template = FakeTemplate.new("<%# render 'messages/message123' %>", :erb)
+ tracker = make_tracker('messages/_message123', template)
+
+ assert_equal ["messages/message123"], tracker.dependencies
+ end
+
+ def test_finds_dependency_in_correct_directory
+ template = FakeTemplate.new("<%# render(message.topic) %>", :erb)
+ tracker = make_tracker('messages/_message', template)
+
+ assert_equal ["topics/topic"], tracker.dependencies
+ end
+
+ def test_finds_dependency_in_correct_directory_with_underscore
+ template = FakeTemplate.new("<%# render(message_type.messages) %>", :erb)
+ tracker = make_tracker('message_types/_message_type', template)
+
+ assert_equal ["messages/message"], tracker.dependencies
+ end
+end
+
diff --git a/actionview/test/template/digestor_test.rb b/actionview/test/template/digestor_test.rb
new file mode 100644
index 0000000000..779a7fb53c
--- /dev/null
+++ b/actionview/test/template/digestor_test.rb
@@ -0,0 +1,284 @@
+require 'abstract_unit'
+require 'fileutils'
+
+class FixtureTemplate
+ attr_reader :source, :handler
+
+ def initialize(template_path)
+ @source = File.read(template_path)
+ @handler = ActionView::Template.handler_for_extension(:erb)
+ rescue Errno::ENOENT
+ raise ActionView::MissingTemplate.new([], "", [], true, [])
+ end
+end
+
+class FixtureFinder
+ FIXTURES_DIR = "#{File.dirname(__FILE__)}/../fixtures/digestor"
+
+ attr_reader :details
+
+ def initialize
+ @details = {}
+ 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")
+ end
+end
+
+class TemplateDigestorTest < ActionView::TestCase
+ def setup
+ @cwd = Dir.pwd
+ @tmp_dir = Dir.mktmpdir
+
+ FileUtils.cp_r FixtureFinder::FIXTURES_DIR, @tmp_dir
+ Dir.chdir @tmp_dir
+ end
+
+ def teardown
+ Dir.chdir @cwd
+ FileUtils.rm_r @tmp_dir
+ ActionView::Digestor.cache.clear
+ end
+
+ def test_top_level_change_reflected
+ assert_digest_difference("messages/show") do
+ change_template("messages/show")
+ end
+ end
+
+ def test_explicit_dependency
+ assert_digest_difference("messages/show") do
+ change_template("messages/_message")
+ end
+ end
+
+ def test_explicit_dependency_in_multiline_erb_tag
+ assert_digest_difference("messages/show") do
+ change_template("messages/_form")
+ end
+ end
+
+ def test_second_level_dependency
+ assert_digest_difference("messages/show") do
+ change_template("comments/_comments")
+ end
+ end
+
+ def test_second_level_dependency_within_same_directory
+ assert_digest_difference("messages/show") do
+ change_template("messages/_header")
+ end
+ end
+
+ def test_third_level_dependency
+ assert_digest_difference("messages/show") do
+ change_template("comments/_comment")
+ end
+ end
+
+ def test_directory_depth_dependency
+ assert_digest_difference("level/below/index") do
+ change_template("level/below/_header")
+ end
+ end
+
+ def test_logging_of_missing_template
+ assert_logged "Couldn't find template for digesting: messages/something_missing.html" 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
+ digest("messages/show")
+ end
+ end
+
+ def test_nested_template_directory
+ assert_digest_difference("messages/show") do
+ change_template("messages/actions/_move")
+ end
+ end
+
+ def test_recursion_in_renders
+ assert digest("level/recursion") # assert recursion is possible
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
+ def test_chaining_the_top_template_on_recursion
+ assert digest("level/recursion") # assert recursion is possible
+
+ assert_digest_difference("level/recursion") do
+ change_template("level/recursion")
+ end
+
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
+ def test_chaining_the_partial_template_on_recursion
+ assert digest("level/recursion") # assert recursion is possible
+
+ assert_digest_difference("level/recursion") do
+ change_template("level/_recursion")
+ end
+
+ assert_not_nil digest("level/recursion") # assert digest is stored
+ end
+
+ def test_dont_generate_a_digest_for_missing_templates
+ assert_equal '', digest("nothing/there")
+ end
+
+ def test_collection_dependency
+ assert_digest_difference("messages/index") do
+ change_template("messages/_message")
+ end
+
+ assert_digest_difference("messages/index") do
+ change_template("events/_event")
+ end
+ end
+
+ def test_collection_derived_from_record_dependency
+ assert_digest_difference("messages/show") do
+ change_template("events/_event")
+ end
+ end
+
+ def test_details_are_included_in_cache_key
+ # Cache the template digest.
+ old_digest = digest("events/_event")
+
+ # Change the template; the cached digest remains unchanged.
+ change_template("events/_event")
+
+ # The details are changed, so a new cache key is generated.
+ finder.details[:foo] = "bar"
+
+ # The cache is busted.
+ assert_not_equal old_digest, digest("events/_event")
+ end
+
+ def test_extra_whitespace_in_render_partial
+ assert_digest_difference("messages/edit") do
+ change_template("messages/_form")
+ end
+ end
+
+ def test_extra_whitespace_in_render_named_partial
+ assert_digest_difference("messages/edit") do
+ change_template("messages/_header")
+ end
+ end
+
+ def test_extra_whitespace_in_render_record
+ assert_digest_difference("messages/edit") do
+ change_template("messages/_message")
+ end
+ end
+
+ def test_extra_whitespace_in_render_with_parenthesis
+ assert_digest_difference("messages/edit") do
+ change_template("events/_event")
+ end
+ end
+
+ def test_old_style_hash_in_render_invocation
+ assert_digest_difference("messages/edit") do
+ change_template("comments/_comment")
+ end
+ end
+
+ def test_dependencies_via_options_results_in_different_digest
+ digest_plain = digest("comments/_comment")
+ digest_fridge = digest("comments/_comment", dependencies: ["fridge"])
+ digest_phone = digest("comments/_comment", dependencies: ["phone"])
+ digest_fridge_phone = digest("comments/_comment", dependencies: ["fridge", "phone"])
+
+ assert_not_equal digest_plain, digest_fridge
+ assert_not_equal digest_plain, digest_phone
+ assert_not_equal digest_plain, digest_fridge_phone
+ assert_not_equal digest_fridge, digest_phone
+ assert_not_equal digest_fridge, digest_fridge_phone
+ assert_not_equal digest_phone, digest_fridge_phone
+ end
+
+ def test_cache_template_loading
+ resolver_before = ActionView::Resolver.caching
+ ActionView::Resolver.caching = false
+ assert_digest_difference("messages/edit", true) do
+ change_template("comments/_comment")
+ end
+ ActionView::Resolver.caching = resolver_before
+ end
+
+ def test_digest_cache_cleanup_with_recursion
+ first_digest = digest("level/_recursion")
+ second_digest = digest("level/_recursion")
+
+ assert first_digest
+
+ # If the cache is cleaned up correctly, subsequent digests should return the same
+ assert_equal first_digest, second_digest
+ end
+
+ def test_digest_cache_cleanup_with_recursion_and_template_caching_off
+ resolver_before = ActionView::Resolver.caching
+ ActionView::Resolver.caching = false
+
+ first_digest = digest("level/_recursion")
+ second_digest = digest("level/_recursion")
+
+ assert first_digest
+
+ # If the cache is cleaned up correctly, subsequent digests should return the same
+ assert_equal first_digest, second_digest
+ ensure
+ ActionView::Resolver.caching = resolver_before
+ end
+
+ private
+ def assert_logged(message)
+ old_logger = ActionView::Base.logger
+ log = StringIO.new
+ ActionView::Base.logger = Logger.new(log)
+
+ begin
+ yield
+
+ log.rewind
+ assert_match message, log.read
+ ensure
+ ActionView::Base.logger = old_logger
+ end
+ end
+
+ def assert_digest_difference(template_name, persistent = false)
+ previous_digest = digest(template_name)
+ ActionView::Digestor.cache.clear unless persistent
+
+ yield
+
+ assert previous_digest != digest(template_name), "digest didn't change"
+ ActionView::Digestor.cache.clear
+ end
+
+ def digest(template_name, options={})
+ ActionView::Digestor.digest(template_name, :html, finder, options)
+ end
+
+ def finder
+ @finder ||= FixtureFinder.new
+ end
+
+ def change_template(template_name)
+ File.open("digestor/#{template_name}.html.erb", "w") do |f|
+ f.write "\nTHIS WAS CHANGED!"
+ end
+ end
+end
diff --git a/actionpack/test/template/erb/form_for_test.rb b/actionview/test/template/erb/form_for_test.rb
index e722b40a9a..e722b40a9a 100644
--- a/actionpack/test/template/erb/form_for_test.rb
+++ b/actionview/test/template/erb/form_for_test.rb
diff --git a/actionpack/test/template/erb/helper.rb b/actionview/test/template/erb/helper.rb
index a1973068d5..a1973068d5 100644
--- a/actionpack/test/template/erb/helper.rb
+++ b/actionview/test/template/erb/helper.rb
diff --git a/actionpack/test/template/erb/tag_helper_test.rb b/actionview/test/template/erb/tag_helper_test.rb
index 84e328d8be..84e328d8be 100644
--- a/actionpack/test/template/erb/tag_helper_test.rb
+++ b/actionview/test/template/erb/tag_helper_test.rb
diff --git a/actionpack/test/template/erb_util_test.rb b/actionview/test/template/erb_util_test.rb
index 3e5b029cea..3e5b029cea 100644
--- a/actionpack/test/template/erb_util_test.rb
+++ b/actionview/test/template/erb_util_test.rb
diff --git a/actionview/test/template/form_collections_helper_test.rb b/actionview/test/template/form_collections_helper_test.rb
new file mode 100644
index 0000000000..d28e4aeb48
--- /dev/null
+++ b/actionview/test/template/form_collections_helper_test.rb
@@ -0,0 +1,365 @@
+require 'abstract_unit'
+
+class Category < Struct.new(:id, :name)
+end
+
+class FormCollectionsHelperTest < ActionView::TestCase
+ def assert_no_select(selector, value = nil)
+ assert_select(selector, :text => value, :count => 0)
+ end
+
+ def with_collection_radio_buttons(*args, &block)
+ @output_buffer = collection_radio_buttons(*args, &block)
+ end
+
+ def with_collection_check_boxes(*args, &block)
+ @output_buffer = collection_check_boxes(*args, &block)
+ end
+
+ # COLLECTION RADIO BUTTONS
+ test 'collection radio accepts a collection and generates inputs from value method' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
+
+ assert_select 'input[type=radio][value=true]#user_active_true'
+ assert_select 'input[type=radio][value=false]#user_active_false'
+ end
+
+ test 'collection radio accepts a collection and generates inputs from label method' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
+
+ assert_select 'label[for=user_active_true]', 'true'
+ assert_select 'label[for=user_active_false]', 'false'
+ end
+
+ test 'collection radio handles camelized collection values for labels correctly' do
+ with_collection_radio_buttons :user, :active, ['Yes', 'No'], :to_s, :to_s
+
+ assert_select 'label[for=user_active_yes]', 'Yes'
+ assert_select 'label[for=user_active_no]', 'No'
+ end
+
+ test 'collection radio should sanitize collection values for labels correctly' do
+ with_collection_radio_buttons :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
+ assert_select 'label[for=user_name_099]', '$0.99'
+ assert_select 'label[for=user_name_199]', '$1.99'
+ end
+
+ test 'collection radio accepts checked item' do
+ with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => true
+
+ assert_select 'input[type=radio][value=true][checked=checked]'
+ assert_no_select 'input[type=radio][value=false][checked=checked]'
+ end
+
+ test 'collection radio accepts multiple disabled items' do
+ collection = [[1, true], [0, false], [2, 'other']]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => [true, false]
+
+ assert_select 'input[type=radio][value=true][disabled=disabled]'
+ assert_select 'input[type=radio][value=false][disabled=disabled]'
+ assert_no_select 'input[type=radio][value=other][disabled=disabled]'
+ end
+
+ test 'collection radio accepts single disable item' do
+ collection = [[1, true], [0, false]]
+ with_collection_radio_buttons :user, :active, collection, :last, :first, :disabled => true
+
+ assert_select 'input[type=radio][value=true][disabled=disabled]'
+ assert_no_select 'input[type=radio][value=false][disabled=disabled]'
+ 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'
+
+ assert_select 'input[type=radio][value=true].special-radio#user_active_true'
+ assert_select 'input[type=radio][value=false].special-radio#user_active_false'
+ end
+
+ test 'collection radio accepts html options as the last element of array' do
+ collection = [[1, true, {class: 'foo'}], [0, false, {class: 'bar'}]]
+ with_collection_radio_buttons :user, :active, collection, :second, :first
+
+ assert_select 'input[type=radio][value=true].foo#user_active_true'
+ assert_select 'input[type=radio][value=false].bar#user_active_false'
+ end
+
+ test 'collection radio does not wrap input inside the label' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s
+
+ assert_select 'input[type=radio] + label'
+ assert_no_select 'label input'
+ end
+
+ test 'collection radio accepts a block to render the label as radio button wrapper' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label { b.radio_button }
+ end
+
+ assert_select 'label[for=user_active_true] > input#user_active_true[type=radio]'
+ assert_select 'label[for=user_active_false] > input#user_active_false[type=radio]'
+ end
+
+ test 'collection radio accepts a block to change the order of label and radio button' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label + b.radio_button
+ end
+
+ assert_select 'label[for=user_active_true] + input#user_active_true[type=radio]'
+ assert_select 'label[for=user_active_false] + input#user_active_false[type=radio]'
+ end
+
+ test 'collection radio with block helpers accept extra html options' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:class => "radio_button") + b.radio_button(:class => "radio_button")
+ end
+
+ assert_select 'label.radio_button[for=user_active_true] + input#user_active_true.radio_button[type=radio]'
+ assert_select 'label.radio_button[for=user_active_false] + input#user_active_false.radio_button[type=radio]'
+ end
+
+ test 'collection radio with block helpers allows access to current text and value' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:"data-value" => b.value) { b.radio_button + b.text }
+ end
+
+ assert_select 'label[for=user_active_true][data-value=true]', 'true' do
+ assert_select 'input#user_active_true[type=radio]'
+ end
+ assert_select 'label[for=user_active_false][data-value=false]', 'false' do
+ assert_select 'input#user_active_false[type=radio]'
+ end
+ end
+
+ test 'collection radio with block helpers allows access to the current object item in the collection to access extra properties' do
+ with_collection_radio_buttons :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:class => b.object) { b.radio_button + b.text }
+ end
+
+ assert_select 'label.true[for=user_active_true]', 'true' do
+ assert_select 'input#user_active_true[type=radio]'
+ end
+ assert_select 'label.false[for=user_active_false]', 'false' do
+ assert_select 'input#user_active_false[type=radio]'
+ end
+ end
+
+ test 'collection radio buttons with fields for' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ @output_buffer = fields_for(:post) do |p|
+ p.collection_radio_buttons :category_id, collection, :id, :name
+ end
+
+ assert_select 'input#post_category_id_1[type=radio][value=1]'
+ assert_select 'input#post_category_id_2[type=radio][value=2]'
+
+ assert_select 'label[for=post_category_id_1]', 'Category 1'
+ assert_select 'label[for=post_category_id_2]', 'Category 2'
+ end
+
+ test 'collection radio accepts checked item which has a value of false' do
+ with_collection_radio_buttons :user, :active, [[1, true], [0, false]], :last, :first, :checked => false
+ assert_no_select 'input[type=radio][value=true][checked=checked]'
+ assert_select 'input[type=radio][value=false][checked=checked]'
+ end
+
+ # COLLECTION CHECK BOXES
+ test 'collection check boxes accepts a collection and generate a serie of checkboxes for value method' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name
+
+ assert_select 'input#user_category_ids_1[type=checkbox][value=1]'
+ assert_select 'input#user_category_ids_2[type=checkbox][value=2]'
+ end
+
+ test 'collection check boxes generates only one hidden field for the entire collection, to ensure something will be sent back to the server when posting an empty collection' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name
+
+ assert_select "input[type=hidden][name='user[category_ids][]'][value=]", :count => 1
+ end
+
+ test 'collection check boxes generates a hidden field using the given :name in :html_options' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ with_collection_check_boxes :user, :category_ids, collection, :id, :name, {}, {name: "user[other_category_ids][]"}
+
+ assert_select "input[type=hidden][name='user[other_category_ids][]'][value=]", :count => 1
+ end
+
+ test 'collection check boxes accepts a collection and generate a serie 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
+
+ assert_select 'label[for=user_category_ids_1]', 'Category 1'
+ assert_select 'label[for=user_category_ids_2]', 'Category 2'
+ end
+
+ test 'collection check boxes handles camelized collection values for labels correctly' do
+ with_collection_check_boxes :user, :active, ['Yes', 'No'], :to_s, :to_s
+
+ assert_select 'label[for=user_active_yes]', 'Yes'
+ assert_select 'label[for=user_active_no]', 'No'
+ end
+
+ test 'collection check box should sanitize collection values for labels correctly' do
+ with_collection_check_boxes :user, :name, ['$0.99', '$1.99'], :to_s, :to_s
+ assert_select 'label[for=user_name_099]', '$0.99'
+ assert_select 'label[for=user_name_199]', '$1.99'
+ end
+
+ test 'collection check boxes accepts html options as the last element of array' do
+ collection = [[1, 'Category 1', {class: 'foo'}], [2, 'Category 2', {class: 'bar'}]]
+ with_collection_check_boxes :user, :active, collection, :first, :second
+
+ assert_select 'input[type=checkbox][value=1].foo'
+ assert_select 'input[type=checkbox][value=2].bar'
+ end
+
+ test 'collection check boxes accepts selected values as :checked option' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => [1, 3]
+
+ assert_select 'input[type=checkbox][value=1][checked=checked]'
+ assert_select 'input[type=checkbox][value=3][checked=checked]'
+ assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ end
+
+ test 'collection check boxes accepts selected string values as :checked option' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => ['1', '3']
+
+ assert_select 'input[type=checkbox][value=1][checked=checked]'
+ assert_select 'input[type=checkbox][value=3][checked=checked]'
+ assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ end
+
+ test 'collection check boxes accepts a single checked value' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :checked => 3
+
+ assert_select 'input[type=checkbox][value=3][checked=checked]'
+ assert_no_select 'input[type=checkbox][value=1][checked=checked]'
+ assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ end
+
+ test 'collection check boxes accepts selected values as :checked option and override the model values' do
+ user = Struct.new(:category_ids).new(2)
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+
+ @output_buffer = fields_for(:user, user) do |p|
+ p.collection_check_boxes :category_ids, collection, :first, :last, :checked => [1, 3]
+ end
+
+ assert_select 'input[type=checkbox][value=1][checked=checked]'
+ assert_select 'input[type=checkbox][value=3][checked=checked]'
+ assert_no_select 'input[type=checkbox][value=2][checked=checked]'
+ end
+
+ test 'collection check boxes accepts multiple disabled items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => [1, 3]
+
+ assert_select 'input[type=checkbox][value=1][disabled=disabled]'
+ assert_select 'input[type=checkbox][value=3][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ end
+
+ test 'collection check boxes accepts single disable item' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => 1
+
+ assert_select 'input[type=checkbox][value=1][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ end
+
+ test 'collection check boxes accepts a proc to disabled items' do
+ collection = (1..3).map{|i| [i, "Category #{i}"] }
+ with_collection_check_boxes :user, :category_ids, collection, :first, :last, :disabled => proc { |i| i.first == 1 }
+
+ assert_select 'input[type=checkbox][value=1][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value=3][disabled=disabled]'
+ assert_no_select 'input[type=checkbox][value=2][disabled=disabled]'
+ 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'
+
+ assert_select 'input.check[type=checkbox][value=1]'
+ assert_select 'input.check[type=checkbox][value=2]'
+ end
+
+ test 'collection check boxes with fields for' do
+ collection = [Category.new(1, 'Category 1'), Category.new(2, 'Category 2')]
+ @output_buffer = fields_for(:post) do |p|
+ p.collection_check_boxes :category_ids, collection, :id, :name
+ end
+
+ assert_select 'input#post_category_ids_1[type=checkbox][value=1]'
+ assert_select 'input#post_category_ids_2[type=checkbox][value=2]'
+
+ assert_select 'label[for=post_category_ids_1]', 'Category 1'
+ assert_select 'label[for=post_category_ids_2]', 'Category 2'
+ end
+
+ test 'collection check boxes does not wrap input inside the label' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s
+
+ assert_select 'input[type=checkbox] + label'
+ assert_no_select 'label input'
+ end
+
+ test 'collection check boxes accepts a block to render the label as check box wrapper' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label { b.check_box }
+ end
+
+ assert_select 'label[for=user_active_true] > input#user_active_true[type=checkbox]'
+ assert_select 'label[for=user_active_false] > input#user_active_false[type=checkbox]'
+ end
+
+ test 'collection check boxes accepts a block to change the order of label and check box' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label + b.check_box
+ end
+
+ assert_select 'label[for=user_active_true] + input#user_active_true[type=checkbox]'
+ assert_select 'label[for=user_active_false] + input#user_active_false[type=checkbox]'
+ end
+
+ test 'collection check boxes with block helpers accept extra html options' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:class => "check_box") + b.check_box(:class => "check_box")
+ end
+
+ assert_select 'label.check_box[for=user_active_true] + input#user_active_true.check_box[type=checkbox]'
+ assert_select 'label.check_box[for=user_active_false] + input#user_active_false.check_box[type=checkbox]'
+ end
+
+ test 'collection check boxes with block helpers allows access to current text and value' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:"data-value" => b.value) { b.check_box + b.text }
+ end
+
+ assert_select 'label[for=user_active_true][data-value=true]', 'true' do
+ assert_select 'input#user_active_true[type=checkbox]'
+ end
+ assert_select 'label[for=user_active_false][data-value=false]', 'false' do
+ assert_select 'input#user_active_false[type=checkbox]'
+ end
+ end
+
+ test 'collection check boxes with block helpers allows access to the current object item in the collection to access extra properties' do
+ with_collection_check_boxes :user, :active, [true, false], :to_s, :to_s do |b|
+ b.label(:class => b.object) { b.check_box + b.text }
+ end
+
+ assert_select 'label.true[for=user_active_true]', 'true' do
+ assert_select 'input#user_active_true[type=checkbox]'
+ end
+ assert_select 'label.false[for=user_active_false]', 'false' do
+ assert_select 'input#user_active_false[type=checkbox]'
+ end
+ end
+end
diff --git a/actionview/test/template/form_helper_test.rb b/actionview/test/template/form_helper_test.rb
new file mode 100644
index 0000000000..3e8a2468ed
--- /dev/null
+++ b/actionview/test/template/form_helper_test.rb
@@ -0,0 +1,3019 @@
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class FormHelperTest < ActionView::TestCase
+ include RenderERBUtils
+
+ tests ActionView::Helpers::FormHelper
+
+ def form_for(*)
+ @output_buffer = super
+ end
+
+ def setup
+ super
+
+ # Create "label" locale for testing I18n label helpers
+ I18n.backend.store_translations 'label', {
+ activemodel: {
+ attributes: {
+ post: {
+ cost: "Total cost"
+ }
+ }
+ },
+ helpers: {
+ label: {
+ post: {
+ body: "Write entire text here",
+ color: {
+ red: "Rojo"
+ },
+ comments: {
+ body: "Write body here"
+ }
+ },
+ tag: {
+ value: "Tag"
+ }
+ }
+ }
+ }
+
+ # Create "submit" locale for testing I18n submit helpers
+ I18n.backend.store_translations 'submit', {
+ helpers: {
+ submit: {
+ create: 'Create %{model}',
+ update: 'Confirm %{model} changes',
+ submit: 'Save changes',
+ another_post: {
+ update: 'Update your %{model}'
+ }
+ }
+ }
+ }
+
+ @post = Post.new
+ @comment = Comment.new
+ def @post.errors()
+ Class.new {
+ def [](field); field == "author_name" ? ["can't be empty"] : [] end
+ def empty?() false end
+ def count() 1 end
+ def full_messages() ["Author name can't be empty"] end
+ }.new
+ end
+ def @post.to_key; [123]; end
+ def @post.id_before_type_cast; 123; end
+ def @post.to_param; '123'; end
+
+ @post.persisted = true
+ @post.title = "Hello World"
+ @post.author_name = ""
+ @post.body = "Back to the hill and over it again!"
+ @post.secret = 1
+ @post.written_on = Date.new(2004, 6, 15)
+
+ @post.comments = []
+ @post.comments << @comment
+
+ @post.tags = []
+ @post.tags << Tag.new
+
+ @car = Car.new("#000FFF")
+ end
+
+ Routes = ActionDispatch::Routing::RouteSet.new
+ Routes.draw do
+ resources :posts do
+ resources :comments
+ end
+
+ namespace :admin do
+ resources :posts do
+ resources :comments
+ end
+ end
+
+ get "/foo", to: "controller#action"
+ root to: "main#index"
+ end
+
+ def _routes
+ Routes
+ end
+
+ include Routes.url_helpers
+
+ def url_for(object)
+ @url_for_options = object
+
+ if object.is_a?(Hash) && object[:use_route].blank? && object[:controller].blank?
+ object.merge!(controller: "main", action: "index")
+ end
+
+ super
+ end
+
+ class FooTag < ActionView::Helpers::Tags::Base
+ def initialize; end
+ end
+
+ def test_tags_base_child_without_render_method
+ assert_raise(NotImplementedError) { FooTag.new.render }
+ end
+
+ def test_label
+ assert_dom_equal('<label for="post_title">Title</label>', label("post", "title"))
+ assert_dom_equal(
+ '<label for="post_title">The title goes here</label>',
+ label("post", "title", "The title goes here")
+ )
+ assert_dom_equal(
+ '<label class="title_label" for="post_title">Title</label>',
+ label("post", "title", nil, class: 'title_label')
+ )
+ assert_dom_equal('<label for="post_secret">Secret?</label>', label("post", "secret?"))
+ end
+
+ def test_label_with_symbols
+ assert_dom_equal('<label for="post_title">Title</label>', label(:post, :title))
+ assert_dom_equal('<label for="post_secret">Secret?</label>', label(:post, :secret?))
+ 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
+ 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
+ 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
+ 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
+ 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
+ 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)
+ 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
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ I18n.locale = old_locale
+ 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)
+ end
+ 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
+ end
+
+ def test_label_with_for_attribute_as_symbol
+ assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, for: "my_for"))
+ end
+
+ def test_label_with_for_attribute_as_string
+ assert_dom_equal('<label for="my_for">Title</label>', label(:post, :title, nil, "for" => "my_for"))
+ end
+
+ def test_label_does_not_generate_for_attribute_when_given_nil
+ assert_dom_equal('<label>Title</label>', label(:post, :title, for: nil))
+ end
+
+ def test_label_with_id_attribute_as_symbol
+ assert_dom_equal(
+ '<label for="post_title" id="my_id">Title</label>',
+ label(:post, :title, nil, id: "my_id")
+ )
+ end
+
+ def test_label_with_id_attribute_as_string
+ assert_dom_equal(
+ '<label for="post_title" id="my_id">Title</label>',
+ label(:post, :title, nil, "id" => "my_id")
+ )
+ end
+
+ def test_label_with_for_and_id_attributes_as_symbol
+ assert_dom_equal(
+ '<label for="my_for" id="my_id">Title</label>',
+ label(:post, :title, nil, for: "my_for", id: "my_id")
+ )
+ end
+
+ def test_label_with_for_and_id_attributes_as_string
+ assert_dom_equal(
+ '<label for="my_for" id="my_id">Title</label>',
+ label(:post, :title, nil, "for" => "my_for", "id" => "my_id")
+ )
+ end
+
+ def test_label_for_radio_buttons_with_value
+ assert_dom_equal(
+ '<label for="post_title_great_title">The title goes here</label>',
+ label("post", "title", "The title goes here", value: "great_title")
+ )
+ assert_dom_equal(
+ '<label for="post_title_great_title">The title goes here</label>',
+ label("post", "title", "The title goes here", value: "great title")
+ )
+ end
+
+ def test_label_with_block
+ assert_dom_equal(
+ '<label for="post_title">The title, please:</label>',
+ label(:post, :title) { "The title, please:" }
+ )
+ end
+
+ def test_label_with_block_and_options
+ assert_dom_equal(
+ '<label for="my_for">The title, please:</label>',
+ label(:post, :title, "for" => "my_for") { "The title, please:" }
+ )
+ end
+
+ def test_label_with_block_in_erb
+ assert_equal(
+ %{<label for="post_message">\n Message\n <input id="post_message" name="post[message]" type="text" />\n</label>},
+ view.render("test/label_with_block")
+ )
+ end
+
+ def test_text_field
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="text" value="Hello World" />',
+ text_field("post", "title")
+ )
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="password" />',
+ password_field("post", "title")
+ )
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="password" value="Hello World" />',
+ password_field("post", "title", value: @post.title)
+ )
+ assert_dom_equal(
+ '<input id="person_name" name="person[name]" type="password" />',
+ password_field("person", "name")
+ )
+ end
+
+ def test_text_field_with_escapes
+ @post.title = "<b>Hello World</b>"
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="text" value="&lt;b&gt;Hello World&lt;/b&gt;" />',
+ text_field("post", "title")
+ )
+ end
+
+ def test_text_field_with_html_entities
+ @post.title = "The HTML Entity for & is &amp;"
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="text" value="The HTML Entity for &amp; is &amp;amp;" />',
+ text_field("post", "title")
+ )
+ end
+
+ def test_text_field_with_options
+ expected = '<input id="post_title" name="post[title]" size="35" type="text" value="Hello World" />'
+ assert_dom_equal expected, text_field("post", "title", "size" => 35)
+ assert_dom_equal expected, text_field("post", "title", size: 35)
+ end
+
+ def test_text_field_assuming_size
+ expected = '<input id="post_title" maxlength="35" name="post[title]" size="35" type="text" value="Hello World" />'
+ assert_dom_equal expected, text_field("post", "title", "maxlength" => 35)
+ assert_dom_equal expected, text_field("post", "title", maxlength: 35)
+ end
+
+ def test_text_field_removing_size
+ expected = '<input id="post_title" maxlength="35" name="post[title]" type="text" value="Hello World" />'
+ assert_dom_equal expected, text_field("post", "title", "maxlength" => 35, "size" => nil)
+ assert_dom_equal expected, text_field("post", "title", maxlength: 35, size: nil)
+ end
+
+ def test_text_field_with_nil_value
+ expected = '<input id="post_title" name="post[title]" type="text" />'
+ assert_dom_equal expected, text_field("post", "title", value: nil)
+ end
+
+ def test_text_field_with_nil_name
+ expected = '<input id="post_title" type="text" value="Hello World" />'
+ assert_dom_equal expected, text_field("post", "title", name: nil)
+ end
+
+ def test_text_field_doesnt_change_param_values
+ object_name = 'post[]'
+ expected = '<input id="post_123_title" name="post[123][title]" type="text" value="Hello World" />'
+ assert_equal expected, text_field(object_name, "title")
+ assert_equal object_name, "post[]"
+ end
+
+ def test_file_field_has_no_size
+ expected = '<input id="user_avatar" name="user[avatar]" type="file" />'
+ assert_dom_equal expected, file_field("user", "avatar")
+ end
+
+ def test_file_field_with_multiple_behavior
+ expected = '<input id="import_file" multiple="multiple" name="import[file][]" type="file" />'
+ assert_dom_equal expected, file_field("import", "file", :multiple => true)
+ end
+
+ def test_file_field_with_multiple_behavior_and_explicit_name
+ expected = '<input id="import_file" multiple="multiple" name="custom" type="file" />'
+ assert_dom_equal expected, file_field("import", "file", :multiple => true, :name => "custom")
+ end
+
+ def test_hidden_field
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="hidden" value="Hello World" />',
+ hidden_field("post", "title")
+ )
+ assert_dom_equal(
+ '<input id="post_secret" name="post[secret]" type="hidden" value="1" />',
+ hidden_field("post", "secret?")
+ )
+ end
+
+ def test_hidden_field_with_escapes
+ @post.title = "<b>Hello World</b>"
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="hidden" value="&lt;b&gt;Hello World&lt;/b&gt;" />',
+ hidden_field("post", "title")
+ )
+ end
+
+ def test_hidden_field_with_nil_value
+ expected = '<input id="post_title" name="post[title]" type="hidden" />'
+ assert_dom_equal expected, hidden_field("post", "title", value: nil)
+ end
+
+ def test_hidden_field_with_options
+ assert_dom_equal(
+ '<input id="post_title" name="post[title]" type="hidden" value="Something Else" />',
+ hidden_field("post", "title", value: "Something Else")
+ )
+ end
+
+ def test_text_field_with_custom_type
+ assert_dom_equal(
+ '<input id="user_email" name="user[email]" type="email" />',
+ text_field("user", "email", type: "email")
+ )
+ end
+
+ def test_check_box_is_html_safe
+ assert check_box("post", "secret").html_safe?
+ end
+
+ def test_check_box_checked_if_object_value_is_same_that_check_value
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+ end
+
+ def test_check_box_not_checked_if_object_value_is_same_that_unchecked_value
+ @post.secret = 0
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+ end
+
+ def test_check_box_checked_if_option_checked_is_present
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", "checked"=>"checked")
+ )
+ end
+
+ def test_check_box_checked_if_object_value_is_true
+ @post.secret = true
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret?")
+ )
+ end
+
+ def test_check_box_checked_if_object_value_includes_checked_value
+ @post.secret = ['0']
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+
+ @post.secret = ['1']
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+
+ @post.secret = Set.new(['1'])
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret")
+ )
+ end
+
+ def test_check_box_with_include_hidden_false
+ @post.secret = false
+ assert_dom_equal(
+ '<input id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", include_hidden: false)
+ )
+ end
+
+ def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_string
+ @post.secret = "on"
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="off" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />',
+ check_box("post", "secret", {}, "on", "off")
+ )
+
+ @post.secret = "off"
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="off" /><input id="post_secret" name="post[secret]" type="checkbox" value="on" />',
+ check_box("post", "secret", {}, "on", "off")
+ )
+ end
+
+ def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_boolean
+ @post.secret = false
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="true" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="false" />',
+ check_box("post", "secret", {}, false, true)
+ )
+
+ @post.secret = true
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="true" /><input id="post_secret" name="post[secret]" type="checkbox" value="false" />',
+ check_box("post", "secret", {}, false, true)
+ )
+ end
+
+ def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_integer
+ @post.secret = 0
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = 1
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = 2
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+ end
+
+ def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_float
+ @post.secret = 0.0
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = 1.1
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = 2.2
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+ end
+
+ def test_check_box_with_explicit_checked_and_unchecked_values_when_object_value_is_big_decimal
+ @post.secret = BigDecimal.new(0)
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = BigDecimal.new(1)
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+
+ @post.secret = BigDecimal.new(2.2, 1)
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="1" /><input id="post_secret" name="post[secret]" type="checkbox" value="0" />',
+ check_box("post", "secret", {}, 0, 1)
+ )
+ end
+
+ def test_check_box_with_nil_unchecked_value
+ @post.secret = "on"
+ assert_dom_equal(
+ '<input checked="checked" id="post_secret" name="post[secret]" type="checkbox" value="on" />',
+ check_box("post", "secret", {}, "on", nil)
+ )
+ end
+
+ def test_check_box_with_nil_unchecked_value_is_html_safe
+ assert check_box("post", "secret", {}, "on", nil).html_safe?
+ end
+
+ def test_check_box_with_multiple_behavior
+ @post.comment_ids = [2,3]
+ assert_dom_equal(
+ '<input name="post[comment_ids][]" type="hidden" value="0" /><input id="post_comment_ids_1" name="post[comment_ids][]" type="checkbox" value="1" />',
+ check_box("post", "comment_ids", { multiple: true }, 1)
+ )
+ assert_dom_equal(
+ '<input name="post[comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_comment_ids_3" name="post[comment_ids][]" type="checkbox" value="3" />',
+ check_box("post", "comment_ids", { multiple: true }, 3)
+ )
+ end
+
+ def test_check_box_with_multiple_behavior_and_index
+ @post.comment_ids = [2,3]
+ assert_dom_equal(
+ '<input name="post[foo][comment_ids][]" type="hidden" value="0" /><input id="post_foo_comment_ids_1" name="post[foo][comment_ids][]" type="checkbox" value="1" />',
+ check_box("post", "comment_ids", { multiple: true, index: "foo" }, 1)
+ )
+ assert_dom_equal(
+ '<input name="post[bar][comment_ids][]" type="hidden" value="0" /><input checked="checked" id="post_bar_comment_ids_3" name="post[bar][comment_ids][]" type="checkbox" value="3" />',
+ check_box("post", "comment_ids", { multiple: true, index: "bar" }, 3)
+ )
+
+ end
+
+ def test_checkbox_disabled_disables_hidden_field
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" disabled="disabled"/><input checked="checked" disabled="disabled" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", { disabled: true })
+ )
+ end
+
+ def test_checkbox_form_html5_attribute
+ assert_dom_equal(
+ '<input form="new_form" name="post[secret]" type="hidden" value="0" /><input checked="checked" form="new_form" id="post_secret" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", form: "new_form")
+ )
+ end
+
+ def test_radio_button
+ assert_dom_equal('<input checked="checked" id="post_title_hello_world" name="post[title]" type="radio" value="Hello World" />',
+ radio_button("post", "title", "Hello World")
+ )
+ assert_dom_equal('<input id="post_title_goodbye_world" name="post[title]" type="radio" value="Goodbye World" />',
+ radio_button("post", "title", "Goodbye World")
+ )
+ assert_dom_equal('<input id="item_subobject_title_inside_world" name="item[subobject][title]" type="radio" value="inside world"/>',
+ radio_button("item[subobject]", "title", "inside world")
+ )
+ end
+
+ def test_radio_button_is_checked_with_integers
+ assert_dom_equal('<input checked="checked" id="post_secret_1" name="post[secret]" type="radio" value="1" />',
+ radio_button("post", "secret", "1")
+ )
+ end
+
+ def test_radio_button_with_negative_integer_value
+ assert_dom_equal('<input id="post_secret_-1" name="post[secret]" type="radio" value="-1" />',
+ radio_button("post", "secret", "-1"))
+ end
+
+ def test_radio_button_respects_passed_in_id
+ assert_dom_equal('<input checked="checked" id="foo" name="post[secret]" type="radio" value="1" />',
+ radio_button("post", "secret", "1", id: "foo")
+ )
+ end
+
+ def test_radio_button_with_booleans
+ assert_dom_equal('<input id="post_secret_true" name="post[secret]" type="radio" value="true" />',
+ radio_button("post", "secret", true)
+ )
+
+ assert_dom_equal('<input id="post_secret_false" name="post[secret]" type="radio" value="false" />',
+ radio_button("post", "secret", false)
+ )
+ end
+
+ def test_text_area
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_area_with_escapes
+ @post.body = "Back to <i>the</i> hill and over it again!"
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\nBack to &lt;i&gt;the&lt;/i&gt; hill and over it again!</textarea>},
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_area_with_alternate_value
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\nTesting alternate values.</textarea>},
+ text_area("post", "body", value: "Testing alternate values.")
+ )
+ end
+
+ def test_text_area_with_nil_alternate_value
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\n</textarea>},
+ text_area("post", "body", value: nil)
+ )
+ end
+
+ def test_text_area_with_html_entities
+ @post.body = "The HTML Entity for & is &amp;"
+ assert_dom_equal(
+ %{<textarea id="post_body" name="post[body]">\nThe HTML Entity for &amp; is &amp;amp;</textarea>},
+ text_area("post", "body")
+ )
+ end
+
+ def test_text_area_with_size_option
+ assert_dom_equal(
+ %{<textarea cols="183" id="post_body" name="post[body]" rows="820">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", size: "183x820")
+ )
+ end
+
+ def test_color_field_with_valid_hex_color_string
+ expected = %{<input id="car_color" name="car[color]" type="color" value="#000fff" />}
+ assert_dom_equal(expected, color_field("car", "color"))
+ end
+
+ def test_color_field_with_invalid_hex_color_string
+ expected = %{<input id="car_color" name="car[color]" type="color" value="#000000" />}
+ @car.color = "#1234TR"
+ assert_dom_equal(expected, color_field("car", "color"))
+ end
+
+ def test_color_field_with_value_attr
+ expected = %{<input id="car_color" name="car[color]" type="color" value="#00FF00" />}
+ assert_dom_equal(expected, color_field("car", "color", value: "#00FF00"))
+ end
+
+ def test_search_field
+ expected = %{<input id="contact_notes_query" name="contact[notes_query]" type="search" />}
+ assert_dom_equal(expected, search_field("contact", "notes_query"))
+ end
+
+ def test_telephone_field
+ expected = %{<input id="user_cell" name="user[cell]" type="tel" />}
+ assert_dom_equal(expected, telephone_field("user", "cell"))
+ end
+
+ def test_date_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
+ assert_dom_equal(expected, date_field("post", "written_on"))
+ end
+
+ def test_date_field_with_datetime_value
+ 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)
+ assert_dom_equal(expected, date_field("post", "written_on"))
+ end
+
+ def test_date_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="2" 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 = DateTime.new(2000, 6, 15)
+ max_value = DateTime.new(2010, 8, 15)
+ step = 2
+ assert_dom_equal(expected, date_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_date_field_with_value_attr
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2013-06-29" />}
+ value = Date.new(2013,6,29)
+ assert_dom_equal(expected, date_field("post", "written_on", value: value))
+ end
+
+ def test_date_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" value="2004-06-15" />}
+ @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ assert_dom_equal(expected, date_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_date_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="date" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, date_field("post", "written_on"))
+ 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"))
+ end
+
+ def test_time_field_with_datetime_value
+ 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)
+ assert_dom_equal(expected, time_field("post", "written_on"))
+ end
+
+ def test_time_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
+ max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
+ step = 60
+ assert_dom_equal(expected, time_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_time_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="time" value="01:02:03.000" />}
+ @post.written_on = Time.zone.parse('2004-06-15 01:02:03')
+ assert_dom_equal(expected, time_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_time_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="time" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, time_field("post", "written_on"))
+ 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"))
+ end
+
+ def test_datetime_field_with_datetime_value
+ 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)
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ end
+
+ def test_datetime_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
+ max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
+ step = 60
+ assert_dom_equal(expected, datetime_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_datetime_field_with_value_attr
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2013-06-29T13:37:00+00:00" />}
+ value = DateTime.new(2013,6,29,13,37)
+ assert_dom_equal(expected, datetime_field("post", "written_on", value: value))
+ end
+
+ def test_datetime_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" value="2004-06-15T15:30:45.000+0000" />}
+ @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_datetime_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, datetime_field("post", "written_on"))
+ 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"))
+ end
+
+ def test_datetime_local_field_with_datetime_value
+ 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)
+ assert_dom_equal(expected, datetime_local_field("post", "written_on"))
+ end
+
+ def test_datetime_local_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="60" 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 = DateTime.new(2000, 6, 15, 20, 45, 30)
+ max_value = DateTime.new(2010, 8, 15, 10, 25, 00)
+ step = 60
+ assert_dom_equal(expected, datetime_local_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_datetime_local_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" value="2004-06-15T15:30:45" />}
+ @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ assert_dom_equal(expected, datetime_local_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_datetime_local_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="datetime-local" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, datetime_local_field("post", "written_on"))
+ 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"))
+ end
+
+ def test_month_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="month" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, month_field("post", "written_on"))
+ end
+
+ def test_month_field_with_datetime_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ assert_dom_equal(expected, month_field("post", "written_on"))
+ end
+
+ def test_month_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="2" max="2010-12" min="2000-02" name="post[written_on]" type="month" value="2004-06" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = DateTime.new(2000, 2, 13)
+ max_value = DateTime.new(2010, 12, 23)
+ step = 2
+ assert_dom_equal(expected, month_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_month_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="month" value="2004-06" />}
+ @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ assert_dom_equal(expected, month_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_week_field
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
+ assert_dom_equal(expected, week_field("post", "written_on"))
+ end
+
+ def test_week_field_with_nil_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" />}
+ @post.written_on = nil
+ assert_dom_equal(expected, week_field("post", "written_on"))
+ end
+
+ def test_week_field_with_datetime_value
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ assert_dom_equal(expected, week_field("post", "written_on"))
+ end
+
+ def test_week_field_with_extra_attrs
+ expected = %{<input id="post_written_on" step="2" max="2010-W51" min="2000-W06" name="post[written_on]" type="week" value="2004-W24" />}
+ @post.written_on = DateTime.new(2004, 6, 15, 1, 2, 3)
+ min_value = DateTime.new(2000, 2, 13)
+ max_value = DateTime.new(2010, 12, 23)
+ step = 2
+ assert_dom_equal(expected, week_field("post", "written_on", min: min_value, max: max_value, step: step))
+ end
+
+ def test_week_field_with_timewithzone_value
+ previous_time_zone, Time.zone = Time.zone, 'UTC'
+ expected = %{<input id="post_written_on" name="post[written_on]" type="week" value="2004-W24" />}
+ @post.written_on = Time.zone.parse('2004-06-15 15:30:45')
+ assert_dom_equal(expected, week_field("post", "written_on"))
+ ensure
+ Time.zone = previous_time_zone
+ end
+
+ def test_url_field
+ expected = %{<input id="user_homepage" name="user[homepage]" type="url" />}
+ assert_dom_equal(expected, url_field("user", "homepage"))
+ end
+
+ def test_email_field
+ expected = %{<input id="user_address" name="user[address]" type="email" />}
+ assert_dom_equal(expected, email_field("user", "address"))
+ end
+
+ def test_number_field
+ expected = %{<input name="order[quantity]" max="9" id="order_quantity" type="number" min="1" />}
+ assert_dom_equal(expected, number_field("order", "quantity", in: 1...10))
+ expected = %{<input name="order[quantity]" size="30" max="9" id="order_quantity" type="number" min="1" />}
+ assert_dom_equal(expected, number_field("order", "quantity", size: 30, in: 1...10))
+ end
+
+ def test_range_input
+ expected = %{<input name="hifi[volume]" step="0.1" max="11" id="hifi_volume" type="range" min="0" />}
+ assert_dom_equal(expected, range_field("hifi", "volume", in: 0..11, step: 0.1))
+ expected = %{<input name="hifi[volume]" step="0.1" size="30" max="11" id="hifi_volume" type="range" min="0" />}
+ assert_dom_equal(expected, range_field("hifi", "volume", size: 30, in: 0..11, step: 0.1))
+ end
+
+ def test_explicit_name
+ assert_dom_equal(
+ '<input id="post_title" name="dont guess" type="text" value="Hello World" />',
+ text_field("post", "title", "name" => "dont guess")
+ )
+ assert_dom_equal(
+ %{<textarea id="post_body" name="really!">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", "name" => "really!")
+ )
+ assert_dom_equal(
+ '<input name="i mean it" type="hidden" value="0" /><input checked="checked" id="post_secret" name="i mean it" type="checkbox" value="1" />',
+ check_box("post", "secret", "name" => "i mean it")
+ )
+ assert_dom_equal(
+ text_field("post", "title", "name" => "dont guess"),
+ text_field("post", "title", name: "dont guess")
+ )
+ assert_dom_equal(
+ text_area("post", "body", "name" => "really!"),
+ text_area("post", "body", name: "really!")
+ )
+ assert_dom_equal(
+ check_box("post", "secret", "name" => "i mean it"),
+ check_box("post", "secret", name: "i mean it")
+ )
+ end
+
+ def test_explicit_id
+ assert_dom_equal(
+ '<input id="dont guess" name="post[title]" type="text" value="Hello World" />',
+ text_field("post", "title", "id" => "dont guess")
+ )
+ assert_dom_equal(
+ %{<textarea id="really!" name="post[body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", "id" => "really!")
+ )
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" id="i mean it" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", "id" => "i mean it")
+ )
+ assert_dom_equal(
+ text_field("post", "title", "id" => "dont guess"),
+ text_field("post", "title", id: "dont guess")
+ )
+ assert_dom_equal(
+ text_area("post", "body", "id" => "really!"),
+ text_area("post", "body", id: "really!")
+ )
+ assert_dom_equal(
+ check_box("post", "secret", "id" => "i mean it"),
+ check_box("post", "secret", id: "i mean it")
+ )
+ end
+
+ def test_nil_id
+ assert_dom_equal(
+ '<input name="post[title]" type="text" value="Hello World" />',
+ text_field("post", "title", "id" => nil)
+ )
+ assert_dom_equal(
+ %{<textarea name="post[body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", "id" => nil)
+ )
+ assert_dom_equal(
+ '<input name="post[secret]" type="hidden" value="0" /><input checked="checked" name="post[secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", "id" => nil)
+ )
+ assert_dom_equal(
+ '<input type="radio" name="post[secret]" value="0" />',
+ radio_button("post", "secret", "0", "id" => nil)
+ )
+ assert_dom_equal(
+ '<select name="post[secret]"></select>',
+ select("post", "secret", [], {}, "id" => nil)
+ )
+ assert_dom_equal(
+ text_field("post", "title", "id" => nil),
+ text_field("post", "title", id: nil)
+ )
+ assert_dom_equal(
+ text_area("post", "body", "id" => nil),
+ text_area("post", "body", id: nil)
+ )
+ assert_dom_equal(
+ check_box("post", "secret", "id" => nil),
+ check_box("post", "secret", id: nil)
+ )
+ assert_dom_equal(
+ radio_button("post", "secret", "0", "id" => nil),
+ radio_button("post", "secret", "0", id: nil)
+ )
+ end
+
+ def test_index
+ assert_dom_equal(
+ '<input name="post[5][title]" id="post_5_title" type="text" value="Hello World" />',
+ text_field("post", "title", "index" => 5)
+ )
+ assert_dom_equal(
+ %{<textarea name="post[5][body]" id="post_5_body">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", "index" => 5)
+ )
+ assert_dom_equal(
+ '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" id="post_5_secret" />',
+ check_box("post", "secret", "index" => 5)
+ )
+ assert_dom_equal(
+ text_field("post", "title", "index" => 5),
+ text_field("post", "title", "index" => 5)
+ )
+ assert_dom_equal(
+ text_area("post", "body", "index" => 5),
+ text_area("post", "body", "index" => 5)
+ )
+ assert_dom_equal(
+ check_box("post", "secret", "index" => 5),
+ check_box("post", "secret", "index" => 5)
+ )
+ end
+
+ def test_index_with_nil_id
+ assert_dom_equal(
+ '<input name="post[5][title]" type="text" value="Hello World" />',
+ text_field("post", "title", "index" => 5, 'id' => nil)
+ )
+ assert_dom_equal(
+ %{<textarea name="post[5][body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post", "body", "index" => 5, 'id' => nil)
+ )
+ assert_dom_equal(
+ '<input name="post[5][secret]" type="hidden" value="0" /><input checked="checked" name="post[5][secret]" type="checkbox" value="1" />',
+ check_box("post", "secret", "index" => 5, 'id' => nil)
+ )
+ assert_dom_equal(
+ text_field("post", "title", "index" => 5, 'id' => nil),
+ text_field("post", "title", index: 5, id: nil)
+ )
+ assert_dom_equal(
+ text_area("post", "body", "index" => 5, 'id' => nil),
+ text_area("post", "body", index: 5, id: nil)
+ )
+ assert_dom_equal(
+ check_box("post", "secret", "index" => 5, 'id' => nil),
+ check_box("post", "secret", index: 5, id: nil)
+ )
+ end
+
+ def test_auto_index
+ pid = 123
+ assert_dom_equal(
+ %{<label for="post_#{pid}_title">Title</label>},
+ label("post[]", "title")
+ )
+ assert_dom_equal(
+ %{<input id="post_#{pid}_title" name="post[#{pid}][title]" type="text" value="Hello World" />},
+ text_field("post[]","title")
+ )
+ assert_dom_equal(
+ %{<textarea id="post_#{pid}_body" name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post[]", "body")
+ )
+ assert_dom_equal(
+ %{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" id="post_#{pid}_secret" name="post[#{pid}][secret]" type="checkbox" value="1" />},
+ check_box("post[]", "secret")
+ )
+ assert_dom_equal(
+ %{<input checked="checked" id="post_#{pid}_title_hello_world" name="post[#{pid}][title]" type="radio" value="Hello World" />},
+ radio_button("post[]", "title", "Hello World")
+ )
+ assert_dom_equal(
+ %{<input id="post_#{pid}_title_goodbye_world" name="post[#{pid}][title]" type="radio" value="Goodbye World" />},
+ radio_button("post[]", "title", "Goodbye World")
+ )
+ end
+
+ def test_auto_index_with_nil_id
+ pid = 123
+ assert_dom_equal(
+ %{<input name="post[#{pid}][title]" type="text" value="Hello World" />},
+ text_field("post[]", "title", id: nil)
+ )
+ assert_dom_equal(
+ %{<textarea name="post[#{pid}][body]">\nBack to the hill and over it again!</textarea>},
+ text_area("post[]", "body", id: nil)
+ )
+ assert_dom_equal(
+ %{<input name="post[#{pid}][secret]" type="hidden" value="0" /><input checked="checked" name="post[#{pid}][secret]" type="checkbox" value="1" />},
+ check_box("post[]", "secret", id: nil)
+ )
+ assert_dom_equal(
+ %{<input checked="checked" name="post[#{pid}][title]" type="radio" value="Hello World" />},
+ radio_button("post[]", "title", "Hello World", id: nil)
+ )
+ assert_dom_equal(
+ %{<input name="post[#{pid}][title]" type="radio" value="Goodbye World" />},
+ radio_button("post[]", "title", "Goodbye World", id: nil)
+ )
+ end
+
+ def test_form_for_requires_block
+ assert_raises(ArgumentError) do
+ form_for(:post, @post, html: { id: 'create-post' })
+ end
+ end
+
+ def test_form_for_requires_arguments
+ error = assert_raises(ArgumentError) do
+ form_for(nil, html: { id: 'create-post' }) do
+ end
+ end
+ assert_equal "First argument in form cannot contain nil or be empty", error.message
+
+ error = assert_raises(ArgumentError) do
+ form_for([nil, nil], html: { id: 'create-post' }) do
+ end
+ end
+ assert_equal "First argument in form cannot contain nil or be empty", error.message
+ end
+
+ def test_form_for
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ concat f.label(:title) { "The Title" }
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit('Create post')
+ concat f.button('Create post')
+ concat f.button {
+ concat content_tag(:span, 'Create post')
+ }
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch") do
+ "<label for='post_title'>The Title</label>" +
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />" +
+ "<input name='commit' type='submit' value='Create post' />" +
+ "<button name='button' type='submit'>Create post</button>" +
+ "<button name='button' type='submit'><span>Create post</span></button>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+ form_for(post) do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
+ "<label for='post_active_true'>true</label>" +
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<label for='post_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_radio_buttons_with_custom_builder_block
+ post = Post.new
+ def post.active; false; end
+
+ form_for(post) do |f|
+ rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b|
+ b.label { b.radio_button + b.text }
+ end
+ concat rendered_radio_buttons
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<label for='post_active_true'>"+
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
+ "true</label>" +
+ "<label for='post_active_false'>"+
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_radio_buttons_with_custom_builder_block_does_not_leak_the_template
+ post = Post.new
+ def post.active; false; end
+ def post.id; 1; end
+
+ form_for(post) do |f|
+ rendered_radio_buttons = f.collection_radio_buttons(:active, [true, false], :to_s, :to_s) do |b|
+ b.label { b.radio_button + b.text }
+ end
+ concat rendered_radio_buttons
+ concat f.hidden_field :id
+ end
+
+ expected = whole_form("/posts", "new_post_1", "new_post") do
+ "<label for='post_active_true'>"+
+ "<input id='post_active_true' name='post[active]' type='radio' value='true' />" +
+ "true</label>" +
+ "<label for='post_active_false'>"+
+ "<input checked='checked' id='post_active_false' name='post[active]' type='radio' value='false' />" +
+ "false</label>"+
+ "<input id='post_id' name='post[id]' type='hidden' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_namespace_and_with_collection_radio_buttons
+ post = Post.new
+ def post.active; false; end
+
+ form_for(post, namespace: 'foo') do |f|
+ concat f.collection_radio_buttons(:active, [true, false], :to_s, :to_s)
+ end
+
+ expected = whole_form("/posts", "foo_new_post", "new_post") do
+ "<input id='foo_post_active_true' name='post[active]' type='radio' value='true' />" +
+ "<label for='foo_post_active_true'>true</label>" +
+ "<input checked='checked' id='foo_post_active_false' name='post[active]' type='radio' value='false' />" +
+ "<label for='foo_post_active_false'>false</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+ form_for(post) do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "<label for='post_tag_ids_1'>Tag 1</label>" +
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
+ "<label for='post_tag_ids_2'>Tag 2</label>" +
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "<label for='post_tag_ids_3'>Tag 3</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_check_boxes_with_custom_builder_block
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+ form_for(post) do |f|
+ rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b|
+ b.label { b.check_box + b.text }
+ end
+ concat rendered_check_boxes
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post") do
+ "<label for='post_tag_ids_1'>" +
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "Tag 1</label>" +
+ "<label for='post_tag_ids_2'>" +
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
+ "Tag 2</label>" +
+ "<label for='post_tag_ids_3'>" +
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "Tag 3</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_collection_check_boxes_with_custom_builder_block_does_not_leak_the_template
+ post = Post.new
+ def post.tag_ids; [1, 3]; end
+ def post.id; 1; end
+ collection = (1..3).map { |i| [i, "Tag #{i}"] }
+
+ form_for(post) do |f|
+ rendered_check_boxes = f.collection_check_boxes(:tag_ids, collection, :first, :last) do |b|
+ b.label { b.check_box + b.text }
+ end
+ concat rendered_check_boxes
+ concat f.hidden_field :id
+ end
+
+ expected = whole_form("/posts", "new_post_1", "new_post") do
+ "<label for='post_tag_ids_1'>" +
+ "<input checked='checked' id='post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "Tag 1</label>" +
+ "<label for='post_tag_ids_2'>" +
+ "<input id='post_tag_ids_2' name='post[tag_ids][]' type='checkbox' value='2' />" +
+ "Tag 2</label>" +
+ "<label for='post_tag_ids_3'>" +
+ "<input checked='checked' id='post_tag_ids_3' name='post[tag_ids][]' type='checkbox' value='3' />" +
+ "Tag 3</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />"+
+ "<input id='post_id' name='post[id]' type='hidden' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_with_namespace_and_with_collection_check_boxes
+ post = Post.new
+ def post.tag_ids; [1]; end
+ collection = [[1, "Tag 1"]]
+
+ form_for(post, namespace: 'foo') do |f|
+ concat f.collection_check_boxes(:tag_ids, collection, :first, :last)
+ end
+
+ expected = whole_form("/posts", "foo_new_post", "new_post") do
+ "<input checked='checked' id='foo_post_tag_ids_1' name='post[tag_ids][]' type='checkbox' value='1' />" +
+ "<label for='foo_post_tag_ids_1'>Tag 1</label>" +
+ "<input name='post[tag_ids][]' type='hidden' value='' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_file_field_generate_multipart
+ Post.send :attr_accessor, :file
+
+ form_for(@post, html: { id: 'create-post' }) do |f|
+ concat f.file_field(:file)
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_post", method: "patch", multipart: true) do
+ "<input name='post[file]' type='file' id='post_file' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_with_file_field_generate_multipart
+ Comment.send :attr_accessor, :file
+
+ form_for(@post) do |f|
+ concat f.fields_for(:comment, @post) { |c|
+ concat c.file_field(:file)
+ }
+ end
+
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch", multipart: true) do
+ "<input name='post[comment][file]' type='file' id='post_comment_file' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_format
+ form_for(@post, format: :json, html: { id: "edit_post_123", class: "edit_post" }) do |f|
+ concat f.label(:title)
+ end
+
+ expected = whole_form("/posts/123.json", "edit_post_123", "edit_post", method: 'patch') do
+ "<label for='post_title'>Title</label>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_model_using_relative_model_naming
+ blog_post = Blog::Post.new("And his name will be forty and four.", 44)
+
+ form_for(blog_post) do |f|
+ concat f.text_field :title
+ concat f.submit('Edit post')
+ end
+
+ expected = whole_form("/posts/44", "edit_post_44", "edit_post", method: "patch") do
+ "<input name='post[title]' type='text' id='post_title' value='And his name will be forty and four.' />" +
+ "<input name='commit' type='submit' value='Edit post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_symbol_object_name
+ form_for(@post, as: "other_name", html: { id: "create-post" }) do |f|
+ concat f.label(:title, class: 'post_title')
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ concat f.submit('Create post')
+ end
+
+ expected = whole_form("/posts/123", "create-post", "edit_other_name", method: "patch") do
+ "<label for='other_name_title' class='post_title'>Title</label>" +
+ "<input name='other_name[title]' id='other_name_title' value='Hello World' type='text' />" +
+ "<textarea name='other_name[body]' id='other_name_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='other_name[secret]' value='0' type='hidden' />" +
+ "<input name='other_name[secret]' checked='checked' id='other_name_secret' value='1' type='checkbox' />" +
+ "<input name='commit' value='Create post' type='submit' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_method_as_part_of_html_options
+ form_for(@post, url: '/', html: { id: 'create-post', method: :delete }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/", "create-post", "edit_post", method: "delete") do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_method
+ form_for(@post, url: '/', method: :delete, html: { id: 'create-post' }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/", "create-post", "edit_post", method: "delete") do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_search_field
+ # Test case for bug which would emit an "object" attribute
+ # when used with form_for using a search_field form helper
+ form_for(Post.new, url: "/search", html: { id: "search-post", method: :get }) do |f|
+ concat f.search_field(:title)
+ end
+
+ expected = whole_form("/search", "search-post", "new_post", method: "get") do
+ "<input name='post[title]' type='search' id='post_title' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_remote
+ form_for(@post, url: '/', remote: true, html: { id: 'create-post', method: :patch }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_remote_in_html
+ form_for(@post, url: '/', html: { remote: true, id: 'create-post', method: :patch }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/", "create-post", "edit_post", method: "patch", remote: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_remote_without_html
+ @post.persisted = false
+ @post.stubs(:to_key).returns(nil)
+ form_for(@post, remote: true) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/posts", "new_post", "new_post", remote: true) do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_without_object
+ form_for(:post, html: { id: 'create-post' }) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form("/", "create-post") do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_index
+ form_for(@post, as: "post[]") do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<label for='post_123_title'>Title</label>" +
+ "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[123][secret]' type='hidden' value='0' />" +
+ "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_nil_index_option_override
+ form_for(@post, as: "post[]", index: nil) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" +
+ "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[][secret]' type='hidden' value='0' />" +
+ "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_label_error_wrapping
+ form_for(@post) do |f|
+ concat f.label(:author_name, class: 'label')
+ concat f.text_field(:author_name)
+ concat f.submit('Create post')
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
+ "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
+ "<input name='commit' type='submit' value='Create post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_label_error_wrapping_without_conventional_instance_variable
+ post = remove_instance_variable :@post
+
+ form_for(post) do |f|
+ concat f.label(:author_name, class: 'label')
+ concat f.text_field(:author_name)
+ concat f.submit('Create post')
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Author name</label></div>" +
+ "<div class='field_with_errors'><input name='post[author_name]' type='text' id='post_author_name' value='' /></div>" +
+ "<input name='commit' type='submit' value='Create post' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_label_error_wrapping_block_and_non_block_versions
+ form_for(@post) do |f|
+ concat f.label(:author_name, 'Name', class: 'label')
+ concat f.label(:author_name, class: 'label') { 'Name' }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>" +
+ "<div class='field_with_errors'><label for='post_author_name' class='label'>Name</label></div>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_namespace
+ form_for(@post, namespace: 'namespace') do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='namespace_post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_namespace_with_date_select
+ form_for(@post, namespace: 'namespace') do |f|
+ concat f.date_select(:written_on)
+ end
+
+ assert_select 'select#namespace_post_written_on_1i'
+ end
+
+ def test_form_for_with_namespace_with_label
+ form_for(@post, namespace: 'namespace') do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='namespace_post_title'>Title</label>" +
+ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_namespace_and_as_option
+ form_for(@post, namespace: 'namespace', as: 'custom_name') do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form('/posts/123', 'namespace_edit_custom_name', 'edit_custom_name', method: 'patch') do
+ "<input id='namespace_custom_name_title' name='custom_name[title]' type='text' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_two_form_for_with_namespace
+ form_for(@post, namespace: 'namespace_1') do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ expected_1 = whole_form('/posts/123', 'namespace_1_edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='namespace_1_post_title'>Title</label>" +
+ "<input name='post[title]' type='text' id='namespace_1_post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected_1, output_buffer
+
+ form_for(@post, namespace: 'namespace_2') do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ expected_2 = whole_form('/posts/123', 'namespace_2_edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='namespace_2_post_title'>Title</label>" +
+ "<input name='post[title]' type='text' id='namespace_2_post_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected_2, output_buffer
+ end
+
+ def test_fields_for_with_namespace
+ @comment.body = 'Hello World'
+ form_for(@post, namespace: 'namespace') do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.fields_for(@comment) { |c|
+ concat c.text_field(:body)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'namespace_edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[title]' type='text' id='namespace_post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='namespace_post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[comment][body]' type='text' id='namespace_post_comment_body' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_submit_with_object_as_new_record_and_locale_strings
+ old_locale, I18n.locale = I18n.locale, :submit
+
+ @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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ 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
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ I18n.locale = old_locale
+ end
+
+ def test_nested_fields_for
+ @comment.body = 'Hello World'
+ form_for(@post) do |f|
+ concat f.fields_for(@comment) { |c|
+ concat c.text_field(:body)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[comment][body]' type='text' id='post_comment_body' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_nested_collections
+ form_for(@post, as: 'post[]') do |f|
+ concat f.text_field(:title)
+ concat f.fields_for('comment[]', @comment) { |c|
+ concat c.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
+ "<input name='post[123][comment][][name]' type='text' id='post_123_comment__name' value='new comment' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_and_parent_fields
+ form_for(@post, index: 1) do |c|
+ concat c.text_field(:title)
+ concat c.fields_for('comment', @comment, index: 1) { |r|
+ concat r.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[1][title]' type='text' id='post_1_title' value='Hello World' />" +
+ "<input name='post[1][comment][1][name]' type='text' id='post_1_comment_1_name' value='new comment' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_index_and_nested_fields_for
+ output_buffer = form_for(@post, index: 1) do |f|
+ concat f.fields_for(:comment, @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[1][comment][title]' type='text' id='post_1_comment_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_on_both
+ form_for(@post, index: 1) do |f|
+ concat f.fields_for(:comment, @post, index: 5) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[1][comment][5][title]' type='text' id='post_1_comment_5_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_auto_index
+ form_for(@post, as: "post[]") do |f|
+ concat f.fields_for(:comment, @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<input name='post[123][comment][title]' type='text' id='post_123_comment_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_radio_button
+ form_for(@post) do |f|
+ concat f.fields_for(:comment, @post, index: 5) { |c|
+ concat c.radio_button(:title, "hello")
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[comment][5][title]' type='radio' id='post_comment_5_title_hello' value='hello' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_auto_index_on_both
+ form_for(@post, as: "post[]") do |f|
+ concat f.fields_for("comment[]", @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<input name='post[123][comment][123][title]' type='text' id='post_123_comment_123_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_index_and_auto_index
+ output_buffer = form_for(@post, as: "post[]") do |f|
+ concat f.fields_for(:comment, @post, index: 5) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ output_buffer << form_for(@post, as: :post, index: 1) do |f|
+ concat f.fields_for("comment[]", @post) { |c|
+ concat c.text_field(:title)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post[]', 'edit_post[]', method: 'patch') do
+ "<input name='post[123][comment][5][title]' type='text' id='post_123_comment_5_title' value='Hello World' />"
+ end + whole_form('/posts/123', 'edit_post', 'edit_post', method: 'patch') do
+ "<input name='post[1][comment][123][title]' type='text' id='post_1_comment_123_title' value='Hello World' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_a_new_record_on_a_nested_attributes_one_to_one_association
+ @post.author = Author.new
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="new author" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_explicitly_passed_object_on_a_nested_attributes_one_to_one_association
+ form_for(@post) do |f|
+ f.fields_for(:author, Author.new(123)) do |af|
+ assert_not_nil af.object
+ assert_equal 123, af.object.id
+ end
+ end
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_using_erb_and_inline_block
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, include_id: false) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_inherited
+ @post.author = Author.new(321)
+
+ form_for(@post, include_id: false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_existing_record_on_a_nested_attributes_one_to_one_association_with_disabled_hidden_id_override
+ @post.author = Author.new(321)
+
+ form_for(@post, include_id: false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, include_id: true) { |af|
+ af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_one_to_one_association_with_explicit_hidden_field_placement
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.hidden_field(:id)
+ concat af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment, include_id: false) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_inherited
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post, include_id: false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_disabled_hidden_id_override
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+ @post.author = Author.new(321)
+
+ form_for(@post, include_id: false) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:author, include_id: true) { |af|
+ concat af.text_field(:name)
+ }
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="author #321" />' +
+ '<input id="post_author_attributes_id" name="post[author_attributes][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_using_erb_and_inline_block
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_nested_attributes_collection_association_with_explicit_hidden_field_placement
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.hidden_field(:id)
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new, Comment.new]
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="new comment" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_and_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new(321), Comment.new]
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ @post.comments.each do |comment|
+ concat f.fields_for(:comments, comment) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_an_empty_supplied_attributes_collection
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ f.fields_for(:comments, []) do |cf|
+ concat cf.text_field(:name)
+ end
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_existing_records_on_a_supplied_nested_attributes_collection
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:comments, @post.comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_arel_like
+ @post.comments = ArelLike.new
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:comments, @post.comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ 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 = []
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:comments, comments) { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #1" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="1" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="comment #2" />' +
+ '<input id="post_comments_attributes_1_id" name="post[comments_attributes][1][id]" type="hidden" value="2" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_on_a_nested_attributes_collection_association_yields_only_builder
+ @post.comments = [Comment.new(321), Comment.new]
+ yielded_comments = []
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.fields_for(:comments) { |cf|
+ concat cf.text_field(:name)
+ yielded_comments << cf.object
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input name="post[title]" type="text" id="post_title" value="Hello World" />' +
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ '<input id="post_comments_attributes_1_name" name="post[comments_attributes][1][name]" type="text" value="new comment" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ assert_equal yielded_comments, @post.comments
+ end
+
+ def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_for(@post) do |f|
+ concat f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ class FakeAssociationProxy
+ def to_ary
+ [1, 2, 3]
+ end
+ end
+
+ def test_nested_fields_for_with_child_index_option_override_on_a_nested_attributes_collection_association_with_proxy
+ @post.comments = FakeAssociationProxy.new
+
+ form_for(@post) do |f|
+ concat f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
+ concat cf.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input id="post_comments_attributes_abc_name" name="post[comments_attributes][abc][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_abc_id" name="post[comments_attributes][abc][id]" type="hidden" value="321" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_index_method_with_existing_records_on_a_nested_attributes_collection_association
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ expected = 0
+ @post.comments.each do |comment|
+ f.fields_for(:comments, comment) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+ end
+
+ def test_nested_fields_for_index_method_with_existing_and_new_records_on_a_nested_attributes_collection_association
+ @post.comments = [Comment.new(321), Comment.new]
+
+ form_for(@post) do |f|
+ expected = 0
+ @post.comments.each do |comment|
+ f.fields_for(:comments, comment) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+ end
+
+ def test_nested_fields_for_index_method_with_existing_records_on_a_supplied_nested_attributes_collection
+ @post.comments = Array.new(2) { |id| Comment.new(id + 1) }
+
+ form_for(@post) do |f|
+ expected = 0
+ f.fields_for(:comments, @post.comments) { |cf|
+ assert_equal cf.index, expected
+ expected += 1
+ }
+ end
+ end
+
+ def test_nested_fields_for_index_method_with_child_index_option_override_on_a_nested_attributes_collection_association
+ @post.comments = []
+
+ form_for(@post) do |f|
+ f.fields_for(:comments, Comment.new(321), child_index: 'abc') { |cf|
+ assert_equal cf.index, 'abc'
+ }
+ end
+ end
+
+ def test_nested_fields_uses_unique_indices_for_different_collection_associations
+ @post.comments = [Comment.new(321)]
+ @post.tags = [Tag.new(123), Tag.new(456)]
+ @post.comments[0].relevances = []
+ @post.tags[0].relevances = []
+ @post.tags[1].relevances = []
+
+ form_for(@post) do |f|
+ concat f.fields_for(:comments, @post.comments[0]) { |cf|
+ concat cf.text_field(:name)
+ concat cf.fields_for(:relevances, CommentRelevance.new(314)) { |crf|
+ concat crf.text_field(:value)
+ }
+ }
+ concat f.fields_for(:tags, @post.tags[0]) { |tf|
+ concat tf.text_field(:value)
+ concat tf.fields_for(:relevances, TagRelevance.new(3141)) { |trf|
+ concat trf.text_field(:value)
+ }
+ }
+ concat f.fields_for('tags', @post.tags[1]) { |tf|
+ concat tf.text_field(:value)
+ concat tf.fields_for(:relevances, TagRelevance.new(31415)) { |trf|
+ concat trf.text_field(:value)
+ }
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input id="post_comments_attributes_0_name" name="post[comments_attributes][0][name]" type="text" value="comment #321" />' +
+ '<input id="post_comments_attributes_0_relevances_attributes_0_value" name="post[comments_attributes][0][relevances_attributes][0][value]" type="text" value="commentrelevance #314" />' +
+ '<input id="post_comments_attributes_0_relevances_attributes_0_id" name="post[comments_attributes][0][relevances_attributes][0][id]" type="hidden" value="314" />' +
+ '<input id="post_comments_attributes_0_id" name="post[comments_attributes][0][id]" type="hidden" value="321" />' +
+ '<input id="post_tags_attributes_0_value" name="post[tags_attributes][0][value]" type="text" value="tag #123" />' +
+ '<input id="post_tags_attributes_0_relevances_attributes_0_value" name="post[tags_attributes][0][relevances_attributes][0][value]" type="text" value="tagrelevance #3141" />' +
+ '<input id="post_tags_attributes_0_relevances_attributes_0_id" name="post[tags_attributes][0][relevances_attributes][0][id]" type="hidden" value="3141" />' +
+ '<input id="post_tags_attributes_0_id" name="post[tags_attributes][0][id]" type="hidden" value="123" />' +
+ '<input id="post_tags_attributes_1_value" name="post[tags_attributes][1][value]" type="text" value="tag #456" />' +
+ '<input id="post_tags_attributes_1_relevances_attributes_0_value" name="post[tags_attributes][1][relevances_attributes][0][value]" type="text" value="tagrelevance #31415" />' +
+ '<input id="post_tags_attributes_1_relevances_attributes_0_id" name="post[tags_attributes][1][relevances_attributes][0][id]" type="hidden" value="31415" />' +
+ '<input id="post_tags_attributes_1_id" name="post[tags_attributes][1][id]" type="hidden" value="456" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_nested_fields_for_with_hash_like_model
+ @author = HashBackedAuthor.new
+
+ form_for(@post) do |f|
+ concat f.fields_for(:author, @author) { |af|
+ concat af.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ '<input id="post_author_attributes_name" name="post[author_attributes][name]" type="text" value="hash backed author" />'
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for
+ output_buffer = fields_for(:post, @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_with_index
+ output_buffer = fields_for("post[]", @post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[123][title]' type='text' id='post_123_title' value='Hello World' />" +
+ "<textarea name='post[123][body]' id='post_123_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[123][secret]' type='hidden' value='0' />" +
+ "<input name='post[123][secret]' checked='checked' type='checkbox' id='post_123_secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_with_nil_index_option_override
+ output_buffer = fields_for("post[]", @post, index: nil) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[][title]' type='text' id='post__title' value='Hello World' />" +
+ "<textarea name='post[][body]' id='post__body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[][secret]' type='hidden' value='0' />" +
+ "<input name='post[][secret]' checked='checked' type='checkbox' id='post__secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_with_index_option_override
+ output_buffer = fields_for("post[]", @post, index: "abc") do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[abc][title]' type='text' id='post_abc_title' value='Hello World' />" +
+ "<textarea name='post[abc][body]' id='post_abc_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[abc][secret]' type='hidden' value='0' />" +
+ "<input name='post[abc][secret]' checked='checked' type='checkbox' id='post_abc_secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_without_object
+ output_buffer = fields_for(:post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_with_only_object
+ output_buffer = fields_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[secret]' type='hidden' value='0' />" +
+ "<input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' />"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_fields_for_object_with_bracketed_name
+ output_buffer = fields_for("author[post]", @post) do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ assert_dom_equal "<label for=\"author_post_title\">Title</label>" +
+ "<input name='author[post][title]' type='text' id='author_post_title' value='Hello World' />",
+ output_buffer
+ end
+
+ def test_fields_for_object_with_bracketed_name_and_index
+ output_buffer = fields_for("author[post]", @post, index: 1) do |f|
+ concat f.label(:title)
+ concat f.text_field(:title)
+ end
+
+ assert_dom_equal "<label for=\"author_post_1_title\">Title</label>" +
+ "<input name='author[post][1][title]' type='text' id='author_post_1_title' value='Hello World' />",
+ output_buffer
+ end
+
+ def test_form_builder_does_not_have_form_for_method
+ assert !ActionView::Helpers::FormBuilder.instance_methods.include?(:form_for)
+ end
+
+ def test_form_for_and_fields_for
+ form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form|
+ concat post_form.text_field(:title)
+ concat post_form.text_area(:body)
+
+ concat fields_for(:parent_post, @post) { |parent_fields|
+ concat parent_fields.check_box(:secret)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'create-post', 'edit_post', method: 'patch') do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='parent_post[secret]' type='hidden' value='0' />" +
+ "<input name='parent_post[secret]' checked='checked' type='checkbox' id='parent_post_secret' value='1' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_and_fields_for_with_object
+ form_for(@post, as: :post, html: { id: 'create-post' }) do |post_form|
+ concat post_form.text_field(:title)
+ concat post_form.text_area(:body)
+
+ concat post_form.fields_for(@comment) { |comment_fields|
+ concat comment_fields.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'create-post', 'edit_post', method: 'patch') do
+ "<input name='post[title]' type='text' id='post_title' value='Hello World' />" +
+ "<textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea>" +
+ "<input name='post[comment][name]' type='text' id='post_comment_name' value='new comment' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_and_fields_for_with_non_nested_association_and_without_object
+ form_for(@post) do |f|
+ concat f.fields_for(:category) { |c|
+ concat c.text_field(:name)
+ }
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<input name='post[category][name]' type='text' id='post_category_name' />"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ class LabelledFormBuilder < ActionView::Helpers::FormBuilder
+ (field_helpers - %w(hidden_field)).each do |selector|
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def #{selector}(field, *args, &proc)
+ ("<label for='\#{field}'>\#{field.to_s.humanize}:</label> " + super + "<br/>").html_safe
+ end
+ RUBY_EVAL
+ end
+ end
+
+ def test_form_for_with_labelled_builder
+ form_for(@post, builder: LabelledFormBuilder) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_default_form_builder
+ old_default_form_builder, ActionView::Base.default_form_builder =
+ ActionView::Base.default_form_builder, LabelledFormBuilder
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Base.default_form_builder = old_default_form_builder
+ end
+
+ def test_lazy_loading_default_form_builder
+ old_default_form_builder, ActionView::Base.default_form_builder =
+ ActionView::Base.default_form_builder, "FormHelperTest::LabelledFormBuilder"
+
+ form_for(@post) do |f|
+ concat f.text_field(:title)
+ end
+
+ expected = whole_form('/posts/123', 'edit_post_123', 'edit_post', method: 'patch') do
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>"
+ end
+
+ assert_dom_equal expected, output_buffer
+ ensure
+ ActionView::Base.default_form_builder = old_default_form_builder
+ end
+
+ def test_fields_for_with_labelled_builder
+ output_buffer = fields_for(:post, @post, builder: LabelledFormBuilder) do |f|
+ concat f.text_field(:title)
+ concat f.text_area(:body)
+ concat f.check_box(:secret)
+ end
+
+ expected =
+ "<label for='title'>Title:</label> <input name='post[title]' type='text' id='post_title' value='Hello World' /><br/>" +
+ "<label for='body'>Body:</label> <textarea name='post[body]' id='post_body'>\nBack to the hill and over it again!</textarea><br/>" +
+ "<label for='secret'>Secret:</label> <input name='post[secret]' type='hidden' value='0' /><input name='post[secret]' checked='checked' type='checkbox' id='post_secret' value='1' /><br/>"
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_labelled_builder_with_nested_fields_for_without_options_hash
+ klass = nil
+
+ form_for(@post, builder: LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new) do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ def test_form_for_with_labelled_builder_with_nested_fields_for_with_options_hash
+ klass = nil
+
+ form_for(@post, builder: LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new, index: 'foo') do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilder, klass
+ end
+
+ def test_form_for_with_labelled_builder_path
+ path = nil
+
+ form_for(@post, builder: LabelledFormBuilder) do |f|
+ path = f.to_partial_path
+ ''
+ end
+
+ assert_equal 'labelled_form', path
+ end
+
+ class LabelledFormBuilderSubclass < LabelledFormBuilder; end
+
+ def test_form_for_with_labelled_builder_with_nested_fields_for_with_custom_builder
+ klass = nil
+
+ form_for(@post, builder: LabelledFormBuilder) do |f|
+ f.fields_for(:comments, Comment.new, builder: LabelledFormBuilderSubclass) do |nested_fields|
+ klass = nested_fields.class
+ ''
+ end
+ end
+
+ assert_equal LabelledFormBuilderSubclass, klass
+ end
+
+ def test_form_for_with_html_options_adds_options_to_form_tag
+ form_for(@post, html: { id: 'some_form', class: 'some_class', multipart: true }) do |f| end
+ expected = whole_form("/posts/123", "some_form", "some_class", method: "patch", multipart: "multipart/form-data")
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_string_url_option
+ form_for(@post, url: 'http://www.otherdomain.com') do |f| end
+
+ assert_equal whole_form("http://www.otherdomain.com", "edit_post_123", "edit_post", method: "patch"), output_buffer
+ end
+
+ def test_form_for_with_hash_url_option
+ form_for(@post, url: { controller: 'controller', action: 'action' }) do |f| end
+
+ assert_equal 'controller', @url_for_options[:controller]
+ assert_equal 'action', @url_for_options[:action]
+ end
+
+ def test_form_for_with_record_url_option
+ form_for(@post, url: @post) do |f| end
+
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
+ assert_equal expected, output_buffer
+ end
+
+ def test_form_for_with_existing_object
+ form_for(@post) do |f| end
+
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
+ assert_equal expected, output_buffer
+ end
+
+ def test_form_for_with_new_object
+ post = Post.new
+ post.persisted = false
+ def post.to_key; nil; end
+
+ form_for(post) do |f| end
+
+ expected = whole_form("/posts", "new_post", "new_post")
+ assert_equal expected, output_buffer
+ end
+
+ def test_form_for_with_existing_object_in_list
+ @comment.save
+ form_for([@post, @comment]) {}
+
+ expected = whole_form(post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_new_object_in_list
+ form_for([@post, @comment]) {}
+
+ expected = whole_form(post_comments_path(@post), "new_comment", "new_comment")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_existing_object_and_namespace_in_list
+ @comment.save
+ form_for([:admin, @post, @comment]) {}
+
+ expected = whole_form(admin_post_comment_path(@post, @comment), "edit_comment_1", "edit_comment", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_new_object_and_namespace_in_list
+ form_for([:admin, @post, @comment]) {}
+
+ expected = whole_form(admin_post_comments_path(@post), "new_comment", "new_comment")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_existing_object_and_custom_url
+ form_for(@post, url: "/super_posts") do |f| end
+
+ expected = whole_form("/super_posts", "edit_post_123", "edit_post", method: "patch")
+ assert_equal expected, output_buffer
+ end
+
+ def test_form_for_with_default_method_as_patch
+ form_for(@post) {}
+ expected = whole_form("/posts/123", "edit_post_123", "edit_post", method: "patch")
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_for_with_data_attributes
+ form_for(@post, data: { behavior: "stuff" }, remote: true) {}
+ assert_match %r|data-behavior="stuff"|, output_buffer
+ assert_match %r|data-remote="true"|, output_buffer
+ end
+
+ def test_fields_for_returns_block_result
+ output = fields_for(Post.new) { |f| "fields" }
+ assert_equal "fields", output
+ end
+
+ def test_form_for_only_instantiates_builder_once
+ initialization_count = 0
+ builder_class = Class.new(ActionView::Helpers::FormBuilder) do
+ define_method :initialize do |*args|
+ super(*args)
+ initialization_count += 1
+ end
+ end
+
+ form_for(@post, builder: builder_class) { }
+ assert_equal 1, initialization_count, 'form builder instantiated more than once'
+ end
+
+ protected
+
+ def hidden_fields(method = nil)
+ txt = %{<div style="margin:0;padding:0;display:inline">}
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />}
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+ txt << %{</div>}
+ end
+
+ def form_text(action = "/", id = nil, html_class = nil, remote = nil, multipart = nil, method = nil)
+ txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if multipart
+ txt << %{ data-remote="true"} if remote
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ method = method.to_s == "get" ? "get" : "post"
+ txt << %{ method="#{method}">}
+ end
+
+ def whole_form(action = "/", id = nil, html_class = nil, options = {})
+ contents = block_given? ? yield : ""
+
+ method, remote, multipart = options.values_at(:method, :remote, :multipart)
+
+ form_text(action, id, html_class, remote, multipart, method) + hidden_fields(method) + contents + "</form>"
+ end
+
+ def protect_against_forgery?
+ false
+ end
+end
diff --git a/actionpack/test/template/form_options_helper_i18n_test.rb b/actionview/test/template/form_options_helper_i18n_test.rb
index 4972ea6511..4972ea6511 100644
--- a/actionpack/test/template/form_options_helper_i18n_test.rb
+++ b/actionview/test/template/form_options_helper_i18n_test.rb
diff --git a/actionview/test/template/form_options_helper_test.rb b/actionview/test/template/form_options_helper_test.rb
new file mode 100644
index 0000000000..50e9d132a7
--- /dev/null
+++ b/actionview/test/template/form_options_helper_test.rb
@@ -0,0 +1,1325 @@
+require 'abstract_unit'
+
+class Map < Hash
+ def category
+ "<mus>"
+ end
+end
+
+class FormOptionsHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::FormOptionsHelper
+
+ silence_warnings do
+ Post = Struct.new('Post', :title, :author_name, :body, :secret, :written_on, :category, :origin, :allow_comments)
+ Continent = Struct.new('Continent', :continent_name, :countries)
+ Country = Struct.new('Country', :country_id, :country_name)
+ Firm = Struct.new('Firm', :time_zone)
+ Album = Struct.new('Album', :id, :title, :genre)
+ end
+
+ def setup
+ @fake_timezones = %w(A B C D E).map do |id|
+ tz = stub(:name => id, :to_s => id)
+ ActiveSupport::TimeZone.stubs(:[]).with(id).returns(tz)
+ tz
+ end
+ ActiveSupport::TimeZone.stubs(:all).returns(@fake_timezones)
+ end
+
+ def test_collection_options
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title")
+ )
+ end
+
+
+ def test_collection_options_with_preselected_value
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", "Babe")
+ )
+ end
+
+ def test_collection_options_with_preselected_value_array
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", [ "Babe", "Cabe" ])
+ )
+ end
+
+ def test_collection_options_with_proc_for_selected
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" selected=\"selected\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", lambda{|p| p.author_name == 'Babe' })
+ )
+ end
+
+ def test_collection_options_with_disabled_value
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => "Babe")
+ )
+ end
+
+ def test_collection_options_with_disabled_array
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => [ "Babe", "Cabe" ])
+ )
+ end
+
+ def test_collection_options_with_preselected_and_disabled_value
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" selected=\"selected\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", :selected => "Cabe", :disabled => "Babe")
+ )
+ end
+
+ def test_collection_options_with_proc_for_disabled
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\" disabled=\"disabled\">Babe went home</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", "title", :disabled => lambda {|p| %w(Babe Cabe).include?(p.author_name)})
+ )
+ end
+
+ def test_collection_options_with_proc_for_value_method
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, lambda { |p| p.author_name }, "title")
+ )
+ end
+
+ def test_collection_options_with_proc_for_text_method
+ assert_dom_equal(
+ "<option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option>",
+ options_from_collection_for_select(dummy_posts, "author_name", lambda { |p| p.title })
+ )
+ end
+
+ def test_collection_options_with_element_attributes
+ assert_dom_equal(
+ "<option value=\"USA\" class=\"bold\">USA</option>",
+ options_from_collection_for_select([[ "USA", "USA", { :class => 'bold' } ]], :first, :second)
+ )
+ end
+
+ def test_string_options_for_select
+ options = "<option value=\"Denmark\">Denmark</option><option value=\"USA\">USA</option><option value=\"Sweden\">Sweden</option>"
+ assert_dom_equal(
+ options,
+ options_for_select(options)
+ )
+ end
+
+ def test_array_options_for_select
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\">USA</option>\n<option value=\"Sweden\">Sweden</option>",
+ options_for_select([ "<Denmark>", "USA", "Sweden" ])
+ )
+ 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>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], "<USA>")
+ )
+ end
+
+ def test_array_options_for_select_with_selection_array
+ assert_dom_equal(
+ "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" selected=\"selected\">&lt;USA&gt;</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], [ "<USA>", "Sweden" ])
+ )
+ end
+
+ def test_array_options_for_select_with_disabled_value
+ assert_dom_equal(
+ "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => "<USA>")
+ )
+ end
+
+ def test_array_options_for_select_with_disabled_array
+ assert_dom_equal(
+ "<option value=\"Denmark\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\" disabled=\"disabled\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], :disabled => ["<USA>", "Sweden"])
+ )
+ end
+
+ def test_array_options_for_select_with_selection_and_disabled_value
+ assert_dom_equal(
+ "<option value=\"Denmark\" selected=\"selected\">Denmark</option>\n<option value=\"&lt;USA&gt;\" disabled=\"disabled\">&lt;USA&gt;</option>\n<option value=\"Sweden\">Sweden</option>",
+ options_for_select([ "Denmark", "<USA>", "Sweden" ], :selected => "Denmark", :disabled => "<USA>")
+ )
+ end
+
+ def test_boolean_array_options_for_select_with_selection_and_disabled_value
+ assert_dom_equal(
+ "<option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option>",
+ options_for_select([ true, false ], :selected => false, :disabled => nil)
+ )
+ end
+
+ def test_range_options_for_select
+ assert_dom_equal(
+ "<option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option>",
+ options_for_select(1..3)
+ )
+ end
+
+ def test_array_options_for_string_include_in_other_string_bug_fix
+ assert_dom_equal(
+ "<option value=\"ruby\">ruby</option>\n<option value=\"rubyonrails\" selected=\"selected\">rubyonrails</option>",
+ options_for_select([ "ruby", "rubyonrails" ], "rubyonrails")
+ )
+ assert_dom_equal(
+ "<option value=\"ruby\" selected=\"selected\">ruby</option>\n<option value=\"rubyonrails\">rubyonrails</option>",
+ options_for_select([ "ruby", "rubyonrails" ], "ruby")
+ )
+ assert_dom_equal(
+ %(<option value="ruby" selected="selected">ruby</option>\n<option value="rubyonrails">rubyonrails</option>\n<option value=""></option>),
+ options_for_select([ "ruby", "rubyonrails", nil ], "ruby")
+ )
+ end
+
+ def test_hash_options_for_select
+ assert_dom_equal(
+ "<option value=\"Dollar\">$</option>\n<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>",
+ options_for_select("$" => "Dollar", "<DKR>" => "<Kroner>").split("\n").join("\n")
+ )
+ assert_dom_equal(
+ "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>",
+ options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, "Dollar").split("\n").join("\n")
+ )
+ assert_dom_equal(
+ "<option value=\"Dollar\" selected=\"selected\">$</option>\n<option value=\"&lt;Kroner&gt;\" selected=\"selected\">&lt;DKR&gt;</option>",
+ options_for_select({ "$" => "Dollar", "<DKR>" => "<Kroner>" }, [ "Dollar", "<Kroner>" ]).split("\n").join("\n")
+ )
+ end
+
+ def test_ducktyped_options_for_select
+ quack = Struct.new(:first, :last)
+ assert_dom_equal(
+ "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\">$</option>",
+ options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")])
+ )
+ assert_dom_equal(
+ "<option value=\"&lt;Kroner&gt;\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
+ options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], "Dollar")
+ )
+ assert_dom_equal(
+ "<option value=\"&lt;Kroner&gt;\" selected=\"selected\">&lt;DKR&gt;</option>\n<option value=\"Dollar\" selected=\"selected\">$</option>",
+ options_for_select([quack.new("<DKR>", "<Kroner>"), quack.new("$", "Dollar")], ["Dollar", "<Kroner>"])
+ )
+ end
+
+ def test_collection_options_with_preselected_value_as_string_and_option_value_is_integer
+ albums = [ Album.new(1, "first","rap"), Album.new(2, "second","pop")]
+ assert_dom_equal(
+ %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :selected => "1")
+ )
+ end
+
+ def test_collection_options_with_preselected_value_as_integer_and_option_value_is_string
+ albums = [ Album.new("1", "first","rap"), Album.new("2", "second","pop")]
+
+ assert_dom_equal(
+ %(<option selected="selected" value="1">rap</option>\n<option value="2">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :selected => 1)
+ )
+ end
+
+ def test_collection_options_with_preselected_value_as_string_and_option_value_is_float
+ albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+
+ assert_dom_equal(
+ %(<option value="1.0">rap</option>\n<option value="2.0" selected="selected">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :selected => "2.0")
+ )
+ end
+
+ def test_collection_options_with_preselected_value_as_nil
+ albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+
+ assert_dom_equal(
+ %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :selected => nil)
+ )
+ end
+
+ def test_collection_options_with_disabled_value_as_nil
+ albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+
+ assert_dom_equal(
+ %(<option value="1.0">rap</option>\n<option value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :disabled => nil)
+ )
+ end
+
+ def test_collection_options_with_disabled_value_as_array
+ albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop")]
+
+ assert_dom_equal(
+ %(<option disabled="disabled" value="1.0">rap</option>\n<option disabled="disabled" value="2.0">pop</option>),
+ options_from_collection_for_select(albums, "id", "genre", :disabled => ["1.0", 2.0])
+ )
+ end
+
+ def test_collection_options_with_preselected_values_as_string_array_and_option_value_is_float
+ albums = [ Album.new(1.0, "first","rap"), Album.new(2.0, "second","pop"), Album.new(3.0, "third","country") ]
+
+ assert_dom_equal(
+ %(<option value="1.0" selected="selected">rap</option>\n<option value="2.0">pop</option>\n<option value="3.0" selected="selected">country</option>),
+ options_from_collection_for_select(albums, "id", "genre", ["1.0","3.0"])
+ )
+ end
+
+ def test_option_groups_from_collection_for_select
+ assert_dom_equal(
+ "<optgroup label=\"&lt;Africa&gt;\"><option value=\"&lt;sa&gt;\">&lt;South Africa&gt;</option>\n<option value=\"so\">Somalia</option></optgroup><optgroup label=\"Europe\"><option value=\"dk\" selected=\"selected\">Denmark</option>\n<option value=\"ie\">Ireland</option></optgroup>",
+ option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk")
+ )
+ end
+
+ def test_option_groups_from_collection_for_select_returns_html_safe_string
+ assert option_groups_from_collection_for_select(dummy_continents, "countries", "continent_name", "country_id", "country_name", "dk").html_safe?
+ end
+
+ def test_grouped_options_for_select_with_array
+ assert_dom_equal(
+ "<optgroup label=\"North America\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+ grouped_options_for_select([
+ ["North America",
+ [['United States','US'],"Canada"]],
+ ["Europe",
+ [["Great Britain","GB"], "Germany"]]
+ ])
+ )
+ end
+
+ def test_grouped_options_for_select_with_array_and_html_attributes
+ assert_dom_equal(
+ "<optgroup label=\"North America\" data-foo=\"bar\"><option value=\"US\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\" disabled=\"disabled\"><option value=\"GB\">Great Britain</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+ grouped_options_for_select([
+ ["North America", [['United States','US'],"Canada"], :data => { :foo => 'bar' }],
+ ["Europe", [["Great Britain","GB"], "Germany"], :disabled => 'disabled']
+ ])
+ )
+ end
+
+ def test_grouped_options_for_select_with_optional_divider
+ assert_dom_equal(
+ "<optgroup label=\"----------\"><option value=\"US\">US</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"----------\"><option value=\"GB\">GB</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+
+ grouped_options_for_select([['US',"Canada"] , ["GB", "Germany"]], nil, divider: "----------")
+ )
+ end
+
+ def test_grouped_options_for_select_with_selected_and_prompt
+ assert_dom_equal(
+ "<option value=\"\">Choose a product...</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
+ grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: "Choose a product...")
+ )
+ end
+
+ def test_grouped_options_for_select_with_selected_and_prompt_true
+ assert_dom_equal(
+ "<option value=\"\">Please select</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option selected=\"selected\" value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
+ grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], "Cowboy Hat", prompt: true)
+ )
+ end
+
+ def test_grouped_options_for_select_returns_html_safe_string
+ assert grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]]).html_safe?
+ end
+
+ def test_grouped_options_for_select_with_prompt_returns_html_escaped_string
+ assert_dom_equal(
+ "<option value=\"\">&lt;Choose One&gt;</option><optgroup label=\"Hats\"><option value=\"Baseball Cap\">Baseball Cap</option>\n<option value=\"Cowboy Hat\">Cowboy Hat</option></optgroup>",
+ grouped_options_for_select([["Hats", ["Baseball Cap","Cowboy Hat"]]], nil, prompt: '<Choose One>'))
+ end
+
+ def test_optgroups_with_with_options_with_hash
+ assert_dom_equal(
+ "<optgroup label=\"North America\"><option value=\"United States\">United States</option>\n<option value=\"Canada\">Canada</option></optgroup><optgroup label=\"Europe\"><option value=\"Denmark\">Denmark</option>\n<option value=\"Germany\">Germany</option></optgroup>",
+ grouped_options_for_select({'North America' => ['United States','Canada'], 'Europe' => ['Denmark','Germany']})
+ )
+ end
+
+ def test_time_zone_options_no_params
+ opts = time_zone_options_for_select
+ assert_dom_equal "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\">D</option>\n" +
+ "<option value=\"E\">E</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_selected
+ opts = time_zone_options_for_select( "D" )
+ assert_dom_equal "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_unknown_selected
+ opts = time_zone_options_for_select( "K" )
+ assert_dom_equal "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\">D</option>\n" +
+ "<option value=\"E\">E</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_priority_zones
+ zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
+ opts = time_zone_options_for_select( nil, zones )
+ assert_dom_equal "<option value=\"B\">B</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\">D</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_selected_priority_zones
+ zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
+ opts = time_zone_options_for_select( "E", zones )
+ assert_dom_equal "<option value=\"B\">B</option>\n" +
+ "<option value=\"E\" selected=\"selected\">E</option>" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\">D</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_unselected_priority_zones
+ zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
+ opts = time_zone_options_for_select( "C", zones )
+ assert_dom_equal "<option value=\"B\">B</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"C\" selected=\"selected\">C</option>\n" +
+ "<option value=\"D\">D</option>",
+ opts
+ end
+
+ def test_time_zone_options_with_priority_zones_does_not_mutate_time_zones
+ original_zones = ActiveSupport::TimeZone.all.dup
+ zones = [ ActiveSupport::TimeZone.new( "B" ), ActiveSupport::TimeZone.new( "E" ) ]
+ time_zone_options_for_select(nil, zones)
+ assert_equal original_zones, ActiveSupport::TimeZone.all
+ end
+
+ def test_time_zone_options_returns_html_safe_string
+ assert time_zone_options_for_select.html_safe?
+ end
+
+ def test_select
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest))
+ )
+ end
+
+ def test_select_without_multiple
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"></select>",
+ select(:post, :category, "", {}, :multiple => false)
+ )
+ end
+
+ def test_select_with_grouped_collection_as_nested_array
+ @post = Post.new
+
+ countries_by_continent = [
+ ["<Africa>", [["<South Africa>", "<sa>"], ["Somalia", "so"]]],
+ ["Europe", [["Denmark", "dk"], ["Ireland", "ie"]]],
+ ]
+
+ assert_dom_equal(
+ [
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
+ %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
+ %Q{<option value="ie">Ireland</option></optgroup></select>},
+ ].join("\n"),
+ select("post", "origin", countries_by_continent)
+ )
+ end
+
+ def test_select_with_grouped_collection_as_hash
+ @post = Post.new
+
+ countries_by_continent = {
+ "<Africa>" => [["<South Africa>", "<sa>"], ["Somalia", "so"]],
+ "Europe" => [["Denmark", "dk"], ["Ireland", "ie"]],
+ }
+
+ assert_dom_equal(
+ [
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>},
+ %Q{<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk">Denmark</option>},
+ %Q{<option value="ie">Ireland</option></optgroup></select>},
+ ].join("\n"),
+ select("post", "origin", countries_by_continent)
+ )
+ end
+
+ def test_select_with_boolean_method
+ @post = Post.new
+ @post.allow_comments = false
+ assert_dom_equal(
+ "<select id=\"post_allow_comments\" name=\"post[allow_comments]\"><option value=\"true\">true</option>\n<option value=\"false\" selected=\"selected\">false</option></select>",
+ select("post", "allow_comments", %w( true false ))
+ )
+ end
+
+ def test_select_under_fields_for
+ @post = Post.new
+ @post.category = "<mus>"
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.select(:category, %w( abe <mus> hest))
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_fields_for_with_record_inherited_from_hash
+ map = Map.new
+
+ output_buffer = fields_for :map, map do |f|
+ concat f.select(:category, %w( abe <mus> hest))
+ end
+
+ assert_dom_equal(
+ "<select id=\"map_category\" name=\"map[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_under_fields_for_with_index
+ @post = Post.new
+ @post.category = "<mus>"
+
+ output_buffer = fields_for :post, @post, :index => 108 do |f|
+ concat f.select(:category, %w( abe <mus> hest))
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_under_fields_for_with_auto_index
+ @post = Post.new
+ @post.category = "<mus>"
+ def @post.to_param; 108; end
+
+ output_buffer = fields_for "post[]", @post do |f|
+ concat f.select(:category, %w( abe <mus> hest))
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_108_category\" name=\"post[108][category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_under_fields_for_with_string_and_given_prompt
+ @post = Post.new
+ options = "<option value=\"abe\">abe</option><option value=\"mus\">mus</option><option value=\"hest\">hest</option>".html_safe
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.select(:category, options, :prompt => 'The prompt')
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n#{options}</select>",
+ output_buffer
+ )
+ end
+
+ def test_select_under_fields_for_with_block
+ @post = Post.new
+
+ output_buffer = fields_for :post, @post do |f|
+ concat(f.select(:category) do
+ concat content_tag(:option, "hello world")
+ end)
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option>hello world</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_multiple_to_add_hidden_input
+ output_buffer = select(:post, :category, "", {}, :multiple => true)
+ assert_dom_equal(
+ "<input type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_multiple_and_without_hidden_input
+ output_buffer = select(:post, :category, "", {:include_hidden => false}, :multiple => true)
+ assert_dom_equal(
+ "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_multiple_and_with_explicit_name_ending_with_brackets
+ output_buffer = select(:post, :category, [], {include_hidden: false}, multiple: true, name: 'post[category][]')
+ assert_dom_equal(
+ "<select multiple=\"multiple\" id=\"post_category\" name=\"post[category][]\"></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_multiple_and_disabled_to_add_disabled_hidden_input
+ output_buffer = select(:post, :category, "", {}, :multiple => true, :disabled => true)
+ assert_dom_equal(
+ "<input disabled=\"disabled\"type=\"hidden\" name=\"post[category][]\" value=\"\"/><select multiple=\"multiple\" disabled=\"disabled\" id=\"post_category\" name=\"post[category][]\"></select>",
+ output_buffer
+ )
+ end
+
+ def test_select_with_blank
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :include_blank => true)
+ )
+ end
+
+ def test_select_with_blank_as_string
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">None</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :include_blank => 'None')
+ )
+ end
+
+ def test_select_with_blank_as_string_escaped
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;None&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :include_blank => '<None>')
+ )
+ end
+
+ def test_select_with_default_prompt
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :prompt => true)
+ )
+ end
+
+ def test_select_no_prompt_when_select_has_value
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :prompt => true)
+ )
+ end
+
+ def test_select_with_given_prompt
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">The prompt</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :prompt => 'The prompt')
+ )
+ end
+
+ def test_select_with_given_prompt_escaped
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">&lt;The prompt&gt;</option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :prompt => '<The prompt>')
+ )
+ end
+
+ def test_select_with_prompt_and_blank
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest), :prompt => true, :include_blank => true)
+ )
+ end
+
+ def test_empty
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n</select>",
+ select("post", "category", [], :prompt => true, :include_blank => true)
+ )
+ end
+
+ def test_select_with_nil
+ @post = Post.new
+ @post.category = "othervalue"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\"></option>\n<option value=\"othervalue\" selected=\"selected\">othervalue</option></select>",
+ select("post", "category", [nil, "othervalue"])
+ )
+ end
+
+ def test_required_select
+ assert_dom_equal(
+ %(<select id="post_category" name="post[category]" required="required"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), {}, required: true)
+ )
+ end
+
+ def test_required_select_with_include_blank_prompt
+ assert_dom_equal(
+ %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), { include_blank: "Select one" }, required: true)
+ )
+ end
+
+ def test_required_select_with_prompt
+ assert_dom_equal(
+ %(<select id="post_category" name="post[category]" required="required"><option value="">Select one</option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), { prompt: "Select one" }, required: true)
+ )
+ end
+
+ def test_required_select_display_size_equals_to_one
+ assert_dom_equal(
+ %(<select id="post_category" name="post[category]" required="required" size="1"><option value=""></option>\n<option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), {}, required: true, size: 1)
+ )
+ end
+
+ def test_required_select_with_display_size_bigger_than_one
+ assert_dom_equal(
+ %(<select id="post_category" name="post[category]" required="required" size="2"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), {}, required: true, size: 2)
+ )
+ end
+
+ def test_required_select_with_multiple_option
+ assert_dom_equal(
+ %(<input name="post[category][]" type="hidden" value=""/><select id="post_category" multiple="multiple" name="post[category][]" required="required"><option value="abe">abe</option>\n<option value="mus">mus</option>\n<option value="hest">hest</option></select>),
+ select("post", "category", %w(abe mus hest), {}, required: true, multiple: true)
+ )
+ end
+
+ def test_select_with_fixnum
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"1\">1</option></select>",
+ select("post", "category", [1], :prompt => true, :include_blank => true)
+ )
+ end
+
+ def test_list_of_lists
+ @post = Post.new
+ @post.category = ""
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"\">Please select</option>\n<option value=\"\"></option>\n<option value=\"number\">Number</option>\n<option value=\"text\">Text</option>\n<option value=\"boolean\">Yes/No</option></select>",
+ select("post", "category", [["Number", "number"], ["Text", "text"], ["Yes/No", "boolean"]], :prompt => true, :include_blank => true)
+ )
+ end
+
+ def test_select_with_selected_value
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" selected=\"selected\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest ), :selected => 'abe')
+ )
+ end
+
+ def test_select_with_index_option
+ @album = Album.new
+ @album.id = 1
+
+ expected = "<select id=\"album__genre\" name=\"album[][genre]\"><option value=\"rap\">rap</option>\n<option value=\"rock\">rock</option>\n<option value=\"country\">country</option></select>"
+
+ assert_dom_equal(
+ expected,
+ select("album[]", "genre", %w[rap rock country], {}, { :index => nil })
+ )
+ end
+
+ def test_select_escapes_options
+ assert_dom_equal(
+ '<select id="post_title" name="post[title]">&lt;script&gt;alert(1)&lt;/script&gt;</select>',
+ select('post', 'title', '<script>alert(1)</script>')
+ )
+ end
+
+ def test_select_with_selected_nil
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\">&lt;mus&gt;</option>\n<option value=\"hest\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest ), :selected => nil)
+ )
+ end
+
+ def test_select_with_disabled_value
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest ), :disabled => 'hest')
+ )
+ end
+
+ def test_select_not_existing_method_with_selected_value
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_locale\" name=\"post[locale]\"><option value=\"en\">en</option>\n<option value=\"ru\" selected=\"selected\">ru</option></select>",
+ select("post", "locale", %w( en ru ), :selected => 'ru')
+ )
+ end
+
+ def test_select_with_prompt_and_selected_value
+ @post = Post.new
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"one\">one</option>\n<option selected=\"selected\" value=\"two\">two</option></select>",
+ select("post", "category", %w( one two ), :selected => 'two', :prompt => true)
+ )
+ end
+
+ def test_select_with_disabled_array
+ @post = Post.new
+ @post.category = "<mus>"
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"abe\" disabled=\"disabled\">abe</option>\n<option value=\"&lt;mus&gt;\" selected=\"selected\">&lt;mus&gt;</option>\n<option value=\"hest\" disabled=\"disabled\">hest</option></select>",
+ select("post", "category", %w( abe <mus> hest ), :disabled => ['hest', 'abe'])
+ )
+ end
+
+ def test_select_with_range
+ @post = Post.new
+ @post.category = 0
+ assert_dom_equal(
+ "<select id=\"post_category\" name=\"post[category]\"><option value=\"1\">1</option>\n<option value=\"2\">2</option>\n<option value=\"3\">3</option></select>",
+ select("post", "category", 1..3)
+ )
+ end
+
+ def test_collection_select
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name")
+ )
+ end
+
+ def test_collection_select_under_fields_for
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_collection_select_under_fields_for_with_index
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ output_buffer = fields_for :post, @post, :index => 815 do |f|
+ concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_collection_select_under_fields_for_with_auto_index
+ @post = Post.new
+ @post.author_name = "Babe"
+ def @post.to_param; 815; end
+
+ output_buffer = fields_for "post[]", @post do |f|
+ concat f.collection_select(:author_name, dummy_posts, :author_name, :author_name)
+ end
+
+ assert_dom_equal(
+ "<select id=\"post_815_author_name\" name=\"post[815][author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ output_buffer
+ )
+ end
+
+ def test_collection_select_with_blank_and_style
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, "style" => "width: 200px")
+ )
+ end
+
+ def test_collection_select_with_blank_as_string_and_style
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\" style=\"width: 200px\"><option value=\"\">No Selection</option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>",
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => 'No Selection' }, "style" => "width: 200px")
+ )
+ end
+
+ def test_collection_select_with_multiple_option_appends_array_brackets_and_hidden_input
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ expected = "<input type=\"hidden\" name=\"post[author_name][]\" value=\"\"/><select id=\"post_author_name\" name=\"post[author_name][]\" multiple=\"multiple\"><option value=\"\"></option>\n<option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\">Cabe</option></select>"
+
+ # Should suffix default name with [].
+ assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true }, :multiple => true)
+
+ # Shouldn't suffix custom name with [].
+ assert_dom_equal expected, collection_select("post", "author_name", dummy_posts, "author_name", "author_name", { :include_blank => true, :name => 'post[author_name][]' }, :multiple => true)
+ end
+
+ def test_collection_select_with_blank_and_selected
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_dom_equal(
+ %{<select id="post_author_name" name="post[author_name]"><option value=""></option>\n<option value="&lt;Abe&gt;" selected="selected">&lt;Abe&gt;</option>\n<option value="Babe">Babe</option>\n<option value="Cabe">Cabe</option></select>},
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", {:include_blank => true, :selected => "<Abe>"})
+ )
+ end
+
+ def test_collection_select_with_disabled
+ @post = Post.new
+ @post.author_name = "Babe"
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt;</option>\n<option value=\"Babe\" selected=\"selected\">Babe</option>\n<option value=\"Cabe\" disabled=\"disabled\">Cabe</option></select>",
+ collection_select("post", "author_name", dummy_posts, "author_name", "author_name", :disabled => 'Cabe')
+ )
+ end
+
+ def test_collection_select_with_proc_for_value_method
+ @post = Post.new
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>",
+ collection_select("post", "author_name", dummy_posts, lambda { |p| p.author_name }, "title")
+ )
+ end
+
+ def test_collection_select_with_proc_for_text_method
+ @post = Post.new
+
+ assert_dom_equal(
+ "<select id=\"post_author_name\" name=\"post[author_name]\"><option value=\"&lt;Abe&gt;\">&lt;Abe&gt; went home</option>\n<option value=\"Babe\">Babe went home</option>\n<option value=\"Cabe\">Cabe went home</option></select>",
+ collection_select("post", "author_name", dummy_posts, "author_name", lambda { |p| p.title })
+ )
+ end
+
+ def test_time_zone_select
+ @firm = Firm.new("D")
+ html = time_zone_select( "firm", "time_zone" )
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_under_fields_for
+ @firm = Firm.new("D")
+
+ output_buffer = fields_for :firm, @firm do |f|
+ concat f.time_zone_select(:time_zone)
+ end
+
+ assert_dom_equal(
+ "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ output_buffer
+ )
+ end
+
+ def test_time_zone_select_under_fields_for_with_index
+ @firm = Firm.new("D")
+
+ output_buffer = fields_for :firm, @firm, :index => 305 do |f|
+ concat f.time_zone_select(:time_zone)
+ end
+
+ assert_dom_equal(
+ "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ output_buffer
+ )
+ end
+
+ def test_time_zone_select_under_fields_for_with_auto_index
+ @firm = Firm.new("D")
+ def @firm.to_param; 305; end
+
+ output_buffer = fields_for "firm[]", @firm do |f|
+ concat f.time_zone_select(:time_zone)
+ end
+
+ assert_dom_equal(
+ "<select id=\"firm_305_time_zone\" name=\"firm[305][time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ output_buffer
+ )
+ end
+
+ def test_time_zone_select_with_blank
+ @firm = Firm.new("D")
+ html = time_zone_select("firm", "time_zone", nil, :include_blank => true)
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"\"></option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_blank_as_string
+ @firm = Firm.new("D")
+ html = time_zone_select("firm", "time_zone", nil, :include_blank => 'No Zone')
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"\">No Zone</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_style
+ @firm = Firm.new("D")
+ html = time_zone_select("firm", "time_zone", nil, {},
+ "style" => "color: red")
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ assert_dom_equal html, time_zone_select("firm", "time_zone", nil, {},
+ :style => "color: red")
+ end
+
+ def test_time_zone_select_with_blank_and_style
+ @firm = Firm.new("D")
+ html = time_zone_select("firm", "time_zone", nil,
+ { :include_blank => true }, "style" => "color: red")
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
+ "<option value=\"\"></option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
+ { :include_blank => true }, :style => "color: red")
+ end
+
+ def test_time_zone_select_with_blank_as_string_and_style
+ @firm = Firm.new("D")
+ html = time_zone_select("firm", "time_zone", nil,
+ { :include_blank => 'No Zone' }, "style" => "color: red")
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\" style=\"color: red\">" +
+ "<option value=\"\">No Zone</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ assert_dom_equal html, time_zone_select("firm", "time_zone", nil,
+ { :include_blank => 'No Zone' }, :style => "color: red")
+ end
+
+ def test_time_zone_select_with_priority_zones
+ @firm = Firm.new("D")
+ zones = [ ActiveSupport::TimeZone.new("A"), ActiveSupport::TimeZone.new("D") ]
+ html = time_zone_select("firm", "time_zone", zones )
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_priority_zones_as_regexp
+ @firm = Firm.new("D")
+
+ @fake_timezones.each_with_index do |tz, i|
+ tz.stubs(:=~).returns(i.zero? || i == 3)
+ end
+
+ html = time_zone_select("firm", "time_zone", /A|D/)
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_priority_zones_as_regexp_using_grep_finds_no_zones
+ @firm = Firm.new("D")
+
+ priority_zones = /A|D/
+ @fake_timezones.each do |tz|
+ priority_zones.stubs(:===).with(tz).raises(Exception)
+ end
+
+ html = time_zone_select("firm", "time_zone", priority_zones)
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"\" disabled=\"disabled\">-------------</option>\n" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_default_time_zone_and_nil_value
+ @firm = Firm.new()
+ @firm.time_zone = nil
+
+ html = time_zone_select( "firm", "time_zone", nil, :default => 'B' )
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\" selected=\"selected\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_time_zone_select_with_default_time_zone_and_value
+ @firm = Firm.new('D')
+
+ html = time_zone_select( "firm", "time_zone", nil, :default => 'B' )
+ assert_dom_equal "<select id=\"firm_time_zone\" name=\"firm[time_zone]\">" +
+ "<option value=\"A\">A</option>\n" +
+ "<option value=\"B\">B</option>\n" +
+ "<option value=\"C\">C</option>\n" +
+ "<option value=\"D\" selected=\"selected\">D</option>\n" +
+ "<option value=\"E\">E</option>" +
+ "</select>",
+ html
+ end
+
+ def test_options_for_select_with_element_attributes
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\" class=\"bold\">&lt;Denmark&gt;</option>\n<option value=\"USA\" onclick=\"alert(&#39;Hello World&#39;);\">USA</option>\n<option value=\"Sweden\">Sweden</option>\n<option value=\"Germany\">Germany</option>",
+ options_for_select([ [ "<Denmark>", { :class => 'bold' } ], [ "USA", { :onclick => "alert('Hello World');" } ], [ "Sweden" ], "Germany" ])
+ )
+ end
+
+ def test_options_for_select_with_data_element
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\" data-test=\"bold\">&lt;Denmark&gt;</option>",
+ options_for_select([ [ "<Denmark>", { :data => { :test => 'bold' } } ] ])
+ )
+ end
+
+ def test_options_for_select_with_data_element_with_special_characters
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\" data-test=\"&lt;bold&gt;\">&lt;Denmark&gt;</option>",
+ options_for_select([ [ "<Denmark>", { :data => { :test => '<bold>' } } ] ])
+ )
+ end
+
+ def test_options_for_select_with_element_attributes_and_selection
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\">Sweden</option>",
+ options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], "USA")
+ )
+ end
+
+ def test_options_for_select_with_element_attributes_and_selection_array
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\">&lt;Denmark&gt;</option>\n<option value=\"USA\" class=\"bold\" selected=\"selected\">USA</option>\n<option value=\"Sweden\" selected=\"selected\">Sweden</option>",
+ options_for_select([ "<Denmark>", [ "USA", { :class => 'bold' } ], "Sweden" ], [ "USA", "Sweden" ])
+ )
+ end
+
+ def test_options_for_select_with_special_characters
+ assert_dom_equal(
+ "<option value=\"&lt;Denmark&gt;\" onclick=\"alert(&quot;&lt;code&gt;&quot;)\">&lt;Denmark&gt;</option>",
+ options_for_select([ [ "<Denmark>", { :onclick => %(alert("<code>")) } ] ])
+ )
+ end
+
+ def test_option_html_attributes_with_no_array_element
+ assert_equal({}, option_html_attributes('foo'))
+ end
+
+ def test_option_html_attributes_without_hash
+ assert_equal({}, option_html_attributes([ 'foo', 'bar' ]))
+ end
+
+ def test_option_html_attributes_with_single_element_hash
+ assert_equal(
+ {:class => 'fancy'},
+ option_html_attributes([ 'foo', 'bar', { :class => 'fancy' } ])
+ )
+ end
+
+ def test_option_html_attributes_with_multiple_element_hash
+ assert_equal(
+ {:class => 'fancy', 'onclick' => "alert('Hello World');"},
+ option_html_attributes([ 'foo', 'bar', { :class => 'fancy', 'onclick' => "alert('Hello World');" } ])
+ )
+ end
+
+ def test_option_html_attributes_with_multiple_hashes
+ assert_equal(
+ {:class => 'fancy', 'onclick' => "alert('Hello World');"},
+ option_html_attributes([ 'foo', 'bar', { :class => 'fancy' }, { 'onclick' => "alert('Hello World');" } ])
+ )
+ end
+
+ def test_option_html_attributes_with_multiple_hashes_does_not_modify_them
+ options1 = { class: 'fancy' }
+ options2 = { onclick: "alert('Hello World');" }
+ option_html_attributes([ 'foo', 'bar', options1, options2 ])
+
+ assert_equal({ class: 'fancy' }, options1)
+ assert_equal({ onclick: "alert('Hello World');" }, options2)
+ end
+
+ def test_grouped_collection_select
+ @post = Post.new
+ @post.origin = 'dk'
+
+ assert_dom_equal(
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
+ grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name)
+ )
+ end
+
+ def test_grouped_collection_select_with_selected
+ @post = Post.new
+
+ assert_dom_equal(
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
+ grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :selected => 'dk')
+ )
+ end
+
+ def test_grouped_collection_select_with_disabled_value
+ @post = Post.new
+
+ assert_dom_equal(
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option disabled="disabled" value="dk">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
+ grouped_collection_select("post", "origin", dummy_continents, :countries, :continent_name, :country_id, :country_name, :disabled => 'dk')
+ )
+ end
+
+ def test_grouped_collection_select_under_fields_for
+ @post = Post.new
+ @post.origin = 'dk'
+
+ output_buffer = fields_for :post, @post do |f|
+ concat f.grouped_collection_select("origin", dummy_continents, :countries, :continent_name, :country_id, :country_name)
+ end
+
+ assert_dom_equal(
+ %Q{<select id="post_origin" name="post[origin]"><optgroup label="&lt;Africa&gt;"><option value="&lt;sa&gt;">&lt;South Africa&gt;</option>\n<option value="so">Somalia</option></optgroup><optgroup label="Europe"><option value="dk" selected="selected">Denmark</option>\n<option value="ie">Ireland</option></optgroup></select>},
+ output_buffer
+ )
+ end
+
+ private
+
+ def dummy_posts
+ [ Post.new("<Abe> went home", "<Abe>", "To a little house", "shh!"),
+ Post.new("Babe went home", "Babe", "To a little house", "shh!"),
+ Post.new("Cabe went home", "Cabe", "To a little house", "shh!") ]
+ end
+
+ def dummy_continents
+ [ Continent.new("<Africa>", [Country.new("<sa>", "<South Africa>"), Country.new("so", "Somalia")]),
+ Continent.new("Europe", [Country.new("dk", "Denmark"), Country.new("ie", "Ireland")]) ]
+ end
+end
diff --git a/actionview/test/template/form_tag_helper_test.rb b/actionview/test/template/form_tag_helper_test.rb
new file mode 100644
index 0000000000..22bf438a56
--- /dev/null
+++ b/actionview/test/template/form_tag_helper_test.rb
@@ -0,0 +1,629 @@
+require 'abstract_unit'
+
+class FormTagHelperTest < ActionView::TestCase
+ include RenderERBUtils
+
+ tests ActionView::Helpers::FormTagHelper
+
+ def setup
+ super
+ @controller = BasicController.new
+ end
+
+ def hidden_fields(options = {})
+ method = options[:method]
+ enforce_utf8 = options.fetch(:enforce_utf8, true)
+
+ txt = %{<div style="margin:0;padding:0;display:inline">}
+ txt << %{<input name="utf8" type="hidden" value="&#x2713;" />} if enforce_utf8
+ if method && !%w(get post).include?(method.to_s)
+ txt << %{<input name="_method" type="hidden" value="#{method}" />}
+ end
+ txt << %{</div>}
+ end
+
+ def form_text(action = "http://www.example.com", options = {})
+ remote, enctype, html_class, id, method = options.values_at(:remote, :enctype, :html_class, :id, :method)
+
+ method = method.to_s == "get" ? "get" : "post"
+
+ txt = %{<form accept-charset="UTF-8" action="#{action}"}
+ txt << %{ enctype="multipart/form-data"} if enctype
+ txt << %{ data-remote="true"} if remote
+ txt << %{ class="#{html_class}"} if html_class
+ txt << %{ id="#{id}"} if id
+ txt << %{ method="#{method}">}
+ end
+
+ def whole_form(action = "http://www.example.com", options = {})
+ out = form_text(action, options) + hidden_fields(options)
+
+ if block_given?
+ out << yield << "</form>"
+ end
+
+ out
+ end
+
+ def url_for(options)
+ if options.is_a?(Hash)
+ "http://www.example.com"
+ else
+ super
+ end
+ end
+
+ VALID_HTML_ID = /^[A-Za-z][-_:.A-Za-z0-9]*$/ # see http://www.w3.org/TR/html4/types.html#type-name
+
+ def test_check_box_tag
+ actual = check_box_tag "admin"
+ expected = %(<input id="admin" name="admin" type="checkbox" value="1" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_check_box_tag_id_sanitized
+ label_elem = root_elem(check_box_tag("project[2][admin]"))
+ assert_match VALID_HTML_ID, label_elem['id']
+ end
+
+ def test_form_tag
+ actual = form_tag
+ expected = whole_form
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_multipart
+ actual = form_tag({}, { 'multipart' => true })
+ expected = whole_form("http://www.example.com", :enctype => true)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_with_method_patch
+ actual = form_tag({}, { :method => :patch })
+ expected = whole_form("http://www.example.com", :method => :patch)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_with_method_put
+ actual = form_tag({}, { :method => :put })
+ expected = whole_form("http://www.example.com", :method => :put)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_with_method_delete
+ actual = form_tag({}, { :method => :delete })
+
+ expected = whole_form("http://www.example.com", :method => :delete)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_with_remote
+ actual = form_tag({}, :remote => true)
+
+ expected = whole_form("http://www.example.com", :remote => true)
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_with_remote_false
+ actual = form_tag({}, :remote => false)
+
+ expected = whole_form
+ assert_dom_equal expected, actual
+ end
+
+ def test_form_tag_enforce_utf8_true
+ actual = form_tag({}, { :enforce_utf8 => true })
+ expected = whole_form("http://www.example.com", :enforce_utf8 => true)
+ assert_dom_equal expected, actual
+ assert actual.html_safe?
+ end
+
+ def test_form_tag_enforce_utf8_false
+ actual = form_tag({}, { :enforce_utf8 => false })
+ expected = whole_form("http://www.example.com", :enforce_utf8 => false)
+ assert_dom_equal expected, actual
+ assert actual.html_safe?
+ end
+
+ def test_form_tag_with_block_in_erb
+ output_buffer = render_erb("<%= form_tag('http://www.example.com') do %>Hello world!<% end %>")
+
+ expected = whole_form { "Hello world!" }
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_form_tag_with_block_and_method_in_erb
+ output_buffer = render_erb("<%= form_tag('http://www.example.com', :method => :put) do %>Hello world!<% end %>")
+
+ expected = whole_form("http://www.example.com", :method => "put") do
+ "Hello world!"
+ end
+
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_hidden_field_tag
+ actual = hidden_field_tag "id", 3
+ expected = %(<input id="id" name="id" type="hidden" value="3" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_hidden_field_tag_id_sanitized
+ input_elem = root_elem(hidden_field_tag("item[][title]"))
+ assert_match VALID_HTML_ID, input_elem['id']
+ end
+
+ def test_file_field_tag
+ assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" />", file_field_tag("picsplz")
+ end
+
+ def test_file_field_tag_with_options
+ assert_dom_equal "<input name=\"picsplz\" type=\"file\" id=\"picsplz\" class=\"pix\"/>", file_field_tag("picsplz", :class => "pix")
+ end
+
+ def test_password_field_tag
+ actual = password_field_tag
+ expected = %(<input id="password" name="password" type="password" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_radio_button_tag
+ actual = radio_button_tag "people", "david"
+ expected = %(<input id="people_david" name="people" type="radio" value="david" />)
+ assert_dom_equal expected, actual
+
+ actual = radio_button_tag("num_people", 5)
+ expected = %(<input id="num_people_5" name="num_people" type="radio" value="5" />)
+ assert_dom_equal expected, actual
+
+ actual = radio_button_tag("gender", "m") + radio_button_tag("gender", "f")
+ expected = %(<input id="gender_m" name="gender" type="radio" value="m" /><input id="gender_f" name="gender" type="radio" value="f" />)
+ assert_dom_equal expected, actual
+
+ actual = radio_button_tag("opinion", "-1") + radio_button_tag("opinion", "1")
+ expected = %(<input id="opinion_-1" name="opinion" type="radio" value="-1" /><input id="opinion_1" name="opinion" type="radio" value="1" />)
+ assert_dom_equal expected, actual
+
+ actual = radio_button_tag("person[gender]", "m")
+ expected = %(<input id="person_gender_m" name="person[gender]" type="radio" value="m" />)
+ assert_dom_equal expected, actual
+
+ actual = radio_button_tag('ctrlname', 'apache2.2')
+ expected = %(<input id="ctrlname_apache2.2" name="ctrlname" type="radio" value="apache2.2" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag
+ actual = select_tag "people", "<option>david</option>".html_safe
+ expected = %(<select id="people" name="people"><option>david</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_multiple
+ actual = select_tag "colors", "<option>Red</option><option>Blue</option><option>Green</option>".html_safe, :multiple => :true
+ expected = %(<select id="colors" multiple="multiple" name="colors"><option>Red</option><option>Blue</option><option>Green</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_disabled
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :disabled => :true
+ expected = %(<select id="places" disabled="disabled" name="places"><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_id_sanitized
+ input_elem = root_elem(select_tag("project[1]people", "<option>david</option>"))
+ assert_match VALID_HTML_ID, input_elem['id']
+ end
+
+ def test_select_tag_with_include_blank
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :include_blank => true
+ expected = %(<select id="places" name="places"><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_prompt
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string"
+ expected = %(<select id="places" name="places"><option value="">string</option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_escapes_prompt
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "<script>alert(1337)</script>"
+ expected = %(<select id="places" name="places"><option value="">&lt;script&gt;alert(1337)&lt;/script&gt;</option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_prompt_and_include_blank
+ actual = select_tag "places", "<option>Home</option><option>Work</option><option>Pub</option>".html_safe, :prompt => "string", :include_blank => true
+ expected = %(<select name="places" id="places"><option value="">string</option><option value=""></option><option>Home</option><option>Work</option><option>Pub</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_nil_option_tags_and_include_blank
+ actual = select_tag "places", nil, :include_blank => true
+ expected = %(<select id="places" name="places"><option value=""></option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_select_tag_with_nil_option_tags_and_prompt
+ actual = select_tag "places", nil, :prompt => "string"
+ expected = %(<select id="places" name="places"><option value="">string</option></select>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_size_string
+ actual = text_area_tag "body", "hello world", "size" => "20x40"
+ expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_size_symbol
+ actual = text_area_tag "body", "hello world", :size => "20x40"
+ expected = %(<textarea cols="20" id="body" name="body" rows="40">\nhello world</textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_should_disregard_size_if_its_given_as_an_integer
+ actual = text_area_tag "body", "hello world", :size => 20
+ expected = %(<textarea id="body" name="body">\nhello world</textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_id_sanitized
+ input_elem = root_elem(text_area_tag("item[][description]"))
+ assert_match VALID_HTML_ID, input_elem['id']
+ end
+
+ def test_text_area_tag_escape_content
+ actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40"
+ expected = %(<textarea cols="20" id="body" name="body" rows="40">\n&lt;b&gt;hello world&lt;/b&gt;</textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_unescaped_content
+ actual = text_area_tag "body", "<b>hello world</b>", :size => "20x40", :escape => false
+ expected = %(<textarea cols="20" id="body" name="body" rows="40">\n<b>hello world</b></textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_area_tag_unescaped_nil_content
+ actual = text_area_tag "body", nil, :escape => false
+ expected = %(<textarea id="body" name="body">\n</textarea>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag
+ actual = text_field_tag "title", "Hello!"
+ expected = %(<input id="title" name="title" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_class_string
+ actual = text_field_tag "title", "Hello!", "class" => "admin"
+ expected = %(<input class="admin" id="title" name="title" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_size_symbol
+ actual = text_field_tag "title", "Hello!", :size => 75
+ expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_size_string
+ actual = text_field_tag "title", "Hello!", "size" => "75"
+ expected = %(<input id="title" name="title" size="75" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_maxlength_symbol
+ actual = text_field_tag "title", "Hello!", :maxlength => 75
+ expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_maxlength_string
+ actual = text_field_tag "title", "Hello!", "maxlength" => "75"
+ expected = %(<input id="title" name="title" maxlength="75" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_disabled
+ actual = text_field_tag "title", "Hello!", :disabled => :true
+ expected = %(<input id="title" name="title" disabled="disabled" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_with_multiple_options
+ actual = text_field_tag "title", "Hello!", :size => 70, :maxlength => 80
+ expected = %(<input id="title" name="title" size="70" maxlength="80" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_text_field_tag_id_sanitized
+ input_elem = root_elem(text_field_tag("item[][title]"))
+ assert_match VALID_HTML_ID, input_elem['id']
+ end
+
+ def test_label_tag_without_text
+ actual = label_tag "title"
+ expected = %(<label for="title">Title</label>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_label_tag_with_symbol
+ actual = label_tag :title
+ expected = %(<label for="title">Title</label>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_label_tag_with_text
+ actual = label_tag "title", "My Title"
+ expected = %(<label for="title">My Title</label>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_label_tag_class_string
+ actual = label_tag "title", "My Title", "class" => "small_label"
+ expected = %(<label for="title" class="small_label">My Title</label>)
+ assert_dom_equal expected, actual
+ end
+
+ def test_label_tag_id_sanitized
+ label_elem = root_elem(label_tag("item[title]"))
+ assert_match VALID_HTML_ID, label_elem['for']
+ end
+
+ def test_label_tag_with_block
+ assert_dom_equal('<label>Blocked</label>', label_tag { "Blocked" })
+ end
+
+ def test_label_tag_with_block_and_argument
+ output = label_tag("clock") { "Grandfather" }
+ assert_dom_equal('<label for="clock">Grandfather</label>', output)
+ end
+
+ def test_label_tag_with_block_and_argument_and_options
+ output = label_tag("clock", :id => "label_clock") { "Grandfather" }
+ assert_dom_equal('<label for="clock" id="label_clock">Grandfather</label>', output)
+ end
+
+ def test_boolean_options
+ assert_dom_equal %(<input checked="checked" disabled="disabled" id="admin" name="admin" readonly="readonly" type="checkbox" value="1" />), check_box_tag("admin", 1, true, 'disabled' => true, :readonly => "yes")
+ assert_dom_equal %(<input checked="checked" id="admin" name="admin" type="checkbox" value="1" />), check_box_tag("admin", 1, true, :disabled => false, :readonly => nil)
+ assert_dom_equal %(<input type="checkbox" />), tag(:input, :type => "checkbox", :checked => false)
+ assert_dom_equal %(<select id="people" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => true)
+ assert_dom_equal %(<select id="people_" multiple="multiple" name="people[]"><option>david</option></select>), select_tag("people[]", "<option>david</option>".html_safe, :multiple => true)
+ assert_dom_equal %(<select id="people" name="people"><option>david</option></select>), select_tag("people", "<option>david</option>".html_safe, :multiple => nil)
+ end
+
+ def test_stringify_symbol_keys
+ actual = text_field_tag "title", "Hello!", :id => "admin"
+ expected = %(<input id="admin" name="title" type="text" value="Hello!" />)
+ assert_dom_equal expected, actual
+ end
+
+ def test_submit_tag
+ assert_dom_equal(
+ %(<input name='commit' data-disable-with="Saving..." onclick="alert(&#39;hello!&#39;)" type="submit" value="Save" />),
+ submit_tag("Save", :onclick => "alert('hello!')", :data => { :disable_with => "Saving..." })
+ )
+ end
+
+ def test_submit_tag_with_no_onclick_options
+ assert_dom_equal(
+ %(<input name='commit' data-disable-with="Saving..." type="submit" value="Save" />),
+ submit_tag("Save", :data => { :disable_with => "Saving..." })
+ )
+ end
+
+ def test_submit_tag_with_confirmation
+ assert_dom_equal(
+ %(<input name='commit' type='submit' value='Save' data-confirm="Are you sure?" />),
+ submit_tag("Save", :data => { :confirm => "Are you sure?" })
+ )
+ end
+
+ def test_button_tag
+ assert_dom_equal(
+ %(<button name="button" type="submit">Button</button>),
+ button_tag
+ )
+ end
+
+ def test_button_tag_with_submit_type
+ assert_dom_equal(
+ %(<button name="button" type="submit">Save</button>),
+ button_tag("Save", :type => "submit")
+ )
+ end
+
+ def test_button_tag_with_button_type
+ assert_dom_equal(
+ %(<button name="button" type="button">Button</button>),
+ button_tag("Button", :type => "button")
+ )
+ end
+
+ def test_button_tag_with_reset_type
+ assert_dom_equal(
+ %(<button name="button" type="reset">Reset</button>),
+ button_tag("Reset", :type => "reset")
+ )
+ end
+
+ def test_button_tag_with_disabled_option
+ assert_dom_equal(
+ %(<button name="button" type="reset" disabled="disabled">Reset</button>),
+ button_tag("Reset", :type => "reset", :disabled => true)
+ )
+ end
+
+ def test_button_tag_escape_content
+ assert_dom_equal(
+ %(<button name="button" type="reset" disabled="disabled">&lt;b&gt;Reset&lt;/b&gt;</button>),
+ button_tag("<b>Reset</b>", :type => "reset", :disabled => true)
+ )
+ end
+
+ def test_button_tag_with_block
+ assert_dom_equal('<button name="button" type="submit">Content</button>', button_tag { 'Content' })
+ end
+
+ def test_button_tag_with_block_and_options
+ output = button_tag(:name => 'temptation', :type => 'button') { content_tag(:strong, 'Do not press me') }
+ assert_dom_equal('<button name="temptation" type="button"><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>),
+ button_tag("Save", :type => "submit", :data => { :confirm => "Are you sure?" })
+ )
+ end
+
+ def test_image_submit_tag_with_confirmation
+ assert_dom_equal(
+ %(<input alt="Save" type="image" src="/images/save.gif" data-confirm="Are you sure?" />),
+ image_submit_tag("save.gif", :data => { :confirm => "Are you sure?" })
+ )
+ end
+
+ def test_color_field_tag
+ expected = %{<input id="car" name="car" type="color" />}
+ assert_dom_equal(expected, color_field_tag("car"))
+ end
+
+ def test_search_field_tag
+ expected = %{<input id="query" name="query" type="search" />}
+ assert_dom_equal(expected, search_field_tag("query"))
+ end
+
+ def test_telephone_field_tag
+ expected = %{<input id="cell" name="cell" type="tel" />}
+ assert_dom_equal(expected, telephone_field_tag("cell"))
+ end
+
+ def test_date_field_tag
+ expected = %{<input id="cell" name="cell" type="date" />}
+ assert_dom_equal(expected, date_field_tag("cell"))
+ end
+
+ def test_time_field_tag
+ expected = %{<input id="cell" name="cell" type="time" />}
+ assert_dom_equal(expected, time_field_tag("cell"))
+ end
+
+ def test_datetime_field_tag
+ expected = %{<input id="appointment" name="appointment" type="datetime" />}
+ assert_dom_equal(expected, datetime_field_tag("appointment"))
+ end
+
+ def test_datetime_local_field_tag
+ expected = %{<input id="appointment" name="appointment" type="datetime-local" />}
+ assert_dom_equal(expected, datetime_local_field_tag("appointment"))
+ end
+
+ def test_month_field_tag
+ expected = %{<input id="birthday" name="birthday" type="month" />}
+ assert_dom_equal(expected, month_field_tag("birthday"))
+ end
+
+ def test_week_field_tag
+ expected = %{<input id="birthday" name="birthday" type="week" />}
+ assert_dom_equal(expected, week_field_tag("birthday"))
+ end
+
+ def test_url_field_tag
+ expected = %{<input id="homepage" name="homepage" type="url" />}
+ assert_dom_equal(expected, url_field_tag("homepage"))
+ end
+
+ def test_email_field_tag
+ expected = %{<input id="address" name="address" type="email" />}
+ assert_dom_equal(expected, email_field_tag("address"))
+ end
+
+ def test_number_field_tag
+ expected = %{<input name="quantity" max="9" id="quantity" type="number" min="1" />}
+ assert_dom_equal(expected, number_field_tag("quantity", nil, :in => 1...10))
+ end
+
+ def test_range_input_tag
+ expected = %{<input name="volume" step="0.1" max="11" id="volume" type="range" min="0" />}
+ assert_dom_equal(expected, range_field_tag("volume", nil, :in => 0..11, :step => 0.1))
+ end
+
+ def test_field_set_tag_in_erb
+ output_buffer = render_erb("<%= field_set_tag('Your details') do %>Hello world!<% end %>")
+
+ expected = %(<fieldset><legend>Your details</legend>Hello world!</fieldset>)
+ assert_dom_equal expected, output_buffer
+
+ output_buffer = render_erb("<%= field_set_tag do %>Hello world!<% end %>")
+
+ expected = %(<fieldset>Hello world!</fieldset>)
+ assert_dom_equal expected, output_buffer
+
+ output_buffer = render_erb("<%= field_set_tag('') do %>Hello world!<% end %>")
+
+ expected = %(<fieldset>Hello world!</fieldset>)
+ assert_dom_equal expected, output_buffer
+
+ output_buffer = render_erb("<%= field_set_tag('', :class => 'format') do %>Hello world!<% end %>")
+
+ expected = %(<fieldset class="format">Hello world!</fieldset>)
+ assert_dom_equal expected, output_buffer
+
+ output_buffer = render_erb("<%= field_set_tag %>")
+
+ expected = %(<fieldset></fieldset>)
+ assert_dom_equal expected, output_buffer
+
+ output_buffer = render_erb("<%= field_set_tag('You legend!') %>")
+
+ expected = %(<fieldset><legend>You legend!</legend></fieldset>)
+ assert_dom_equal expected, output_buffer
+ end
+
+ def test_text_area_tag_options_symbolize_keys_side_effects
+ options = { :option => "random_option" }
+ text_area_tag "body", "hello world", options
+ assert_equal options, { :option => "random_option" }
+ end
+
+ def test_submit_tag_options_symbolize_keys_side_effects
+ options = { :option => "random_option" }
+ submit_tag "submit value", options
+ assert_equal options, { :option => "random_option" }
+ end
+
+ def test_button_tag_options_symbolize_keys_side_effects
+ options = { :option => "random_option" }
+ button_tag "button value", options
+ assert_equal options, { :option => "random_option" }
+ end
+
+ def test_image_submit_tag_options_symbolize_keys_side_effects
+ options = { :option => "random_option" }
+ image_submit_tag "submit source", options
+ assert_equal options, { :option => "random_option" }
+ end
+
+ def test_image_label_tag_options_symbolize_keys_side_effects
+ options = { :option => "random_option" }
+ label_tag "submit source", "title", options
+ assert_equal options, { :option => "random_option" }
+ end
+
+ def protect_against_forgery?
+ false
+ end
+
+ private
+
+ def root_elem(rendered_content)
+ HTML::Document.new(rendered_content).root.children[0]
+ end
+end
diff --git a/actionpack/test/template/html-scanner/cdata_node_test.rb b/actionview/test/template/html-scanner/cdata_node_test.rb
index 9b58174641..9b58174641 100644
--- a/actionpack/test/template/html-scanner/cdata_node_test.rb
+++ b/actionview/test/template/html-scanner/cdata_node_test.rb
diff --git a/actionpack/test/template/html-scanner/document_test.rb b/actionview/test/template/html-scanner/document_test.rb
index 17f045d549..17f045d549 100644
--- a/actionpack/test/template/html-scanner/document_test.rb
+++ b/actionview/test/template/html-scanner/document_test.rb
diff --git a/actionpack/test/template/html-scanner/node_test.rb b/actionview/test/template/html-scanner/node_test.rb
index 5b5d092036..5b5d092036 100644
--- a/actionpack/test/template/html-scanner/node_test.rb
+++ b/actionview/test/template/html-scanner/node_test.rb
diff --git a/actionview/test/template/html-scanner/sanitizer_test.rb b/actionview/test/template/html-scanner/sanitizer_test.rb
new file mode 100644
index 0000000000..b1c1b83807
--- /dev/null
+++ b/actionview/test/template/html-scanner/sanitizer_test.rb
@@ -0,0 +1,330 @@
+require 'abstract_unit'
+
+class SanitizerTest < ActionController::TestCase
+ def setup
+ @sanitizer = nil # used by assert_sanitizer
+ end
+
+ def test_strip_tags_with_quote
+ sanitizer = HTML::FullSanitizer.new
+ string = '<" <img src="trollface.gif" onload="alert(1)"> hi'
+
+ assert_equal ' hi', sanitizer.sanitize(string)
+ end
+
+ def test_strip_tags
+ sanitizer = HTML::FullSanitizer.new
+ assert_equal("<<<bad html", sanitizer.sanitize("<<<bad html"))
+ assert_equal("<<", sanitizer.sanitize("<<<bad html>"))
+ assert_equal("Dont touch me", sanitizer.sanitize("Dont touch me"))
+ assert_equal("This is a test.", sanitizer.sanitize("<p>This <u>is<u> a <a href='test.html'><strong>test</strong></a>.</p>"))
+ assert_equal("Weirdos", sanitizer.sanitize("Wei<<a>a onclick='alert(document.cookie);'</a>/>rdos"))
+ assert_equal("This is a test.", sanitizer.sanitize("This is a test."))
+ assert_equal(
+ %{This is a test.\n\n\nIt no longer contains any HTML.\n}, sanitizer.sanitize(
+ %{<title>This is <b>a <a href="" target="_blank">test</a></b>.</title>\n\n<!-- it has a comment -->\n\n<p>It no <b>longer <strong>contains <em>any <strike>HTML</strike></em>.</strong></b></p>\n}))
+ assert_equal "This has a here.", sanitizer.sanitize("This has a <!-- comment --> here.")
+ assert_equal "This has a here.", sanitizer.sanitize("This has a <![CDATA[<section>]]> here.")
+ assert_equal "This has an unclosed ", sanitizer.sanitize("This has an unclosed <![CDATA[<section>]] here...")
+ [nil, '', ' '].each { |blank| assert_equal blank, sanitizer.sanitize(blank) }
+ assert_nothing_raised { sanitizer.sanitize("This is a frozen string with no tags".freeze) }
+ end
+
+ def test_strip_links
+ sanitizer = HTML::LinkSanitizer.new
+ assert_equal "Dont touch me", sanitizer.sanitize("Dont touch me")
+ assert_equal "on my mind\nall day long", sanitizer.sanitize("<a href='almost'>on my mind</a>\n<A href='almost'>all day long</A>")
+ assert_equal "0wn3d", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'><a href='http://www.rubyonrails.com/' onlclick='steal()'>0wn3d</a></a>")
+ assert_equal "Magic", sanitizer.sanitize("<a href='http://www.rubyonrails.com/'>Mag<a href='http://www.ruby-lang.org/'>ic")
+ assert_equal "FrrFox", sanitizer.sanitize("<href onlclick='steal()'>FrrFox</a></href>")
+ assert_equal "My mind\nall <b>day</b> long", sanitizer.sanitize("<a href='almost'>My mind</a>\n<A href='almost'>all <b>day</b> long</A>")
+ assert_equal "all <b>day</b> long", sanitizer.sanitize("<<a>a href='hello'>all <b>day</b> long<</A>/a>")
+
+ assert_equal "<a<a", sanitizer.sanitize("<a<a")
+ end
+
+ def test_sanitize_form
+ assert_sanitized "<form action=\"/foo/bar\" method=\"post\"><input></form>", ''
+ end
+
+ def test_sanitize_plaintext
+ raw = "<plaintext><span>foo</span></plaintext>"
+ assert_sanitized raw, "<span>foo</span>"
+ end
+
+ def test_sanitize_script
+ assert_sanitized "a b c<script language=\"Javascript\">blah blah blah</script>d e f", "a b cd e f"
+ end
+
+ def test_sanitize_js_handlers
+ raw = %{onthis="do that" <a href="#" onclick="hello" name="foo" onbogus="remove me">hello</a>}
+ assert_sanitized raw, %{onthis="do that" <a name="foo" href="#">hello</a>}
+ end
+
+ def test_sanitize_javascript_href
+ raw = %{href="javascript:bang" <a href="javascript:bang" name="hello">foo</a>, <span href="javascript:bang">bar</span>}
+ assert_sanitized raw, %{href="javascript:bang" <a name="hello">foo</a>, <span>bar</span>}
+ end
+
+ def test_sanitize_image_src
+ raw = %{src="javascript:bang" <img src="javascript:bang" width="5">foo</img>, <span src="javascript:bang">bar</span>}
+ assert_sanitized raw, %{src="javascript:bang" <img width="5">foo</img>, <span>bar</span>}
+ end
+
+ HTML::WhiteListSanitizer.allowed_tags.each do |tag_name|
+ define_method "test_should_allow_#{tag_name}_tag" do
+ assert_sanitized "start <#{tag_name} title=\"1\" onclick=\"foo\">foo <bad>bar</bad> baz</#{tag_name}> end", %(start <#{tag_name} title="1">foo bar baz</#{tag_name}> end)
+ end
+ end
+
+ def test_should_allow_anchors
+ assert_sanitized %(<a href="foo" onclick="bar"><script>baz</script></a>), %(<a href="foo"></a>)
+ end
+
+ # RFC 3986, sec 4.2
+ def test_allow_colons_in_path_component
+ assert_sanitized("<a href=\"./this:that\">foo</a>")
+ end
+
+ %w(src width height alt).each do |img_attr|
+ define_method "test_should_allow_image_#{img_attr}_attribute" do
+ assert_sanitized %(<img #{img_attr}="foo" onclick="bar" />), %(<img #{img_attr}="foo" />)
+ end
+ end
+
+ def test_should_handle_non_html
+ assert_sanitized 'abc'
+ end
+
+ def test_should_handle_blank_text
+ assert_sanitized nil
+ assert_sanitized ''
+ end
+
+ def test_should_allow_custom_tags
+ text = "<u>foo</u>"
+ sanitizer = HTML::WhiteListSanitizer.new
+ assert_equal(text, sanitizer.sanitize(text, :tags => %w(u)))
+ end
+
+ def test_should_allow_only_custom_tags
+ text = "<u>foo</u> with <i>bar</i>"
+ sanitizer = HTML::WhiteListSanitizer.new
+ assert_equal("<u>foo</u> with bar", sanitizer.sanitize(text, :tags => %w(u)))
+ end
+
+ def test_should_allow_custom_tags_with_attributes
+ text = %(<blockquote cite="http://example.com/">foo</blockquote>)
+ sanitizer = HTML::WhiteListSanitizer.new
+ assert_equal(text, sanitizer.sanitize(text))
+ end
+
+ def test_should_allow_custom_tags_with_custom_attributes
+ text = %(<blockquote foo="bar">Lorem ipsum</blockquote>)
+ sanitizer = HTML::WhiteListSanitizer.new
+ assert_equal(text, sanitizer.sanitize(text, :attributes => ['foo']))
+ end
+
+ def test_should_raise_argument_error_if_tags_is_not_enumerable
+ sanitizer = HTML::WhiteListSanitizer.new
+ e = assert_raise(ArgumentError) do
+ sanitizer.sanitize('', :tags => 'foo')
+ end
+
+ assert_equal "You should pass :tags as an Enumerable", e.message
+ end
+
+ def test_should_raise_argument_error_if_attributes_is_not_enumerable
+ sanitizer = HTML::WhiteListSanitizer.new
+ e = assert_raise(ArgumentError) do
+ sanitizer.sanitize('', :attributes => 'foo')
+ end
+
+ assert_equal "You should pass :attributes as an Enumerable", e.message
+ end
+
+ [%w(img src), %w(a href)].each do |(tag, attr)|
+ define_method "test_should_strip_#{attr}_attribute_in_#{tag}_with_bad_protocols" do
+ assert_sanitized %(<#{tag} #{attr}="javascript:bang" title="1">boo</#{tag}>), %(<#{tag} title="1">boo</#{tag}>)
+ end
+ end
+
+ def test_should_flag_bad_protocols
+ sanitizer = HTML::WhiteListSanitizer.new
+ %w(about chrome data disk hcp help javascript livescript lynxcgi lynxexec ms-help ms-its mhtml mocha opera res resource shell vbscript view-source vnd.ms.radio wysiwyg).each do |proto|
+ assert sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://bad")
+ end
+ end
+
+ def test_should_accept_good_protocols_ignoring_case
+ sanitizer = HTML::WhiteListSanitizer.new
+ HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
+ assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto.capitalize}://good")
+ end
+ end
+
+ def test_should_accept_good_protocols_ignoring_space
+ sanitizer = HTML::WhiteListSanitizer.new
+ HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
+ assert !sanitizer.send(:contains_bad_protocols?, 'src', " #{proto}://good")
+ end
+ end
+
+ def test_should_accept_good_protocols
+ sanitizer = HTML::WhiteListSanitizer.new
+ HTML::WhiteListSanitizer.allowed_protocols.each do |proto|
+ assert !sanitizer.send(:contains_bad_protocols?, 'src', "#{proto}://good")
+ end
+ end
+
+ def test_should_reject_hex_codes_in_protocol
+ assert_sanitized %(<a href="&#37;6A&#37;61&#37;76&#37;61&#37;73&#37;63&#37;72&#37;69&#37;70&#37;74&#37;3A&#37;61&#37;6C&#37;65&#37;72&#37;74&#37;28&#37;22&#37;58&#37;53&#37;53&#37;22&#37;29">1</a>), "<a>1</a>"
+ assert @sanitizer.send(:contains_bad_protocols?, 'src', "%6A%61%76%61%73%63%72%69%70%74%3A%61%6C%65%72%74%28%22%58%53%53%22%29")
+ end
+
+ def test_should_block_script_tag
+ assert_sanitized %(<SCRIPT\nSRC=http://ha.ckers.org/xss.js></SCRIPT>), ""
+ end
+
+ [%(<IMG SRC="javascript:alert('XSS');">),
+ %(<IMG SRC=javascript:alert('XSS')>),
+ %(<IMG SRC=JaVaScRiPt:alert('XSS')>),
+ %(<IMG """><SCRIPT>alert("XSS")</SCRIPT>">),
+ %(<IMG SRC=javascript:alert(&quot;XSS&quot;)>),
+ %(<IMG SRC=javascript:alert(String.fromCharCode(88,83,83))>),
+ %(<IMG SRC=&#106;&#97;&#118;&#97;&#115;&#99;&#114;&#105;&#112;&#116;&#58;&#97;&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>),
+ %(<IMG SRC=&#0000106&#0000097&#0000118&#0000097&#0000115&#0000099&#0000114&#0000105&#0000112&#0000116&#0000058&#0000097&#0000108&#0000101&#0000114&#0000116&#0000040&#0000039&#0000088&#0000083&#0000083&#0000039&#0000041>),
+ %(<IMG SRC=&#x6A&#x61&#x76&#x61&#x73&#x63&#x72&#x69&#x70&#x74&#x3A&#x61&#x6C&#x65&#x72&#x74&#x28&#x27&#x58&#x53&#x53&#x27&#x29>),
+ %(<IMG SRC="jav\tascript:alert('XSS');">),
+ %(<IMG SRC="jav&#x09;ascript:alert('XSS');">),
+ %(<IMG SRC="jav&#x0A;ascript:alert('XSS');">),
+ %(<IMG SRC="jav&#x0D;ascript:alert('XSS');">),
+ %(<IMG SRC=" &#14; javascript:alert('XSS');">),
+ %(<IMG SRC="javascript&#x3a;alert('XSS');">),
+ %(<IMG SRC=`javascript:alert("RSnake says, 'XSS'")`>)].each_with_index do |img_hack, i|
+ define_method "test_should_not_fall_for_xss_image_hack_#{i+1}" do
+ assert_sanitized img_hack, "<img>"
+ end
+ end
+
+ def test_should_sanitize_tag_broken_up_by_null
+ assert_sanitized %(<SCR\0IPT>alert(\"XSS\")</SCR\0IPT>), "alert(\"XSS\")"
+ end
+
+ def test_should_sanitize_invalid_script_tag
+ assert_sanitized %(<SCRIPT/XSS SRC="http://ha.ckers.org/xss.js"></SCRIPT>), ""
+ end
+
+ def test_should_sanitize_script_tag_with_multiple_open_brackets
+ assert_sanitized %(<<SCRIPT>alert("XSS");//<</SCRIPT>), "&lt;"
+ assert_sanitized %(<iframe src=http://ha.ckers.org/scriptlet.html\n<a), %(&lt;a)
+ end
+
+ def test_should_sanitize_unclosed_script
+ assert_sanitized %(<SCRIPT SRC=http://ha.ckers.org/xss.js?<B>), "<b>"
+ end
+
+ def test_should_sanitize_half_open_scripts
+ assert_sanitized %(<IMG SRC="javascript:alert('XSS')"), "<img>"
+ end
+
+ def test_should_not_fall_for_ridiculous_hack
+ img_hack = %(<IMG\nSRC\n=\n"\nj\na\nv\na\ns\nc\nr\ni\np\nt\n:\na\nl\ne\nr\nt\n(\n'\nX\nS\nS\n'\n)\n"\n>)
+ assert_sanitized img_hack, "<img>"
+ end
+
+ def test_should_sanitize_attributes
+ assert_sanitized %(<SPAN title="'><script>alert()</script>">blah</SPAN>), %(<span title="#{CGI.escapeHTML "'><script>alert()</script>"}">blah</span>)
+ end
+
+ def test_should_sanitize_illegal_style_properties
+ raw = %(display:block; position:absolute; left:0; top:0; width:100%; height:100%; z-index:1; background-color:black; background-image:url(http://www.ragingplatypus.com/i/cam-full.jpg); background-x:center; background-y:center; background-repeat:repeat;)
+ expected = %(display: block; width: 100%; height: 100%; background-color: black; background-image: ; background-x: center; background-y: center;)
+ assert_equal expected, sanitize_css(raw)
+ end
+
+ def test_should_sanitize_with_trailing_space
+ raw = "display:block; "
+ expected = "display: block;"
+ assert_equal expected, sanitize_css(raw)
+ end
+
+ def test_should_sanitize_xul_style_attributes
+ raw = %(-moz-binding:url('http://ha.ckers.org/xssmoz.xml#xss'))
+ assert_equal '', sanitize_css(raw)
+ end
+
+ def test_should_sanitize_invalid_tag_names
+ assert_sanitized(%(a b c<script/XSS src="http://ha.ckers.org/xss.js"></script>d e f), "a b cd e f")
+ end
+
+ def test_should_sanitize_non_alpha_and_non_digit_characters_in_tags
+ assert_sanitized('<a onclick!#$%&()*~+-_.,:;?@[/|\]^`=alert("XSS")>foo</a>', "<a>foo</a>")
+ end
+
+ def test_should_sanitize_invalid_tag_names_in_single_tags
+ assert_sanitized('<img/src="http://ha.ckers.org/xss.js"/>', "<img />")
+ end
+
+ def test_should_sanitize_img_dynsrc_lowsrc
+ assert_sanitized(%(<img lowsrc="javascript:alert('XSS')" />), "<img />")
+ end
+
+ def test_should_sanitize_div_background_image_unicode_encoded
+ raw = %(background-image:\0075\0072\006C\0028'\006a\0061\0076\0061\0073\0063\0072\0069\0070\0074\003a\0061\006c\0065\0072\0074\0028.1027\0058.1053\0053\0027\0029'\0029)
+ assert_equal '', sanitize_css(raw)
+ end
+
+ def test_should_sanitize_div_style_expression
+ raw = %(width: expression(alert('XSS'));)
+ assert_equal '', sanitize_css(raw)
+ end
+
+ def test_should_sanitize_across_newlines
+ raw = %(\nwidth:\nexpression(alert('XSS'));\n)
+ assert_equal '', sanitize_css(raw)
+ end
+
+ def test_should_sanitize_img_vbscript
+ assert_sanitized %(<img src='vbscript:msgbox("XSS")' />), '<img />'
+ end
+
+ def test_should_sanitize_cdata_section
+ assert_sanitized "<![CDATA[<span>section</span>]]>", "&lt;![CDATA[&lt;span>section&lt;/span>]]>"
+ end
+
+ def test_should_sanitize_unterminated_cdata_section
+ assert_sanitized "<![CDATA[<span>neverending...", "&lt;![CDATA[&lt;span>neverending...]]>"
+ end
+
+ def test_should_not_mangle_urls_with_ampersand
+ assert_sanitized %{<a href=\"http://www.domain.com?var1=1&amp;var2=2\">my link</a>}
+ end
+
+ def test_should_sanitize_neverending_attribute
+ assert_sanitized "<span class=\"\\", "<span class=\"\\\">"
+ end
+
+ def test_x03a
+ assert_sanitized %(<a href="javascript&#x3a;alert('XSS');">), "<a>"
+ assert_sanitized %(<a href="javascript&#x003a;alert('XSS');">), "<a>"
+ assert_sanitized %(<a href="http&#x3a;//legit">), %(<a href="http://legit">)
+ assert_sanitized %(<a href="javascript&#x3A;alert('XSS');">), "<a>"
+ assert_sanitized %(<a href="javascript&#x003A;alert('XSS');">), "<a>"
+ assert_sanitized %(<a href="http&#x3A;//legit">), %(<a href="http://legit">)
+ end
+
+protected
+ def assert_sanitized(input, expected = nil)
+ @sanitizer ||= HTML::WhiteListSanitizer.new
+ if input
+ assert_dom_equal expected || input, @sanitizer.sanitize(input)
+ else
+ assert_nil @sanitizer.sanitize(input)
+ end
+ end
+
+ def sanitize_css(input)
+ (@sanitizer ||= HTML::WhiteListSanitizer.new).sanitize_css(input)
+ end
+end
diff --git a/actionpack/test/template/html-scanner/tag_node_test.rb b/actionview/test/template/html-scanner/tag_node_test.rb
index a29d2d43d7..a29d2d43d7 100644
--- a/actionpack/test/template/html-scanner/tag_node_test.rb
+++ b/actionview/test/template/html-scanner/tag_node_test.rb
diff --git a/actionpack/test/template/html-scanner/text_node_test.rb b/actionview/test/template/html-scanner/text_node_test.rb
index cbcb9e78f0..cbcb9e78f0 100644
--- a/actionpack/test/template/html-scanner/text_node_test.rb
+++ b/actionview/test/template/html-scanner/text_node_test.rb
diff --git a/actionpack/test/template/html-scanner/tokenizer_test.rb b/actionview/test/template/html-scanner/tokenizer_test.rb
index 1d59de23b6..1d59de23b6 100644
--- a/actionpack/test/template/html-scanner/tokenizer_test.rb
+++ b/actionview/test/template/html-scanner/tokenizer_test.rb
diff --git a/actionview/test/template/javascript_helper_test.rb b/actionview/test/template/javascript_helper_test.rb
new file mode 100644
index 0000000000..4703111741
--- /dev/null
+++ b/actionview/test/template/javascript_helper_test.rb
@@ -0,0 +1,69 @@
+require 'abstract_unit'
+
+class JavaScriptHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::JavaScriptHelper
+
+ def _evaluate_assigns_and_ivars() end
+
+ attr_accessor :formats, :output_buffer
+
+ def update_details(details)
+ @details = details
+ yield if block_given?
+ end
+
+ def setup
+ super
+ ActiveSupport.escape_html_entities_in_json = true
+ @template = self
+ end
+
+ def teardown
+ ActiveSupport.escape_html_entities_in_json = false
+ end
+
+ def test_escape_javascript
+ assert_equal '', escape_javascript(nil)
+ assert_equal %(This \\"thing\\" is really\\n netos\\'), escape_javascript(%(This "thing" is really\n netos'))
+ assert_equal %(backslash\\\\test), escape_javascript( %(backslash\\test) )
+ assert_equal %(dont <\\/close> tags), escape_javascript(%(dont </close> tags))
+ assert_equal %(unicode &#x2028; newline), escape_javascript(%(unicode \342\200\250 newline).force_encoding(Encoding::UTF_8).encode!)
+ assert_equal %(unicode &#x2029; newline), escape_javascript(%(unicode \342\200\251 newline).force_encoding(Encoding::UTF_8).encode!)
+
+ assert_equal %(dont <\\/close> tags), j(%(dont </close> tags))
+ end
+
+ def test_escape_javascript_with_safebuffer
+ given = %('quoted' "double-quoted" new-line:\n </closed>)
+ expect = %(\\'quoted\\' \\"double-quoted\\" new-line:\\n <\\/closed>)
+ assert_equal expect, escape_javascript(given)
+ assert_equal expect, escape_javascript(ActiveSupport::SafeBuffer.new(given))
+ assert_instance_of String, escape_javascript(given)
+ assert_instance_of ActiveSupport::SafeBuffer, escape_javascript(ActiveSupport::SafeBuffer.new(given))
+ end
+
+ def test_javascript_tag
+ self.output_buffer = 'foo'
+
+ assert_dom_equal "<script>\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
+ javascript_tag("alert('hello')")
+
+ assert_equal 'foo', output_buffer, 'javascript_tag without a block should not concat to output_buffer'
+ end
+
+ # Setting the :extname option will control what extension (if any) is appended to the url for assets
+ def test_javascript_include_tag
+ assert_dom_equal "<script src='/foo.js'></script>", javascript_include_tag('/foo')
+ assert_dom_equal "<script src='/foo'></script>", javascript_include_tag('/foo', extname: false )
+ assert_dom_equal "<script src='/foo.bar'></script>", javascript_include_tag('/foo', extname: '.bar' )
+ end
+
+ def test_javascript_tag_with_options
+ assert_dom_equal "<script id=\"the_js_tag\">\n//<![CDATA[\nalert('hello')\n//]]>\n</script>",
+ javascript_tag("alert('hello')", :id => "the_js_tag")
+ end
+
+ def test_javascript_cdata_section
+ assert_dom_equal "\n//<![CDATA[\nalert('hello')\n//]]>\n", javascript_cdata_section("alert('hello')")
+ end
+end
diff --git a/actionpack/test/template/log_subscriber_test.rb b/actionview/test/template/log_subscriber_test.rb
index 7f4c84929f..7f4c84929f 100644
--- a/actionpack/test/template/log_subscriber_test.rb
+++ b/actionview/test/template/log_subscriber_test.rb
diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb
new file mode 100644
index 0000000000..203ad6d910
--- /dev/null
+++ b/actionview/test/template/lookup_context_test.rb
@@ -0,0 +1,263 @@
+require "abstract_unit"
+require "abstract_controller/rendering"
+
+class LookupContextTest < ActiveSupport::TestCase
+ def setup
+ @lookup_context = ActionView::LookupContext.new(FIXTURE_LOAD_PATH, {})
+ ActionView::LookupContext::DetailsKey.clear
+ end
+
+ def teardown
+ I18n.locale = :en
+ end
+
+ test "allows to override default_formats with ActionView::Base.default_formats" do
+ begin
+ formats = ActionView::Base.default_formats
+ ActionView::Base.default_formats = [:foo, :bar]
+
+ assert_equal [:foo, :bar], ActionView::LookupContext.new([]).default_formats
+ ensure
+ ActionView::Base.default_formats = formats
+ end
+ end
+
+ test "process view paths on initialization" do
+ assert_kind_of ActionView::PathSet, @lookup_context.view_paths
+ end
+
+ test "normalizes details on initialization" do
+ assert_equal Mime::SET, @lookup_context.formats
+ assert_equal :en, @lookup_context.locale
+ end
+
+ test "allows me to freeze and retrieve frozen formats" do
+ @lookup_context.formats.freeze
+ assert @lookup_context.formats.frozen?
+ end
+
+ test "provides getters and setters for formats" do
+ @lookup_context.formats = [:html]
+ assert_equal [:html], @lookup_context.formats
+ end
+
+ test "handles */* formats" do
+ @lookup_context.formats = ["*/*"]
+ assert_equal Mime::SET, @lookup_context.formats
+ end
+
+ test "handles explicitly defined */* formats fallback to :js" do
+ @lookup_context.formats = [:js, Mime::ALL]
+ assert_equal [:js, *Mime::SET.symbols], @lookup_context.formats
+ end
+
+ test "adds :html fallback to :js formats" do
+ @lookup_context.formats = [:js]
+ assert_equal [:js, :html], @lookup_context.formats
+ end
+
+ test "provides getters and setters for locale" do
+ @lookup_context.locale = :pt
+ assert_equal :pt, @lookup_context.locale
+ end
+
+ test "changing lookup_context locale, changes I18n.locale" do
+ @lookup_context.locale = :pt
+ assert_equal :pt, I18n.locale
+ end
+
+ test "delegates changing the locale to the I18n configuration object if it contains a lookup_context object" do
+ begin
+ I18n.config = ActionView::I18nProxy.new(I18n.config, @lookup_context)
+ @lookup_context.locale = :pt
+ assert_equal :pt, I18n.locale
+ assert_equal :pt, @lookup_context.locale
+ ensure
+ I18n.config = I18n.config.original_config
+ end
+
+ assert_equal :pt, I18n.locale
+ end
+
+ test "find templates using the given view paths and configured details" do
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hello world!", template.source
+
+ @lookup_context.locale = :da
+ template = @lookup_context.find("hello_world", %w(test))
+ assert_equal "Hey verden", 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]
+ template = @lookup_context.find("hello", %w(test))
+ assert_equal [:text], template.formats
+ end
+
+ test "adds fallbacks to view paths when required" do
+ assert_equal 1, @lookup_context.view_paths.size
+
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new(""))
+ assert @lookup_context.view_paths.include?(ActionView::FallbackFileSystemResolver.new("/"))
+ end
+ end
+
+ test "add fallbacks just once in nested fallbacks calls" do
+ @lookup_context.with_fallbacks do
+ @lookup_context.with_fallbacks do
+ assert_equal 3, @lookup_context.view_paths.size
+ end
+ end
+ end
+
+ test "generates a new details key for each details hash" do
+ keys = []
+ keys << @lookup_context.details_key
+ assert_equal 1, keys.uniq.size
+
+ @lookup_context.locale = :da
+ keys << @lookup_context.details_key
+ assert_equal 2, keys.uniq.size
+
+ @lookup_context.locale = :en
+ keys << @lookup_context.details_key
+ assert_equal 2, keys.uniq.size
+
+ @lookup_context.formats = [:html]
+ keys << @lookup_context.details_key
+ assert_equal 3, keys.uniq.size
+
+ @lookup_context.formats = nil
+ keys << @lookup_context.details_key
+ assert_equal 3, keys.uniq.size
+ end
+
+ test "gives the key forward to the resolver, so it can be used as cache key" do
+ @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now we are going to change the template, but it won't change the returned template
+ # since we will hit the cache.
+ @lookup_context.view_paths.first.hash["test/_foo.erb"] = "Bar"
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # This time we will change the locale. The updated template should be picked since
+ # lookup_context generated a new key after we changed the locale.
+ @lookup_context.locale = :da
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Bar", template.source
+
+ # Now we will change back the locale and it will still pick the old template.
+ # This is expected because lookup_context will reuse the previous key for :en locale.
+ @lookup_context.locale = :en
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Finally, we can expire the cache. And the expected template will be used.
+ @lookup_context.view_paths.first.clear_cache
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Bar", template.source
+ end
+
+ test "can disable the cache on demand" do
+ @lookup_context.view_paths = ActionView::FixtureResolver.new("test/_foo.erb" => "Foo")
+ old_template = @lookup_context.find("foo", %w(test), true)
+
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal template, old_template
+
+ assert @lookup_context.cache
+ template = @lookup_context.disable_cache do
+ assert !@lookup_context.cache
+ @lookup_context.find("foo", %w(test), true)
+ end
+ assert @lookup_context.cache
+
+ assert_not_equal template, old_template
+ end
+
+ test "responds to #prefixes" do
+ assert_equal [], @lookup_context.prefixes
+ @lookup_context.prefixes = ["foo"]
+ assert_equal ["foo"], @lookup_context.prefixes
+ end
+end
+
+class LookupContextWithFalseCaching < ActiveSupport::TestCase
+ def setup
+ @resolver = ActionView::FixtureResolver.new("test/_foo.erb" => ["Foo", Time.utc(2000)])
+ ActionView::Resolver.stubs(:caching?).returns(false)
+ @lookup_context = ActionView::LookupContext.new(@resolver, {})
+ end
+
+ test "templates are always found in the resolver but timestamp is checked before being compiled" do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now we are going to change the template, but it won't change the returned template
+ # since the timestamp is the same.
+ @resolver.hash["test/_foo.erb"][0] = "Bar"
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ # Now update the timestamp.
+ @resolver.hash["test/_foo.erb"][1] = Time.now.utc
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Bar", template.source
+ end
+
+ test "if no template was found in the second lookup, with no cache, raise error" do
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
+ end
+
+ test "if no template was cached in the first lookup, retrieval should work in the second call" do
+ @resolver.hash.clear
+ assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(test), true)
+ end
+
+ @resolver.hash["test/_foo.erb"] = ["Foo", Time.utc(2000)]
+ template = @lookup_context.find("foo", %w(test), true)
+ assert_equal "Foo", template.source
+ end
+end
+
+class TestMissingTemplate < ActiveSupport::TestCase
+ def setup
+ @lookup_context = ActionView::LookupContext.new("/Path/to/views", {})
+ end
+
+ test "if no template was found we get a helpful error message including the inheritance chain" do
+ e = assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(parent child))
+ end
+ assert_match %r{Missing template parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ end
+
+ test "if no partial was found we get a helpful error message including the inheritance chain" do
+ e = assert_raise ActionView::MissingTemplate do
+ @lookup_context.find("foo", %w(parent child), true)
+ end
+ assert_match %r{Missing partial parent/foo, child/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ end
+
+ test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do
+ e = assert_raise ActionView::MissingTemplate do
+ details = {:handlers=>[], :formats=>[], :locale=>[]}
+ @lookup_context.view_paths.find("foo", "parent", true, details)
+ end
+ assert_match %r{Missing partial parent/foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message
+ end
+
+end
diff --git a/actionview/test/template/number_helper_test.rb b/actionview/test/template/number_helper_test.rb
new file mode 100644
index 0000000000..6e640889d2
--- /dev/null
+++ b/actionview/test/template/number_helper_test.rb
@@ -0,0 +1,151 @@
+require "abstract_unit"
+
+class NumberHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::NumberHelper
+
+ def test_number_to_phone
+ assert_equal nil, number_to_phone(nil)
+ 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: "")
+ end
+
+ def test_number_to_currency
+ assert_equal nil, number_to_currency(nil)
+ assert_equal "$1,234,567,890.50", number_to_currency(1234567890.50)
+ assert_equal "$1,234,567,892", number_to_currency(1234567891.50, precision: 0)
+ assert_equal "1,234,567,890.50 - K&#269;", number_to_currency("-1234567890.50", unit: "K&#269;", format: "%n %u", negative_format: "%n - %u")
+ end
+
+ def test_number_to_percentage
+ assert_equal nil, number_to_percentage(nil)
+ assert_equal "100.000%", number_to_percentage(100)
+ 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: ",")
+ end
+
+ def test_number_with_delimiter
+ assert_equal nil, number_with_delimiter(nil)
+ assert_equal "12,345,678", number_with_delimiter(12345678)
+ assert_equal "0", number_with_delimiter(0)
+ end
+
+ def test_number_with_precision
+ assert_equal nil, number_with_precision(nil)
+ 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)
+ end
+
+ def test_number_to_human_size
+ assert_equal nil, number_to_human_size(nil)
+ assert_equal "3 Bytes", number_to_human_size(3.14159265)
+ assert_equal "1.2 MB", number_to_human_size(1234567, precision: 2)
+ end
+
+ def test_number_to_human
+ assert_equal nil, number_to_human(nil)
+ assert_equal "0", number_to_human(0)
+ assert_equal "1.23 Thousand", number_to_human(1234)
+ assert_equal "489.0 Thousand", number_to_human(489000, precision: 4, strip_insignificant_zeros: false)
+ 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>")
+
+ assert_equal "$1&lt;script&gt;&lt;/script&gt;01", number_to_currency(1.01, separator: "<script></script>")
+ assert_equal "$1&lt;script&gt;&lt;/script&gt;000.00", number_to_currency(1000, delimiter: "<script></script>")
+
+ assert_equal "1&lt;script&gt;&lt;/script&gt;010%", number_to_percentage(1.01, separator: "<script></script>")
+ assert_equal "1&lt;script&gt;&lt;/script&gt;000.000%", number_to_percentage(1000, delimiter: "<script></script>")
+
+ assert_equal "1&lt;script&gt;&lt;/script&gt;01", number_with_delimiter(1.01, separator: "<script></script>")
+ assert_equal "1&lt;script&gt;&lt;/script&gt;000", number_with_delimiter(1000, delimiter: "<script></script>")
+
+ assert_equal "1&lt;script&gt;&lt;/script&gt;010", number_with_precision(1.01, separator: "<script></script>")
+ assert_equal "1&lt;script&gt;&lt;/script&gt;000.000", number_with_precision(1000, delimiter: "<script></script>")
+
+ assert_equal "9&lt;script&gt;&lt;/script&gt;86 KB", number_to_human_size(10100, separator: "<script></script>")
+
+ assert_equal "1&lt;script&gt;&lt;/script&gt;01", number_to_human(1.01, separator: "<script></script>")
+ assert_equal "100&lt;script&gt;&lt;/script&gt;000 Quadrillion", number_to_human(10**20, delimiter: "<script></script>")
+ end
+
+ def test_number_helpers_outputs_are_html_safe
+ assert number_to_human(1).html_safe?
+ assert !number_to_human("<script></script>").html_safe?
+ assert number_to_human("asdf".html_safe).html_safe?
+ assert number_to_human("1".html_safe).html_safe?
+
+ assert number_to_human_size(1).html_safe?
+ assert number_to_human_size(1000000).html_safe?
+ assert !number_to_human_size("<script></script>").html_safe?
+ assert number_to_human_size("asdf".html_safe).html_safe?
+ assert number_to_human_size("1".html_safe).html_safe?
+
+ assert number_with_precision(1, strip_insignificant_zeros: false).html_safe?
+ assert number_with_precision(1, strip_insignificant_zeros: true).html_safe?
+ assert !number_with_precision("<script></script>").html_safe?
+ assert number_with_precision("asdf".html_safe).html_safe?
+ assert number_with_precision("1".html_safe).html_safe?
+
+ assert number_to_currency(1).html_safe?
+ assert !number_to_currency("<script></script>").html_safe?
+ assert number_to_currency("asdf".html_safe).html_safe?
+ assert number_to_currency("1".html_safe).html_safe?
+
+ assert number_to_percentage(1).html_safe?
+ assert !number_to_percentage("<script></script>").html_safe?
+ assert number_to_percentage("asdf".html_safe).html_safe?
+ assert number_to_percentage("1".html_safe).html_safe?
+
+ assert number_to_phone(1).html_safe?
+ assert_equal "&lt;script&gt;&lt;/script&gt;", number_to_phone("<script></script>")
+ assert number_to_phone("<script></script>").html_safe?
+ assert number_to_phone("asdf".html_safe).html_safe?
+ assert number_to_phone("1".html_safe).html_safe?
+
+ assert number_with_delimiter(1).html_safe?
+ assert !number_with_delimiter("<script></script>").html_safe?
+ assert number_with_delimiter("asdf".html_safe).html_safe?
+ assert number_with_delimiter("1".html_safe).html_safe?
+ end
+
+ def test_number_helpers_should_raise_error_if_invalid_when_specified
+ exception = assert_raise InvalidNumberError do
+ number_to_human("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_to_human_size("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_with_precision("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_to_currency("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_to_percentage("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_with_delimiter("x", raise: true)
+ end
+ assert_equal "x", exception.number
+
+ exception = assert_raise InvalidNumberError do
+ number_to_phone("x", raise: true)
+ end
+ assert_equal "x", exception.number
+ end
+end
diff --git a/actionpack/test/template/output_buffer_test.rb b/actionview/test/template/output_buffer_test.rb
index eb0df3d1ab..eb0df3d1ab 100644
--- a/actionpack/test/template/output_buffer_test.rb
+++ b/actionview/test/template/output_buffer_test.rb
diff --git a/actionpack/test/template/output_safety_helper_test.rb b/actionview/test/template/output_safety_helper_test.rb
index 76c71c9e6d..76c71c9e6d 100644
--- a/actionpack/test/template/output_safety_helper_test.rb
+++ b/actionview/test/template/output_safety_helper_test.rb
diff --git a/actionpack/test/template/record_identifier_test.rb b/actionview/test/template/record_identifier_test.rb
index 22038110a5..22038110a5 100644
--- a/actionpack/test/template/record_identifier_test.rb
+++ b/actionview/test/template/record_identifier_test.rb
diff --git a/actionview/test/template/record_tag_helper_test.rb b/actionview/test/template/record_tag_helper_test.rb
new file mode 100644
index 0000000000..ab84bccb56
--- /dev/null
+++ b/actionview/test/template/record_tag_helper_test.rb
@@ -0,0 +1,117 @@
+require 'abstract_unit'
+
+class RecordTagPost
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ attr_accessor :id, :body
+
+ def initialize
+ @id = 45
+ @body = "What a wonderful world!"
+
+ yield self if block_given?
+ end
+end
+
+class RecordTagHelperTest < ActionView::TestCase
+ include RenderERBUtils
+
+ tests ActionView::Helpers::RecordTagHelper
+
+ def setup
+ super
+ @post = RecordTagPost.new
+ end
+
+ def test_content_tag_for
+ expected = %(<li class="record_tag_post" id="record_tag_post_45"></li>)
+ actual = content_tag_for(:li, @post)
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_prefix
+ expected = %(<ul class="archived_record_tag_post" id="archived_record_tag_post_45"></ul>)
+ actual = content_tag_for(:ul, @post, :archived)
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_with_extra_html_options
+ expected = %(<tr class="record_tag_post special" id="record_tag_post_45" style='background-color: #f0f0f0'></tr>)
+ actual = content_tag_for(:tr, @post, class: "special", style: "background-color: #f0f0f0")
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_with_array_css_class
+ expected = %(<tr class="record_tag_post special odd" id="record_tag_post_45"></tr>)
+ actual = content_tag_for(:tr, @post, class: ["special", "odd"])
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_with_prefix_and_extra_html_options
+ expected = %(<tr class="archived_record_tag_post special" id="archived_record_tag_post_45" style='background-color: #f0f0f0'></tr>)
+ actual = content_tag_for(:tr, @post, :archived, class: "special", style: "background-color: #f0f0f0")
+ assert_dom_equal expected, actual
+ end
+
+ def test_block_not_in_erb_multiple_calls
+ expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
+ actual = div_for(@post, class: "special") { @post.body }
+ assert_dom_equal expected, actual
+ actual = div_for(@post, class: "special") { @post.body }
+ assert_dom_equal expected, actual
+ end
+
+ def test_block_works_with_content_tag_for_in_erb
+ expected = %(<tr class="record_tag_post" id="record_tag_post_45">What a wonderful world!</tr>)
+ actual = render_erb("<%= content_tag_for(:tr, @post) do %><%= @post.body %><% end %>")
+ assert_dom_equal expected, actual
+ end
+
+ def test_div_for_in_erb
+ expected = %(<div class="record_tag_post special" id="record_tag_post_45">What a wonderful world!</div>)
+ actual = render_erb("<%= div_for(@post, class: 'special') do %><%= @post.body %><% end %>")
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_collection
+ post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
+ post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
+ expected = %(<li class="record_tag_post" id="record_tag_post_101">Hello!</li>\n<li class="record_tag_post" id="record_tag_post_102">World!</li>)
+ actual = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_collection_without_given_block
+ post_1 = RecordTagPost.new.tap { |post| post.id = 101; post.body = "Hello!" }
+ post_2 = RecordTagPost.new.tap { |post| post.id = 102; post.body = "World!" }
+ expected = %(<li class="record_tag_post" id="record_tag_post_101"></li>\n<li class="record_tag_post" id="record_tag_post_102"></li>)
+ actual = content_tag_for(:li, [post_1, post_2])
+ assert_dom_equal expected, actual
+ end
+
+ def test_div_for_collection
+ post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
+ post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
+ expected = %(<div class="record_tag_post" id="record_tag_post_101">Hello!</div>\n<div class="record_tag_post" id="record_tag_post_102">World!</div>)
+ actual = div_for([post_1, post_2]) { |post| post.body }
+ assert_dom_equal expected, actual
+ end
+
+ def test_content_tag_for_single_record_is_html_safe
+ result = div_for(@post, class: "special") { @post.body }
+ assert result.html_safe?
+ end
+
+ def test_content_tag_for_collection_is_html_safe
+ post_1 = RecordTagPost.new { |post| post.id = 101; post.body = "Hello!" }
+ post_2 = RecordTagPost.new { |post| post.id = 102; post.body = "World!" }
+ result = content_tag_for(:li, [post_1, post_2]) { |post| post.body }
+ assert result.html_safe?
+ end
+
+ def test_content_tag_for_does_not_change_options_hash
+ options = { class: "important" }
+ content_tag_for(:li, @post, options)
+ assert_equal({ class: "important" }, options)
+ end
+end
diff --git a/actionview/test/template/render_test.rb b/actionview/test/template/render_test.rb
new file mode 100644
index 0000000000..5a7d11f513
--- /dev/null
+++ b/actionview/test/template/render_test.rb
@@ -0,0 +1,543 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'controller/fake_models'
+
+class TestController < ActionController::Base
+end
+
+module RenderTestCases
+ def setup_view(paths)
+ @assigns = { :secret => 'in the sauce' }
+ @view = ActionView::Base.new(paths, @assigns)
+ @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', {}
+
+ # Ensure original are still the same since we are reindexing view paths
+ assert_equal ORIGINAL_LOCALES, I18n.available_locales.map {|l| l.to_s }.sort
+ end
+
+ 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
+ end
+
+ def test_render_file
+ assert_equal "Hello world!", @view.render(:file => "test/hello_world")
+ end
+
+ # Test if :formats, :locale etc. options are passed correctly to the resolvers.
+ def test_render_file_with_format
+ assert_match "<h1>No Comment</h1>", @view.render(:file => "comments/empty", :formats => [:html])
+ assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => [:xml])
+ assert_match "<error>No Comment</error>", @view.render(:file => "comments/empty", :formats => :xml)
+ end
+
+ def test_render_template_with_format
+ assert_match "<h1>No Comment</h1>", @view.render(:template => "comments/empty", :formats => [:html])
+ assert_match "<error>No Comment</error>", @view.render(:template => "comments/empty", :formats => [:xml])
+ end
+
+ def test_rendered_format_without_format
+ @view.render(:inline => "test")
+ assert_equal :html, @view.lookup_context.rendered_format
+ end
+
+ def test_render_partial_implicitly_use_format_of_the_rendered_template
+ @view.lookup_context.formats = [:json]
+ assert_equal "Hello world", @view.render(:template => "test/one", :formats => [:html])
+ end
+
+ def test_render_partial_implicitly_use_format_of_the_rendered_partial
+ @view.lookup_context.formats = [:html]
+ assert_equal "Third level", @view.render(:template => "test/html_template")
+ end
+
+ def test_render_partial_use_last_prepended_format_for_partials_with_the_same_names
+ @view.lookup_context.formats = [:html]
+ assert_equal "\nHTML Template, but JSON partial", @view.render(:template => "test/change_priority")
+ end
+
+ def test_render_template_with_a_missing_partial_of_another_format
+ @view.lookup_context.formats = [:html]
+ assert_raise ActionView::Template::Error, "Missing partial /missing with {:locale=>[:en], :formats=>[:json], :handlers=>[:erb, :builder]}" do
+ @view.render(:template => "with_format", :formats => [:json])
+ end
+ end
+
+ def test_render_file_with_locale
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => [:de])
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(:file => "comments/empty", :locale => :de)
+ end
+
+ def test_render_template_with_locale
+ assert_equal "<h1>Kein Kommentar</h1>", @view.render(:template => "comments/empty", :locale => [:de])
+ end
+
+ def test_render_file_with_handlers
+ assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => [:builder])
+ assert_equal "<h1>No Comment</h1>\n", @view.render(:file => "comments/empty", :handlers => :builder)
+ end
+
+ def test_render_template_with_handlers
+ assert_equal "<h1>No Comment</h1>\n", @view.render(:template => "comments/empty", :handlers => [:builder])
+ end
+
+ def test_render_raw_template_with_handlers
+ assert_equal "<%= hello_world %>\n", @view.render(:template => "plain_text")
+ end
+
+ def test_render_raw_template_with_quotes
+ assert_equal %q;Here are some characters: !@#$%^&*()-="'}{`; + "\n", @view.render(:template => "plain_text_with_characters")
+ end
+
+ def test_render_ruby_template_with_handlers
+ assert_equal "Hello from Ruby code", @view.render(:template => "ruby_template")
+ end
+
+ def test_render_ruby_template_inline
+ assert_equal '4', @view.render(:inline => "(2**2).to_s", :type => :ruby)
+ end
+
+ def test_render_file_with_localization_on_context_level
+ old_locale, @view.locale = @view.locale, :da
+ assert_equal "Hey verden", @view.render(:file => "test/hello_world")
+ ensure
+ @view.locale = old_locale
+ end
+
+ def test_render_file_with_dashed_locale
+ old_locale, @view.locale = @view.locale, :"pt-BR"
+ assert_equal "Ola mundo", @view.render(:file => "test/hello_world")
+ ensure
+ @view.locale = old_locale
+ end
+
+ def test_render_file_at_top_level
+ assert_equal 'Elastica', @view.render(:file => '/shared')
+ end
+
+ def test_render_file_with_full_path
+ template_path = File.join(File.dirname(__FILE__), '../fixtures/test/hello_world')
+ assert_equal "Hello world!", @view.render(:file => template_path)
+ end
+
+ def test_render_file_with_instance_variables
+ assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_ivar")
+ end
+
+ def test_render_file_with_locals
+ locals = { :secret => 'in the sauce' }
+ assert_equal "The secret is in the sauce\n", @view.render(:file => "test/render_file_with_locals", :locals => locals)
+ end
+
+ def test_render_file_not_using_full_path_with_dot_in_path
+ assert_equal "The secret is in the sauce\n", @view.render(:file => "test/dot.directory/render_file_with_ivar")
+ end
+
+ def test_render_partial_from_default
+ assert_equal "only partial", @view.render("test/partial_only")
+ end
+
+ def test_render_partial
+ assert_equal "only partial", @view.render(:partial => "test/partial_only")
+ end
+
+ def test_render_partial_with_format
+ assert_equal 'partial html', @view.render(:partial => 'test/partial')
+ end
+
+ def test_render_partial_with_selected_format
+ assert_equal 'partial html', @view.render(:partial => 'test/partial', :formats => :html)
+ assert_equal 'partial js', @view.render(:partial => 'test/partial', :formats => [:js])
+ end
+
+ def test_render_partial_at_top_level
+ # file fixtures/_top_level_partial_only (not fixtures/test)
+ assert_equal 'top level partial', @view.render(:partial => '/top_level_partial_only')
+ end
+
+ def test_render_partial_with_format_at_top_level
+ # file fixtures/_top_level_partial.html (not fixtures/test, with format extension)
+ assert_equal 'top level partial html', @view.render(:partial => '/top_level_partial')
+ end
+
+ def test_render_partial_with_locals
+ assert_equal "5", @view.render(:partial => "test/counter", :locals => { :counter_counter => 5 })
+ end
+
+ def test_render_partial_with_locals_from_default
+ assert_equal "only partial", @view.render("test/partial_only", :counter_counter => 5)
+ end
+
+ def test_render_partial_with_invalid_name
+ e = assert_raises(ArgumentError) { @view.render(:partial => "test/200") }
+ assert_equal "The partial name (test/200) is not a valid Ruby identifier; " +
+ "make sure your partial name starts with a lowercase letter or underscore, " +
+ "and is followed by any combination of letters, numbers and underscores.", e.message
+ end
+
+ def test_render_partial_with_missing_filename
+ e = assert_raises(ArgumentError) { @view.render(:partial => "test/") }
+ assert_equal "The partial name (test/) is not a valid Ruby identifier; " +
+ "make sure your partial name starts with a lowercase letter or underscore, " +
+ "and is followed by any combination of letters, numbers and underscores.", e.message
+ end
+
+ def test_render_partial_with_incompatible_object
+ e = assert_raises(ArgumentError) { @view.render(:partial => nil) }
+ assert_equal "'#{nil.inspect}' is not an ActiveModel-compatible object. It must implement :to_partial_path.", e.message
+ end
+
+ def test_render_partial_with_hyphen
+ e = assert_raises(ArgumentError) { @view.render(:partial => "test/a-in") }
+ assert_equal "The partial name (test/a-in) is not a valid Ruby identifier; " +
+ "make sure your partial name starts with a lowercase letter or underscore, " +
+ "and is followed by any combination of letters, numbers and underscores.", e.message
+ end
+
+ def test_render_partial_with_errors
+ e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise") }
+ assert_match %r!method.*doesnt_exist!, e.message
+ assert_equal "", e.sub_template_message
+ assert_equal "1", e.line_number
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
+ end
+
+ def test_render_error_indentation
+ e = assert_raises(ActionView::Template::Error) { @view.render(:partial => "test/raise_indentation") }
+ error_lines = e.annoted_source_code.split("\n")
+ assert_match %r!error\shere!, e.message
+ assert_equal "11", e.line_number
+ assert_equal " 9: <p>Ninth paragraph</p>", error_lines.second
+ assert_equal " 10: <p>Tenth paragraph</p>", error_lines.third
+ end
+
+ def test_render_sub_template_with_errors
+ e = assert_raises(ActionView::Template::Error) { @view.render(:template => "test/sub_template_raise") }
+ assert_match %r!method.*doesnt_exist!, e.message
+ assert_equal "Trace of template inclusion: #{File.expand_path("#{FIXTURE_LOAD_PATH}/test/sub_template_raise.html.erb")}", e.sub_template_message
+ assert_equal "1", e.line_number
+ assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
+ end
+
+ def test_render_file_with_errors
+ e = assert_raises(ActionView::Template::Error) { @view.render(:file => File.expand_path("test/_raise", FIXTURE_LOAD_PATH)) }
+ assert_match %r!method.*doesnt_exist!, e.message
+ assert_equal "", e.sub_template_message
+ assert_equal "1", e.line_number
+ assert_equal "1: <%= doesnt_exist %>", e.annoted_source_code.strip
+ assert_equal File.expand_path("#{FIXTURE_LOAD_PATH}/test/_raise.html.erb"), e.file_name
+ end
+
+ def test_render_object
+ assert_equal "Hello: david", @view.render(:partial => "test/customer", :object => Customer.new("david"))
+ end
+
+ def test_render_object_with_array
+ assert_equal "[1, 2, 3]", @view.render(:partial => "test/object_inspector", :object => [1, 2, 3])
+ end
+
+ def test_render_partial_collection
+ assert_equal "Hello: davidHello: mary", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), Customer.new("mary") ])
+ end
+
+ def test_render_partial_collection_as_by_string
+ assert_equal "david david davidmary mary mary",
+ @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => 'customer')
+ end
+
+ def test_render_partial_collection_as_by_symbol
+ assert_equal "david david davidmary mary mary",
+ @view.render(:partial => "test/customer_with_var", :collection => [ Customer.new("david"), Customer.new("mary") ], :as => :customer)
+ end
+
+ def test_render_partial_collection_without_as
+ assert_equal "local_inspector,local_inspector_counter",
+ @view.render(:partial => "test/local_inspector", :collection => [ Customer.new("mary") ])
+ end
+
+ def test_render_partial_with_empty_collection_should_return_nil
+ assert_nil @view.render(:partial => "test/customer", :collection => [])
+ end
+
+ def test_render_partial_with_nil_collection_should_return_nil
+ assert_nil @view.render(:partial => "test/customer", :collection => nil)
+ end
+
+ def test_render_partial_with_nil_values_in_collection
+ assert_equal "Hello: davidHello: Anonymous", @view.render(:partial => "test/customer", :collection => [ Customer.new("david"), nil ])
+ end
+
+ def test_render_partial_with_layout_using_collection_and_template
+ assert_equal "<b>Hello: Amazon</b><b>Hello: Yahoo</b>", @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ end
+
+ def test_render_partial_with_layout_using_collection_and_template_makes_current_item_available_in_layout
+ assert_equal '<b class="amazon">Hello: Amazon</b><b class="yahoo">Hello: Yahoo</b>',
+ @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ end
+
+ def test_render_partial_with_layout_using_collection_and_template_makes_current_item_counter_available_in_layout
+ assert_equal '<b data-counter="0">Hello: Amazon</b><b data-counter="1">Hello: Yahoo</b>',
+ @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object_counter', :collection => [ Customer.new("Amazon"), Customer.new("Yahoo") ])
+ end
+
+ def test_render_partial_with_layout_using_object_and_template_makes_object_available_in_layout
+ assert_equal '<b class="amazon">Hello: Amazon</b>',
+ @view.render(:partial => "test/customer", :layout => 'test/b_layout_for_partial_with_object', :object => Customer.new("Amazon"))
+ end
+
+ def test_render_partial_with_empty_array_should_return_nil
+ assert_nil @view.render(:partial => [])
+ end
+
+ def test_render_partial_using_string
+ assert_equal "Hello: Anonymous", @controller_view.render('customer')
+ end
+
+ def test_render_partial_with_locals_using_string
+ assert_equal "Hola: david", @controller_view.render('customer_greeting', :greeting => 'Hola', :customer_greeting => Customer.new("david"))
+ end
+
+ def test_render_partial_using_object
+ assert_equal "Hello: lifo",
+ @controller_view.render(Customer.new("lifo"), :greeting => "Hello")
+ end
+
+ def test_render_partial_using_collection
+ customers = [ Customer.new("Amazon"), Customer.new("Yahoo") ]
+ assert_equal "Hello: AmazonHello: Yahoo",
+ @controller_view.render(customers, :greeting => "Hello")
+ 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
+ end
+
+ # TODO: The reason for this test is unclear, improve documentation
+ def test_render_partial_and_fallback_to_layout
+ assert_equal "Before (Josh)\n\nAfter", @view.render(:partial => "test/layout_for_partial", :locals => { :name => "Josh" })
+ end
+
+ # TODO: The reason for this test is unclear, improve documentation
+ def test_render_missing_xml_partial_and_raise_missing_template
+ @view.formats = [:xml]
+ assert_raises(ActionView::MissingTemplate) { @view.render(:partial => "test/layout_for_partial") }
+ ensure
+ @view.formats = nil
+ end
+
+ def test_render_layout_with_block_and_other_partial_inside
+ render = @view.render(:layout => "test/layout_with_partial_and_yield") { "Yield!" }
+ assert_equal "Before\npartial html\nYield!\nAfter\n", render
+ end
+
+ def test_render_inline
+ assert_equal "Hello, World!", @view.render(:inline => "Hello, World!")
+ end
+
+ def test_render_inline_with_locals
+ assert_equal "Hello, Josh!", @view.render(:inline => "Hello, <%= name %>!", :locals => { :name => "Josh" })
+ end
+
+ def test_render_fallbacks_to_erb_for_unknown_types
+ assert_equal "Hello, World!", @view.render(:inline => "Hello, World!", :type => :bar)
+ end
+
+ CustomHandler = lambda do |template|
+ "@output_buffer = ''\n" +
+ "@output_buffer << 'source: #{template.source.inspect}'\n"
+ end
+
+ 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)
+ 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)
+ 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)
+ 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)
+ end
+
+ def test_render_ignores_templates_with_malformed_template_handlers
+ ActiveSupport::Deprecation.silence do
+ %w(malformed malformed.erb malformed.html.erb malformed.en.html.erb).each do |name|
+ assert File.exist?(File.expand_path("#{FIXTURE_LOAD_PATH}/test/malformed/#{name}~")), "Malformed file (#{name}~) which should be ignored does not exists"
+ assert_raises(ActionView::MissingTemplate) { @view.render(:file => "test/malformed/#{name}") }
+ end
+ end
+ end
+
+ def test_render_with_layout
+ assert_equal %(<title></title>\nHello world!\n),
+ @view.render(:file => "test/hello_world", :layout => "layouts/yield")
+ end
+
+ def test_render_with_layout_which_has_render_inline
+ assert_equal %(welcome\nHello world!\n),
+ @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
+ end
+
+ def test_render_with_layout_which_renders_another_partial
+ assert_equal %(partial html\nHello world!\n),
+ @view.render(:file => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
+ end
+
+ def test_render_layout_with_block_and_yield
+ assert_equal %(Content from block!\n),
+ @view.render(:layout => "layouts/yield_only") { "Content from block!" }
+ end
+
+ def test_render_layout_with_block_and_yield_with_params
+ assert_equal %(Yield! Content from block!\n),
+ @view.render(:layout => "layouts/yield_with_params") { |param| "#{param} Content from block!" }
+ end
+
+ def test_render_layout_with_block_which_renders_another_partial_and_yields
+ assert_equal %(partial html\nContent from block!\n),
+ @view.render(:layout => "layouts/partial_and_yield") { "Content from block!" }
+ end
+
+ def test_render_partial_and_layout_without_block_with_locals
+ assert_equal %(Before (Foo!)\npartial html\nAfter),
+ @view.render(:partial => 'test/partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ end
+
+ def test_render_partial_and_layout_without_block_with_locals_and_rendering_another_partial
+ assert_equal %(Before (Foo!)\npartial html\npartial with partial\n\nAfter),
+ @view.render(:partial => 'test/partial_with_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ end
+
+ def test_render_layout_with_a_nested_render_layout_call
+ assert_equal %(Before (Foo!)\nBefore (Bar!)\npartial html\nAfter\npartial with layout\n\nAfter),
+ @view.render(:partial => 'test/partial_with_layout', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ end
+
+ def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_partial
+ assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n partial html\n\nAfterpartial with layout\n\nAfter),
+ @view.render(:partial => 'test/partial_with_layout_block_partial', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ end
+
+ def test_render_layout_with_a_nested_render_layout_call_using_block_with_render_content
+ assert_equal %(Before (Foo!)\nBefore (Bar!)\n\n Content from inside layout!\n\nAfterpartial with layout\n\nAfter),
+ @view.render(:partial => 'test/partial_with_layout_block_content', :layout => 'test/layout_for_partial', :locals => { :name => 'Foo!'})
+ end
+
+ def test_render_partial_with_layout_raises_descriptive_error
+ e = assert_raises(ActionView::MissingTemplate) { @view.render(partial: 'test/partial', layout: true) }
+ assert_match "Missing partial /true with", e.message
+ end
+
+ def test_render_with_nested_layout
+ assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
+ @view.render(:file => "test/nested_layout", :layout => "layouts/yield")
+ end
+
+ def test_render_with_file_in_layout
+ assert_equal %(\n<title>title</title>\n\n),
+ @view.render(:file => "test/layout_render_file")
+ end
+
+ def test_render_layout_with_object
+ assert_equal %(<title>David</title>),
+ @view.render(:file => "test/layout_render_object")
+ end
+
+ 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)
+ end
+
+ def test_render_throws_exception_when_no_extensions_passed_to_register_template_handler_function_call
+ assert_raises(ArgumentError) { ActionView::Template.register_template_handler CustomHandler }
+ end
+end
+
+class CachedViewRenderTest < ActiveSupport::TestCase
+ include RenderTestCases
+
+ # Ensure view path cache is primed
+ def setup
+ view_paths = ActionController::Base.view_paths
+ assert_equal ActionView::OptimizedFileSystemResolver, view_paths.first.class
+ setup_view(view_paths)
+ end
+
+ def teardown
+ GC.start
+ end
+end
+
+class LazyViewRenderTest < ActiveSupport::TestCase
+ include RenderTestCases
+
+ # Test the same thing as above, but make sure the view path
+ # is not eager loaded
+ def setup
+ path = ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH)
+ view_paths = ActionView::PathSet.new([path])
+ assert_equal ActionView::FileSystemResolver.new(FIXTURE_LOAD_PATH), view_paths.first
+ setup_view(view_paths)
+ end
+
+ def teardown
+ GC.start
+ end
+
+ def test_render_utf8_template_with_magic_comment
+ with_external_encoding Encoding::ASCII_8BIT do
+ result = @view.render(:file => "test/utf8_magic", :formats => [:html], :layouts => "layouts/yield")
+ assert_equal Encoding::UTF_8, result.encoding
+ assert_equal "\nРуÑÑкий \nтекÑÑ‚\n\nUTF-8\nUTF-8\nUTF-8\n", result
+ end
+ end
+
+ def test_render_utf8_template_with_default_external_encoding
+ with_external_encoding Encoding::UTF_8 do
+ result = @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield")
+ assert_equal Encoding::UTF_8, result.encoding
+ assert_equal "РуÑÑкий текÑÑ‚\n\nUTF-8\nUTF-8\nUTF-8\n", result
+ end
+ end
+
+ def test_render_utf8_template_with_incompatible_external_encoding
+ with_external_encoding Encoding::SHIFT_JIS do
+ e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8", :formats => [:html], :layouts => "layouts/yield") }
+ assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
+ end
+ end
+
+ def test_render_utf8_template_with_partial_with_incompatible_encoding
+ with_external_encoding Encoding::SHIFT_JIS do
+ e = assert_raises(ActionView::Template::Error) { @view.render(:file => "test/utf8_magic_with_bare_partial", :formats => [:html], :layouts => "layouts/yield") }
+ assert_match 'Your template was not saved as valid Shift_JIS', e.original_exception.message
+ end
+ end
+
+ def with_external_encoding(encoding)
+ old = Encoding.default_external
+ silence_warnings { Encoding.default_external = encoding }
+ yield
+ ensure
+ silence_warnings { Encoding.default_external = old }
+ end
+end
diff --git a/actionview/test/template/resolver_patterns_test.rb b/actionview/test/template/resolver_patterns_test.rb
new file mode 100644
index 0000000000..575eb9bd28
--- /dev/null
+++ b/actionview/test/template/resolver_patterns_test.rb
@@ -0,0 +1,31 @@
+require 'abstract_unit'
+
+class ResolverPatternsTest < ActiveSupport::TestCase
+ def setup
+ path = File.expand_path("../../fixtures/", __FILE__)
+ pattern = ":prefix/{:formats/,}:action{.:formats,}{.:handlers,}"
+ @resolver = ActionView::FileSystemResolver.new(path, pattern)
+ end
+
+ def test_should_return_empty_list_for_unknown_path
+ templates = @resolver.find_all("unknown", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ assert_equal [], templates, "expected an empty list of templates"
+ end
+
+ def test_should_return_template_for_declared_path
+ templates = @resolver.find_all("path", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ assert_equal 1, templates.size, "expected one template"
+ assert_equal "Hello custom patterns!", templates.first.source
+ assert_equal "custom_pattern/path", templates.first.virtual_path
+ assert_equal [:html], templates.first.formats
+ end
+
+ def test_should_return_all_templates_when_ambiguous_pattern
+ templates = @resolver.find_all("another", "custom_pattern", false, {:locale => [], :formats => [:html], :handlers => [:erb]})
+ assert_equal 2, templates.size, "expected two templates"
+ assert_equal "Another template!", templates[0].source
+ assert_equal "custom_pattern/another", templates[0].virtual_path
+ assert_equal "Hello custom patterns!", templates[1].source
+ assert_equal "custom_pattern/another", templates[1].virtual_path
+ end
+end
diff --git a/actionpack/test/template/sanitize_helper_test.rb b/actionview/test/template/sanitize_helper_test.rb
index 12d5260a9d..12d5260a9d 100644
--- a/actionpack/test/template/sanitize_helper_test.rb
+++ b/actionview/test/template/sanitize_helper_test.rb
diff --git a/actionview/test/template/streaming_render_test.rb b/actionview/test/template/streaming_render_test.rb
new file mode 100644
index 0000000000..8a24d78e74
--- /dev/null
+++ b/actionview/test/template/streaming_render_test.rb
@@ -0,0 +1,108 @@
+# encoding: utf-8
+require 'abstract_unit'
+
+class TestController < ActionController::Base
+end
+
+class FiberedTest < ActiveSupport::TestCase
+ def setup
+ view_paths = ActionController::Base.view_paths
+ @assigns = { :secret => 'in the sauce', :name => nil }
+ @view = ActionView::Base.new(view_paths, @assigns)
+ @controller_view = TestController.new.view_context
+ end
+
+ def render_body(options)
+ @view.view_renderer.render_body(@view, options)
+ end
+
+ def buffered_render(options)
+ body = render_body(options)
+ string = ""
+ body.each do |piece|
+ string << piece
+ end
+ string
+ end
+
+ def test_streaming_works
+ content = []
+ body = render_body(:template => "test/hello_world", :layout => "layouts/yield")
+
+ body.each do |piece|
+ content << piece
+ end
+
+ assert_equal "<title>", content[0]
+ assert_equal "", content[1]
+ assert_equal "</title>\n", content[2]
+ assert_equal "Hello world!", content[3]
+ assert_equal "\n", content[4]
+ end
+
+ def test_render_file
+ assert_equal "Hello world!", buffered_render(:file => "test/hello_world")
+ end
+
+ def test_render_file_with_locals
+ locals = { :secret => 'in the sauce' }
+ assert_equal "The secret is in the sauce\n", buffered_render(:file => "test/render_file_with_locals", :locals => locals)
+ end
+
+ def test_render_partial
+ assert_equal "only partial", buffered_render(:partial => "test/partial_only")
+ end
+
+ def test_render_inline
+ assert_equal "Hello, World!", buffered_render(:inline => "Hello, World!")
+ end
+
+ def test_render_without_layout
+ assert_equal "Hello world!", buffered_render(:template => "test/hello_world")
+ end
+
+ def test_render_with_layout
+ assert_equal %(<title></title>\nHello world!\n),
+ buffered_render(:template => "test/hello_world", :layout => "layouts/yield")
+ end
+
+ def test_render_with_layout_which_has_render_inline
+ assert_equal %(welcome\nHello world!\n),
+ buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_inline_inside")
+ end
+
+ def test_render_with_layout_which_renders_another_partial
+ assert_equal %(partial html\nHello world!\n),
+ buffered_render(:template => "test/hello_world", :layout => "layouts/yield_with_render_partial_inside")
+ end
+
+ def test_render_with_nested_layout
+ assert_equal %(<title>title</title>\n\n<div id="column">column</div>\n<div id="content">content</div>\n),
+ buffered_render(:template => "test/nested_layout", :layout => "layouts/yield")
+ end
+
+ def test_render_with_file_in_layout
+ assert_equal %(\n<title>title</title>\n\n),
+ buffered_render(:template => "test/layout_render_file")
+ end
+
+ def test_render_with_handler_without_streaming_support
+ assert_match "<p>This is grand!</p>", buffered_render(:template => "test/hello")
+ end
+
+ def test_render_with_streaming_multiple_yields_provide_and_content_for
+ assert_equal "Yes, \nthis works\n like a charm.",
+ buffered_render(:template => "test/streaming", :layout => "layouts/streaming")
+ end
+
+ def test_render_with_streaming_with_fake_yields_and_streaming_buster
+ assert_equal "This won't look\n good.",
+ buffered_render(:template => "test/streaming_buster", :layout => "layouts/streaming")
+ end
+
+ def test_render_with_nested_streaming_multiple_yields_provide_and_content_for
+ assert_equal "?Yes, \n\nthis works\n\n? like a charm.",
+ buffered_render(:template => "test/nested_streaming", :layout => "layouts/streaming")
+ end
+
+end
diff --git a/actionview/test/template/tag_helper_test.rb b/actionview/test/template/tag_helper_test.rb
new file mode 100644
index 0000000000..fb016a52de
--- /dev/null
+++ b/actionview/test/template/tag_helper_test.rb
@@ -0,0 +1,134 @@
+require 'abstract_unit'
+
+class TagHelperTest < ActionView::TestCase
+ include RenderERBUtils
+
+ tests ActionView::Helpers::TagHelper
+
+ def test_tag
+ assert_equal "<br />", tag("br")
+ assert_equal "<br clear=\"left\" />", tag(:br, :clear => "left")
+ assert_equal "<br>", tag("br", nil, true)
+ end
+
+ def test_tag_options
+ str = tag("p", "class" => "show", :class => "elsewhere")
+ assert_match(/class="show"/, str)
+ assert_match(/class="elsewhere"/, str)
+ end
+
+ def test_tag_options_rejects_nil_option
+ assert_equal "<p />", tag("p", :ignored => nil)
+ end
+
+ def test_tag_options_accepts_false_option
+ assert_equal "<p value=\"false\" />", tag("p", :value => false)
+ end
+
+ def test_tag_options_accepts_blank_option
+ assert_equal "<p included=\"\" />", tag("p", :included => '')
+ end
+
+ def test_tag_options_converts_boolean_option
+ assert_dom_equal '<p disabled="disabled" itemscope="itemscope" multiple="multiple" readonly="readonly" allowfullscreen="allowfullscreen" seamless="seamless" typemustmatch="typemustmatch" sortable="sortable" default="default" inert="inert" truespeed="truespeed" />',
+ tag("p", :disabled => true, :itemscope => true, :multiple => true, :readonly => true, :allowfullscreen => true, :seamless => true, :typemustmatch => true, :sortable => true, :default => true, :inert => true, :truespeed => true)
+ end
+
+ def test_content_tag
+ assert_equal "<a href=\"create\">Create</a>", content_tag("a", "Create", "href" => "create")
+ assert content_tag("a", "Create", "href" => "create").html_safe?
+ assert_equal content_tag("a", "Create", "href" => "create"),
+ content_tag("a", "Create", :href => "create")
+ assert_equal "<p>&lt;script&gt;evil_js&lt;/script&gt;</p>",
+ content_tag(:p, '<script>evil_js</script>')
+ assert_equal "<p><script>evil_js</script></p>",
+ content_tag(:p, '<script>evil_js</script>', nil, false)
+ end
+
+ def test_content_tag_with_block_in_erb
+ buffer = render_erb("<%= content_tag(:div) do %>Hello world!<% end %>")
+ assert_dom_equal "<div>Hello world!</div>", buffer
+ end
+
+ def test_content_tag_with_block_and_options_in_erb
+ buffer = render_erb("<%= content_tag(:div, :class => 'green') do %>Hello world!<% end %>")
+ assert_dom_equal %(<div class="green">Hello world!</div>), buffer
+ end
+
+ def test_content_tag_with_block_and_options_out_of_erb
+ assert_dom_equal %(<div class="green">Hello world!</div>), content_tag(:div, :class => "green") { "Hello world!" }
+ end
+
+ def test_content_tag_with_block_and_options_outside_out_of_erb
+ assert_equal content_tag("a", "Create", :href => "create"),
+ content_tag("a", "href" => "create") { "Create" }
+ end
+
+ def test_content_tag_nested_in_content_tag_out_of_erb
+ assert_equal content_tag("p", content_tag("b", "Hello")),
+ content_tag("p") { content_tag("b", "Hello") },
+ output_buffer
+ end
+
+ def test_content_tag_nested_in_content_tag_in_erb
+ assert_equal "<p>\n <b>Hello</b>\n</p>", view.render("test/content_tag_nested_in_content_tag")
+ end
+
+ def test_content_tag_with_escaped_array_class
+ str = content_tag('p', "limelight", :class => ["song", "play>"])
+ assert_equal "<p class=\"song play&gt;\">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
+ end
+
+ def test_content_tag_with_data_attributes
+ assert_dom_equal '<p data-number="1" data-string="hello" data-string-with-quotes="double&quot;quote&quot;party&quot;">limelight</p>',
+ content_tag('p', "limelight", data: { number: 1, string: 'hello', string_with_quotes: 'double"quote"party"' })
+ end
+
+ def test_cdata_section
+ assert_equal "<![CDATA[<hello world>]]>", cdata_section("<hello world>")
+ end
+
+ def test_cdata_section_with_string_conversion
+ assert_equal "<![CDATA[]]>", cdata_section(nil)
+ end
+
+ def test_cdata_section_splitted
+ assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]>", cdata_section("hello]]>world")
+ assert_equal "<![CDATA[hello]]]]><![CDATA[>world]]]]><![CDATA[>again]]>", cdata_section("hello]]>world]]>again")
+ end
+
+ def test_escape_once
+ assert_equal '1 &lt; 2 &amp; 3', escape_once('1 < 2 &amp; 3')
+ end
+
+ def test_tag_honors_html_safe_for_param_values
+ ['1&amp;2', '1 &lt; 2', '&#8220;test&#8220;'].each do |escaped|
+ assert_equal %(<a href="#{escaped}" />), tag('a', :href => escaped.html_safe)
+ end
+ end
+
+ def test_skip_invalid_escaped_attributes
+ ['&1;', '&#1dfa3;', '& #123;'].each do |escaped|
+ assert_equal %(<a href="#{escaped.gsub(/&/, '&amp;')}" />), tag('a', :href => escaped)
+ end
+ end
+
+ def test_disable_escaping
+ assert_equal '<a href="&amp;" />', tag('a', { :href => '&amp;' }, false, false)
+ end
+
+ def test_data_attributes
+ ['data', :data].each { |data|
+ assert_dom_equal '<a data-a-float="3.14" data-a-big-decimal="-123.456" data-a-number="1" data-array="[1,2,3]" data-hash="{&quot;key&quot;:&quot;value&quot;}" data-string-with-quotes="double&quot;quote&quot;party&quot;" data-string="hello" data-symbol="foo" />',
+ tag('a', { data => { a_float: 3.14, a_big_decimal: BigDecimal.new("-123.456"), a_number: 1, string: 'hello', symbol: :foo, array: [1, 2, 3], hash: { key: 'value'}, string_with_quotes: 'double"quote"party"' } })
+ }
+ end
+end
diff --git a/actionpack/test/template/template_error_test.rb b/actionview/test/template/template_error_test.rb
index 91424daeed..91424daeed 100644
--- a/actionpack/test/template/template_error_test.rb
+++ b/actionview/test/template/template_error_test.rb
diff --git a/actionview/test/template/template_test.rb b/actionview/test/template/template_test.rb
new file mode 100644
index 0000000000..c94508d678
--- /dev/null
+++ b/actionview/test/template/template_test.rb
@@ -0,0 +1,200 @@
+# encoding: US-ASCII
+require "abstract_unit"
+require "logger"
+
+class TestERBTemplate < ActiveSupport::TestCase
+ ERBHandler = ActionView::Template::Handlers::ERB.new
+
+ class LookupContext
+ def disable_cache
+ yield
+ end
+
+ def find_template(*args)
+ end
+
+ attr_accessor :formats
+ end
+
+ class Context
+ def initialize
+ @output_buffer = "original"
+ @virtual_path = nil
+ end
+
+ def hello
+ "Hello"
+ end
+
+ def apostrophe
+ "l'apostrophe"
+ end
+
+ def partial
+ ActionView::Template.new(
+ "<%= @virtual_path %>",
+ "partial",
+ ERBHandler,
+ :virtual_path => "partial"
+ )
+ end
+
+ def lookup_context
+ @lookup_context ||= LookupContext.new
+ end
+
+ def logger
+ ActiveSupport::Logger.new(STDERR)
+ end
+
+ def my_buffer
+ @output_buffer
+ end
+ end
+
+ def new_template(body = "<%= hello %>", details = { format: :html })
+ ActionView::Template.new(body, "hello template", details.fetch(:handler) { ERBHandler }, {:virtual_path => "hello"}.merge!(details))
+ end
+
+ def render(locals = {})
+ @template.render(@context, locals)
+ end
+
+ def setup
+ @context = Context.new
+ end
+
+ def test_basic_template
+ @template = new_template
+ assert_equal "Hello", render
+ end
+
+ def test_basic_template_does_html_escape
+ @template = new_template("<%= apostrophe %>")
+ assert_equal "l&#39;apostrophe", render
+ end
+
+ def test_text_template_does_not_html_escape
+ @template = new_template("<%= apostrophe %> <%== apostrophe %>", format: :text)
+ assert_equal "l'apostrophe l'apostrophe", render
+ end
+
+ def test_raw_template
+ @template = new_template("<%= hello %>", :handler => ActionView::Template::Handlers::Raw.new)
+ assert_equal "<%= hello %>", render
+ end
+
+ def test_template_loses_its_source_after_rendering
+ @template = new_template
+ render
+ assert_nil @template.source
+ end
+
+ def test_template_does_not_lose_its_source_after_rendering_if_it_does_not_have_a_virtual_path
+ @template = new_template("Hello", :virtual_path => nil)
+ render
+ assert_equal "Hello", @template.source
+ end
+
+ def test_locals
+ @template = new_template("<%= my_local %>")
+ @template.locals = [:my_local]
+ assert_equal "I am a local", render(:my_local => "I am a local")
+ end
+
+ def test_restores_buffer
+ @template = new_template
+ assert_equal "Hello", render
+ assert_equal "original", @context.my_buffer
+ end
+
+ def test_virtual_path
+ @template = new_template("<%= @virtual_path %>" \
+ "<%= partial.render(self, {}) %>" \
+ "<%= @virtual_path %>")
+ assert_equal "hellopartialhello", render
+ end
+
+ def test_refresh_with_templates
+ @template = new_template("Hello", :virtual_path => "test/foo/bar")
+ @template.locals = [:key]
+ @context.lookup_context.expects(:find_template).with("bar", %w(test/foo), false, [:key]).returns("template")
+ assert_equal "template", @template.refresh(@context)
+ end
+
+ def test_refresh_with_partials
+ @template = new_template("Hello", :virtual_path => "test/_foo")
+ @template.locals = [:key]
+ @context.lookup_context.expects(:find_template).with("foo", %w(test), true, [:key]).returns("partial")
+ assert_equal "partial", @template.refresh(@context)
+ end
+
+ def test_refresh_raises_an_error_without_virtual_path
+ @template = new_template("Hello", :virtual_path => nil)
+ assert_raise RuntimeError do
+ @template.refresh(@context)
+ end
+ end
+
+ def test_resulting_string_is_utf8
+ @template = new_template
+ assert_equal Encoding::UTF_8, render.encoding
+ end
+
+ def test_no_magic_comment_word_with_utf_8
+ @template = new_template("hello \u{fc}mlat")
+ assert_equal Encoding::UTF_8, render.encoding
+ assert_equal "hello \u{fc}mlat", render
+ end
+
+ # This test ensures that if the default_external
+ # is set to something other than UTF-8, we don't
+ # get any errors and get back a UTF-8 String.
+ def test_default_external_works
+ with_external_encoding "ISO-8859-1" do
+ @template = new_template("hello \xFCmlat")
+ assert_equal Encoding::UTF_8, render.encoding
+ assert_equal "hello \u{fc}mlat", render
+ end
+ end
+
+ def test_encoding_can_be_specified_with_magic_comment
+ @template = new_template("# encoding: ISO-8859-1\nhello \xFCmlat")
+ assert_equal Encoding::UTF_8, render.encoding
+ assert_equal "\nhello \u{fc}mlat", render
+ end
+
+ # TODO: This is currently handled inside ERB. The case of explicitly
+ # lying about encodings via the normal Rails API should be handled
+ # inside Rails.
+ def test_lying_with_magic_comment
+ assert_raises(ActionView::Template::Error) do
+ @template = new_template("# encoding: UTF-8\nhello \xFCmlat", :virtual_path => nil)
+ render
+ end
+ end
+
+ def test_encoding_can_be_specified_with_magic_comment_in_erb
+ with_external_encoding Encoding::UTF_8 do
+ @template = new_template("<%# encoding: ISO-8859-1 %>hello \xFCmlat", :virtual_path => nil)
+ assert_equal Encoding::UTF_8, render.encoding
+ assert_equal "hello \u{fc}mlat", render
+ end
+ end
+
+ def test_error_when_template_isnt_valid_utf8
+ assert_raises(ActionView::Template::Error, /\xFC/) do
+ @template = new_template("hello \xFCmlat", :virtual_path => nil)
+ render
+ end
+ end
+
+ def with_external_encoding(encoding)
+ old = Encoding.default_external
+ Encoding::Converter.new old, encoding if old != encoding
+ silence_warnings { Encoding.default_external = encoding }
+ yield
+ ensure
+ silence_warnings { Encoding.default_external = old }
+ end
+end
diff --git a/actionview/test/template/test_case_test.rb b/actionview/test/template/test_case_test.rb
new file mode 100644
index 0000000000..4ee0930341
--- /dev/null
+++ b/actionview/test/template/test_case_test.rb
@@ -0,0 +1,366 @@
+require 'abstract_unit'
+
+module ActionView
+
+ module ATestHelper
+ end
+
+ module AnotherTestHelper
+ def from_another_helper
+ 'Howdy!'
+ end
+ end
+
+ module ASharedTestHelper
+ def from_shared_helper
+ 'Holla!'
+ end
+ end
+
+ class TestCase
+ helper ASharedTestHelper
+
+ module SharedTests
+ def self.included(test_case)
+ test_case.class_eval do
+ test "helpers defined on ActionView::TestCase are available" do
+ assert test_case.ancestors.include?(ASharedTestHelper)
+ assert_equal 'Holla!', from_shared_helper
+ end
+ end
+ end
+ end
+ end
+
+ class GeneralViewTest < ActionView::TestCase
+ include SharedTests
+ test_case = self
+
+ test "memoizes the view" do
+ assert_same view, view
+ end
+
+ test "exposes view as _view for backwards compatibility" do
+ assert_same _view, view
+ end
+
+ test "retrieve non existing config values" do
+ assert_equal nil, ActionView::Base.new.config.something_odd
+ end
+
+ test "works without testing a helper module" do
+ assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
+ end
+
+ test "can render a layout with block" do
+ assert_equal "Before (ChrisCruft)\n!\nAfter",
+ render(:layout => "test/layout_for_partial", :locals => {:name => "ChrisCruft"}) {"!"}
+ end
+
+ helper AnotherTestHelper
+ test "additional helper classes can be specified as in a controller" do
+ assert test_case.ancestors.include?(AnotherTestHelper)
+ assert_equal 'Howdy!', from_another_helper
+ end
+
+ test "determine_default_helper_class returns nil if the test name constant resolves to a class" do
+ assert_nil self.class.determine_default_helper_class("String")
+ end
+
+ test "delegates notice to request.flash[:notice]" do
+ view.request.flash.expects(:[]).with(:notice)
+ view.notice
+ end
+
+ test "delegates alert to request.flash[:alert]" do
+ view.request.flash.expects(:[]).with(:alert)
+ view.alert
+ end
+
+ test "uses controller lookup context" do
+ assert_equal self.lookup_context, @controller.lookup_context
+ end
+ end
+
+ class ClassMethodsTest < ActionView::TestCase
+ include SharedTests
+ test_case = self
+
+ tests ATestHelper
+ test "tests the specified helper module" do
+ assert_equal ATestHelper, test_case.helper_class
+ assert test_case.ancestors.include?(ATestHelper)
+ end
+
+ helper AnotherTestHelper
+ test "additional helper classes can be specified as in a controller" do
+ assert test_case.ancestors.include?(AnotherTestHelper)
+ assert_equal 'Howdy!', from_another_helper
+
+ test_case.helper_class.module_eval do
+ def render_from_helper
+ from_another_helper
+ end
+ end
+ assert_equal 'Howdy!', render(:partial => 'test/from_helper')
+ end
+ end
+
+ class HelperInclusionTest < ActionView::TestCase
+ module RenderHelper
+ def render_from_helper
+ render :partial => 'customer', :collection => @customers
+ end
+ end
+
+ helper RenderHelper
+
+ test "helper class that is being tested is always included in view instance" do
+ @controller.controller_path = 'test'
+
+ @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ assert_match(/Hello: EloyHello: Manfred/, render(:partial => 'test/from_helper'))
+ end
+ end
+
+ class ControllerHelperMethod < ActionView::TestCase
+ module SomeHelper
+ def some_method
+ render :partial => 'test/from_helper'
+ end
+ end
+
+ helper SomeHelper
+
+ test "can call a helper method defined on the current controller from a helper" do
+ @controller.singleton_class.class_eval <<-EOF, __FILE__, __LINE__ + 1
+ def render_from_helper
+ 'controller_helper_method'
+ end
+ EOF
+ @controller.class.helper_method :render_from_helper
+
+ assert_equal 'controller_helper_method', some_method
+ end
+ end
+
+ class ViewAssignsTest < ActionView::TestCase
+ test "view_assigns returns a Hash of user defined ivars" do
+ @a = 'b'
+ @c = 'd'
+ assert_equal({:a => 'b', :c => 'd'}, view_assigns)
+ end
+
+ test "view_assigns excludes internal ivars" do
+ INTERNAL_IVARS.each do |ivar|
+ assert defined?(ivar), "expected #{ivar} to be defined"
+ assert !view_assigns.keys.include?(ivar.to_s.sub('@', '').to_sym), "expected #{ivar} to be excluded from view_assigns"
+ end
+ end
+ end
+
+ class HelperExposureTest < ActionView::TestCase
+ helper(Module.new do
+ def render_from_helper
+ from_test_case
+ end
+ end)
+ test "is able to make methods available to the view" do
+ assert_equal 'Word!', render(:partial => 'test/from_helper')
+ end
+
+ def from_test_case; 'Word!'; end
+ helper_method :from_test_case
+ end
+
+ class IgnoreProtectAgainstForgeryTest < ActionView::TestCase
+ module HelperThatInvokesProtectAgainstForgery
+ def help_me
+ protect_against_forgery?
+ end
+ end
+
+ helper HelperThatInvokesProtectAgainstForgery
+
+ test "protect_from_forgery? in any helpers returns false" do
+ assert !view.help_me
+ end
+
+ end
+
+ class ATestHelperTest < ActionView::TestCase
+ include SharedTests
+ test_case = self
+
+ test "inflects the name of the helper module to test from the test case class" do
+ assert_equal ATestHelper, test_case.helper_class
+ assert test_case.ancestors.include?(ATestHelper)
+ end
+
+ test "a configured test controller is available" do
+ assert_kind_of ActionController::Base, controller
+ assert_equal '', controller.controller_path
+ end
+
+ test "no additional helpers should shared across test cases" do
+ assert !test_case.ancestors.include?(AnotherTestHelper)
+ assert_raise(NoMethodError) { send :from_another_helper }
+ end
+
+ test "is able to use routes" do
+ controller.request.assign_parameters(@routes, 'foo', 'index')
+ assert_equal '/foo', url_for
+ assert_equal '/bar', url_for(:controller => 'bar')
+ end
+
+ test "is able to use named routes" do
+ with_routing do |set|
+ set.draw { resources :contents }
+ assert_equal 'http://test.host/contents/new', new_content_url
+ assert_equal 'http://test.host/contents/1', content_url(:id => 1)
+ end
+ end
+
+ test "is able to use mounted routes" do
+ with_routing do |set|
+ app = Class.new do
+ def self.routes
+ @routes ||= ActionDispatch::Routing::RouteSet.new
+ end
+
+ routes.draw { get "bar", :to => lambda {} }
+
+ def self.call(*)
+ end
+ end
+
+ set.draw { mount app => "/foo", :as => "foo_app" }
+
+ assert_equal '/foo/bar', foo_app.bar_path
+ end
+ end
+
+ test "named routes can be used from helper included in view" do
+ with_routing do |set|
+ set.draw { resources :contents }
+ _helpers.module_eval do
+ def render_from_helper
+ new_content_url
+ end
+ end
+
+ assert_equal 'http://test.host/contents/new', render(:partial => 'test/from_helper')
+ end
+ end
+
+ test "is able to render partials with local variables" do
+ assert_equal 'Eloy', render('developers/developer', :developer => stub(:name => 'Eloy'))
+ assert_equal 'Eloy', render(:partial => 'developers/developer',
+ :locals => { :developer => stub(:name => 'Eloy') })
+ end
+
+ test "is able to render partials from templates and also use instance variables" do
+ @controller.controller_path = "test"
+
+ @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ end
+
+ test "is able to render partials from templates and also use instance variables after view has been referenced" do
+ @controller.controller_path = "test"
+
+ view
+
+ @customers = [stub(:name => 'Eloy'), stub(:name => 'Manfred')]
+ assert_match(/Hello: EloyHello: Manfred/, render(:file => 'test/list'))
+ end
+
+ end
+
+ class AssertionsTest < ActionView::TestCase
+ def render_from_helper
+ form_tag('/foo') do
+ safe_concat render(:text => '<ul><li>foo</li></ul>')
+ end
+ end
+ helper_method :render_from_helper
+
+ test "uses the output_buffer for assert_select" do
+ render(:partial => 'test/from_helper')
+
+ assert_select 'form' do
+ assert_select 'li', :text => 'foo'
+ end
+ end
+ end
+
+ class RenderTemplateTest < ActionView::TestCase
+ test "supports specifying templates with a Regexp" do
+ controller.controller_path = "fun"
+ render(:template => "fun/games/hello_world")
+ assert_template %r{\Afun/games/hello_world\Z}
+ end
+
+ test "supports specifying partials" do
+ controller.controller_path = "test"
+ render(:template => "test/calling_partial_with_layout")
+ assert_template :partial => "_partial_for_use_in_layout"
+ end
+
+ test "supports specifying locals (passing)" do
+ controller.controller_path = "test"
+ render(:template => "test/calling_partial_with_layout")
+ assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "David" }
+ end
+
+ test "supports specifying locals (failing)" do
+ controller.controller_path = "test"
+ render(:template => "test/calling_partial_with_layout")
+ assert_raise ActiveSupport::TestCase::Assertion, /Somebody else.*David/m do
+ assert_template :partial => "_partial_for_use_in_layout", :locals => { :name => "Somebody Else" }
+ end
+ end
+
+ test 'supports different locals on the same partial' do
+ controller.controller_path = "test"
+ render(:template => "test/render_two_partials")
+ assert_template partial: '_partial', locals: { 'first' => '1' }
+ assert_template partial: '_partial', locals: { 'second' => '2' }
+ end
+
+ test 'raises descriptive error message when template was not rendered' do
+ controller.controller_path = "test"
+ render(template: "test/hello_world_with_partial")
+ e = assert_raise ActiveSupport::TestCase::Assertion do
+ assert_template partial: 'i_was_never_rendered', locals: { 'did_not' => 'happen' }
+ end
+ assert_match "i_was_never_rendered to be rendered but it was not.", e.message
+ assert_match 'Expected ["/test/partial"] to include "i_was_never_rendered"', e.message
+ end
+
+ test 'specifying locals works when the partial is inside a directory with underline prefix' do
+ controller.controller_path = "test"
+ render(template: 'test/render_partial_inside_directory')
+ assert_template partial: 'test/_directory/_partial_with_locales', locals: { 'name' => 'Jane' }
+ end
+
+ test 'specifying locals works when the partial is inside a directory without underline prefix' do
+ controller.controller_path = "test"
+ render(template: 'test/render_partial_inside_directory')
+ assert_template partial: 'test/_directory/partial_with_locales', locals: { 'name' => 'Jane' }
+ end
+ end
+
+ module AHelperWithInitialize
+ def initialize(*)
+ super
+ @called_initialize = true
+ end
+ end
+
+ class AHelperWithInitializeTest < ActionView::TestCase
+ test "the helper's initialize was actually called" do
+ assert @called_initialize
+ end
+ end
+end
diff --git a/actionview/test/template/test_test.rb b/actionview/test/template/test_test.rb
new file mode 100644
index 0000000000..108a674d95
--- /dev/null
+++ b/actionview/test/template/test_test.rb
@@ -0,0 +1,80 @@
+require 'abstract_unit'
+
+module PeopleHelper
+ def title(text)
+ content_tag(:h1, text)
+ end
+
+ def homepage_path
+ people_path
+ end
+
+ def homepage_url
+ people_url
+ end
+
+ def link_to_person(person)
+ link_to person.name, person
+ end
+end
+
+class PeopleHelperTest < ActionView::TestCase
+ def test_title
+ assert_equal "<h1>Ruby on Rails</h1>", title("Ruby on Rails")
+ end
+
+ def test_homepage_path
+ with_test_route_set do
+ assert_equal "/people", homepage_path
+ end
+ end
+
+ def test_homepage_url
+ with_test_route_set do
+ assert_equal "http://test.host/people", homepage_url
+ end
+ end
+
+ 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")
+ assert_equal '<a href="/people/1">David</a>', link_to_person(person)
+ end
+ end
+
+ private
+ def with_test_route_set
+ with_routing do |set|
+ set.draw do
+ get 'people', :to => 'people#index', :as => :people
+ end
+ yield
+ end
+ end
+end
+
+class CrazyHelperTest < ActionView::TestCase
+ tests PeopleHelper
+
+ def test_helper_class_can_be_set_manually_not_just_inferred
+ assert_equal PeopleHelper, self.class.helper_class
+ end
+end
+
+class CrazySymbolHelperTest < ActionView::TestCase
+ tests :people
+
+ def test_set_helper_class_using_symbol
+ assert_equal PeopleHelper, self.class.helper_class
+ end
+end
+
+class CrazyStringHelperTest < ActionView::TestCase
+ tests 'people'
+
+ def test_set_helper_class_using_string
+ assert_equal PeopleHelper, self.class.helper_class
+ end
+end
diff --git a/actionpack/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb
index 9649f349cb..9649f349cb 100644
--- a/actionpack/test/template/testing/fixture_resolver_test.rb
+++ b/actionview/test/template/testing/fixture_resolver_test.rb
diff --git a/actionpack/test/template/testing/null_resolver_test.rb b/actionview/test/template/testing/null_resolver_test.rb
index 55ec36e753..55ec36e753 100644
--- a/actionpack/test/template/testing/null_resolver_test.rb
+++ b/actionview/test/template/testing/null_resolver_test.rb
diff --git a/actionview/test/template/text_helper_test.rb b/actionview/test/template/text_helper_test.rb
new file mode 100644
index 0000000000..c2999fcb85
--- /dev/null
+++ b/actionview/test/template/text_helper_test.rb
@@ -0,0 +1,470 @@
+# encoding: utf-8
+require 'abstract_unit'
+
+class TextHelperTest < ActionView::TestCase
+ tests ActionView::Helpers::TextHelper
+
+ def setup
+ super
+ # This simulates the fact that instance variables are reset every time
+ # a view is rendered. The cycle helper depends on this behavior.
+ @_cycles = nil if (defined? @_cycles)
+ end
+
+ def test_concat
+ self.output_buffer = 'foo'
+ assert_equal 'foobar', concat('bar')
+ assert_equal 'foobar', output_buffer
+ end
+
+ def test_simple_format_should_be_html_safe
+ assert simple_format("<b> test with html tags </b>").html_safe?
+ end
+
+ def test_simple_format
+ assert_equal "<p></p>", simple_format(nil)
+
+ assert_equal "<p>crazy\n<br /> cross\n<br /> platform linebreaks</p>", simple_format("crazy\r\n cross\r platform linebreaks")
+ assert_equal "<p>A paragraph</p>\n\n<p>and another one!</p>", simple_format("A paragraph\n\nand another one!")
+ assert_equal "<p>A paragraph\n<br /> With a newline</p>", simple_format("A paragraph\n With a newline")
+
+ text = "A\nB\nC\nD".freeze
+ assert_equal "<p>A\n<br />B\n<br />C\n<br />D</p>", simple_format(text)
+
+ text = "A\r\n \nB\n\n\r\n\t\nC\nD".freeze
+ assert_equal "<p>A\n<br /> \n<br />B</p>\n\n<p>\t\n<br />C\n<br />D</p>", simple_format(text)
+
+ assert_equal %q(<p class="test">This is a classy test</p>), simple_format("This is a classy test", :class => 'test')
+ assert_equal %Q(<p class="test">para 1</p>\n\n<p class="test">para 2</p>), simple_format("para 1\n\npara 2", :class => 'test')
+ end
+
+ def test_simple_format_should_sanitize_input_when_sanitize_option_is_not_false
+ assert_equal "<p><b> test with unsafe string </b></p>", simple_format("<b> test with unsafe string </b><script>code!</script>")
+ end
+
+ def test_simple_format_should_not_sanitize_input_when_sanitize_option_is_false
+ assert_equal "<p><b> test with unsafe string </b><script>code!</script></p>", simple_format("<b> test with unsafe string </b><script>code!</script>", {}, :sanitize => false)
+ end
+
+ def test_simple_format_with_custom_wrapper
+ assert_equal "<div></div>", simple_format(nil, {}, :wrapper_tag => "div")
+ end
+
+ def test_simple_format_with_custom_wrapper_and_multi_line_breaks
+ assert_equal "<div>We want to put a wrapper...</div>\n\n<div>...right there.</div>", simple_format("We want to put a wrapper...\n\n...right there.", {}, :wrapper_tag => "div")
+ end
+
+ def test_simple_format_should_not_change_the_text_passed
+ text = "<b>Ok</b><script>code!</script>"
+ text_clone = text.dup
+ simple_format(text)
+ assert_equal text_clone, text
+ end
+
+ def test_simple_format_does_not_modify_the_html_options_hash
+ options = { :class => "foobar"}
+ passed_options = options.dup
+ simple_format("some text", passed_options)
+ assert_equal options, passed_options
+ end
+
+ def test_simple_format_does_not_modify_the_options_hash
+ options = { :wrapper_tag => :div, :sanitize => false }
+ passed_options = options.dup
+ simple_format("some text", {}, passed_options)
+ assert_equal options, passed_options
+ end
+
+ def test_truncate
+ assert_equal "Hello World!", truncate("Hello World!", :length => 12)
+ assert_equal "Hello Wor...", truncate("Hello World!!", :length => 12)
+ end
+
+ def test_truncate_should_use_default_length_of_30
+ str = "This is a string that will go longer then the default truncate length of 30"
+ assert_equal str[0...27] + "...", truncate(str)
+ end
+
+ def test_truncate_with_options_hash
+ assert_equal "This is a string that wil[...]", truncate("This is a string that will go longer then the default truncate length of 30", :omission => "[...]")
+ assert_equal "Hello W...", truncate("Hello World!", :length => 10)
+ assert_equal "Hello[...]", truncate("Hello World!", :omission => "[...]", :length => 10)
+ assert_equal "Hello[...]", truncate("Hello Big World!", :omission => "[...]", :length => 13, :separator => ' ')
+ assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 14, :separator => ' ')
+ assert_equal "Hello Big[...]", truncate("Hello Big World!", :omission => "[...]", :length => 15, :separator => ' ')
+ 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),
+ truncate("\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 \354\225\204\353\235\274\353\246\254\354\230\244".force_encoding(Encoding::UTF_8), :length => 10)
+ end
+
+ def test_truncate_does_not_modify_the_options_hash
+ options = { :length => 10 }
+ passed_options = options.dup
+ truncate("some text", passed_options)
+ assert_equal options, passed_options
+ end
+
+ def test_truncate_with_link_options
+ assert_equal "Here is a long test and ...<a href=\"#\">Continue</a>",
+ truncate("Here is a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
+ end
+
+ def test_truncate_should_be_html_safe
+ assert truncate("Hello World!", :length => 12).html_safe?
+ end
+
+ def test_truncate_should_escape_the_input
+ assert_equal "Hello &lt;sc...", truncate("Hello <script>code!</script>World!!", :length => 12)
+ end
+
+ def test_truncate_should_not_escape_the_input_with_escape_false
+ assert_equal "Hello <sc...", truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
+ end
+
+ def test_truncate_with_escape_false_should_be_html_safe
+ truncated = truncate("Hello <script>code!</script>World!!", :length => 12, :escape => false)
+ assert truncated.html_safe?
+ end
+
+ def test_truncate_with_block_should_be_html_safe
+ truncated = truncate("Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
+ assert truncated.html_safe?
+ end
+
+ def test_truncate_with_block_should_escape_the_input
+ assert_equal "&lt;script&gt;code!&lt;/script&gt;He...<a href=\"#\">Continue</a>",
+ truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27) { link_to 'Continue', '#' }
+ end
+
+ def test_truncate_with_block_should_not_escape_the_input_with_escape_false
+ assert_equal "<script>code!</script>He...<a href=\"#\">Continue</a>",
+ truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
+ end
+
+ def test_truncate_with_block_with_escape_false_should_be_html_safe
+ truncated = truncate("<script>code!</script>Here's a long test and I need a continue to read link", :length => 27, :escape => false) { link_to 'Continue', '#' }
+ assert truncated.html_safe?
+ end
+
+ def test_truncate_with_block_should_escape_the_block
+ assert_equal "Here is a long test and ...&lt;script&gt;alert(&#39;foo&#39;);&lt;/script&gt;",
+ truncate("Here is a long test and I need a continue to read link", :length => 27) { "<script>alert('foo');</script>" }
+ end
+
+ def test_highlight_should_be_html_safe
+ assert highlight("This is a beautiful morning", "beautiful").html_safe?
+ end
+
+ def test_highlight
+ assert_equal(
+ "This is a <mark>beautiful</mark> morning",
+ highlight("This is a beautiful morning", "beautiful")
+ )
+
+ assert_equal(
+ "This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful")
+ )
+
+ assert_equal(
+ "This is a <b>beautiful</b> morning, but also a <b>beautiful</b> day",
+ highlight("This is a beautiful morning, but also a beautiful day", "beautiful", :highlighter => '<b>\1</b>')
+ )
+
+ assert_equal(
+ "This text is not changed because we supplied an empty phrase",
+ highlight("This text is not changed because we supplied an empty phrase", nil)
+ )
+
+ assert_equal ' ', highlight(' ', 'blank text is returned verbatim')
+ end
+
+ def test_highlight_should_sanitize_input
+ assert_equal(
+ "This is a <mark>beautiful</mark> morning",
+ highlight("This is a beautiful morning<script>code!</script>", "beautiful")
+ )
+ end
+
+ def test_highlight_should_not_sanitize_if_sanitize_option_if_false
+ assert_equal(
+ "This is a <mark>beautiful</mark> morning<script>code!</script>",
+ highlight("This is a beautiful morning<script>code!</script>", "beautiful", :sanitize => false)
+ )
+ end
+
+ def test_highlight_with_regexp
+ assert_equal(
+ "This is a <mark>beautiful!</mark> morning",
+ highlight("This is a beautiful! morning", "beautiful!")
+ )
+
+ assert_equal(
+ "This is a <mark>beautiful! morning</mark>",
+ highlight("This is a beautiful! morning", "beautiful! morning")
+ )
+
+ assert_equal(
+ "This is a <mark>beautiful? morning</mark>",
+ highlight("This is a beautiful? morning", "beautiful? morning")
+ )
+ 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
+
+ def test_highlight_with_html
+ assert_equal(
+ "<p>This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>",
+ highlight("<p>This is a beautiful morning, but also a beautiful day</p>", "beautiful")
+ )
+ assert_equal(
+ "<p>This is a <em><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> day</p>",
+ highlight("<p>This is a <em>beautiful</em> morning, but also a beautiful day</p>", "beautiful")
+ )
+ assert_equal(
+ "<p>This is a <em class=\"error\"><mark>beautiful</mark></em> morning, but also a <mark>beautiful</mark> <span class=\"last\">day</span></p>",
+ highlight("<p>This is a <em class=\"error\">beautiful</em> morning, but also a beautiful <span class=\"last\">day</span></p>", "beautiful")
+ )
+ assert_equal(
+ "<p class=\"beautiful\">This is a <mark>beautiful</mark> morning, but also a <mark>beautiful</mark> day</p>",
+ highlight("<p class=\"beautiful\">This is a beautiful morning, but also a beautiful day</p>", "beautiful")
+ )
+ assert_equal(
+ "<p>This is a <mark>beautiful</mark> <a href=\"http://example.com/beautiful#top?what=beautiful%20morning&amp;when=now+then\">morning</a>, but also a <mark>beautiful</mark> day</p>",
+ highlight("<p>This is a beautiful <a href=\"http://example.com/beautiful\#top?what=beautiful%20morning&when=now+then\">morning</a>, but also a beautiful day</p>", "beautiful")
+ )
+ assert_equal(
+ "<div>abc <b>div</b></div>",
+ highlight("<div>abc div</div>", "div", :highlighter => '<b>\1</b>')
+ )
+ end
+
+ def test_highlight_does_not_modify_the_options_hash
+ options = { :highlighter => '<b>\1</b>', :sanitize => false }
+ passed_options = options.dup
+ highlight("<div>abc div</div>", "div", passed_options)
+ assert_equal options, passed_options
+ 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))
+ assert_equal("...iful morning", excerpt("This is a beautiful morning", "morning", :radius => 5))
+ assert_nil excerpt("This is a beautiful morning", "day")
+ end
+
+ def test_excerpt_should_not_be_html_safe
+ assert !excerpt('This is a beautiful! morning', 'beautiful', :radius => 5).html_safe?
+ end
+
+ def test_excerpt_in_borderline_cases
+ assert_equal("", excerpt("", "", :radius => 0))
+ assert_equal("a", excerpt("a", "a", :radius => 0))
+ assert_equal("...b...", excerpt("abc", "b", :radius => 0))
+ assert_equal("abc", excerpt("abc", "b", :radius => 1))
+ assert_equal("abc...", excerpt("abcd", "b", :radius => 1))
+ assert_equal("...abc", excerpt("zabc", "b", :radius => 1))
+ assert_equal("...abc...", excerpt("zabcd", "b", :radius => 1))
+ assert_equal("zabcd", excerpt("zabcd", "b", :radius => 2))
+
+ # excerpt strips the resulting string before ap-/prepending excerpt_string.
+ # whether this behavior is meaningful when excerpt_string is not to be
+ # appended is questionable.
+ assert_equal("zabcd", excerpt(" zabcd ", "b", :radius => 4))
+ 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(
+ "This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome tempera[...]",
+ excerpt("This is the ultimate supercalifragilisticexpialidoceous very looooooooooooooooooong looooooooooooong beautiful morning with amazing sunshine and awesome temperatures. So what are you gonna do about it?", "very",
+ :omission => "[...]")
+ )
+ end
+
+ def test_excerpt_with_utf8
+ assert_equal("...\357\254\203ciency could not be...".force_encoding(Encoding::UTF_8), excerpt("That's why e\357\254\203ciency could not be helped".force_encoding(Encoding::UTF_8), 'could', :radius => 8))
+ end
+
+ def test_excerpt_does_not_modify_the_options_hash
+ options = { :omission => "[...]",:radius => 5 }
+ passed_options = options.dup
+ excerpt("This is a beautiful morning", "beautiful", passed_options)
+ assert_equal options, passed_options
+ end
+
+ def test_excerpt_with_separator
+ options = { :separator => ' ', :radius => 1 }
+ assert_equal('...a very beautiful...', excerpt('This is a very beautiful morning', 'very', options))
+ assert_equal('This is...', excerpt('This is a very beautiful morning', 'this', options))
+ assert_equal('...beautiful morning', excerpt('This is a very beautiful morning', 'morning', options))
+
+ options = { :separator => "\n", :radius => 0 }
+ assert_equal("...very long...", excerpt("my very\nvery\nvery long\nstring", 'long', options))
+
+ options = { :separator => "\n", :radius => 1 }
+ assert_equal("...very\nvery long\nstring", excerpt("my very\nvery\nvery long\nstring", 'long', options))
+
+ assert_equal excerpt('This is a beautiful morning', 'a'),
+ excerpt('This is a beautiful morning', 'a', separator: nil)
+ end
+
+ def test_word_wrap
+ assert_equal("my very very\nvery long\nstring", word_wrap("my very very very long string", :line_width => 15))
+ end
+
+ def test_word_wrap_with_extra_newlines
+ assert_equal("my very very\nvery long\nstring\n\nwith another\nline", word_wrap("my very very very long string\n\nwith another line", :line_width => 15))
+ end
+
+ def test_word_wrap_does_not_modify_the_options_hash
+ options = { :line_width => 15 }
+ passed_options = options.dup
+ word_wrap("some text", passed_options)
+ assert_equal options, passed_options
+ end
+
+ def test_pluralization
+ assert_equal("1 count", pluralize(1, "count"))
+ assert_equal("2 counts", pluralize(2, "count"))
+ assert_equal("1 count", pluralize('1', "count"))
+ assert_equal("2 counts", pluralize('2', "count"))
+ assert_equal("1,066 counts", pluralize('1,066', "count"))
+ assert_equal("1.25 counts", pluralize('1.25', "count"))
+ assert_equal("1.0 count", pluralize('1.0', "count"))
+ assert_equal("1.00 count", pluralize('1.00', "count"))
+ assert_equal("2 counters", pluralize(2, "count", "counters"))
+ assert_equal("0 counters", pluralize(nil, "count", "counters"))
+ assert_equal("2 people", pluralize(2, "person"))
+ assert_equal("10 buffaloes", pluralize(10, "buffalo"))
+ assert_equal("1 berry", pluralize(1, "berry"))
+ assert_equal("12 berries", pluralize(12, "berry"))
+ end
+
+ def test_cycle_class
+ value = Cycle.new("one", 2, "3")
+ assert_equal("one", value.to_s)
+ assert_equal("2", value.to_s)
+ assert_equal("3", value.to_s)
+ assert_equal("one", value.to_s)
+ value.reset
+ assert_equal("one", value.to_s)
+ assert_equal("2", value.to_s)
+ assert_equal("3", value.to_s)
+ end
+
+ def test_cycle_class_with_no_arguments
+ assert_raise(ArgumentError) { Cycle.new }
+ end
+
+ def test_cycle
+ assert_equal("one", cycle("one", 2, "3"))
+ assert_equal("2", cycle("one", 2, "3"))
+ assert_equal("3", cycle("one", 2, "3"))
+ assert_equal("one", cycle("one", 2, "3"))
+ assert_equal("2", cycle("one", 2, "3"))
+ assert_equal("3", cycle("one", 2, "3"))
+ end
+
+ def test_cycle_with_no_arguments
+ assert_raise(ArgumentError) { cycle }
+ end
+
+ def test_cycle_resets_with_new_values
+ assert_equal("even", cycle("even", "odd"))
+ assert_equal("odd", cycle("even", "odd"))
+ assert_equal("even", cycle("even", "odd"))
+ assert_equal("1", cycle(1, 2, 3))
+ assert_equal("2", cycle(1, 2, 3))
+ assert_equal("3", cycle(1, 2, 3))
+ assert_equal("1", cycle(1, 2, 3))
+ end
+
+ def test_named_cycles
+ assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("red", cycle("red", "blue", :name => "colors"))
+ assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("blue", cycle("red", "blue", :name => "colors"))
+ assert_equal("3", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("red", cycle("red", "blue", :name => "colors"))
+ end
+
+ def test_current_cycle_with_default_name
+ cycle("even","odd")
+ assert_equal "even", current_cycle
+ cycle("even","odd")
+ assert_equal "odd", current_cycle
+ cycle("even","odd")
+ assert_equal "even", current_cycle
+ end
+
+ def test_current_cycle_with_named_cycles
+ cycle("red", "blue", :name => "colors")
+ assert_equal "red", current_cycle("colors")
+ cycle("red", "blue", :name => "colors")
+ assert_equal "blue", current_cycle("colors")
+ cycle("red", "blue", :name => "colors")
+ assert_equal "red", current_cycle("colors")
+ end
+
+ def test_current_cycle_safe_call
+ assert_nothing_raised { current_cycle }
+ assert_nothing_raised { current_cycle("colors") }
+ end
+
+ def test_current_cycle_with_more_than_two_names
+ cycle(1,2,3)
+ assert_equal "1", current_cycle
+ cycle(1,2,3)
+ assert_equal "2", current_cycle
+ cycle(1,2,3)
+ assert_equal "3", current_cycle
+ cycle(1,2,3)
+ assert_equal "1", current_cycle
+ end
+
+ def test_default_named_cycle
+ assert_equal("1", cycle(1, 2, 3))
+ assert_equal("2", cycle(1, 2, 3, :name => "default"))
+ assert_equal("3", cycle(1, 2, 3))
+ end
+
+ def test_reset_cycle
+ assert_equal("1", cycle(1, 2, 3))
+ assert_equal("2", cycle(1, 2, 3))
+ reset_cycle
+ assert_equal("1", cycle(1, 2, 3))
+ end
+
+ def test_reset_unknown_cycle
+ reset_cycle("colors")
+ end
+
+ def test_recet_named_cycle
+ assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("red", cycle("red", "blue", :name => "colors"))
+ reset_cycle("numbers")
+ assert_equal("1", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("blue", cycle("red", "blue", :name => "colors"))
+ assert_equal("2", cycle(1, 2, 3, :name => "numbers"))
+ assert_equal("red", cycle("red", "blue", :name => "colors"))
+ end
+
+ def test_cycle_no_instance_variable_clashes
+ @cycles = %w{Specialized Fuji Giant}
+ assert_equal("red", cycle("red", "blue"))
+ assert_equal("blue", cycle("red", "blue"))
+ assert_equal("red", cycle("red", "blue"))
+ assert_equal(%w{Specialized Fuji Giant}, @cycles)
+ end
+end
diff --git a/actionpack/test/template/translation_helper_test.rb b/actionview/test/template/translation_helper_test.rb
index d496dbb35e..d496dbb35e 100644
--- a/actionpack/test/template/translation_helper_test.rb
+++ b/actionview/test/template/translation_helper_test.rb
diff --git a/actionview/test/template/url_helper_test.rb b/actionview/test/template/url_helper_test.rb
new file mode 100644
index 0000000000..deba33510a
--- /dev/null
+++ b/actionview/test/template/url_helper_test.rb
@@ -0,0 +1,791 @@
+# encoding: utf-8
+require 'abstract_unit'
+require 'minitest/mock'
+
+class UrlHelperTest < ActiveSupport::TestCase
+
+ # In a few cases, the helper proxies to 'controller'
+ # or request.
+ #
+ # In those cases, we'll set up a simple mock
+ attr_accessor :controller, :request
+
+ cattr_accessor :request_forgery
+ self.request_forgery = false
+
+ routes = ActionDispatch::Routing::RouteSet.new
+ routes.draw do
+ get "/" => "foo#bar"
+ get "/other" => "foo#other"
+ get "/article/:id" => "foo#article", :as => :article
+ get "/category/:category" => "foo#category"
+ end
+
+ include ActionView::Helpers::UrlHelper
+ include routes.url_helpers
+
+ include ActionView::Helpers::JavaScriptHelper
+ include ActionDispatch::Assertions::DomAssertions
+ include ActionView::Context
+ include RenderERBUtils
+
+ setup :_prepare_context
+
+ def hash_for(options = {})
+ { controller: "foo", action: "bar" }.merge!(options)
+ end
+ alias url_hash hash_for
+
+ def test_url_for_does_not_escape_urls
+ assert_equal "/?a=b&c=d", url_for(hash_for(a: :b, c: :d))
+ end
+
+ def test_url_for_with_back
+ referer = 'http://www.example.com/referer'
+ @controller = Struct.new(:request).new(Struct.new(:env).new("HTTP_REFERER" => referer))
+
+ assert_equal 'http://www.example.com/referer', url_for(:back)
+ end
+
+ def test_url_for_with_back_and_no_referer
+ @controller = Struct.new(:request).new(Struct.new(:env).new({}))
+ assert_equal 'javascript:history.back()', url_for(:back)
+ 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")
+ end
+
+ def test_button_to_with_straight_url_and_request_forgery
+ self.request_forgery = true
+
+ assert_dom_equal(
+ %{<form method="post" action="http://www.example.com" class="button_to"><div><input type="submit" value="Hello" /><input name="form_token" type="hidden" value="secret" /></div></form>},
+ button_to("Hello", "http://www.example.com")
+ )
+ ensure
+ self.request_forgery = false
+ 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')
+ 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>')
+ 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")
+ 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)
+ 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")
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ 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>},
+ button_to("Hello", "http://www.example.com", params: {foo: :bar, baz: "quux"})
+ )
+ end
+
+ def test_link_tag_with_straight_url
+ assert_dom_equal %{<a href="http://www.example.com">Hello</a>}, link_to("Hello", "http://www.example.com")
+ end
+
+ def test_link_tag_without_host_option
+ assert_dom_equal(%{<a href="/">Test Link</a>}, link_to('Test Link', url_hash))
+ end
+
+ def test_link_tag_with_host_option
+ hash = hash_for(host: "www.example.com")
+ expected = %{<a href="http://www.example.com/">Test Link</a>}
+ assert_dom_equal(expected, link_to('Test Link', hash))
+ end
+
+ def test_link_tag_with_query
+ expected = %{<a href="http://www.example.com?q1=v1&amp;q2=v2">Hello</a>}
+ assert_dom_equal expected, link_to("Hello", "http://www.example.com?q1=v1&q2=v2")
+ end
+
+ def test_link_tag_with_query_and_no_name
+ expected = %{<a href="http://www.example.com?q1=v1&amp;q2=v2">http://www.example.com?q1=v1&amp;q2=v2</a>}
+ assert_dom_equal expected, link_to(nil, "http://www.example.com?q1=v1&q2=v2")
+ end
+
+ def test_link_tag_with_back
+ env = {"HTTP_REFERER" => "http://www.example.com/referer"}
+ @controller = Struct.new(:request).new(Struct.new(:env).new(env))
+ expected = %{<a href="#{env["HTTP_REFERER"]}">go back</a>}
+ assert_dom_equal expected, link_to('go back', :back)
+ end
+
+ def test_link_tag_with_back_and_no_referer
+ @controller = Struct.new(:request).new(Struct.new(:env).new({}))
+ link = link_to('go back', :back)
+ assert_dom_equal %{<a href="javascript:history.back()">go back</a>}, link
+ end
+
+ def test_link_tag_with_img
+ link = link_to("<img src='/favicon.jpg' />".html_safe, "/")
+ expected = %{<a href="/"><img src='/favicon.jpg' /></a>}
+ assert_dom_equal expected, link
+ end
+
+ def test_link_with_nil_html_options
+ link = link_to("Hello", url_hash, nil)
+ assert_dom_equal %{<a href="/">Hello</a>}, link
+ end
+
+ def test_link_tag_with_custom_onclick
+ link = link_to("Hello", "http://www.example.com", onclick: "alert('yay!')")
+ expected = %{<a href="http://www.example.com" onclick="alert(&#39;yay!&#39;)">Hello</a>}
+ assert_dom_equal expected, link
+ end
+
+ def test_link_tag_with_javascript_confirm
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-confirm="Are you sure?">Hello</a>},
+ link_to("Hello", "http://www.example.com", data: { confirm: "Are you sure?" })
+ )
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-confirm="You cant possibly be sure, can you?">Hello</a>},
+ link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure, can you?" })
+ )
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-confirm="You cant possibly be sure,\n can you?">Hello</a>},
+ link_to("Hello", "http://www.example.com", data: { confirm: "You cant possibly be sure,\n can you?" })
+ )
+ end
+
+ def test_link_to_with_remote
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-remote="true">Hello</a>},
+ link_to("Hello", "http://www.example.com", remote: true)
+ )
+ end
+
+ def test_link_to_with_remote_false
+ assert_dom_equal(
+ %{<a href="http://www.example.com">Hello</a>},
+ link_to("Hello", "http://www.example.com", remote: false)
+ )
+ end
+
+ def test_link_to_with_symbolic_remote_in_non_html_options
+ assert_dom_equal(
+ %{<a href="/" data-remote="true">Hello</a>},
+ link_to("Hello", hash_for(remote: true), {})
+ )
+ end
+
+ def test_link_to_with_string_remote_in_non_html_options
+ assert_dom_equal(
+ %{<a href="/" data-remote="true">Hello</a>},
+ link_to("Hello", hash_for('remote' => true), {})
+ )
+ end
+
+ def test_link_tag_using_post_javascript
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-method="post" rel="nofollow">Hello</a>},
+ link_to("Hello", "http://www.example.com", method: :post)
+ )
+ end
+
+ def test_link_tag_using_delete_javascript
+ assert_dom_equal(
+ %{<a href="http://www.example.com" rel="nofollow" data-method="delete">Destroy</a>},
+ link_to("Destroy", "http://www.example.com", method: :delete)
+ )
+ end
+
+ def test_link_tag_using_delete_javascript_and_href
+ assert_dom_equal(
+ %{<a href="\#" rel="nofollow" data-method="delete">Destroy</a>},
+ link_to("Destroy", "http://www.example.com", method: :delete, href: '#')
+ )
+ end
+
+ def test_link_tag_using_post_javascript_and_rel
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-method="post" rel="example nofollow">Hello</a>},
+ link_to("Hello", "http://www.example.com", method: :post, rel: 'example')
+ )
+ end
+
+ def test_link_tag_using_post_javascript_and_confirm
+ assert_dom_equal(
+ %{<a href="http://www.example.com" data-method="post" rel="nofollow" data-confirm="Are you serious?">Hello</a>},
+ link_to("Hello", "http://www.example.com", method: :post, data: { confirm: "Are you serious?" })
+ )
+ end
+
+ def test_link_tag_using_delete_javascript_and_href_and_confirm
+ assert_dom_equal(
+ %{<a href="\#" rel="nofollow" data-confirm="Are you serious?" data-method="delete">Destroy</a>},
+ link_to("Destroy", "http://www.example.com", method: :delete, href: '#', data: { confirm: "Are you serious?" })
+ )
+ end
+
+ def test_link_tag_with_block
+ assert_dom_equal %{<a href="/"><span>Example site</span></a>},
+ link_to('/') { content_tag(:span, 'Example site') }
+ end
+
+ def test_link_tag_with_block_and_html_options
+ assert_dom_equal %{<a class="special" href="/"><span>Example site</span></a>},
+ link_to('/', class: "special") { content_tag(:span, 'Example site') }
+ end
+
+ def test_link_tag_using_block_and_hash
+ assert_dom_equal(
+ %{<a href="/"><span>Example site</span></a>},
+ link_to(url_hash) { content_tag(:span, 'Example site') }
+ )
+ end
+
+ def test_link_tag_using_block_in_erb
+ out = render_erb %{<%= link_to('/') do %>Example site<% end %>}
+ assert_equal '<a href="/">Example site</a>', out
+ end
+
+ def test_link_tag_with_html_safe_string
+ assert_dom_equal(
+ %{<a href="/article/Gerd_M%C3%BCller">Gerd Müller</a>},
+ link_to("Gerd Müller", article_path("Gerd_Müller".html_safe))
+ )
+ end
+
+ def test_link_tag_escapes_content
+ assert_dom_equal %{<a href="/">Malicious &lt;script&gt;content&lt;/script&gt;</a>},
+ link_to("Malicious <script>content</script>", "/")
+ end
+
+ def test_link_tag_does_not_escape_html_safe_content
+ assert_dom_equal %{<a href="/">Malicious <script>content</script></a>},
+ link_to("Malicious <script>content</script>".html_safe, "/")
+ end
+
+ def test_link_to_unless
+ assert_equal "Showing", link_to_unless(true, "Showing", url_hash)
+
+ assert_dom_equal %{<a href="/">Listing</a>},
+ link_to_unless(false, "Listing", url_hash)
+
+ assert_equal "<strong>Showing</strong>",
+ link_to_unless(true, "Showing", url_hash) { |name|
+ "<strong>#{name}</strong>".html_safe
+ }
+
+ assert_equal "test",
+ link_to_unless(true, "Showing", url_hash) {
+ "test"
+ }
+
+ assert_equal %{&lt;b&gt;Showing&lt;/b&gt;}, link_to_unless(true, "<b>Showing</b>", url_hash)
+ assert_equal %{<a href="/">&lt;b&gt;Showing&lt;/b&gt;</a>}, link_to_unless(false, "<b>Showing</b>", url_hash)
+ assert_equal %{<b>Showing</b>}, link_to_unless(true, "<b>Showing</b>".html_safe, url_hash)
+ assert_equal %{<a href="/"><b>Showing</b></a>}, link_to_unless(false, "<b>Showing</b>".html_safe, url_hash)
+ end
+
+ def test_link_to_if
+ assert_equal "Showing", link_to_if(false, "Showing", url_hash)
+ assert_dom_equal %{<a href="/">Listing</a>}, link_to_if(true, "Listing", url_hash)
+ end
+
+ def request_for_url(url, opts = {})
+ env = Rack::MockRequest.env_for("http://www.example.com#{url}", opts)
+ ActionDispatch::Request.new(env)
+ end
+
+ def test_current_page_with_http_head_method
+ @request = request_for_url("/", :method => :head)
+ assert current_page?(url_hash)
+ assert current_page?("http://www.example.com/")
+ end
+
+ def test_current_page_with_simple_url
+ @request = request_for_url("/")
+ assert current_page?(url_hash)
+ assert current_page?("http://www.example.com/")
+ end
+
+ def test_current_page_ignoring_params
+ @request = request_for_url("/?order=desc&page=1")
+
+ assert current_page?(url_hash)
+ assert current_page?("http://www.example.com/")
+ end
+
+ def test_current_page_with_params_that_match
+ @request = request_for_url("/?order=desc&page=1")
+
+ assert current_page?(hash_for(order: "desc", page: "1"))
+ assert current_page?("http://www.example.com/?order=desc&page=1")
+ end
+
+ def test_current_page_with_not_get_verb
+ @request = request_for_url("/events", method: :post)
+
+ assert !current_page?('/events')
+ end
+
+ def test_current_page_with_escaped_params
+ @request = request_for_url("/category/administra%c3%a7%c3%a3o")
+
+ assert current_page?(controller: 'foo', action: 'category', category: 'administração')
+ end
+
+ def test_current_page_with_escaped_params_with_different_encoding
+ @request = request_for_url("/")
+ @request.stub(:path, "/category/administra%c3%a7%c3%a3o".force_encoding(Encoding::ASCII_8BIT)) do
+ assert current_page?(:controller => 'foo', :action => 'category', category: 'administração')
+ assert current_page?("http://www.example.com/category/administra%c3%a7%c3%a3o")
+ end
+ end
+
+ def test_current_page_with_double_escaped_params
+ @request = request_for_url("/category/administra%c3%a7%c3%a3o?callback_url=http%3a%2f%2fexample.com%2ffoo")
+
+ assert current_page?(controller: 'foo', action: 'category', category: 'administração', callback_url: 'http://example.com/foo')
+ end
+
+ def test_link_unless_current
+ @request = request_for_url("/")
+
+ assert_equal "Showing",
+ link_to_unless_current("Showing", url_hash)
+ assert_equal "Showing",
+ link_to_unless_current("Showing", "http://www.example.com/")
+
+ @request = request_for_url("/?order=desc")
+
+ assert_equal "Showing",
+ link_to_unless_current("Showing", url_hash)
+ assert_equal "Showing",
+ link_to_unless_current("Showing", "http://www.example.com/")
+
+ @request = request_for_url("/?order=desc&page=1")
+
+ assert_equal "Showing",
+ link_to_unless_current("Showing", hash_for(order: 'desc', page: '1'))
+ assert_equal "Showing",
+ link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=1")
+
+ @request = request_for_url("/?order=desc")
+
+ assert_equal %{<a href="/?order=asc">Showing</a>},
+ link_to_unless_current("Showing", hash_for(order: :asc))
+ assert_equal %{<a href="http://www.example.com/?order=asc">Showing</a>},
+ link_to_unless_current("Showing", "http://www.example.com/?order=asc")
+
+ @request = request_for_url("/?order=desc")
+ assert_equal %{<a href="/?order=desc&amp;page=2\">Showing</a>},
+ link_to_unless_current("Showing", hash_for(order: "desc", page: 2))
+ assert_equal %{<a href="http://www.example.com/?order=desc&amp;page=2">Showing</a>},
+ link_to_unless_current("Showing", "http://www.example.com/?order=desc&page=2")
+
+ @request = request_for_url("/show")
+
+ assert_equal %{<a href="/">Listing</a>},
+ link_to_unless_current("Listing", url_hash)
+ assert_equal %{<a href="http://www.example.com/">Listing</a>},
+ link_to_unless_current("Listing", "http://www.example.com/")
+ end
+
+ def test_mail_to
+ assert_dom_equal %{<a href="mailto:david@loudthinking.com">david@loudthinking.com</a>}, mail_to("david@loudthinking.com")
+ assert_dom_equal %{<a href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>}, mail_to("david@loudthinking.com", "David Heinemeier Hansson")
+ assert_dom_equal(
+ %{<a class="admin" href="mailto:david@loudthinking.com">David Heinemeier Hansson</a>},
+ mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin")
+ )
+ assert_equal mail_to("david@loudthinking.com", "David Heinemeier Hansson", "class" => "admin"),
+ mail_to("david@loudthinking.com", "David Heinemeier Hansson", class: "admin")
+ end
+
+ def test_mail_with_options
+ assert_dom_equal(
+ %{<a href="mailto:me@example.com?cc=ccaddress%40example.com&amp;bcc=bccaddress%40example.com&amp;body=This%20is%20the%20body%20of%20the%20message.&amp;subject=This%20is%20an%20example%20email">My email</a>},
+ mail_to("me@example.com", "My email", cc: "ccaddress@example.com", bcc: "bccaddress@example.com", subject: "This is an example email", body: "This is the body of the message.")
+ )
+ end
+
+ def test_mail_to_with_img
+ assert_dom_equal %{<a href="mailto:feedback@example.com"><img src="/feedback.png" /></a>},
+ mail_to('feedback@example.com', '<img src="/feedback.png" />'.html_safe)
+ end
+
+ def test_mail_to_returns_html_safe_string
+ assert mail_to("david@loudthinking.com").html_safe?
+ end
+
+ def test_mail_to_with_block
+ assert_dom_equal %{<a href="mailto:me@example.com"><span>Email me</span></a>},
+ mail_to('me@example.com') { content_tag(:span, 'Email me') }
+ end
+
+ def test_mail_to_with_block_and_options
+ assert_dom_equal %{<a class="special" href="mailto:me@example.com?cc=ccaddress%40example.com"><span>Email me</span></a>},
+ mail_to('me@example.com', cc: "ccaddress@example.com", class: "special") { content_tag(:span, 'Email me') }
+ end
+
+ def test_mail_to_does_not_modify_html_options_hash
+ options = { class: 'special' }
+ mail_to 'me@example.com', 'ME!', options
+ assert_equal({ class: 'special' }, options)
+ end
+
+ def protect_against_forgery?
+ self.request_forgery
+ end
+
+ def form_authenticity_token
+ "secret"
+ end
+
+ def request_forgery_protection_token
+ "form_token"
+ end
+
+ private
+ def sort_query_string_params(uri)
+ path, qs = uri.split('?')
+ qs = qs.split('&amp;').sort.join('&amp;') if qs
+ qs ? "#{path}?#{qs}" : path
+ end
+end
+
+class UrlHelperControllerTest < ActionController::TestCase
+ class UrlHelperController < ActionController::Base
+ test_routes do
+ get 'url_helper_controller_test/url_helper/show/:id',
+ to: 'url_helper_controller_test/url_helper#show',
+ as: :show
+
+ get 'url_helper_controller_test/url_helper/profile/:name',
+ to: 'url_helper_controller_test/url_helper#show',
+ as: :profile
+
+ get 'url_helper_controller_test/url_helper/show_named_route',
+ to: 'url_helper_controller_test/url_helper#show_named_route',
+ as: :show_named_route
+
+ get "/:controller(/:action(/:id))"
+
+ get 'url_helper_controller_test/url_helper/normalize_recall_params',
+ to: UrlHelperController.action(:normalize_recall),
+ as: :normalize_recall_params
+
+ get '/url_helper_controller_test/url_helper/override_url_helper/default',
+ to: 'url_helper_controller_test/url_helper#override_url_helper',
+ as: :override_url_helper
+ end
+
+ def show
+ if params[:name]
+ render inline: 'ok'
+ else
+ redirect_to profile_path(params[:id])
+ end
+ end
+
+ def show_url_for
+ render inline: "<%= url_for controller: 'url_helper_controller_test/url_helper', action: 'show_url_for' %>"
+ end
+
+ def show_overridden_url_for
+ render inline: "<%= url_for params.merge(controller: 'url_helper_controller_test/url_helper', action: 'show_url_for') %>"
+ end
+
+ def show_named_route
+ render inline: "<%= show_named_route_#{params[:kind]} %>"
+ end
+
+ def nil_url_for
+ render inline: '<%= url_for(nil) %>'
+ end
+
+ def normalize_recall_params
+ render inline: '<%= normalize_recall_params_path %>'
+ end
+
+ def recall_params_not_changed
+ render inline: '<%= url_for(action: :show_url_for) %>'
+ end
+
+ def override_url_helper
+ render inline: '<%= override_url_helper_path %>'
+ end
+
+ def override_url_helper_path
+ '/url_helper_controller_test/url_helper/override_url_helper/override'
+ end
+ helper_method :override_url_helper_path
+ end
+
+ tests UrlHelperController
+
+ def test_url_for_shows_only_path
+ get :show_url_for
+ assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
+ end
+
+ def test_overridden_url_for_shows_only_path
+ get :show_overridden_url_for
+ assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
+ end
+
+ def test_named_route_url_shows_host_and_path
+ get :show_named_route, kind: 'url'
+ assert_equal 'http://test.host/url_helper_controller_test/url_helper/show_named_route',
+ @response.body
+ end
+
+ def test_named_route_path_shows_only_path
+ get :show_named_route, kind: 'path'
+ assert_equal '/url_helper_controller_test/url_helper/show_named_route', @response.body
+ end
+
+ def test_url_for_nil_returns_current_path
+ get :nil_url_for
+ assert_equal '/url_helper_controller_test/url_helper/nil_url_for', @response.body
+ end
+
+ def test_named_route_should_show_host_and_path_using_controller_default_url_options
+ class << @controller
+ def default_url_options
+ { host: 'testtwo.host' }
+ end
+ end
+
+ get :show_named_route, kind: 'url'
+ assert_equal 'http://testtwo.host/url_helper_controller_test/url_helper/show_named_route', @response.body
+ end
+
+ def test_recall_params_should_be_normalized
+ get :normalize_recall_params
+ assert_equal '/url_helper_controller_test/url_helper/normalize_recall_params', @response.body
+ end
+
+ def test_recall_params_should_not_be_changed
+ get :recall_params_not_changed
+ assert_equal '/url_helper_controller_test/url_helper/show_url_for', @response.body
+ end
+
+ def test_recall_params_should_normalize_id
+ get :show, id: '123'
+ assert_equal 302, @response.status
+ assert_equal 'http://test.host/url_helper_controller_test/url_helper/profile/123', @response.location
+
+ get :show, name: '123'
+ assert_equal 'ok', @response.body
+ end
+
+ def test_url_helper_can_be_overridden
+ get :override_url_helper
+ assert_equal '/url_helper_controller_test/url_helper/override_url_helper/override', @response.body
+ end
+end
+
+class TasksController < ActionController::Base
+ test_routes do
+ resources :tasks
+ end
+
+ def index
+ render_default
+ end
+
+ def show
+ render_default
+ end
+
+ protected
+ def render_default
+ render inline: "<%= link_to_unless_current('tasks', tasks_path) %>\n" +
+ "<%= link_to_unless_current('tasks', tasks_url) %>"
+ end
+end
+
+class LinkToUnlessCurrentWithControllerTest < ActionController::TestCase
+ tests TasksController
+
+ def test_link_to_unless_current_to_current
+ get :index
+ assert_equal "tasks\ntasks", @response.body
+ end
+
+ def test_link_to_unless_current_shows_link
+ get :show, id: 1
+ assert_equal %{<a href="/tasks">tasks</a>\n} +
+ %{<a href="#{@request.protocol}#{@request.host_with_port}/tasks">tasks</a>},
+ @response.body
+ end
+end
+
+class Session
+ extend ActiveModel::Naming
+ include ActiveModel::Conversion
+ attr_accessor :id, :workshop_id
+
+ def initialize(id)
+ @id = id
+ end
+
+ def persisted?
+ id.present?
+ end
+
+ def to_s
+ id.to_s
+ end
+end
+
+class WorkshopsController < ActionController::Base
+ test_routes do
+ resources :workshops do
+ resources :sessions
+ end
+ end
+
+ def index
+ @workshop = Workshop.new(nil)
+ render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>"
+ end
+
+ def show
+ @workshop = Workshop.new(params[:id])
+ render inline: "<%= url_for(@workshop) %>\n<%= link_to('Workshop', @workshop) %>"
+ end
+end
+
+class SessionsController < ActionController::Base
+ test_routes do
+ resources :workshops do
+ resources :sessions
+ end
+ end
+
+ def index
+ @workshop = Workshop.new(params[:workshop_id])
+ @session = Session.new(nil)
+ render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
+ end
+
+ def show
+ @workshop = Workshop.new(params[:workshop_id])
+ @session = Session.new(params[:id])
+ render inline: "<%= url_for([@workshop, @session]) %>\n<%= link_to('Session', [@workshop, @session]) %>"
+ end
+end
+
+class PolymorphicControllerTest < ActionController::TestCase
+ def test_new_resource
+ @controller = WorkshopsController.new
+
+ get :index
+ assert_equal %{/workshops\n<a href="/workshops">Workshop</a>}, @response.body
+ end
+
+ def test_existing_resource
+ @controller = WorkshopsController.new
+
+ get :show, id: 1
+ assert_equal %{/workshops/1\n<a href="/workshops/1">Workshop</a>}, @response.body
+ end
+
+ def test_new_nested_resource
+ @controller = SessionsController.new
+
+ get :index, workshop_id: 1
+ assert_equal %{/workshops/1/sessions\n<a href="/workshops/1/sessions">Session</a>}, @response.body
+ end
+
+ def test_existing_nested_resource
+ @controller = SessionsController.new
+
+ get :show, workshop_id: 1, id: 1
+ assert_equal %{/workshops/1/sessions/1\n<a href="/workshops/1/sessions/1">Session</a>}, @response.body
+ end
+end
diff --git a/guides/code/getting_started/lib/tasks/.gitkeep b/actionview/test/tmp/.gitkeep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/lib/tasks/.gitkeep
+++ b/actionview/test/tmp/.gitkeep
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md
index 2c966943ee..e8602ecbcf 100644
--- a/activemodel/CHANGELOG.md
+++ b/activemodel/CHANGELOG.md
@@ -1,81 +1,39 @@
-## Rails 4.0.0 (unreleased) ##
+* Fix `has_secure_password` to honor bcrypt-ruby's cost attribute.
-* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to
- protect attributes from mass assignment when non-permitted attributes are passed.
+ *T.J. Schuck*
- *DHH + Guillermo Iguaran*
+* Updated the `ActiveModel::Dirty#changed_attributes` method to be indifferent between using
+ symbols and strings as keys.
-* `ActiveModel::MassAssignmentSecurity` has been extracted from Active Model and the
- `protected_attributes` gem should be added to Gemfile in order to use
- `attr_accessible` and `attr_protected` macros in your models.
+ *William Myers*
- *Guillermo Iguaran*
-
-* Due to a change in builder, nil values and empty strings now generates
- closed tags, so instead of this:
-
- <pseudonyms nil=\"true\"></pseudonyms>
-
- It generates this:
-
- <pseudonyms nil=\"true\"/>
-
- *Carlos Antonio da Silva*
-
-* Changed inclusion and exclusion validators to accept a symbol for `:in` option.
-
- This allows to use dynamic inclusion/exclusion values using methods, besides the current lambda/proc support.
-
- *Gabriel Sobrinho*
-
-* `AM::Validation#validates` ability to pass custom exception to `:strict` option.
+* Added new API methods `reset_changes` and `changed_applied` to `ActiveModel::Dirty`
+ that control changes state. Previsously you needed to update internal
+ instance variables, but now API methods are available.
*Bogdan Gusiev*
-* Changed `ActiveModel::Serializers::Xml::Serializer#add_associations` to by default
- propagate `:skip_types, :dasherize, :camelize` keys to included associations.
- It can be overriden on each association by explicitly specifying the option on one
- or more associations
-
- *Anthony Alberto*
-
-* Changed `AM::Serializers::JSON.include_root_in_json' default value to false.
- Now, AM Serializers and AR objects have the same default behaviour. Fixes #6578.
-
- class User < ActiveRecord::Base; end
-
- class Person
- include ActiveModel::Model
- include ActiveModel::AttributeMethods
- include ActiveModel::Serializers::JSON
-
- attr_accessor :name, :age
-
- def attributes
- instance_values
- end
- end
-
- user.as_json
- => {"id"=>1, "name"=>"Konata Izumi", "age"=>16, "awesome"=>true}
- # root is not included
+* Fix has_secure_password. `password_confirmation` validations are triggered
+ even if no `password_confirmation` is set.
- person.as_json
- => {"name"=>"Francesco", "age"=>22}
- # root is not included
+ *Vladimir Kiselev*
- *Francesco Rodriguez*
+* `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.
-* Passing false hash values to `validates` will no longer enable the corresponding validators *Steve Purcell*
+ Fixes range validations like `:a..:f` that used to pass with values like `:be`.
+ Fixes #10593
-* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute` *Brian Cardarella*
+ *Charles Bergeron*
-* Added ActiveModel::Model, a mixin to make Ruby objects work with AP out of box *Guillermo Iguaran*
+* Fix regression in has_secure_password. When a password is set, but a
+ confirmation is an empty string, it would incorrectly save.
-* `AM::Errors#to_json`: support `:full_messages` parameter *Bogdan Gusiev*
+ *Steve Klabnik* and *Phillip Calvin*
-* Trim down Active Model API by removing `valid?` and `errors.full_messages` *José Valim*
+* Deprecate `Validator#setup`. This should be done manually now in the validator's constructor.
-* When `^` or `$` are used in the regular expression provided to `validates_format_of` and the :multiline option is not set to true, an exception will be raised. This is to prevent security vulnerabilities when using `validates_format_of`. The problem is described in detail in the Rails security guide.
+ *Nick Sutterer*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activemodel/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for previous changes.
diff --git a/activemodel/MIT-LICENSE b/activemodel/MIT-LICENSE
index 810daf856c..5c668d9624 100644
--- a/activemodel/MIT-LICENSE
+++ b/activemodel/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activemodel/README.rdoc b/activemodel/README.rdoc
index fc34d69ff6..a399fe9051 100644
--- a/activemodel/README.rdoc
+++ b/activemodel/README.rdoc
@@ -1,8 +1,8 @@
= Active Model -- model interfaces for Rails
Active Model provides a known set of interfaces for usage in model classes.
-They allow for Action Pack helpers to interact with non-ActiveRecord models,
-for example. Active Model also helps building custom ORMs for use outside of
+They allow for Action Pack helpers to interact with non-Active Record models,
+for example. Active Model also helps with building custom ORMs for use outside of
the Rails framework.
Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
@@ -11,7 +11,7 @@ code from Rails, or monkey patch entire helpers to make them handle objects
that did not exactly conform to the Active Record interface. This would result
in code duplication and fragile applications that broke on upgrades. Active
Model solves this by defining an explicit API. You can read more about the
-API in ActiveModel::Lint::Tests.
+API in <tt>ActiveModel::Lint::Tests</tt>.
Active Model provides a default module that implements the basic API required
to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>.
@@ -24,8 +24,8 @@ to integrate with Action Pack out of the box: <tt>ActiveModel::Model</tt>.
end
person = Person.new(name: 'bob', age: '18')
- person.name # => 'bob'
- person.age # => '18'
+ person.name # => 'bob'
+ person.age # => '18'
person.valid? # => true
It includes model name introspections, conversions, translations and
@@ -75,15 +75,19 @@ behavior out of the box:
* Tracking value changes
- The ActiveModel::Dirty module allows for tracking attribute changes:
+ class Person
+ include ActiveModel::Dirty
+
+ attr_accessor :name
+ end
person = Person.new
- person.name # => nil
- person.changed? # => false
+ person.name # => nil
+ person.changed? # => false
person.name = 'bob'
- person.changed? # => true
- person.changed # => ['name']
- person.changes # => { 'name' => [nil, 'bob'] }
+ person.changed? # => true
+ person.changed # => ['name']
+ person.changes # => { 'name' => [nil, 'bob'] }
person.name = 'robert'
person.save
person.previous_changes # => {'name' => ['bob, 'robert']}
@@ -129,32 +133,36 @@ behavior out of the box:
{Learn more}[link:classes/ActiveModel/Naming.html]
-* Observer support
+* Making objects serializable
- ActiveModel::Observers allows your object to implement the Observer
- pattern in a Rails App and take advantage of all the standard observer
- functions.
+ ActiveModel::Serialization provides a standard interface for your object
+ to provide +to_json+ or +to_xml+ serialization.
- class PersonObserver < ActiveModel::Observer
- def after_create(person)
- person.logger.info("New person added!")
- end
+ class SerialPerson
+ include ActiveModel::Serialization
- def after_destroy(person)
- person.logger.warn("Person with an id of #{person.id} was destroyed!")
+ attr_accessor :name
+
+ def attributes
+ {'name' => name}
end
end
- {Learn more}[link:classes/ActiveModel/Observer.html]
-
-* Making objects serializable
+ s = SerialPerson.new
+ s.serializable_hash # => {"name"=>nil}
- ActiveModel::Serialization provides a standard interface for your object
- to provide +to_json+ or +to_xml+ serialization.
+ class SerialPerson
+ include ActiveModel::Serializers::JSON
+ end
s = SerialPerson.new
- s.serializable_hash # => {"name"=>nil}
s.to_json # => "{\"name\":null}"
+
+ class SerialPerson
+ include ActiveModel::Serializers::Xml
+ end
+
+ s = SerialPerson.new
s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
{Learn more}[link:classes/ActiveModel/Serialization.html]
diff --git a/activemodel/Rakefile b/activemodel/Rakefile
index fc5aaf9f8f..407dda2ec3 100644
--- a/activemodel/Rakefile
+++ b/activemodel/Rakefile
@@ -13,14 +13,12 @@ end
namespace :test do
task :isolated do
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
Dir.glob("#{dir}/test/**/*_test.rb").all? do |file|
- sh(ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
+ sh(Gem.ruby, '-w', "-I#{dir}/lib", "-I#{dir}/test", file)
end or raise "Failures"
end
end
-require 'rake/packagetask'
require 'rubygems/package_task'
spec = eval(File.read("#{dir}/activemodel.gemspec"))
@@ -29,7 +27,7 @@ Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/activemodel/activemodel.gemspec b/activemodel/activemodel.gemspec
index be5d5d3ca8..11e755649c 100644
--- a/activemodel/activemodel.gemspec
+++ b/activemodel/activemodel.gemspec
@@ -5,18 +5,20 @@ Gem::Specification.new do |s|
s.name = 'activemodel'
s.version = version
s.summary = 'A toolkit for building modeling frameworks (part of Rails).'
- s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, observers, serialization, internationalization, and testing.'
+ s.description = 'A toolkit for building modeling frameworks like Active Record. Rich support for attributes, callbacks, validations, serialization, internationalization, and testing.'
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
s.require_path = 'lib'
- s.add_dependency('activesupport', version)
- s.add_dependency('builder', '~> 3.1.0')
+ s.add_dependency 'activesupport', version
+
+ s.add_dependency 'builder', '~> 3.1.0'
end
diff --git a/activemodel/examples/validations.rb b/activemodel/examples/validations.rb
index 0b2706076f..b8e74acd5e 100644
--- a/activemodel/examples/validations.rb
+++ b/activemodel/examples/validations.rb
@@ -1,10 +1,11 @@
+require File.expand_path('../../../load_paths', __FILE__)
require 'active_model'
class Person
include ActiveModel::Conversion
include ActiveModel::Validations
- validates_presence_of :name
+ validates :name, presence: true
attr_accessor :name
@@ -22,8 +23,8 @@ class Person
end
person1 = Person.new
-p person1.valid?
-person1.errors
+p person1.valid? # => false
+p person1.errors.messages # => {:name=>["can't be blank"]}
-person2 = Person.new(:name => "matz")
-p person2.valid?
+person2 = Person.new(name: 'matz')
+p person2.valid? # => true
diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb
index f757ba9843..ef4f2514be 100644
--- a/activemodel/lib/active_model.rb
+++ b/activemodel/lib/active_model.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -37,11 +37,8 @@ module ActiveModel
autoload :ForbiddenAttributesProtection
autoload :Lint
autoload :Model
- autoload :DeprecatedMassAssignmentSecurity
autoload :Name, 'active_model/naming'
autoload :Naming
- autoload :Observer, 'active_model/observing'
- autoload :Observing
autoload :SecurePassword
autoload :Serialization
autoload :TestCase
diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb
index ef04f1fa49..f336c759d2 100644
--- a/activemodel/lib/active_model/attribute_methods.rb
+++ b/activemodel/lib/active_model/attribute_methods.rb
@@ -1,3 +1,5 @@
+require 'thread_safe'
+require 'mutex_m'
module ActiveModel
# Raised when an attribute is not defined.
@@ -8,22 +10,24 @@ module ActiveModel
#
# user = User.first
# user.pets.select(:id).first.user_id
- # # => ActiveModel::MissingAttributeError: missing attribute: user_id
+ # # => ActiveModel::MissingAttributeError: missing attribute: user_id
class MissingAttributeError < NoMethodError
end
- # == Active Model Attribute Methods
+
+ # == Active \Model Attribute Methods
#
# <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and
- # suffixes to your methods as well as handling the creation of Active Record
- # like class methods such as +table_name+.
+ # suffixes to your methods as well as handling the creation of
+ # <tt>ActiveRecord::Base</tt>-like class methods such as +table_name+.
#
- # The requirements to implement ActiveModel::AttributeMethods are to:
+ # The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
#
- # * <tt>include ActiveModel::AttributeMethods</tt> in your object.
- # * Call each Attribute Method module method you want to add, such as
- # +attribute_method_suffix+ or +attribute_method_prefix+.
+ # * <tt>include ActiveModel::AttributeMethods</tt> in your class.
+ # * Call each of its method you want to add, such as +attribute_method_suffix+
+ # or +attribute_method_prefix+.
# * Call +define_attribute_methods+ after the other methods are called.
# * Define the various generic +_attribute+ methods that you have declared.
+ # * Define an +attributes+ method, see below.
#
# A minimal implementation could be:
#
@@ -37,6 +41,10 @@ module ActiveModel
#
# attr_accessor :name
#
+ # def attributes
+ # {'name' => @name}
+ # end
+ #
# private
#
# def attribute_contrived?(attr)
@@ -52,10 +60,10 @@ module ActiveModel
# end
# end
#
- # Note that whenever you include ActiveModel::AttributeMethods in your class,
- # it requires you to implement an +attributes+ method which returns a hash
- # with each attribute name in your model as hash key and the attribute value as
- # hash value.
+ # Note that whenever you include <tt>ActiveModel::AttributeMethods</tt> in
+ # your class, it requires you to implement an +attributes+ method which
+ # returns a hash with each attribute name in your model as hash key and the
+ # attribute value as hash value.
#
# Hash keys must be strings.
module AttributeMethods
@@ -178,7 +186,6 @@ module ActiveModel
undefine_attribute_methods
end
-
# Allows you to make aliases for attributes.
#
# class Person
@@ -202,7 +209,7 @@ module ActiveModel
# person.name # => "Bob"
# person.nickname # => "Bob"
# person.name_short? # => true
- # person.nickname_short? # => true
+ # person.nickname_short? # => true
def alias_attribute(new_name, old_name)
self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
attribute_method_matchers.each do |matcher|
@@ -212,6 +219,16 @@ module ActiveModel
end
end
+ # Is +new_name+ an alias?
+ def attribute_alias?(new_name)
+ attribute_aliases.key? new_name.to_s
+ end
+
+ # Returns the original name for the alias +name+
+ def attribute_alias(name)
+ attribute_aliases[name.to_s]
+ end
+
# Declares the attributes that should be prefixed and suffixed by
# ActiveModel::AttributeMethods.
#
@@ -316,9 +333,10 @@ module ActiveModel
attribute_method_matchers_cache.clear
end
- # Returns true if the attribute methods defined have been generated.
def generated_attribute_methods #:nodoc:
- @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
+ @generated_attribute_methods ||= Module.new {
+ extend Mutex_m
+ }.tap { |mod| include mod }
end
protected
@@ -337,17 +355,17 @@ module ActiveModel
# significantly (in our case our test suite finishes 10% faster with
# this cache).
def attribute_method_matchers_cache #:nodoc:
- @attribute_method_matchers_cache ||= {}
+ @attribute_method_matchers_cache ||= ThreadSafe::Cache.new(initial_capacity: 4)
end
def attribute_method_matcher(method_name) #:nodoc:
- attribute_method_matchers_cache.fetch(method_name) do |name|
+ attribute_method_matchers_cache.compute_if_absent(method_name) do
# Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
# will match every time.
matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
match = nil
- matchers.detect { |method| match = method.match(name) }
- attribute_method_matchers_cache[name] = match
+ matchers.detect { |method| match = method.match(method_name) }
+ match
end
end
@@ -382,15 +400,6 @@ module ActiveModel
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
def initialize(options = {})
- if options[:prefix] == '' || options[:suffix] == ''
- ActiveSupport::Deprecation.warn(
- "Specifying an empty prefix/suffix for an attribute method is no longer " \
- "necessary. If the un-prefixed/suffixed version of the method has not been " \
- "defined when `define_attribute_methods` is called, it will be defined " \
- "automatically."
- )
- end
-
@prefix, @suffix = options.fetch(:prefix, ''), options.fetch(:suffix, '')
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
@method_missing_target = "#{@prefix}attribute#{@suffix}"
@@ -413,17 +422,16 @@ module ActiveModel
end
end
- # Allows access to the object attributes, which are held in the
- # <tt>@attributes</tt> hash, as though they were first-class methods. So a
- # Person class with a name attribute can use Person#name and Person#name=
- # and never directly use the attributes hash -- except for multiple assigns
- # with ActiveRecord#attributes=. A Milestone class can also ask
- # Milestone#completed? to test that the completed attribute is not +nil+
- # or 0.
+ # Allows access to the object attributes, which are held in the hash
+ # returned by <tt>attributes</tt>, as though they were first-class
+ # methods. So a +Person+ class with a +name+ attribute can for example use
+ # <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
+ # the attributes hash -- except for multiple assigns with
+ # <tt>ActiveRecord::Base#attributes=</tt>.
#
- # It's also possible to instantiate related objects, so a Client class
- # belonging to the clients table with a +master_id+ foreign key can
- # instantiate master through Client#master.
+ # It's also possible to instantiate related objects, so a <tt>Client</tt>
+ # class belonging to the +clients+ table with a +master_id+ foreign key
+ # can instantiate master through <tt>Client#master</tt>.
def method_missing(method, *args, &block)
if respond_to_without_attributes?(method, true)
super
@@ -433,17 +441,17 @@ module ActiveModel
end
end
- # attribute_missing is like method_missing, but for attributes. When method_missing is
- # called we check to see if there is a matching attribute method. If so, we call
- # attribute_missing to dispatch the attribute. This method can be overloaded to
- # customise the behaviour.
+ # +attribute_missing+ is like +method_missing+, but for attributes. When
+ # +method_missing+ is called we check to see if there is a matching
+ # attribute method. If so, we tell +attribute_missing+ to dispatch the
+ # attribute. This method can be overloaded to customize the behavior.
def attribute_missing(match, *args, &block)
__send__(match.target, match.attr_name, *args, &block)
end
- # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
- # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
- # which will all return +true+.
+ # A +Person+ instance with a +name+ attribute can ask
+ # <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
+ # and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
alias :respond_to_without_attributes? :respond_to?
def respond_to?(method, include_private_methods = false)
if super
diff --git a/activemodel/lib/active_model/callbacks.rb b/activemodel/lib/active_model/callbacks.rb
index e442455a53..377aa6ee27 100644
--- a/activemodel/lib/active_model/callbacks.rb
+++ b/activemodel/lib/active_model/callbacks.rb
@@ -1,7 +1,7 @@
-require 'active_support/callbacks'
+require 'active_support/core_ext/array/extract_options'
module ActiveModel
- # == Active Model Callbacks
+ # == Active \Model \Callbacks
#
# Provides an interface for any class to have Active Record like callbacks.
#
@@ -100,10 +100,10 @@ module ActiveModel
def define_model_callbacks(*callbacks)
options = callbacks.extract_options!
options = {
- :terminator => "result == false",
- :skip_after_callbacks_if_terminated => true,
- :scope => [:kind, :name],
- :only => [:before, :around, :after]
+ terminator: ->(_,result) { result == false },
+ skip_after_callbacks_if_terminated: true,
+ scope: [:kind, :name],
+ only: [:before, :around, :after]
}.merge!(options)
types = Array(options.delete(:only))
@@ -120,30 +120,27 @@ module ActiveModel
private
def _define_before_model_callback(klass, callback) #:nodoc:
- klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
- def self.before_#{callback}(*args, &block)
- set_callback(:#{callback}, :before, *args, &block)
- end
- CALLBACK
+ klass.define_singleton_method("before_#{callback}") do |*args, &block|
+ set_callback(:"#{callback}", :before, *args, &block)
+ end
end
def _define_around_model_callback(klass, callback) #:nodoc:
- klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
- def self.around_#{callback}(*args, &block)
- set_callback(:#{callback}, :around, *args, &block)
- end
- CALLBACK
+ klass.define_singleton_method("around_#{callback}") do |*args, &block|
+ set_callback(:"#{callback}", :around, *args, &block)
+ end
end
def _define_after_model_callback(klass, callback) #:nodoc:
- klass.class_eval <<-CALLBACK, __FILE__, __LINE__ + 1
- def self.after_#{callback}(*args, &block)
- options = args.extract_options!
- options[:prepend] = true
- options[:if] = Array(options[:if]) << "value != false"
- set_callback(:#{callback}, :after, *(args << options), &block)
- end
- CALLBACK
+ klass.define_singleton_method("after_#{callback}") do |*args, &block|
+ options = args.extract_options!
+ options[:prepend] = true
+ conditional = ActiveSupport::Callbacks::Conditionals::Value.new { |v|
+ v != false
+ }
+ options[:if] = Array(options[:if]) << conditional
+ set_callback(:"#{callback}", :after, *(args << options), &block)
+ end
end
end
end
diff --git a/activemodel/lib/active_model/conversion.rb b/activemodel/lib/active_model/conversion.rb
index 48c53f0789..21e4eb3c86 100644
--- a/activemodel/lib/active_model/conversion.rb
+++ b/activemodel/lib/active_model/conversion.rb
@@ -1,7 +1,5 @@
-require 'active_support/inflector'
-
module ActiveModel
- # == Active Model Conversions
+ # == Active \Model Conversion
#
# Handles default conversions: to_model, to_key, to_param, and to_partial_path.
#
diff --git a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb b/activemodel/lib/active_model/deprecated_mass_assignment_security.rb
deleted file mode 100644
index 2ea69991fc..0000000000
--- a/activemodel/lib/active_model/deprecated_mass_assignment_security.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module ActiveModel
- module DeprecatedMassAssignmentSecurity # :nodoc:
- extend ActiveSupport::Concern
-
- module ClassMethods # :nodoc:
- def attr_protected(*args)
- raise "`attr_protected` is extracted out of Rails into a gem. " \
- "Please use new recommended protection model for params " \
- "or add `protected_attributes` to your Gemfile to use old one."
- end
-
- def attr_accessible(*args)
- raise "`attr_accessible` is extracted out of Rails into a gem. " \
- "Please use new recommended protection model for params " \
- "or add `protected_attributes` to your Gemfile to use old one."
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb
index c0b268fa4d..c5f1b3f11a 100644
--- a/activemodel/lib/active_model/dirty.rb
+++ b/activemodel/lib/active_model/dirty.rb
@@ -1,9 +1,8 @@
-require 'active_model/attribute_methods'
require 'active_support/hash_with_indifferent_access'
require 'active_support/core_ext/object/duplicable'
module ActiveModel
- # == Active Model Dirty
+ # == Active \Model \Dirty
#
# Provides a way to track changes in your object in the same way as
# Active Record does.
@@ -15,13 +14,9 @@ module ActiveModel
# track.
# * Call <tt>attr_name_will_change!</tt> before each change to the tracked
# attribute.
- #
- # If you wish to also track previous changes on save or update, you need to
- # add:
- #
- # @previously_changed = changes
- #
- # inside of your save or update method.
+ # * Call <tt>changes_applied</tt> after the changes are persisted.
+ # * Call <tt>reset_changes</tt> when you want to reset the changes
+ # information.
#
# A minimal implementation could be:
#
@@ -40,14 +35,18 @@ module ActiveModel
# end
#
# def save
- # @previously_changed = changes
- # @changed_attributes.clear
+ # # do persistence work
+ # changes_applied
+ # end
+ #
+ # def reload!
+ # reset_changes
# end
# end
#
# A newly instantiated object is unchanged:
#
- # person = Person.find_by_name('Uncle Bob')
+ # person = Person.find_by(name: 'Uncle Bob')
# person.changed? # => false
#
# Change the name:
@@ -66,6 +65,12 @@ module ActiveModel
# person.changed? # => false
# person.name_changed? # => false
#
+ # Reset the changes:
+ #
+ # person.previous_changes # => {"name" => ["Uncle Bob", "Bill"]}
+ # person.reload!
+ # person.previous_changes # => {}
+ #
# Assigning the same value leaves the attribute unchanged:
#
# person.name = 'Bill'
@@ -92,7 +97,7 @@ module ActiveModel
included do
attribute_method_suffix '_changed?', '_change', '_will_change!', '_was'
- attribute_method_affix :prefix => 'reset_', :suffix => '!'
+ attribute_method_affix prefix: 'reset_', suffix: '!'
end
# Returns +true+ if any attribute have unsaved changes, +false+ otherwise.
@@ -120,7 +125,7 @@ module ActiveModel
# person.name = 'bob'
# person.changes # => { "name" => ["bill", "bob"] }
def changes
- HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
+ ActiveSupport::HashWithIndifferentAccess[changed.map { |attr| [attr, attribute_change(attr)] }]
end
# Returns a hash of attributes that were changed before the model was saved.
@@ -130,7 +135,7 @@ module ActiveModel
# person.save
# person.previous_changes # => {"name" => ["bob", "robert"]}
def previous_changes
- @previously_changed
+ @previously_changed ||= {}
end
# Returns a hash of the attributes with unsaved changes indicating their original
@@ -140,14 +145,31 @@ module ActiveModel
# person.name = 'robert'
# person.changed_attributes # => {"name" => "bob"}
def changed_attributes
- @changed_attributes ||= {}
+ @changed_attributes ||= ActiveSupport::HashWithIndifferentAccess.new
+ end
+
+ # Handle <tt>*_changed?</tt> for +method_missing+.
+ def attribute_changed?(attr)
+ changed_attributes.include?(attr)
+ end
+
+ # Handle <tt>*_was</tt> for +method_missing+.
+ def attribute_was(attr)
+ attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
end
private
- # Handle <tt>*_changed?</tt> for +method_missing+.
- def attribute_changed?(attr)
- changed_attributes.include?(attr)
+ # Removes current changes and makes them accessible through +previous_changes+.
+ def changes_applied
+ @previously_changed = changes
+ @changed_attributes = {}
+ end
+
+ # Removes all dirty data: current changes and previous changes
+ def reset_changes
+ @previously_changed = {}
+ @changed_attributes = {}
end
# Handle <tt>*_change</tt> for +method_missing+.
@@ -155,11 +177,6 @@ module ActiveModel
[changed_attributes[attr], __send__(attr)] if attribute_changed?(attr)
end
- # Handle <tt>*_was</tt> for +method_missing+.
- def attribute_was(attr)
- attribute_changed?(attr) ? changed_attributes[attr] : __send__(attr)
- end
-
# Handle <tt>*_will_change!</tt> for +method_missing+.
def attribute_will_change!(attr)
return if attribute_changed?(attr)
@@ -175,7 +192,10 @@ module ActiveModel
# Handle <tt>reset_*!</tt> for +method_missing+.
def reset_attribute!(attr)
- __send__("#{attr}=", changed_attributes[attr]) if attribute_changed?(attr)
+ if attribute_changed?(attr)
+ __send__("#{attr}=", changed_attributes[attr])
+ changed_attributes.delete(attr)
+ end
end
end
end
diff --git a/activemodel/lib/active_model/errors.rb b/activemodel/lib/active_model/errors.rb
index b3b9ba8e56..cf7551e4f4 100644
--- a/activemodel/lib/active_model/errors.rb
+++ b/activemodel/lib/active_model/errors.rb
@@ -4,7 +4,7 @@ require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/string/inflections'
module ActiveModel
- # == Active Model Errors
+ # == Active \Model \Errors
#
# Provides a modified +Hash+ that you can include in your object
# for handling error messages and interacting with Action Pack helpers.
@@ -12,7 +12,6 @@ module ActiveModel
# A minimal implementation could be:
#
# class Person
- #
# # Required dependency for ActiveModel::Errors
# extend ActiveModel::Naming
#
@@ -40,7 +39,6 @@ module ActiveModel
# def Person.lookup_ancestors
# [self]
# end
- #
# end
#
# The last three methods are required in your object for Errors to be
@@ -52,7 +50,7 @@ module ActiveModel
#
# The above allows you to do:
#
- # p = Person.new
+ # person = Person.new
# person.validate! # => ["can not be nil"]
# person.errors.full_messages # => ["name can not be nil"]
# # etc..
@@ -75,14 +73,14 @@ module ActiveModel
@messages = {}
end
- def initialize_dup(other) #:nodoc:
+ def initialize_dup(other) # :nodoc:
@messages = other.messages.dup
super
end
# Clear the error messages.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name can not be nil"]
# person.errors.clear
# person.errors.full_messages # => []
def clear
@@ -92,9 +90,9 @@ module ActiveModel
# Returns +true+ if the error messages include an error for the given key
# +attribute+, +false+ otherwise.
#
- # person.errors.messages # => { :name => ["can not be nil"] }
+ # person.errors.messages # => {:name=>["can not be nil"]}
# person.errors.include?(:name) # => true
- # person.errors.include?(:age) # => false
+ # person.errors.include?(:age) # => false
def include?(attribute)
(v = messages[attribute]) && v.any?
end
@@ -103,7 +101,7 @@ module ActiveModel
# Get messages for +key+.
#
- # person.errors.messages # => { :name => ["can not be nil"] }
+ # person.errors.messages # => {:name=>["can not be nil"]}
# person.errors.get(:name) # => ["can not be nil"]
# person.errors.get(:age) # => nil
def get(key)
@@ -122,7 +120,7 @@ module ActiveModel
# Delete messages for +key+. Returns the deleted messages.
#
# person.errors.get(:name) # => ["can not be nil"]
- # person.errors.delete(:name) # => ["can not be nil"]
+ # person.errors.delete(:name) # => ["can not be nil"]
# person.errors.get(:name) # => nil
def delete(key)
messages.delete(key)
@@ -177,7 +175,7 @@ module ActiveModel
# Returns all message values.
#
- # person.errors.messages # => { :name => ["can not be nil", "must be specified"] }
+ # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
# person.errors.values # => [["can not be nil", "must be specified"]]
def values
messages.values
@@ -185,7 +183,7 @@ module ActiveModel
# Returns all message keys.
#
- # person.errors.messages # => { :name => ["can not be nil", "must be specified"] }
+ # person.errors.messages # => {:name=>["can not be nil", "must be specified"]}
# person.errors.keys # => [:name]
def keys
messages.keys
@@ -213,7 +211,7 @@ module ActiveModel
# Returns +true+ if no errors are found, +false+ otherwise.
# If the error message is a string it can be empty.
#
- # person.errors.full_messages # => ["name can not be nil"]
+ # person.errors.full_messages # => ["name can not be nil"]
# person.errors.empty? # => false
def empty?
all? { |k, v| v && v.empty? && !v.is_a?(String) }
@@ -233,24 +231,24 @@ module ActiveModel
# # <error>name must be specified</error>
# # </errors>
def to_xml(options={})
- to_a.to_xml({ :root => "errors", :skip_types => true }.merge!(options))
+ to_a.to_xml({ root: "errors", skip_types: true }.merge!(options))
end
# Returns a Hash that can be used as the JSON representation for this
# object. You can pass the <tt>:full_messages</tt> option. This determines
# if the json object should contain full messages or not (false by default).
#
- # person.as_json # => { :name => ["can not be nil"] }
- # person.as_json(full_messages: true) # => { :name => ["name can not be nil"] }
+ # person.errors.as_json # => {:name=>["can not be nil"]}
+ # person.errors.as_json(full_messages: true) # => {:name=>["name can not be nil"]}
def as_json(options=nil)
to_hash(options && options[:full_messages])
end
- # Returns a Hash of attributes with their error messages. If +full_messages+
+ # Returns a Hash of attributes with their error messages. If +full_messages+
# is +true+, it will contain full messages (see +full_message+).
#
- # person.to_hash # => { :name => ["can not be nil"] }
- # person.to_hash(true) # => { :name => ["name can not be nil"] }
+ # person.errors.to_hash # => {:name=>["can not be nil"]}
+ # person.errors.to_hash(true) # => {:name=>["name can not be nil"]}
def to_hash(full_messages = false)
if full_messages
messages = {}
@@ -273,7 +271,7 @@ module ActiveModel
# # => ["is invalid", "must be implemented"]
#
# person.errors.messages
- # # => { :name => ["must be implemented", "is invalid"] }
+ # # => {:name=>["must be implemented", "is invalid"]}
#
# If +message+ is a symbol, it will be translated using the appropriate
# scope (see +generate_message+).
@@ -286,12 +284,12 @@ module ActiveModel
# <tt>:strict</tt> option can also be set to any other exception.
#
# person.errors.add(:name, nil, strict: true)
- # # => ActiveModel::StrictValidationFailed: name is invalid
+ # # => ActiveModel::StrictValidationFailed: name is invalid
# person.errors.add(:name, nil, strict: NameIsInvalid)
- # # => NameIsInvalid: name is invalid
+ # # => NameIsInvalid: name is invalid
#
# person.errors.messages # => {}
- def add(attribute, message = nil, options = {})
+ def add(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
if exception = options[:strict]
exception = ActiveModel::StrictValidationFailed if exception == true
@@ -306,9 +304,9 @@ module ActiveModel
#
# person.errors.add_on_empty(:name)
# person.errors.messages
- # # => { :name => ["can't be empty"] }
+ # # => {:name=>["can't be empty"]}
def add_on_empty(attributes, options = {})
- [attributes].flatten.each do |attribute|
+ Array(attributes).each do |attribute|
value = @base.send(:read_attribute_for_validation, attribute)
is_empty = value.respond_to?(:empty?) ? value.empty? : false
add(attribute, :empty, options) if value.nil? || is_empty
@@ -320,9 +318,9 @@ module ActiveModel
#
# person.errors.add_on_blank(:name)
# person.errors.messages
- # # => { :name => ["can't be blank"] }
+ # # => {:name=>["can't be blank"]}
def add_on_blank(attributes, options = {})
- [attributes].flatten.each do |attribute|
+ Array(attributes).each do |attribute|
value = @base.send(:read_attribute_for_validation, attribute)
add(attribute, :blank, options) if value.blank?
end
@@ -333,7 +331,7 @@ module ActiveModel
#
# person.errors.add :name, :blank
# person.errors.added? :name, :blank # => true
- def added?(attribute, message = nil, options = {})
+ def added?(attribute, message = :invalid, options = {})
message = normalize_message(attribute, message, options)
self[attribute].include? message
end
@@ -352,17 +350,31 @@ module ActiveModel
map { |attribute, message| full_message(attribute, message) }
end
+ # Returns all the full error messages for a given attribute in an array.
+ #
+ # class Person
+ # validates_presence_of :name, :email
+ # validates_length_of :name, in: 5..30
+ # end
+ #
+ # person = Person.create()
+ # person.errors.full_messages_for(:name)
+ # # => ["Name is too short (minimum is 5 characters)", "Name can't be blank"]
+ def full_messages_for(attribute)
+ (get(attribute) || []).map { |message| full_message(attribute, message) }
+ end
+
# Returns a full message for a given attribute.
#
# person.errors.full_message(:name, 'is invalid') # => "Name is invalid"
def full_message(attribute, message)
return message if attribute == :base
attr_name = attribute.to_s.tr('.', '_').humanize
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
I18n.t(:"errors.format", {
- :default => "%{attribute} %{message}",
- :attribute => attr_name,
- :message => message
+ default: "%{attribute} %{message}",
+ attribute: attr_name,
+ message: message
})
end
@@ -414,10 +426,10 @@ module ActiveModel
value = (attribute != :base ? @base.send(:read_attribute_for_validation, attribute) : nil)
options = {
- :default => defaults,
- :model => @base.class.model_name.human,
- :attribute => @base.class.human_attribute_name(attribute),
- :value => value
+ default: defaults,
+ model: @base.class.model_name.human,
+ attribute: @base.class.human_attribute_name(attribute),
+ value: value
}.merge!(options)
I18n.translate(key, options)
@@ -425,8 +437,6 @@ module ActiveModel
private
def normalize_message(attribute, message, options)
- message ||= :invalid
-
case message
when Symbol
generate_message(attribute, message, options.except(*CALLBACKS_OPTIONS))
diff --git a/activemodel/lib/active_model/forbidden_attributes_protection.rb b/activemodel/lib/active_model/forbidden_attributes_protection.rb
index 4c05b19cba..7468f95548 100644
--- a/activemodel/lib/active_model/forbidden_attributes_protection.rb
+++ b/activemodel/lib/active_model/forbidden_attributes_protection.rb
@@ -16,7 +16,7 @@ module ActiveModel
module ForbiddenAttributesProtection # :nodoc:
protected
- def sanitize_for_mass_assignment(attributes, options = {})
+ def sanitize_for_mass_assignment(attributes)
if attributes.respond_to?(:permitted?) && !attributes.permitted?
raise ActiveModel::ForbiddenAttributesError
else
diff --git a/activemodel/lib/active_model/lint.rb b/activemodel/lib/active_model/lint.rb
index 550fa474ea..46b446dc08 100644
--- a/activemodel/lib/active_model/lint.rb
+++ b/activemodel/lib/active_model/lint.rb
@@ -1,8 +1,8 @@
module ActiveModel
module Lint
- # == Active Model Lint Tests
+ # == Active \Model \Lint \Tests
#
- # You can test whether an object is compliant with the Active Model API by
+ # You can test whether an object is compliant with the Active \Model API by
# including <tt>ActiveModel::Lint::Tests</tt> in your TestCase. It will
# include tests that tell you whether your object is fully compliant,
# or if not, which aspects of the API are not implemented.
@@ -71,7 +71,7 @@ module ActiveModel
assert_boolean model.persisted?, "persisted?"
end
- # == Naming
+ # == \Naming
#
# Model.model_name must return a string with some convenience methods:
# <tt>:human</tt>, <tt>:singular</tt> and <tt>:plural</tt>. Check
@@ -85,7 +85,7 @@ module ActiveModel
assert model_name.plural.respond_to?(:to_str)
end
- # == Errors Testing
+ # == \Errors Testing
#
# Returns an object that implements [](attribute) defined which returns an
# Array of Strings that are the errors for the attribute in question.
@@ -98,7 +98,7 @@ module ActiveModel
private
def model
- assert @model.respond_to?(:to_model), "The object should respond_to to_model"
+ assert @model.respond_to?(:to_model), "The object should respond to to_model"
@model.to_model
end
diff --git a/activemodel/lib/active_model/locale/en.yml b/activemodel/lib/active_model/locale/en.yml
index d17848c861..540e8132d3 100644
--- a/activemodel/lib/active_model/locale/en.yml
+++ b/activemodel/lib/active_model/locale/en.yml
@@ -13,6 +13,7 @@ en:
accepted: "must be accepted"
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)"
diff --git a/activemodel/lib/active_model/model.rb b/activemodel/lib/active_model/model.rb
index 33a530e6bd..f048dda5c6 100644
--- a/activemodel/lib/active_model/model.rb
+++ b/activemodel/lib/active_model/model.rb
@@ -1,6 +1,6 @@
module ActiveModel
- # == Active Model Basic Model
+ # == Active \Model Basic \Model
#
# Includes the required interface for an object to interact with
# <tt>ActionPack</tt>, using different <tt>ActiveModel</tt> modules.
@@ -79,6 +79,8 @@ module ActiveModel
params.each do |attr, value|
self.public_send("#{attr}=", value)
end if params
+
+ super()
end
# Indicates if the model is persisted. Default is +false+.
diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb
index 4322592a7e..198efc5088 100644
--- a/activemodel/lib/active_model/naming.rb
+++ b/activemodel/lib/active_model/naming.rb
@@ -1,4 +1,3 @@
-require 'active_support/inflector'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/module/introspection'
@@ -56,8 +55,8 @@ module ActiveModel
# end
#
# BlogPost.model_name <=> 'BlogPost' # => 0
- # BlogPost.model_name <=> 'Blog' # => 1
- # BlogPost.model_name <=> 'BlogPosts' # => -1
+ # BlogPost.model_name <=> 'Blog' # => 1
+ # BlogPost.model_name <=> 'BlogPosts' # => -1
##
# :method: =~
@@ -130,7 +129,7 @@ module ActiveModel
#
# Equivalent to +to_s+.
delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
- :to_str, :to => :name
+ :to_str, to: :name
# Returns a new ActiveModel::Name instance. By default, the +namespace+
# and +name+ option will take the namespace and name of the given class
@@ -184,7 +183,7 @@ module ActiveModel
defaults << options[:default] if options[:default]
defaults << @human
- options = { :scope => [@klass.i18n_scope, :models], :count => 1, :default => defaults }.merge!(options.except(:default))
+ options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
I18n.translate(defaults.shift, options)
end
@@ -195,7 +194,7 @@ module ActiveModel
end
end
- # == Active Model Naming
+ # == Active \Model \Naming
#
# Creates a +model_name+ method on your object.
#
diff --git a/activemodel/lib/active_model/observer_array.rb b/activemodel/lib/active_model/observer_array.rb
deleted file mode 100644
index 77bc0f71e3..0000000000
--- a/activemodel/lib/active_model/observer_array.rb
+++ /dev/null
@@ -1,152 +0,0 @@
-require 'set'
-
-module ActiveModel
- # Stores the enabled/disabled state of individual observers for
- # a particular model class.
- class ObserverArray < Array
- attr_reader :model_class
- def initialize(model_class, *args) #:nodoc:
- @model_class = model_class
- super(*args)
- end
-
- # Returns +true+ if the given observer is disabled for the model class,
- # +false+ otherwise.
- def disabled_for?(observer) #:nodoc:
- disabled_observers.include?(observer.class)
- end
-
- # Disables one or more observers. This supports multiple forms:
- #
- # ORM.observers.disable :all
- # # => disables all observers for all models subclassed from
- # # an ORM base class that includes ActiveModel::Observing
- # # e.g. ActiveRecord::Base
- #
- # ORM.observers.disable :user_observer
- # # => disables the UserObserver
- #
- # User.observers.disable AuditTrail
- # # => disables the AuditTrail observer for User notifications.
- # # Other models will still notify the AuditTrail observer.
- #
- # ORM.observers.disable :observer_1, :observer_2
- # # => disables Observer1 and Observer2 for all models.
- #
- # User.observers.disable :all do
- # # all user observers are disabled for
- # # just the duration of the block
- # end
- def disable(*observers, &block)
- set_enablement(false, observers, &block)
- end
-
- # Enables one or more observers. This supports multiple forms:
- #
- # ORM.observers.enable :all
- # # => enables all observers for all models subclassed from
- # # an ORM base class that includes ActiveModel::Observing
- # # e.g. ActiveRecord::Base
- #
- # ORM.observers.enable :user_observer
- # # => enables the UserObserver
- #
- # User.observers.enable AuditTrail
- # # => enables the AuditTrail observer for User notifications.
- # # Other models will not be affected (i.e. they will not
- # # trigger notifications to AuditTrail if previously disabled)
- #
- # ORM.observers.enable :observer_1, :observer_2
- # # => enables Observer1 and Observer2 for all models.
- #
- # User.observers.enable :all do
- # # all user observers are enabled for
- # # just the duration of the block
- # end
- #
- # Note: all observers are enabled by default. This method is only
- # useful when you have previously disabled one or more observers.
- def enable(*observers, &block)
- set_enablement(true, observers, &block)
- end
-
- protected
-
- def disabled_observers #:nodoc:
- @disabled_observers ||= Set.new
- end
-
- def observer_class_for(observer) #:nodoc:
- return observer if observer.is_a?(Class)
-
- if observer.respond_to?(:to_sym) # string/symbol
- observer.to_s.camelize.constantize
- else
- raise ArgumentError, "#{observer} was not a class or a " +
- "lowercase, underscored class name as expected."
- end
- end
-
- def start_transaction #:nodoc:
- disabled_observer_stack.push(disabled_observers.dup)
- each_subclass_array do |array|
- array.start_transaction
- end
- end
-
- def disabled_observer_stack #:nodoc:
- @disabled_observer_stack ||= []
- end
-
- def end_transaction #:nodoc:
- @disabled_observers = disabled_observer_stack.pop
- each_subclass_array do |array|
- array.end_transaction
- end
- end
-
- def transaction #:nodoc:
- start_transaction
-
- begin
- yield
- ensure
- end_transaction
- end
- end
-
- def each_subclass_array #:nodoc:
- model_class.descendants.each do |subclass|
- yield subclass.observers
- end
- end
-
- def set_enablement(enabled, observers) #:nodoc:
- if block_given?
- transaction do
- set_enablement(enabled, observers)
- yield
- end
- else
- observers = ActiveModel::Observer.descendants if observers == [:all]
- observers.each do |obs|
- klass = observer_class_for(obs)
-
- unless klass < ActiveModel::Observer
- raise ArgumentError.new("#{obs} does not refer to a valid observer")
- end
-
- if enabled
- disabled_observers.delete(klass)
- else
- disabled_observers << klass
- end
- end
-
- each_subclass_array do |array|
- array.set_enablement(enabled, observers)
- end
- end
- end
- end
-end
diff --git a/activemodel/lib/active_model/observing.rb b/activemodel/lib/active_model/observing.rb
deleted file mode 100644
index 9db7639ea3..0000000000
--- a/activemodel/lib/active_model/observing.rb
+++ /dev/null
@@ -1,374 +0,0 @@
-require 'singleton'
-require 'active_model/observer_array'
-require 'active_support/core_ext/module/aliasing'
-require 'active_support/core_ext/module/remove_method'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/enumerable'
-require 'active_support/core_ext/object/try'
-require 'active_support/descendants_tracker'
-
-module ActiveModel
- # == Active Model Observers Activation
- module Observing
- extend ActiveSupport::Concern
-
- included do
- extend ActiveSupport::DescendantsTracker
- end
-
- module ClassMethods
- # Activates the observers assigned.
- #
- # class ORM
- # include ActiveModel::Observing
- # end
- #
- # # Calls PersonObserver.instance
- # ORM.observers = :person_observer
- #
- # # Calls Cacher.instance and GarbageCollector.instance
- # ORM.observers = :cacher, :garbage_collector
- #
- # # Same as above, just using explicit class references
- # ORM.observers = Cacher, GarbageCollector
- #
- # Note: Setting this does not instantiate the observers yet.
- # <tt>instantiate_observers</tt> is called during startup, and before
- # each development request.
- def observers=(*values)
- observers.replace(values.flatten)
- end
-
- # Gets an array of observers observing this model. The array also provides
- # +enable+ and +disable+ methods that allow you to selectively enable and
- # disable observers (see ActiveModel::ObserverArray.enable and
- # ActiveModel::ObserverArray.disable for more on this).
- #
- # class ORM
- # include ActiveModel::Observing
- # end
- #
- # ORM.observers = :cacher, :garbage_collector
- # ORM.observers # => [:cacher, :garbage_collector]
- # ORM.observers.class # => ActiveModel::ObserverArray
- def observers
- @observers ||= ObserverArray.new(self)
- end
-
- # Returns the current observer instances.
- #
- # class Foo
- # include ActiveModel::Observing
- #
- # attr_accessor :status
- # end
- #
- # class FooObserver < ActiveModel::Observer
- # def on_spec(record, *args)
- # record.status = true
- # end
- # end
- #
- # Foo.observers = FooObserver
- # Foo.instantiate_observers
- #
- # Foo.observer_instances # => [#<FooObserver:0x007fc212c40820>]
- def observer_instances
- @observer_instances ||= []
- end
-
- # Instantiate the global observers.
- #
- # class Foo
- # include ActiveModel::Observing
- #
- # attr_accessor :status
- # end
- #
- # class FooObserver < ActiveModel::Observer
- # def on_spec(record, *args)
- # record.status = true
- # end
- # end
- #
- # Foo.observers = FooObserver
- #
- # foo = Foo.new
- # foo.status = false
- # foo.notify_observers(:on_spec)
- # foo.status # => false
- #
- # Foo.instantiate_observers # => [FooObserver]
- #
- # foo = Foo.new
- # foo.status = false
- # foo.notify_observers(:on_spec)
- # foo.status # => true
- def instantiate_observers
- observers.each { |o| instantiate_observer(o) }
- end
-
- # Add a new observer to the pool. The new observer needs to respond to
- # <tt>update</tt>, otherwise it raises an +ArgumentError+ exception.
- #
- # class Foo
- # include ActiveModel::Observing
- # end
- #
- # class FooObserver < ActiveModel::Observer
- # end
- #
- # Foo.add_observer(FooObserver.instance)
- #
- # Foo.observers_instance
- # # => [#<FooObserver:0x007fccf55d9390>]
- def add_observer(observer)
- unless observer.respond_to? :update
- raise ArgumentError, "observer needs to respond to 'update'"
- end
- observer_instances << observer
- end
-
- # Fires notifications to model's observers.
- #
- # def save
- # notify_observers(:before_save)
- # ...
- # notify_observers(:after_save)
- # end
- #
- # Custom notifications can be sent in a similar fashion:
- #
- # notify_observers(:custom_notification, :foo)
- #
- # This will call <tt>custom_notification</tt>, passing as arguments
- # the current object and <tt>:foo</tt>.
- def notify_observers(*args)
- observer_instances.each { |observer| observer.update(*args) }
- end
-
- # Returns the total number of instantiated observers.
- #
- # class Foo
- # include ActiveModel::Observing
- #
- # attr_accessor :status
- # end
- #
- # class FooObserver < ActiveModel::Observer
- # def on_spec(record, *args)
- # record.status = true
- # end
- # end
- #
- # Foo.observers = FooObserver
- # Foo.observers_count # => 0
- # Foo.instantiate_observers
- # Foo.observers_count # => 1
- def observers_count
- observer_instances.size
- end
-
- # <tt>count_observers</tt> is deprecated. Use #observers_count.
- def count_observers
- msg = "count_observers is deprecated in favor of observers_count"
- ActiveSupport::Deprecation.warn(msg)
- observers_count
- end
-
- protected
- def instantiate_observer(observer) #:nodoc:
- # string/symbol
- if observer.respond_to?(:to_sym)
- observer = observer.to_s.camelize.constantize
- end
- if observer.respond_to?(:instance)
- observer.instance
- else
- raise ArgumentError,
- "#{observer} must be a lowercase, underscored class name (or " +
- "the class itself) responding to the method :instance. " +
- "Example: Person.observers = :big_brother # calls " +
- "BigBrother.instance"
- end
- end
-
- # Notify observers when the observed class is subclassed.
- def inherited(subclass) #:nodoc:
- super
- notify_observers :observed_class_inherited, subclass
- end
- end
-
- # Notify a change to the list of observers.
- #
- # class Foo
- # include ActiveModel::Observing
- #
- # attr_accessor :status
- # end
- #
- # class FooObserver < ActiveModel::Observer
- # def on_spec(record, *args)
- # record.status = true
- # end
- # end
- #
- # Foo.observers = FooObserver
- # Foo.instantiate_observers # => [FooObserver]
- #
- # foo = Foo.new
- # foo.status = false
- # foo.notify_observers(:on_spec)
- # foo.status # => true
- #
- # See ActiveModel::Observing::ClassMethods.notify_observers for more
- # information.
- def notify_observers(method, *extra_args)
- self.class.notify_observers(method, self, *extra_args)
- end
- end
-
- # == Active Model Observers
- #
- # Observer classes respond to life cycle callbacks to implement trigger-like
- # behavior outside the original class. This is a great way to reduce the
- # clutter that normally comes when the model class is burdened with
- # functionality that doesn't pertain to the core responsibility of the
- # class.
- #
- # class CommentObserver < ActiveModel::Observer
- # def after_save(comment)
- # Notifications.comment('admin@do.com', 'New comment was posted', comment).deliver
- # end
- # end
- #
- # This Observer sends an email when a <tt>Comment#save</tt> is finished.
- #
- # class ContactObserver < ActiveModel::Observer
- # def after_create(contact)
- # contact.logger.info('New contact added!')
- # end
- #
- # def after_destroy(contact)
- # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
- # end
- # end
- #
- # This Observer uses logger to log when specific callbacks are triggered.
- #
- # == Observing a class that can't be inferred
- #
- # Observers will by default be mapped to the class with which they share a
- # name. So <tt>CommentObserver</tt> will be tied to observing <tt>Comment</tt>,
- # <tt>ProductManagerObserver</tt> to <tt>ProductManager</tt>, and so on. If
- # you want to name your observer differently than the class you're interested
- # in observing, you can use the <tt>Observer.observe</tt> class method which
- # takes either the concrete class (<tt>Product</tt>) or a symbol for that
- # class (<tt>:product</tt>):
- #
- # class AuditObserver < ActiveModel::Observer
- # observe :account
- #
- # def after_update(account)
- # AuditTrail.new(account, 'UPDATED')
- # end
- # end
- #
- # If the audit observer needs to watch more than one kind of object, this can
- # be specified with multiple arguments:
- #
- # class AuditObserver < ActiveModel::Observer
- # observe :account, :balance
- #
- # def after_update(record)
- # AuditTrail.new(record, 'UPDATED')
- # end
- # end
- #
- # The <tt>AuditObserver</tt> will now act on both updates to <tt>Account</tt>
- # and <tt>Balance</tt> by treating them both as records.
- #
- # If you're using an Observer in a Rails application with Active Record, be
- # sure to read about the necessary configuration in the documentation for
- # ActiveRecord::Observer.
- class Observer
- include Singleton
- extend ActiveSupport::DescendantsTracker
-
- class << self
- # Attaches the observer to the supplied model classes.
- #
- # class AuditObserver < ActiveModel::Observer
- # observe :account, :balance
- # end
- #
- # AuditObserver.observed_classes # => [Account, Balance]
- def observe(*models)
- models.flatten!
- models.collect! { |model| model.respond_to?(:to_sym) ? model.to_s.camelize.constantize : model }
- singleton_class.redefine_method(:observed_classes) { models }
- end
-
- # Returns an array of Classes to observe.
- #
- # AccountObserver.observed_classes # => [Account]
- #
- # You can override this instead of using the +observe+ helper.
- #
- # class AuditObserver < ActiveModel::Observer
- # def self.observed_classes
- # [Account, Balance]
- # end
- # end
- def observed_classes
- Array(observed_class)
- end
-
- # Returns the class observed by default. It's inferred from the observer's
- # class name.
- #
- # PersonObserver.observed_class # => Person
- # AccountObserver.observed_class # => Account
- def observed_class
- name[/(.*)Observer/, 1].try :constantize
- end
- end
-
- # Start observing the declared classes and their subclasses.
- # Called automatically by the instance method.
- def initialize #:nodoc:
- observed_classes.each { |klass| add_observer!(klass) }
- end
-
- def observed_classes #:nodoc:
- self.class.observed_classes
- end
-
- # Send observed_method(object) if the method exists and
- # the observer is enabled for the given object's class.
- def update(observed_method, object, *extra_args, &block) #:nodoc:
- return if !respond_to?(observed_method) || disabled_for?(object)
- send(observed_method, object, *extra_args, &block)
- end
-
- # Special method sent by the observed class when it is inherited.
- # Passes the new subclass.
- def observed_class_inherited(subclass) #:nodoc:
- self.class.observe(observed_classes + [subclass])
- add_observer!(subclass)
- end
-
- protected
- def add_observer!(klass) #:nodoc:
- klass.add_observer(self)
- end
-
- # Returns true if notifications are disabled for this object.
- def disabled_for?(object) #:nodoc:
- klass = object.class
- return false unless klass.respond_to?(:observers)
- klass.observers.disabled_for?(self)
- end
- end
-end
diff --git a/activemodel/lib/active_model/railtie.rb b/activemodel/lib/active_model/railtie.rb
index f239758b35..1671eb7bd4 100644
--- a/activemodel/lib/active_model/railtie.rb
+++ b/activemodel/lib/active_model/railtie.rb
@@ -2,7 +2,11 @@ require "active_model"
require "rails"
module ActiveModel
- class Railtie < Rails::Railtie
+ class Railtie < Rails::Railtie # :nodoc:
config.eager_load_namespaces << ActiveModel
+
+ initializer "active_model.secure_password" do
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
+ end
end
-end \ No newline at end of file
+end
diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index d011402081..7e694b5c50 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
+ class << self
+ attr_accessor :min_cost # :nodoc:
+ end
+ self.min_cost = false
+
module ClassMethods
# Adds methods to set and authenticate against a BCrypt password.
# This mechanism requires you to have a password_digest attribute.
@@ -11,9 +16,13 @@ module ActiveModel
# you wish to turn off validations, pass <tt>validations: false</tt> as an
# argument. You can add more validations by hand if need be.
#
- # You need to add bcrypt-ruby (~> 3.0.0) to Gemfile to use #has_secure_password:
+ # 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.
#
- # gem 'bcrypt-ruby', '~> 3.0.0'
+ # You need to add bcrypt-ruby (~> 3.1.2) to Gemfile to use #has_secure_password:
+ #
+ # gem 'bcrypt-ruby', '~> 3.1.2'
#
# Example using Active Record (which automatically includes ActiveModel::SecurePassword):
#
@@ -23,33 +32,38 @@ module ActiveModel
# end
#
# user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
- # user.save # => false, password required
+ # user.save # => false, password required
# user.password = 'mUc3m00RsqyRe'
- # user.save # => false, confirmation doesn't match
+ # user.save # => false, confirmation doesn't match
# user.password_confirmation = 'mUc3m00RsqyRe'
- # user.save # => true
- # user.authenticate('notright') # => false
- # user.authenticate('mUc3m00RsqyRe') # => user
- # User.find_by_name('david').try(:authenticate, 'notright') # => false
- # User.find_by_name('david').try(:authenticate, 'mUc3m00RsqyRe') # => user
+ # user.save # => true
+ # user.authenticate('notright') # => false
+ # user.authenticate('mUc3m00RsqyRe') # => user
+ # 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.
# This is to avoid ActiveModel (and by extension the entire framework)
# being dependent on a binary library.
- gem 'bcrypt-ruby', '~> 3.0.0'
- require 'bcrypt'
+ 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"
+ raise
+ end
attr_reader :password
+ include InstanceMethodsOnActivation
+
if options.fetch(:validations, true)
- validates_confirmation_of :password
- validates_presence_of :password, :on => :create
-
+ validates_confirmation_of :password, if: :should_confirm_password?
+ validates_presence_of :password, on: :create
+ validates_presence_of :password_confirmation, if: :should_confirm_password?
+
before_create { raise "Password digest missing on new record" if password_digest.blank? }
end
- include InstanceMethodsOnActivation
-
if respond_to?(:attributes_protected_by_default)
def self.attributes_protected_by_default #:nodoc:
super + ['password_digest']
@@ -68,7 +82,7 @@ module ActiveModel
# user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
# user.save
# user.authenticate('notright') # => false
- #  user.authenticate('mUc3m00RsqyRe') # => user
+ # user.authenticate('mUc3m00RsqyRe') # => user
def authenticate(unencrypted_password)
BCrypt::Password.new(password_digest) == unencrypted_password && self
end
@@ -84,13 +98,24 @@ module ActiveModel
# user.password = nil
# user.password_digest # => nil
# user.password = 'mUc3m00RsqyRe'
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
+ # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
def password=(unencrypted_password)
unless unencrypted_password.blank?
@password = unencrypted_password
- self.password_digest = BCrypt::Password.create(unencrypted_password)
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+ self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
end
end
+
+ def password_confirmation=(unencrypted_password)
+ @password_confirmation = unencrypted_password
+ end
+
+ private
+
+ def should_confirm_password?
+ password_confirmation && password.present?
+ end
end
end
end
diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb
index 8a63014ffb..fdb06aebb9 100644
--- a/activemodel/lib/active_model/serialization.rb
+++ b/activemodel/lib/active_model/serialization.rb
@@ -2,7 +2,7 @@ require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
module ActiveModel
- # == Active Model Serialization
+ # == Active \Model \Serialization
#
# Provides a basic serialization to a serializable_hash for your object.
#
@@ -90,7 +90,7 @@ module ActiveModel
# person.name = 'bob'
# person.age = 22
# person.serializable_hash # => {"name"=>"bob", "age"=>22}
- # person.serializable_hash(only: :name) # => {"name"=>"bob"}
+ # person.serializable_hash(only: :name) # => {"name"=>"bob"}
# person.serializable_hash(except: :name) # => {"age"=>22}
# person.serializable_hash(methods: :capitalized_name)
# # => {"name"=>"bob", "age"=>22, "capitalized_name"=>"Bob"}
diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb
index a4252b995d..05e2e089e5 100644
--- a/activemodel/lib/active_model/serializers/json.rb
+++ b/activemodel/lib/active_model/serializers/json.rb
@@ -1,8 +1,8 @@
require 'active_support/json'
module ActiveModel
- # == Active Model JSON Serializer
module Serializers
+ # == Active Model JSON Serializer
module JSON
extend ActiveSupport::Concern
include ActiveModel::Serialization
@@ -109,7 +109,7 @@ module ActiveModel
#
# def attributes=(hash)
# hash.each do |key, value|
- # instance_variable_set("@#{key}", value)
+ # send("#{key}=", value)
# end
# end
#
diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb
index cf742d0569..2864c2ba11 100755..100644
--- a/activemodel/lib/active_model/serializers/xml.rb
+++ b/activemodel/lib/active_model/serializers/xml.rb
@@ -2,21 +2,30 @@ require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/time/acts_like'
module ActiveModel
- # == Active Model XML Serializer
module Serializers
+ # == Active Model XML Serializer
module Xml
extend ActiveSupport::Concern
include ActiveModel::Serialization
+ included do
+ extend ActiveModel::Naming
+ end
+
class Serializer #:nodoc:
class Attribute #:nodoc:
attr_reader :name, :value, :type
def initialize(name, serializable, value)
@name, @serializable = name, serializable
- value = value.in_time_zone if value.respond_to?(:in_time_zone)
+
+ if value.acts_like?(:time) && value.respond_to?(:in_time_zone)
+ value = value.in_time_zone
+ end
+
@value = value
@type = compute_type
end
@@ -70,7 +79,7 @@ module ActiveModel
require 'builder' unless defined? ::Builder
options[:indent] ||= 2
- options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
+ options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
@builder = options[:builder]
@builder.instruct! unless options[:skip_instruct]
@@ -79,8 +88,8 @@ module ActiveModel
root = ActiveSupport::XmlMini.rename_key(root, options)
args = [root]
- args << {:xmlns => options[:namespace]} if options[:namespace]
- args << {:type => options[:type]} if options[:type] && !options[:skip_types]
+ args << { xmlns: options[:namespace] } if options[:namespace]
+ args << { type: options[:type] } if options[:type] && !options[:skip_types]
@builder.tag!(*args) do
add_attributes_and_methods
@@ -123,7 +132,7 @@ module ActiveModel
records = records.to_ary
tag = ActiveSupport::XmlMini.rename_key(association.to_s, options)
- type = options[:skip_types] ? { } : {:type => "array"}
+ type = options[:skip_types] ? { } : { type: "array" }
association_name = association.to_s.singularize
merged_options[:root] = association_name
@@ -136,7 +145,7 @@ module ActiveModel
record_type = {}
else
record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name
- record_type = {:type => record_class}
+ record_type = { type: record_class }
end
record.to_xml merged_options.merge(record_type)
@@ -145,7 +154,12 @@ module ActiveModel
end
else
merged_options[:root] = association.to_s
- records.to_xml(merged_options)
+
+ unless records.class.to_s.underscore == association.to_s
+ merged_options[:type] = records.class.name
+ end
+
+ records.to_xml merged_options
end
end
@@ -191,7 +205,7 @@ module ActiveModel
Serializer.new(self, options).serialize(&block)
end
- # Sets the model +attributes+ from a JSON string. Returns +self+.
+ # Sets the model +attributes+ from an XML string. Returns +self+.
#
# class Person
# include ActiveModel::Serializers::Xml
diff --git a/activemodel/lib/active_model/translation.rb b/activemodel/lib/active_model/translation.rb
index 7a86701f73..8470915abb 100644
--- a/activemodel/lib/active_model/translation.rb
+++ b/activemodel/lib/active_model/translation.rb
@@ -1,6 +1,6 @@
module ActiveModel
- # == Active Model Translation
+ # == Active \Model \Translation
#
# Provides integration between your object and the Rails internationalization
# (i18n) framework.
@@ -41,7 +41,7 @@ module ActiveModel
#
# Specify +options+ with additional translating options.
def human_attribute_name(attribute, options = {})
- options = { :count => 1 }.merge!(options)
+ options = { count: 1 }.merge!(options)
parts = attribute.to_s.split(".")
attribute = parts.pop
namespace = parts.join("/") unless parts.empty?
diff --git a/activemodel/lib/active_model/validations.rb b/activemodel/lib/active_model/validations.rb
index 243d911f71..31c2245265 100644
--- a/activemodel/lib/active_model/validations.rb
+++ b/activemodel/lib/active_model/validations.rb
@@ -1,12 +1,10 @@
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/keys'
require 'active_support/core_ext/hash/except'
-require 'active_model/errors'
-require 'active_model/validations/callbacks'
module ActiveModel
- # == Active Model Validations
+ # == Active \Model Validations
#
# Provides a full validation framework to your objects.
#
@@ -32,7 +30,7 @@ module ActiveModel
# person.first_name = 'zoolander'
# person.valid? # => false
# person.invalid? # => true
- # person.errors.messages # => {:first_name=>["starts with z."]}
+ # person.errors.messages # => {first_name:["starts with z."]}
#
# Note that <tt>ActiveModel::Validations</tt> automatically adds an +errors+
# method to your instances initialized with a new <tt>ActiveModel::Errors</tt>
@@ -48,7 +46,7 @@ module ActiveModel
include HelperMethods
attr_accessor :validation_context
- define_callbacks :validate, :scope => :name
+ define_callbacks :validate, scope: :name
class_attribute :_validators
self._validators = Hash.new { |h,k| h[k] = [] }
@@ -144,7 +142,9 @@ module ActiveModel
if options.key?(:on)
options = options.dup
options[:if] = Array(options[:if])
- options[:if].unshift("validation_context == :#{options[:on]}")
+ options[:if].unshift lambda { |o|
+ o.validation_context == options[:on]
+ }
end
args << options
set_callback(:validate, *args, &block)
@@ -164,13 +164,56 @@ module ActiveModel
# Person.validators
# # => [
# # #<MyValidator:0x007fbff403e808 @options={}>,
- # # #<OtherValidator:0x007fbff403d930 @options={:on=>:create}>,
- # # #<StrictValidator:0x007fbff3204a30 @options={:strict=>true}>
+ # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
+ # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
# # ]
def validators
_validators.values.flatten.uniq
end
+ # Clears all of the validators and validations.
+ #
+ # Note that this will clear anything that is being used to validate
+ # the model for both the +validates_with+ and +validate+ methods.
+ # It clears the validators that are created with an invocation of
+ # +validates_with+ and the callbacks that are set by an invocation
+ # of +validate+.
+ #
+ # class Person
+ # include ActiveModel::Validations
+ #
+ # validates_with MyValidator
+ # validates_with OtherValidator, on: :create
+ # validates_with StrictValidator, strict: true
+ # validate :cannot_be_robot
+ #
+ # def cannot_be_robot
+ # errors.add(:base, 'A person cannot be a robot') if person_is_robot
+ # end
+ # end
+ #
+ # Person.validators
+ # # => [
+ # # #<MyValidator:0x007fbff403e808 @options={}>,
+ # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
+ # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
+ # # ]
+ #
+ # If one runs Person.clear_validators! and then checks to see what
+ # validators this class has, you would obtain:
+ #
+ # Person.validators # => []
+ #
+ # Also, the callback set by +validate :cannot_be_robot+ will be erased
+ # so that:
+ #
+ # Person._validate_callbacks.empty? # => true
+ #
+ def clear_validators!
+ reset_callbacks(:validate)
+ _validators.clear
+ end
+
# List all validators that are being used to validate a specific attribute.
#
# class Person
@@ -185,7 +228,6 @@ module ActiveModel
# Person.validators_on(:name)
# # => [
# # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
- # # #<ActiveModel::Validations::InclusionValidator:0x007fe603bb8780 @attributes=[:age], @options={:in=>0..99}>
# # ]
def validators_on(*attributes)
attributes.flat_map do |attribute|
@@ -233,7 +275,7 @@ module ActiveModel
#
# person = Person.new
# person.valid? # => false
- # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={:name=>["can't be blank"]}>
+ # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
def errors
@errors ||= Errors.new(self)
end
@@ -250,7 +292,7 @@ module ActiveModel
#
# person = Person.new
# person.name = ''
- # person.valid? # => false
+ # person.valid? # => false
# person.name = 'david'
# person.valid? # => true
#
@@ -265,7 +307,7 @@ module ActiveModel
# end
#
# person = Person.new
- # person.valid? # => true
+ # person.valid? # => true
# person.valid?(:new) # => false
def valid?(context = nil)
current_context, self.validation_context = validation_context, context
@@ -287,7 +329,7 @@ module ActiveModel
#
# person = Person.new
# person.name = ''
- # person.invalid? # => true
+ # person.invalid? # => true
# person.name = 'david'
# person.invalid? # => false
#
@@ -302,7 +344,7 @@ module ActiveModel
# end
#
# person = Person.new
- # person.invalid? # => false
+ # person.invalid? # => false
# person.invalid?(:new) # => true
def invalid?(context = nil)
!valid?(context)
@@ -335,7 +377,4 @@ module ActiveModel
end
end
-Dir[File.dirname(__FILE__) + "/validations/*.rb"].sort.each do |path|
- filename = File.basename(path)
- require "active_model/validations/#{filename}"
-end
+Dir[File.dirname(__FILE__) + "/validations/*.rb"].each { |file| require file }
diff --git a/activemodel/lib/active_model/validations/absence.rb b/activemodel/lib/active_model/validations/absence.rb
new file mode 100644
index 0000000000..1a1863370b
--- /dev/null
+++ b/activemodel/lib/active_model/validations/absence.rb
@@ -0,0 +1,31 @@
+module ActiveModel
+ module Validations
+ # == Active Model Absence Validator
+ class AbsenceValidator < EachValidator #:nodoc:
+ def validate_each(record, attr_name, value)
+ record.errors.add(attr_name, :present, options) if value.present?
+ end
+ end
+
+ module HelperMethods
+ # Validates that the specified attributes are blank (as defined by
+ # Object#blank?). Happens by default on save.
+ #
+ # class Person < ActiveRecord::Base
+ # validates_absence_of :first_name
+ # end
+ #
+ # The first_name attribute must be in the object and it must be blank.
+ #
+ # Configuration options:
+ # * <tt>:message</tt> - A custom error message (default is: "must be blank").
+ #
+ # There is also a list of default options supported by every validator:
+ # +:if+, +:unless+, +:on+ and +:strict+.
+ # See <tt>ActiveModel::Validation#validates</tt> for more information
+ def validates_absence_of(*attr_names)
+ validates_with AbsenceValidator, _merge_attributes(attr_names)
+ end
+ end
+ end
+end
diff --git a/activemodel/lib/active_model/validations/acceptance.rb b/activemodel/lib/active_model/validations/acceptance.rb
index 8d5ebf527f..139de16326 100644
--- a/activemodel/lib/active_model/validations/acceptance.rb
+++ b/activemodel/lib/active_model/validations/acceptance.rb
@@ -1,10 +1,10 @@
module ActiveModel
- # == Active Model Acceptance Validator
module Validations
- class AcceptanceValidator < EachValidator #:nodoc:
+ class AcceptanceValidator < EachValidator # :nodoc:
def initialize(options)
- super({ :allow_nil => true, :accept => "1" }.merge!(options))
+ super({ allow_nil: true, accept: "1" }.merge!(options))
+ setup!(options[:class])
end
def validate_each(record, attribute, value)
@@ -13,7 +13,8 @@ module ActiveModel
end
end
- def setup(klass)
+ private
+ def setup!(klass)
attr_readers = attributes.reject { |name| klass.attribute_method?(name) }
attr_writers = attributes.reject { |name| klass.attribute_method?("#{name}=") }
klass.send(:attr_reader, *attr_readers)
@@ -27,7 +28,7 @@ module ActiveModel
#
# class Person < ActiveRecord::Base
# validates_acceptance_of :terms_of_service
- # validates_acceptance_of :eula, message: "must be abided"
+ # validates_acceptance_of :eula, message: 'must be abided'
# end
#
# If the database column does not exist, the +terms_of_service+ attribute
diff --git a/activemodel/lib/active_model/validations/callbacks.rb b/activemodel/lib/active_model/validations/callbacks.rb
index c153ef4309..fde53b9f89 100644
--- a/activemodel/lib/active_model/validations/callbacks.rb
+++ b/activemodel/lib/active_model/validations/callbacks.rb
@@ -1,8 +1,6 @@
-require 'active_support/callbacks'
-
module ActiveModel
module Validations
- # == Active Model Validation callbacks
+ # == Active \Model Validation Callbacks
#
# Provides an interface for any class to have +before_validation+ and
# +after_validation+ callbacks.
@@ -24,7 +22,10 @@ module ActiveModel
included do
include ActiveSupport::Callbacks
- define_callbacks :validation, :terminator => "result == false", :skip_after_callbacks_if_terminated => true, :scope => [:kind, :name]
+ define_callbacks :validation,
+ terminator: ->(_,result) { result == false },
+ skip_after_callbacks_if_terminated: true,
+ scope: [:kind, :name]
end
module ClassMethods
@@ -57,7 +58,9 @@ module ActiveModel
if options.is_a?(Hash) && options[:on]
options[:if] = Array(options[:if])
options[:on] = Array(options[:on])
- options[:if].unshift("#{options[:on]}.include? self.validation_context")
+ options[:if].unshift lambda { |o|
+ options[:on].include? o.validation_context
+ }
end
set_callback(:validation, :before, *args, &block)
end
@@ -85,8 +88,8 @@ module ActiveModel
# person = Person.new
# person.name = ''
# person.valid? # => false
- # person.status # => false
- #  person.name = 'bob'
+ # person.status # => false
+ # person.name = 'bob'
# person.valid? # => true
# person.status # => true
def after_validation(*args, &block)
diff --git a/activemodel/lib/active_model/validations/clusivity.rb b/activemodel/lib/active_model/validations/clusivity.rb
index 3d7067fbcb..fd6cc1edb4 100644
--- a/activemodel/lib/active_model/validations/clusivity.rb
+++ b/activemodel/lib/active_model/validations/clusivity.rb
@@ -3,7 +3,7 @@ require 'active_support/core_ext/range'
module ActiveModel
module Validations
module Clusivity #:nodoc:
- ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " <<
+ ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
"and must be supplied as the :in (or :within) option of the configuration hash"
def check_validity!
@@ -15,26 +15,33 @@ module ActiveModel
private
def include?(record, value)
- exclusions = if delimiter.respond_to?(:call)
- delimiter.call(record)
- elsif delimiter.respond_to?(:to_sym)
- record.send(delimiter)
- else
- delimiter
- end
+ members = if delimiter.respond_to?(:call)
+ delimiter.call(record)
+ elsif delimiter.respond_to?(:to_sym)
+ record.send(delimiter)
+ else
+ delimiter
+ end
- exclusions.send(inclusion_method(exclusions), value)
+ members.send(inclusion_method(members), value)
end
def delimiter
@delimiter ||= options[:in] || options[:within]
end
- # In Ruby 1.9 <tt>Range#include?</tt> on non-numeric ranges checks all possible values in the
- # range for equality, so it may be slow for large ranges. The new <tt>Range#cover?</tt>
- # uses the previous logic of comparing a value with the range endpoints.
+ # In Ruby 1.9 <tt>Range#include?</tt> on non-number-or-time-ish ranges checks all
+ # possible values in the range for equality, which is slower but more accurate.
+ # <tt>Range#cover?</tt> uses the previous logic of comparing a value with the range
+ # endpoints, which is fast but is only accurate on Numeric, Time, or DateTime ranges.
def inclusion_method(enumerable)
- enumerable.is_a?(Range) ? :cover? : :include?
+ return :include? unless enumerable.is_a?(Range)
+ case enumerable.first
+ when Numeric, Time, DateTime
+ :cover?
+ else
+ :include?
+ end
end
end
end
diff --git a/activemodel/lib/active_model/validations/confirmation.rb b/activemodel/lib/active_model/validations/confirmation.rb
index baa034eca6..b0542661af 100644
--- a/activemodel/lib/active_model/validations/confirmation.rb
+++ b/activemodel/lib/active_model/validations/confirmation.rb
@@ -1,19 +1,28 @@
module ActiveModel
- # == Active Model Confirmation Validator
module Validations
- class ConfirmationValidator < EachValidator #:nodoc:
+ class ConfirmationValidator < EachValidator # :nodoc:
+ def initialize(options)
+ super
+ setup!(options[:class])
+ end
+
def validate_each(record, attribute, value)
if (confirmed = record.send("#{attribute}_confirmation")) && (value != confirmed)
human_attribute_name = record.class.human_attribute_name(attribute)
- record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(:attribute => human_attribute_name))
+ record.errors.add(:"#{attribute}_confirmation", :confirmation, options.merge(attribute: human_attribute_name))
end
end
- def setup(klass)
- klass.send(:attr_accessor, *attributes.map do |attribute|
+ private
+ def setup!(klass)
+ klass.send(:attr_reader, *attributes.map do |attribute|
:"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
end.compact)
+
+ klass.send(:attr_writer, *attributes.map do |attribute|
+ :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
+ end.compact)
end
end
diff --git a/activemodel/lib/active_model/validations/exclusion.rb b/activemodel/lib/active_model/validations/exclusion.rb
index 3ec552c372..48bf5cd802 100644
--- a/activemodel/lib/active_model/validations/exclusion.rb
+++ b/activemodel/lib/active_model/validations/exclusion.rb
@@ -2,14 +2,13 @@ require "active_model/validations/clusivity"
module ActiveModel
- # == Active Model Exclusion Validator
module Validations
- class ExclusionValidator < EachValidator #:nodoc:
+ class ExclusionValidator < EachValidator # :nodoc:
include Clusivity
def validate_each(record, attribute, value)
if include?(record, value)
- record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(:value => value))
+ record.errors.add(attribute, :exclusion, options.except(:in, :within).merge!(value: value))
end
end
end
diff --git a/activemodel/lib/active_model/validations/format.rb b/activemodel/lib/active_model/validations/format.rb
index 80150229a0..be7cae588f 100644
--- a/activemodel/lib/active_model/validations/format.rb
+++ b/activemodel/lib/active_model/validations/format.rb
@@ -1,8 +1,7 @@
module ActiveModel
- # == Active Model Format Validator
module Validations
- class FormatValidator < EachValidator #:nodoc:
+ class FormatValidator < EachValidator # :nodoc:
def validate_each(record, attribute, value)
if options[:with]
regexp = option_call(record, :with)
@@ -30,7 +29,7 @@ module ActiveModel
end
def record_error(record, attribute, name, value)
- record.errors.add(attribute, :invalid, options.except(name).merge!(:value => value))
+ record.errors.add(attribute, :invalid, options.except(name).merge!(value: value))
end
def regexp_using_multiline_anchors?(regexp)
diff --git a/activemodel/lib/active_model/validations/inclusion.rb b/activemodel/lib/active_model/validations/inclusion.rb
index babc8982da..24337614c5 100644
--- a/activemodel/lib/active_model/validations/inclusion.rb
+++ b/activemodel/lib/active_model/validations/inclusion.rb
@@ -2,14 +2,13 @@ require "active_model/validations/clusivity"
module ActiveModel
- # == Active Model Inclusion Validator
module Validations
- class InclusionValidator < EachValidator #:nodoc:
+ class InclusionValidator < EachValidator # :nodoc:
include Clusivity
def validate_each(record, attribute, value)
unless include?(record, value)
- record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(:value => value))
+ record.errors.add(attribute, :inclusion, options.except(:in, :within).merge!(value: value))
end
end
end
@@ -29,7 +28,7 @@ module ActiveModel
# Configuration options:
# * <tt>:in</tt> - An enumerable object of available items. This can be
# supplied as a proc, lambda or symbol which returns an enumerable. If the
- # enumerable is a range the test is performed with <tt>Range#cover?</tt>,
+ # enumerable is a numerical range the test is performed with <tt>Range#cover?</tt>,
# otherwise with <tt>include?</tt>.
# * <tt>:within</tt> - A synonym(or alias) for <tt>:in</tt>
# * <tt>:message</tt> - Specifies a custom error message (default is: "is
diff --git a/activemodel/lib/active_model/validations/length.rb b/activemodel/lib/active_model/validations/length.rb
index e4a1f9e80a..ddfd8a342e 100644
--- a/activemodel/lib/active_model/validations/length.rb
+++ b/activemodel/lib/active_model/validations/length.rb
@@ -1,10 +1,10 @@
module ActiveModel
- # == Active Model Length Validator
+ # == Active \Model Length \Validator
module Validations
- class LengthValidator < EachValidator #:nodoc:
- MESSAGES = { :is => :wrong_length, :minimum => :too_short, :maximum => :too_long }.freeze
- CHECKS = { :is => :==, :minimum => :>=, :maximum => :<= }.freeze
+ class LengthValidator < EachValidator # :nodoc:
+ MESSAGES = { is: :wrong_length, minimum: :too_short, maximum: :too_long }.freeze
+ CHECKS = { is: :==, minimum: :>=, maximum: :<= }.freeze
RESERVED_OPTIONS = [:minimum, :maximum, :within, :is, :tokenizer, :too_short, :too_long]
@@ -14,6 +14,10 @@ module ActiveModel
options[:minimum], options[:maximum] = range.min, range.max
end
+ if options[:allow_blank] == false && options[:minimum].nil? && options[:is].nil?
+ options[:minimum] = 1
+ end
+
super
end
@@ -37,10 +41,13 @@ module ActiveModel
value = tokenize(value)
value_length = value.respond_to?(:length) ? value.length : value.to_s.length
errors_options = options.except(*RESERVED_OPTIONS)
-
+
CHECKS.each do |key, validity_check|
next unless check_value = options[key]
- next if value_length.send(validity_check, check_value)
+
+ if !value.nil? || skip_nil_check?(key)
+ next if value_length.send(validity_check, check_value)
+ end
errors_options[:count] = check_value
@@ -58,6 +65,10 @@ module ActiveModel
options[:tokenizer].call(value)
end || value
end
+
+ def skip_nil_check?(key)
+ key == :maximum && options[:allow_nil].nil? && options[:allow_blank].nil?
+ end
end
module HelperMethods
@@ -79,7 +90,8 @@ module ActiveModel
#
# Configuration options:
# * <tt>:minimum</tt> - The minimum size of the attribute.
- # * <tt>:maximum</tt> - The maximum size of the attribute.
+ # * <tt>:maximum</tt> - The maximum size of the attribute. Allows +nil+ by
+ # default if not used with :minimum.
# * <tt>:is</tt> - The exact size of the attribute.
# * <tt>:within</tt> - A range specifying the minimum and maximum size of
# the attribute.
diff --git a/activemodel/lib/active_model/validations/numericality.rb b/activemodel/lib/active_model/validations/numericality.rb
index edebca94a8..c6abe45f4a 100644
--- a/activemodel/lib/active_model/validations/numericality.rb
+++ b/activemodel/lib/active_model/validations/numericality.rb
@@ -1,11 +1,10 @@
module ActiveModel
- # == Active Model Numericality Validator
module Validations
- class NumericalityValidator < EachValidator #:nodoc:
- CHECKS = { :greater_than => :>, :greater_than_or_equal_to => :>=,
- :equal_to => :==, :less_than => :<, :less_than_or_equal_to => :<=,
- :odd => :odd?, :even => :even?, :other_than => :!= }.freeze
+ class NumericalityValidator < EachValidator # :nodoc:
+ CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
+ equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
+ odd: :odd?, even: :even?, other_than: :!= }.freeze
RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
@@ -18,9 +17,9 @@ module ActiveModel
end
def validate_each(record, attr_name, value)
- before_type_cast = "#{attr_name}_before_type_cast"
+ before_type_cast = :"#{attr_name}_before_type_cast"
- raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast.to_sym)
+ raw_value = record.send(before_type_cast) if record.respond_to?(before_type_cast)
raw_value ||= value
return if options[:allow_nil] && raw_value.nil?
@@ -48,7 +47,7 @@ module ActiveModel
option_value = record.send(option_value) if option_value.is_a?(Symbol)
unless value.send(CHECKS[option], option_value)
- record.errors.add(attr_name, option, filtered_options(value).merge(:count => option_value))
+ record.errors.add(attr_name, option, filtered_options(value).merge(count: option_value))
end
end
end
@@ -74,7 +73,7 @@ module ActiveModel
end
def filtered_options(value)
- options.except(*RESERVED_OPTIONS).merge!(:value => value)
+ options.except(*RESERVED_OPTIONS).merge!(value: value)
end
end
@@ -126,7 +125,7 @@ module ActiveModel
# For example:
#
# class Person < ActiveRecord::Base
- # validates_numericality_of :width, less_than: Proc.new { |person| person.height }
+ # validates_numericality_of :width, less_than: ->(person) { person.height }
# validates_numericality_of :width, greater_than: :minimum_weight
# end
def validates_numericality_of(*attr_names)
diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb
index f159e40858..ab8c8359fc 100644
--- a/activemodel/lib/active_model/validations/presence.rb
+++ b/activemodel/lib/active_model/validations/presence.rb
@@ -1,11 +1,10 @@
module ActiveModel
- # == Active Model Presence Validator
module Validations
- class PresenceValidator < EachValidator #:nodoc:
- def validate(record)
- record.errors.add_on_blank(attributes, options)
+ class PresenceValidator < EachValidator # :nodoc:
+ def validate_each(record, attr_name, value)
+ record.errors.add(attr_name, :blank, options) if value.blank?
end
end
diff --git a/activemodel/lib/active_model/validations/validates.rb b/activemodel/lib/active_model/validations/validates.rb
index 03046a543a..9a1ff2ad39 100644
--- a/activemodel/lib/active_model/validations/validates.rb
+++ b/activemodel/lib/active_model/validations/validates.rb
@@ -1,7 +1,6 @@
require 'active_support/core_ext/hash/slice'
module ActiveModel
- # == Active Model validates method
module Validations
module ClassMethods
# This method is a shortcut to all default validators and any custom
@@ -105,7 +104,7 @@ module ActiveModel
raise ArgumentError, "You need to supply at least one attribute" if attributes.empty?
raise ArgumentError, "You need to supply at least one validation" if validations.empty?
- defaults.merge!(:attributes => attributes)
+ defaults[:attributes] = attributes
validations.each do |key, options|
next unless options
@@ -129,15 +128,15 @@ module ActiveModel
# the validation itself.
#
# class Person
- #  include ActiveModel::Validations
+ # include ActiveModel::Validations
#
# attr_accessor :name
# validates! :name, presence: true
# end
#
# person = Person.new
- #  person.name = ''
- #  person.valid?
+ # person.name = ''
+ # person.valid?
# # => ActiveModel::StrictValidationFailed: Name can't be blank
def validates!(*attributes)
options = attributes.extract_options!
@@ -149,20 +148,20 @@ module ActiveModel
# When creating custom validators, it might be useful to be able to specify
# additional default keys. This can be done by overwriting this method.
- def _validates_default_keys #:nodoc:
+ def _validates_default_keys # :nodoc:
[:if, :unless, :on, :allow_blank, :allow_nil , :strict]
end
- def _parse_validates_options(options) #:nodoc:
+ def _parse_validates_options(options) # :nodoc:
case options
when TrueClass
{}
when Hash
options
when Range, Array
- { :in => options }
+ { in: options }
else
- { :with => options }
+ { with: options }
end
end
end
diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb
index 869591cd9e..16bd6670d1 100644
--- a/activemodel/lib/active_model/validations/with.rb
+++ b/activemodel/lib/active_model/validations/with.rb
@@ -10,7 +10,7 @@ module ActiveModel
end
end
- class WithValidator < EachValidator #:nodoc:
+ class WithValidator < EachValidator # :nodoc:
def validate_each(record, attr, val)
method_name = options[:with]
@@ -83,9 +83,10 @@ module ActiveModel
# end
def validates_with(*args, &block)
options = args.extract_options!
+ options[:class] = self
+
args.each do |klass|
validator = klass.new(options, &block)
- validator.setup(self) if validator.respond_to?(:setup)
if validator.respond_to?(:attributes) && !validator.attributes.empty?
validator.attributes.each do |attribute|
diff --git a/activemodel/lib/active_model/validator.rb b/activemodel/lib/active_model/validator.rb
index 85aec00f25..690856aee1 100644
--- a/activemodel/lib/active_model/validator.rb
+++ b/activemodel/lib/active_model/validator.rb
@@ -2,7 +2,7 @@ require "active_support/core_ext/module/anonymous"
module ActiveModel
- # == Active Model Validator
+ # == Active \Model \Validator
#
# A simple base class that can be used along with
# ActiveModel::Validations::ClassMethods.validates_with
@@ -60,6 +60,9 @@ module ActiveModel
# end
# end
#
+ # Note that the validator is initialized only once for the whole application
+ # lifecycle, and not on each validation run.
+ #
# The easiest way to add custom validators for validating individual attributes
# is with the convenient <tt>ActiveModel::EachValidator</tt>.
#
@@ -76,21 +79,19 @@ module ActiveModel
# include ActiveModel::Validations
# attr_accessor :title
#
- # validates :title, :presence => true
+ # validates :title, presence: true
# end
#
- # Validator may also define a +setup+ instance method which will get called
- # with the class that using that validator as its argument. This can be
- # useful when there are prerequisites such as an +attr_accessor+ being present.
+ # It can be useful to access the class that is using that validator when there are prerequisites such
+ # as an +attr_accessor+ being present. This class is accessable via +options[:class]+ in the constructor.
+ # To setup your validator override the constructor.
#
# class MyValidator < ActiveModel::Validator
- # def setup(klass)
- # klass.send :attr_accessor, :custom_attribute
+ # def initialize(options={})
+ # super
+ # options[:class].send :attr_accessor, :custom_attribute
# end
# end
- #
- # This setup method is only called when used with validation macros or the
- # class level <tt>validates_with</tt> method.
class Validator
attr_reader :options
@@ -103,14 +104,15 @@ module ActiveModel
end
# Accepts options that will be made available through the +options+ reader.
- def initialize(options)
- @options = options.freeze
+ def initialize(options = {})
+ @options = options.except(:class).freeze
+ deprecated_setup(options)
end
# Return the kind for this validator.
#
- # PresenceValidator.new.kind # => :presence
- # UniquenessValidator.new.kind # => :uniqueness 
+ # PresenceValidator.new.kind # => :presence
+ # UniquenessValidator.new.kind # => :uniqueness
def kind
self.class.kind
end
@@ -120,6 +122,21 @@ 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
@@ -135,7 +152,7 @@ module ActiveModel
# and instead be made available through the +attributes+ reader.
def initialize(options)
@attributes = Array(options.delete(:attributes))
- raise ":attributes cannot be blank" if @attributes.empty?
+ raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
super
check_validity!
end
diff --git a/activemodel/lib/active_model/version.rb b/activemodel/lib/active_model/version.rb
index e195c12a4d..86340bba37 100644
--- a/activemodel/lib/active_model/version.rb
+++ b/activemodel/lib/active_model/version.rb
@@ -1,10 +1,11 @@
module ActiveModel
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActiveModel as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActiveModel.version.segments
+ STRING = ActiveModel.version.to_s
end
end
diff --git a/activemodel/test/cases/attribute_methods_test.rb b/activemodel/test/cases/attribute_methods_test.rb
index baaf842222..e9cb5ccc96 100644
--- a/activemodel/test/cases/attribute_methods_test.rb
+++ b/activemodel/test/cases/attribute_methods_test.rb
@@ -10,7 +10,7 @@ class ModelWithAttributes
end
def attributes
- { :foo => 'value of foo', :baz => 'value of baz' }
+ { foo: 'value of foo', baz: 'value of baz' }
end
private
@@ -80,7 +80,7 @@ class ModelWithRubyKeywordNamedAttributes
include ActiveModel::AttributeMethods
def attributes
- { :begin => 'value of begin', :end => 'value of end' }
+ { begin: 'value of begin', end: 'value of end' }
end
private
@@ -194,7 +194,7 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert_raises(NoMethodError) { ModelWithAttributes.new.foo }
end
- test 'acessing a suffixed attribute' do
+ test 'accessing a suffixed attribute' do
m = ModelWithAttributes2.new
m.attributes = { 'foo' => 'bar' }
@@ -202,17 +202,6 @@ class AttributeMethodsTest < ActiveModel::TestCase
assert_equal 'bar', m.foo_test
end
- test 'explicitly specifying an empty prefix/suffix is deprecated' do
- klass = Class.new(ModelWithAttributes)
-
- assert_deprecated { klass.attribute_method_suffix '' }
- assert_deprecated { klass.attribute_method_prefix '' }
-
- klass.define_attribute_methods(:foo)
-
- assert_equal 'value of foo', klass.new.foo
- end
-
test 'should not interfere with method_missing if the attr has a private/protected method' do
m = ModelWithAttributes2.new
m.attributes = { 'private_method' => '<3', 'protected_method' => 'O_o' }
diff --git a/activemodel/test/cases/callbacks_test.rb b/activemodel/test/cases/callbacks_test.rb
index 086e7266ff..5fede098d1 100644
--- a/activemodel/test/cases/callbacks_test.rb
+++ b/activemodel/test/cases/callbacks_test.rb
@@ -15,9 +15,9 @@ class CallbacksTest < ActiveModel::TestCase
extend ActiveModel::Callbacks
define_model_callbacks :create
- define_model_callbacks :initialize, :only => :after
- define_model_callbacks :multiple, :only => [:before, :around]
- define_model_callbacks :empty, :only => []
+ define_model_callbacks :initialize, only: :after
+ define_model_callbacks :multiple, only: [:before, :around]
+ define_model_callbacks :empty, only: []
before_create :before_create
around_create CallbackValidator.new
@@ -107,7 +107,7 @@ class CallbacksTest < ActiveModel::TestCase
test "after_create callbacks with both callbacks declared in one line" do
assert_equal ["callback1", "callback2"], Violin1.new.create.history
end
- test "after_create callbacks with both callbacks declared in differnt lines" do
+ test "after_create callbacks with both callbacks declared in different lines" do
assert_equal ["callback1", "callback2"], Violin2.new.create.history
end
diff --git a/activemodel/test/cases/conversion_test.rb b/activemodel/test/cases/conversion_test.rb
index d679ad41aa..3bb177591d 100644
--- a/activemodel/test/cases/conversion_test.rb
+++ b/activemodel/test/cases/conversion_test.rb
@@ -13,7 +13,7 @@ class ConversionTest < ActiveModel::TestCase
end
test "to_key default implementation returns the id in an array for persisted records" do
- assert_equal [1], Contact.new(:id => 1).to_key
+ assert_equal [1], Contact.new(id: 1).to_key
end
test "to_param default implementation returns nil for new records" do
@@ -21,7 +21,7 @@ class ConversionTest < ActiveModel::TestCase
end
test "to_param default implementation returns a string of ids for persisted records" do
- assert_equal "1", Contact.new(:id => 1).to_param
+ assert_equal "1", Contact.new(id: 1).to_param
end
test "to_partial_path default implementation returns a string giving a relative path" do
@@ -29,4 +29,8 @@ class ConversionTest < ActiveModel::TestCase
assert_equal "helicopters/helicopter", Helicopter.new.to_partial_path,
"ActiveModel::Conversion#to_partial_path caching should be class-specific"
end
+
+ test "to_partial_path handles namespaced models" do
+ assert_equal "helicopter/comanches/comanche", Helicopter::Comanche.new.to_partial_path
+ end
end
diff --git a/activemodel/test/cases/deprecated_mass_assignment_security_test.rb b/activemodel/test/cases/deprecated_mass_assignment_security_test.rb
deleted file mode 100644
index c1fe8822cd..0000000000
--- a/activemodel/test/cases/deprecated_mass_assignment_security_test.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'cases/helper'
-require 'models/project'
-
-class DeprecatedMassAssignmentSecurityTest < ActiveModel::TestCase
- def test_attr_accessible_raise_error
- assert_raise RuntimeError, /protected_attributes/ do
- Project.attr_accessible :username
- end
- end
-
- def test_attr_protected_raise_error
- assert_raise RuntimeError, /protected_attributes/ do
- Project.attr_protected :username
- end
- end
-end
diff --git a/activemodel/test/cases/dirty_test.rb b/activemodel/test/cases/dirty_test.rb
index eaaf910bac..a90d0b1299 100644
--- a/activemodel/test/cases/dirty_test.rb
+++ b/activemodel/test/cases/dirty_test.rb
@@ -3,11 +3,12 @@ require "cases/helper"
class DirtyTest < ActiveModel::TestCase
class DirtyModel
include ActiveModel::Dirty
- define_attribute_methods :name, :color
+ define_attribute_methods :name, :color, :size
def initialize
@name = nil
@color = nil
+ @size = nil
end
def name
@@ -28,9 +29,17 @@ class DirtyTest < ActiveModel::TestCase
@color = val
end
+ def size
+ @size
+ end
+
+ def size=(val)
+ attribute_will_change!(:size) unless val == @size
+ @size = val
+ end
+
def save
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
@@ -46,7 +55,7 @@ class DirtyTest < ActiveModel::TestCase
assert @model.name_changed?
end
- test "list of changed attributes" do
+ test "list of changed attribute keys" do
assert_equal [], @model.changed
@model.name = "Paul"
assert_equal ['name'], @model.changed
@@ -78,7 +87,7 @@ class DirtyTest < ActiveModel::TestCase
@model.name = "Bob"
@model.reset_name!
assert_nil @model.name
- #assert !@model.name_changed #Doesn't work yet
+ assert !@model.name_changed?
end
test "setting color to same value should not result in change being recorded" do
@@ -106,6 +115,17 @@ class DirtyTest < ActiveModel::TestCase
assert_equal [nil, "Jericho Cane"], @model.previous_changes['name']
end
+ test "previous value is preserved when changed after save" do
+ assert_equal({}, @model.changed_attributes)
+ @model.name = "Paul"
+ assert_equal({ "name" => nil }, @model.changed_attributes)
+
+ @model.save
+
+ @model.name = "John"
+ assert_equal({ "name" => "Paul" }, @model.changed_attributes)
+ end
+
test "changing the same attribute multiple times retains the correct original value" do
@model.name = "Otto"
@model.save
@@ -115,4 +135,8 @@ class DirtyTest < ActiveModel::TestCase
assert_equal @model.name_was, "Otto"
end
+ test "using attribute_will_change! with a symbol" do
+ @model.size = 1
+ assert @model.size_changed?
+ end
end
diff --git a/activemodel/test/cases/errors_test.rb b/activemodel/test/cases/errors_test.rb
index 3bc0d58351..4e07e0e00b 100644
--- a/activemodel/test/cases/errors_test.rb
+++ b/activemodel/test/cases/errors_test.rb
@@ -7,7 +7,7 @@ class ErrorsTest < ActiveModel::TestCase
@errors = ActiveModel::Errors.new(self)
end
- attr_accessor :name
+ attr_accessor :name, :age
attr_reader :errors
def validate!
@@ -54,7 +54,46 @@ class ErrorsTest < ActiveModel::TestCase
assert errors.has_key?(:foo), 'errors should have key :foo'
end
- test "should return true if no errors" do
+ test "clear errors" do
+ person = Person.new
+ person.validate!
+
+ assert_equal 1, person.errors.count
+ person.errors.clear
+ assert person.errors.empty?
+ end
+
+ test "get returns the errors for the provided key" do
+ errors = ActiveModel::Errors.new(self)
+ errors[:foo] = "omg"
+
+ assert_equal ["omg"], errors.get(:foo)
+ end
+
+ test "sets the error with the provided key" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+
+ assert_equal({ foo: "omg" }, errors.messages)
+ end
+
+ test "values returns an array of messages" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+ errors.set(:baz, "zomg")
+
+ assert_equal ["omg", "zomg"], errors.values
+ end
+
+ test "keys returns the error keys" do
+ errors = ActiveModel::Errors.new(self)
+ errors.set(:foo, "omg")
+ errors.set(:baz, "zomg")
+
+ assert_equal [:foo, :baz], errors.keys
+ end
+
+ test "detecting whether there are errors with empty?, blank?, include?" do
person = Person.new
person.errors[:foo]
assert person.errors.empty?
@@ -62,144 +101,203 @@ class ErrorsTest < ActiveModel::TestCase
assert !person.errors.include?(:foo)
end
- test "method validate! should work" do
+ test "adding errors using conditionals with Person#validate!" do
person = Person.new
person.validate!
assert_equal ["name can not be nil"], person.errors.full_messages
assert_equal ["can not be nil"], person.errors[:name]
end
- test 'should be able to assign error' do
+ test "assign error" do
person = Person.new
person.errors[:name] = 'should not be nil'
assert_equal ["should not be nil"], person.errors[:name]
end
- test 'should be able to add an error on an attribute' do
+ test "add an error message on a specific attribute" do
person = Person.new
person.errors.add(:name, "can not be blank")
assert_equal ["can not be blank"], person.errors[:name]
end
- test "should be able to add an error with a symbol" do
+ test "add an error with a symbol" do
person = Person.new
person.errors.add(:name, :blank)
message = person.errors.generate_message(:name, :blank)
assert_equal [message], person.errors[:name]
end
- test "should be able to add an error with a proc" do
+ test "add an error with a proc" do
person = Person.new
message = Proc.new { "can not be blank" }
person.errors.add(:name, message)
assert_equal ["can not be blank"], person.errors[:name]
end
- test "added? should be true if that error was added" do
+ test "added? detects if a specific error was added to the object" do
person = Person.new
person.errors.add(:name, "can not be blank")
assert person.errors.added?(:name, "can not be blank")
end
- test "added? should handle when message is a symbol" do
+ test "added? handles symbol message" do
person = Person.new
person.errors.add(:name, :blank)
assert person.errors.added?(:name, :blank)
end
- test "added? should handle when message is a proc" do
+ test "added? handles proc messages" do
person = Person.new
message = Proc.new { "can not be blank" }
person.errors.add(:name, message)
assert person.errors.added?(:name, message)
end
- test "added? should default message to :invalid" do
+ test "added? defaults message to :invalid" do
person = Person.new
- person.errors.add(:name, :invalid)
+ person.errors.add(:name)
assert person.errors.added?(:name)
end
- test "added? should be true when several errors are present, and we ask for one of them" do
+ test "added? matches the given message when several errors are present for the same attribute" do
person = Person.new
person.errors.add(:name, "can not be blank")
person.errors.add(:name, "is invalid")
assert person.errors.added?(:name, "can not be blank")
end
- test "added? should be false if no errors are present" do
+ test "added? returns false when no errors are present" do
person = Person.new
assert !person.errors.added?(:name)
end
- test "added? should be false when an error is present, but we check for another error" do
+ test "added? returns false when checking a nonexisting error and other errors are present for the given attribute" do
person = Person.new
person.errors.add(:name, "is invalid")
assert !person.errors.added?(:name, "can not be blank")
end
- test 'should respond to size' do
+ test "size calculates the number of error messages" do
person = Person.new
person.errors.add(:name, "can not be blank")
assert_equal 1, person.errors.size
end
- test 'to_a should return an array' do
+ test "to_a returns the list of errors with complete messages containing the attribute names" do
person = Person.new
person.errors.add(:name, "can not be blank")
person.errors.add(:name, "can not be nil")
assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a
end
- test 'to_hash should return a hash' do
+ test "to_hash returns the error messages hash" do
person = Person.new
person.errors.add(:name, "can not be blank")
- assert_instance_of ::Hash, person.errors.to_hash
+ assert_equal({ name: ["can not be blank"] }, person.errors.to_hash)
end
- test 'full_messages should return an array of error messages, with the attribute name included' do
+ test "full_messages creates a list of error messages with the attribute name included" do
person = Person.new
person.errors.add(:name, "can not be blank")
person.errors.add(:name, "can not be nil")
- assert_equal ["name can not be blank", "name can not be nil"], person.errors.to_a
+ assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages
+ end
+
+ test "full_messages_for contains all the error messages for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "can not be blank")
+ person.errors.add(:name, "can not be nil")
+ assert_equal ["name can not be blank", "name can not be nil"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for does not contain error messages from other attributes" do
+ person = Person.new
+ person.errors.add(:name, "can not be blank")
+ person.errors.add(:email, "can not be blank")
+ assert_equal ["name can not be blank"], person.errors.full_messages_for(:name)
+ end
+
+ test "full_messages_for returns an empty list in case there are no errors for the given attribute" do
+ person = Person.new
+ person.errors.add(:name, "can not be blank")
+ assert_equal [], person.errors.full_messages_for(:email)
end
- test 'full_message should return the given message if attribute equals :base' do
+ test "full_message returns the given message when attribute is :base" do
person = Person.new
assert_equal "press the button", person.errors.full_message(:base, "press the button")
end
- test 'full_message should return the given message with the attribute name included' do
+ test "full_message returns the given message with the attribute name included" do
person = Person.new
assert_equal "name can not be blank", person.errors.full_message(:name, "can not be blank")
+ assert_equal "name_test can not be blank", person.errors.full_message(:name_test, "can not be blank")
end
- test 'should return a JSON hash representation of the errors' do
+ test "as_json creates a json formatted representation of the errors hash" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:name, "can not be nil")
- person.errors.add(:email, "is invalid")
- hash = person.errors.as_json
- assert_equal ["can not be blank", "can not be nil"], hash[:name]
- assert_equal ["is invalid"], hash[:email]
+ person.validate!
+
+ assert_equal({ name: ["can not be nil"] }, person.errors.as_json)
end
- test 'should return a JSON hash representation of the errors with full messages' do
+ test "as_json with :full_messages option creates a json formatted representation of the errors containing complete messages" do
person = Person.new
- person.errors.add(:name, "can not be blank")
- person.errors.add(:name, "can not be nil")
- person.errors.add(:email, "is invalid")
- hash = person.errors.as_json(:full_messages => true)
- assert_equal ["name can not be blank", "name can not be nil"], hash[:name]
- assert_equal ["email is invalid"], hash[:email]
+ person.validate!
+
+ assert_equal({ name: ["name can not be nil"] }, person.errors.as_json(full_messages: true))
end
- test "generate_message should work without i18n_scope" do
+ test "generate_message works without i18n_scope" do
person = Person.new
assert !Person.respond_to?(:i18n_scope)
assert_nothing_raised {
person.errors.generate_message(:name, :blank)
}
end
-end
+ test "add_on_empty generates message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.add_on_empty :name
+ end
+
+ test "add_on_empty generates message for multiple attributes" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.expects(:generate_message).with(:age, :empty, {})
+ person.errors.add_on_empty [:name, :age]
+ end
+
+ test "add_on_empty generates message with custom default message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :empty, { message: 'custom' })
+ person.errors.add_on_empty :name, message: 'custom'
+ end
+
+ test "add_on_empty generates message with empty string value" do
+ person = Person.new
+ person.name = ''
+ person.errors.expects(:generate_message).with(:name, :empty, {})
+ person.errors.add_on_empty :name
+ end
+
+ test "add_on_blank generates message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, {})
+ person.errors.add_on_blank :name
+ end
+
+ test "add_on_blank generates message for multiple attributes" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, {})
+ person.errors.expects(:generate_message).with(:age, :blank, {})
+ person.errors.add_on_blank [:name, :age]
+ end
+
+ test "add_on_blank generates message with custom default message" do
+ person = Person.new
+ person.errors.expects(:generate_message).with(:name, :blank, { message: 'custom' })
+ person.errors.add_on_blank :name, message: 'custom'
+ end
+end
diff --git a/activemodel/test/cases/helper.rb b/activemodel/test/cases/helper.rb
index 7d6f11b5a5..7a63674757 100644
--- a/activemodel/test/cases/helper.rb
+++ b/activemodel/test/cases/helper.rb
@@ -7,4 +7,4 @@ require 'active_support/core_ext/string/access'
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
diff --git a/activemodel/test/cases/model_test.rb b/activemodel/test/cases/model_test.rb
index d93fd96b88..ee0fa26546 100644
--- a/activemodel/test/cases/model_test.rb
+++ b/activemodel/test/cases/model_test.rb
@@ -3,7 +3,30 @@ require 'cases/helper'
class ModelTest < ActiveModel::TestCase
include ActiveModel::Lint::Tests
+ module DefaultValue
+ def self.included(klass)
+ klass.class_eval { attr_accessor :hello }
+ end
+
+ def initialize(*args)
+ @attr ||= 'default value'
+ super
+ end
+ end
+
class BasicModel
+ include DefaultValue
+ include ActiveModel::Model
+ attr_accessor :attr
+ end
+
+ class BasicModelWithReversedMixins
+ include ActiveModel::Model
+ include DefaultValue
+ attr_accessor :attr
+ end
+
+ class SimpleModel
include ActiveModel::Model
attr_accessor :attr
end
@@ -13,14 +36,40 @@ class ModelTest < ActiveModel::TestCase
end
def test_initialize_with_params
- object = BasicModel.new(:attr => "value")
- assert_equal object.attr, "value"
+ object = BasicModel.new(attr: "value")
+ assert_equal "value", object.attr
+ end
+
+ def test_initialize_with_params_and_mixins_reversed
+ object = BasicModelWithReversedMixins.new(attr: "value")
+ assert_equal "value", object.attr
end
def test_initialize_with_nil_or_empty_hash_params_does_not_explode
assert_nothing_raised do
BasicModel.new()
+ BasicModel.new(nil)
BasicModel.new({})
+ SimpleModel.new(attr: 'value')
end
end
+
+ def test_persisted_is_always_false
+ object = BasicModel.new(attr: "value")
+ assert object.persisted? == false
+ end
+
+ def test_mixin_inclusion_chain
+ object = BasicModel.new
+ assert_equal 'default value', object.attr
+ end
+
+ def test_mixin_initializer_when_args_exist
+ object = BasicModel.new(hello: 'world')
+ assert_equal 'world', object.hello
+ end
+
+ def test_mixin_initializer_when_args_dont_exist
+ assert_raises(NoMethodError) { SimpleModel.new(hello: 'world') }
+ end
end
diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb
index 49d8706ac2..aa683f4152 100644
--- a/activemodel/test/cases/naming_test.rb
+++ b/activemodel/test/cases/naming_test.rb
@@ -29,6 +29,14 @@ class NamingTest < ActiveModel::TestCase
assert_equal 'Track back', @model_name.human
end
+ def test_route_key
+ assert_equal 'post_track_backs', @model_name.route_key
+ end
+
+ def test_param_key
+ assert_equal 'post_track_back', @model_name.param_key
+ end
+
def test_i18n_key
assert_equal :"post/track_back", @model_name.i18n_key
end
@@ -237,7 +245,7 @@ class NamingHelpersTest < ActiveModel::TestCase
end
def test_uncountable
- assert uncountable?(@uncountable), "Expected 'sheep' to be uncoutable"
+ assert uncountable?(@uncountable), "Expected 'sheep' to be uncountable"
assert !uncountable?(@klass), "Expected 'contact' to be countable"
end
diff --git a/activemodel/test/cases/observer_array_test.rb b/activemodel/test/cases/observer_array_test.rb
deleted file mode 100644
index fc5f18008b..0000000000
--- a/activemodel/test/cases/observer_array_test.rb
+++ /dev/null
@@ -1,220 +0,0 @@
-require 'cases/helper'
-require 'models/observers'
-
-class ObserverArrayTest < ActiveModel::TestCase
- def teardown
- ORM.observers.enable :all
- Budget.observers.enable :all
- Widget.observers.enable :all
- end
-
- def assert_observer_notified(model_class, observer_class)
- observer_class.instance.before_save_invocations.clear
- model_instance = model_class.new
- model_instance.save
- assert_equal [model_instance], observer_class.instance.before_save_invocations
- end
-
- def assert_observer_not_notified(model_class, observer_class)
- observer_class.instance.before_save_invocations.clear
- model_instance = model_class.new
- model_instance.save
- assert_equal [], observer_class.instance.before_save_invocations
- end
-
- test "all observers are enabled by default" do
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can disable individual observers using a class constant" do
- ORM.observers.disable WidgetObserver
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can enable individual observers using a class constant" do
- ORM.observers.disable :all
- ORM.observers.enable AuditTrail
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can disable individual observers using a symbol" do
- ORM.observers.disable :budget_observer
-
- assert_observer_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can enable individual observers using a symbol" do
- ORM.observers.disable :all
- ORM.observers.enable :audit_trail
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can disable multiple observers at a time" do
- ORM.observers.disable :widget_observer, :budget_observer
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can enable multiple observers at a time" do
- ORM.observers.disable :all
- ORM.observers.enable :widget_observer, :budget_observer
-
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_not_notified Budget, AuditTrail
- end
-
- test "can disable all observers using :all" do
- ORM.observers.disable :all
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_not_notified Budget, AuditTrail
- end
-
- test "can enable all observers using :all" do
- ORM.observers.disable :all
- ORM.observers.enable :all
-
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can disable observers on individual models without affecting those observers on other models" do
- Widget.observers.disable :all
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can enable observers on individual models without affecting those observers on other models" do
- ORM.observers.disable :all
- Budget.observers.enable AuditTrail
-
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can disable observers for the duration of a block" do
- yielded = false
- ORM.observers.disable :budget_observer do
- yielded = true
- assert_observer_notified Widget, WidgetObserver
- assert_observer_not_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- assert yielded
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "can enable observers for the duration of a block" do
- yielded = false
- Widget.observers.disable :all
-
- Widget.observers.enable :all do
- yielded = true
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- assert yielded
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "raises an appropriate error when a developer accidentally enables or disables the wrong class (i.e. Widget instead of WidgetObserver)" do
- assert_raise ArgumentError do
- ORM.observers.enable :widget
- end
-
- assert_raise ArgumentError do
- ORM.observers.enable Widget
- end
-
- assert_raise ArgumentError do
- ORM.observers.disable :widget
- end
-
- assert_raise ArgumentError do
- ORM.observers.disable Widget
- end
- end
-
- test "allows #enable at the superclass level to override #disable at the subclass level when called last" do
- Widget.observers.disable :all
- ORM.observers.enable :all
-
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- test "allows #disable at the superclass level to override #enable at the subclass level when called last" do
- Budget.observers.enable :audit_trail
- ORM.observers.disable :audit_trail
-
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_not_notified Budget, AuditTrail
- end
-
- test "can use the block form at different levels of the hierarchy" do
- yielded = false
- Widget.observers.disable :all
-
- ORM.observers.enable :all do
- yielded = true
- assert_observer_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-
- assert yielded
- assert_observer_not_notified Widget, WidgetObserver
- assert_observer_notified Budget, BudgetObserver
- assert_observer_not_notified Widget, AuditTrail
- assert_observer_notified Budget, AuditTrail
- end
-end
-
diff --git a/activemodel/test/cases/observing_test.rb b/activemodel/test/cases/observing_test.rb
deleted file mode 100644
index ade6026602..0000000000
--- a/activemodel/test/cases/observing_test.rb
+++ /dev/null
@@ -1,181 +0,0 @@
-require 'cases/helper'
-
-class ObservedModel
- include ActiveModel::Observing
-
- class Observer
- end
-end
-
-class FooObserver < ActiveModel::Observer
- class << self
- public :new
- end
-
- attr_accessor :stub
-
- def on_spec(record, *args)
- stub.event_with(record, *args) if stub
- end
-
- def around_save(record)
- yield :in_around_save
- end
-end
-
-class Foo
- include ActiveModel::Observing
-end
-
-class ObservingTest < ActiveModel::TestCase
- def setup
- ObservedModel.observers.clear
- end
-
- test "initializes model with no cached observers" do
- assert ObservedModel.observers.empty?, "Not empty: #{ObservedModel.observers.inspect}"
- end
-
- test "stores cached observers in an array" do
- ObservedModel.observers << :foo
- assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
- end
-
- test "flattens array of assigned cached observers" do
- ObservedModel.observers = [[:foo], :bar]
- assert ObservedModel.observers.include?(:foo), ":foo not in #{ObservedModel.observers.inspect}"
- assert ObservedModel.observers.include?(:bar), ":bar not in #{ObservedModel.observers.inspect}"
- end
-
- test "uses an ObserverArray so observers can be disabled" do
- ObservedModel.observers = [:foo, :bar]
- assert ObservedModel.observers.is_a?(ActiveModel::ObserverArray)
- end
-
- test "instantiates observer names passed as strings" do
- ObservedModel.observers << 'foo_observer'
- FooObserver.expects(:instance)
- ObservedModel.instantiate_observers
- end
-
- test "instantiates observer names passed as symbols" do
- ObservedModel.observers << :foo_observer
- FooObserver.expects(:instance)
- ObservedModel.instantiate_observers
- end
-
- test "instantiates observer classes" do
- ObservedModel.observers << ObservedModel::Observer
- ObservedModel::Observer.expects(:instance)
- ObservedModel.instantiate_observers
- end
-
- test "raises an appropriate error when a developer accidentally adds the wrong class (i.e. Widget instead of WidgetObserver)" do
- assert_raise ArgumentError do
- ObservedModel.observers = ['string']
- ObservedModel.instantiate_observers
- end
- assert_raise ArgumentError do
- ObservedModel.observers = [:string]
- ObservedModel.instantiate_observers
- end
- assert_raise ArgumentError do
- ObservedModel.observers = [String]
- ObservedModel.instantiate_observers
- end
- end
-
- test "passes observers to subclasses" do
- FooObserver.instance
- bar = Class.new(Foo)
- assert_equal Foo.observers_count, bar.observers_count
- end
-end
-
-class ObserverTest < ActiveModel::TestCase
- def setup
- ObservedModel.observers = :foo_observer
- FooObserver.singleton_class.instance_eval do
- alias_method :original_observed_classes, :observed_classes
- end
- end
-
- def teardown
- FooObserver.singleton_class.instance_eval do
- undef_method :observed_classes
- alias_method :observed_classes, :original_observed_classes
- end
- end
-
- test "guesses implicit observable model name" do
- assert_equal Foo, FooObserver.observed_class
- end
-
- test "tracks implicit observable models" do
- instance = FooObserver.new
- assert_equal [Foo], instance.observed_classes
- end
-
- test "tracks explicit observed model class" do
- FooObserver.observe ObservedModel
- instance = FooObserver.new
- assert_equal [ObservedModel], instance.observed_classes
- end
-
- test "tracks explicit observed model as string" do
- FooObserver.observe 'observed_model'
- instance = FooObserver.new
- assert_equal [ObservedModel], instance.observed_classes
- end
-
- test "tracks explicit observed model as symbol" do
- FooObserver.observe :observed_model
- instance = FooObserver.new
- assert_equal [ObservedModel], instance.observed_classes
- end
-
- test "calls existing observer event" do
- foo = Foo.new
- FooObserver.instance.stub = stub
- FooObserver.instance.stub.expects(:event_with).with(foo)
- Foo.notify_observers(:on_spec, foo)
- end
-
- test "calls existing observer event from the instance" do
- foo = Foo.new
- FooObserver.instance.stub = stub
- FooObserver.instance.stub.expects(:event_with).with(foo)
- foo.notify_observers(:on_spec)
- end
-
- test "passes extra arguments" do
- foo = Foo.new
- FooObserver.instance.stub = stub
- FooObserver.instance.stub.expects(:event_with).with(foo, :bar)
- Foo.send(:notify_observers, :on_spec, foo, :bar)
- end
-
- test "skips nonexistent observer event" do
- foo = Foo.new
- Foo.notify_observers(:whatever, foo)
- end
-
- test "update passes a block on to the observer" do
- yielded_value = nil
- FooObserver.instance.update(:around_save, Foo.new) do |val|
- yielded_value = val
- end
- assert_equal :in_around_save, yielded_value
- end
-
- test "observe redefines observed_classes class method" do
- class BarObserver < ActiveModel::Observer
- observe :foo
- end
-
- assert_equal [Foo], BarObserver.observed_classes
-
- BarObserver.observe(ObservedModel)
- assert_equal [ObservedModel], BarObserver.observed_classes
- end
-end
diff --git a/activemodel/test/cases/railtie_test.rb b/activemodel/test/cases/railtie_test.rb
new file mode 100644
index 0000000000..96b3b07e50
--- /dev/null
+++ b/activemodel/test/cases/railtie_test.rb
@@ -0,0 +1,32 @@
+require 'cases/helper'
+require 'active_support/testing/isolation'
+
+class RailtieTest < ActiveModel::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ require 'active_model/railtie'
+
+ # Set a fake logger to avoid creating the log directory automatically
+ fake_logger = Logger.new(nil)
+
+ @app ||= Class.new(::Rails::Application) do
+ config.eager_load = false
+ config.logger = fake_logger
+ end
+ end
+
+ test 'secure password min_cost is false in the development environment' do
+ Rails.env = 'development'
+ @app.initialize!
+
+ assert_equal false, ActiveModel::SecurePassword.min_cost
+ end
+
+ test 'secure password min_cost is true in the test environment' do
+ Rails.env = 'test'
+ @app.initialize!
+
+ assert_equal true, ActiveModel::SecurePassword.min_cost
+ end
+end
diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb
index 509e2fdbb5..41d0b2263e 100644
--- a/activemodel/test/cases/secure_password_test.rb
+++ b/activemodel/test/cases/secure_password_test.rb
@@ -5,13 +5,18 @@ require 'models/visitor'
require 'models/administrator'
class SecurePasswordTest < ActiveModel::TestCase
-
setup do
+ ActiveModel::SecurePassword.min_cost = true
+
@user = User.new
@visitor = Visitor.new
@oauthed_user = OauthedUser.new
end
+ teardown do
+ ActiveModel::SecurePassword.min_cost = false
+ end
+
test "blank password" do
@user.password = @visitor.password = ''
assert !@user.valid?(:create), 'user should be invalid'
@@ -63,10 +68,52 @@ class SecurePasswordTest < ActiveModel::TestCase
@user.run_callbacks :create
end
end
-
+
test "Oauthed user can be created with blank digest" do
assert_nothing_raised do
@oauthed_user.run_callbacks :create
end
end
+
+ test "Password digest cost defaults to bcrypt default cost when min_cost is false" do
+ ActiveModel::SecurePassword.min_cost = false
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine::DEFAULT_COST, @user.password_digest.cost
+ end
+
+ test "Password digest cost honors bcrypt cost attribute when min_cost is false" do
+ ActiveModel::SecurePassword.min_cost = false
+ BCrypt::Engine.cost = 5
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine.cost, @user.password_digest.cost
+ end
+
+ test "Password digest cost can be set to bcrypt min cost to speed up tests" do
+ ActiveModel::SecurePassword.min_cost = true
+
+ @user.password = "secret"
+ assert_equal BCrypt::Engine::MIN_COST, @user.password_digest.cost
+ end
+
+ test "blank password_confirmation does not result in a confirmation error" do
+ @user.password = ""
+ @user.password_confirmation = ""
+ assert @user.valid?(:update), "user should be valid"
+ end
+
+ test "password_confirmation validations will not be triggered if password_confirmation is not sent" do
+ @user.password = "password"
+ assert @user.valid?(:create)
+ end
+
+ test "will not save if confirmation is blank but password is not" do
+ @user.password = "password"
+ @user.password_confirmation = ""
+ assert_not @user.valid?(:create)
+
+ @user.password_confirmation = "password"
+ assert @user.valid?(:create)
+ end
end
diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serialization_test.rb
index d2ba9fd95d..4ae41aa19c 100644
--- a/activemodel/test/cases/serialization_test.rb
+++ b/activemodel/test/cases/serialization_test.rb
@@ -49,32 +49,32 @@ class SerializationTest < ActiveModel::TestCase
def test_method_serializable_hash_should_work_with_only_option
expected = {"name"=>"David"}
- assert_equal expected, @user.serializable_hash(:only => [:name])
+ assert_equal expected, @user.serializable_hash(only: [:name])
end
def test_method_serializable_hash_should_work_with_except_option
expected = {"gender"=>"male", "email"=>"david@example.com"}
- assert_equal expected, @user.serializable_hash(:except => [:name])
+ assert_equal expected, @user.serializable_hash(except: [:name])
end
def test_method_serializable_hash_should_work_with_methods_option
expected = {"name"=>"David", "gender"=>"male", "foo"=>"i_am_foo", "email"=>"david@example.com"}
- assert_equal expected, @user.serializable_hash(:methods => [:foo])
+ assert_equal expected, @user.serializable_hash(methods: [:foo])
end
def test_method_serializable_hash_should_work_with_only_and_methods
expected = {"foo"=>"i_am_foo"}
- assert_equal expected, @user.serializable_hash(:only => [], :methods => [:foo])
+ assert_equal expected, @user.serializable_hash(only: [], methods: [:foo])
end
def test_method_serializable_hash_should_work_with_except_and_methods
expected = {"gender"=>"male", "foo"=>"i_am_foo"}
- assert_equal expected, @user.serializable_hash(:except => [:name, :email], :methods => [:foo])
+ assert_equal expected, @user.serializable_hash(except: [:name, :email], methods: [:foo])
end
def test_should_not_call_methods_that_dont_respond
expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com"}
- assert_equal expected, @user.serializable_hash(:methods => [:bar])
+ assert_equal expected, @user.serializable_hash(methods: [:bar])
end
def test_should_use_read_attribute_for_serialization
@@ -83,26 +83,26 @@ class SerializationTest < ActiveModel::TestCase
end
expected = { "name" => "Jon" }
- assert_equal expected, @user.serializable_hash(:only => :name)
+ assert_equal expected, @user.serializable_hash(only: :name)
end
def test_include_option_with_singular_association
expected = {"name"=>"David", "gender"=>"male", "email"=>"david@example.com",
"address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111}}
- assert_equal expected, @user.serializable_hash(:include => :address)
+ assert_equal expected, @user.serializable_hash(include: :address)
end
def test_include_option_with_plural_association
expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
"friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
- assert_equal expected, @user.serializable_hash(:include => :friends)
+ assert_equal expected, @user.serializable_hash(include: :friends)
end
def test_include_option_with_empty_association
@user.friends = []
expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David", "friends"=>[]}
- assert_equal expected, @user.serializable_hash(:include => :friends)
+ assert_equal expected, @user.serializable_hash(include: :friends)
end
class FriendList
@@ -120,7 +120,7 @@ class SerializationTest < ActiveModel::TestCase
expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
"friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
- assert_equal expected, @user.serializable_hash(:include => :friends)
+ assert_equal expected, @user.serializable_hash(include: :friends)
end
def test_multiple_includes
@@ -128,13 +128,13 @@ class SerializationTest < ActiveModel::TestCase
"address"=>{"street"=>"123 Lane", "city"=>"Springfield", "state"=>"CA", "zip"=>11111},
"friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
- assert_equal expected, @user.serializable_hash(:include => [:address, :friends])
+ assert_equal expected, @user.serializable_hash(include: [:address, :friends])
end
def test_include_with_options
expected = {"email"=>"david@example.com", "gender"=>"male", "name"=>"David",
"address"=>{"street"=>"123 Lane"}}
- assert_equal expected, @user.serializable_hash(:include => {:address => {:only => "street"}})
+ assert_equal expected, @user.serializable_hash(include: { address: { only: "street" } })
end
def test_nested_include
@@ -143,19 +143,19 @@ class SerializationTest < ActiveModel::TestCase
"friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male',
"friends"=> [{"email"=>"david@example.com", "gender"=>"male", "name"=>"David"}]},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female', "friends"=> []}]}
- assert_equal expected, @user.serializable_hash(:include => {:friends => {:include => :friends}})
+ assert_equal expected, @user.serializable_hash(include: { friends: { include: :friends } })
end
def test_only_include
expected = {"name"=>"David", "friends" => [{"name" => "Joe"}, {"name" => "Sue"}]}
- assert_equal expected, @user.serializable_hash(:only => :name, :include => {:friends => {:only => :name}})
+ assert_equal expected, @user.serializable_hash(only: :name, include: { friends: { only: :name } })
end
def test_except_include
expected = {"name"=>"David", "email"=>"david@example.com",
"friends"=> [{"name" => 'Joe', "email" => 'joe@example.com'},
{"name" => "Sue", "email" => 'sue@example.com'}]}
- assert_equal expected, @user.serializable_hash(:except => :gender, :include => {:friends => {:except => :gender}})
+ assert_equal expected, @user.serializable_hash(except: :gender, include: { friends: { except: :gender } })
end
def test_multiple_includes_with_options
@@ -163,6 +163,6 @@ class SerializationTest < ActiveModel::TestCase
"address"=>{"street"=>"123 Lane"},
"friends"=>[{"name"=>'Joe', "email"=>'joe@example.com', "gender"=>'male'},
{"name"=>'Sue', "email"=>'sue@example.com', "gender"=>'female'}]}
- assert_equal expected, @user.serializable_hash(:include => [{:address => {:only => "street"}}, :friends])
+ assert_equal expected, @user.serializable_hash(include: [{ address: {only: "street" } }, :friends])
end
end
diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb
index e2690f1827..bc185c737f 100644
--- a/activemodel/test/cases/serializers/json_serialization_test.rb
+++ b/activemodel/test/cases/serializers/json_serialization_test.rb
@@ -4,7 +4,6 @@ require 'models/automobile'
require 'active_support/core_ext/object/instance_variables'
class Contact
- extend ActiveModel::Naming
include ActiveModel::Serializers::JSON
include ActiveModel::Validations
@@ -92,7 +91,7 @@ class JsonSerializationTest < ActiveModel::TestCase
end
test "should allow attribute filtering with only" do
- json = @contact.to_json(:only => [:name, :age])
+ json = @contact.to_json(only: [:name, :age])
assert_match %r{"name":"Konata Izumi"}, json
assert_match %r{"age":16}, json
@@ -199,7 +198,7 @@ class JsonSerializationTest < ActiveModel::TestCase
assert_no_match %r{"preferences":}, json
end
- test "custom as_json options should be extendible" do
+ test "custom as_json options should be extensible" do
def @contact.as_json(options = {}); super(options.merge(only: [:name])); end
json = @contact.to_json
diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb
index e2bb0dda0b..11ee17bb27 100755..100644
--- a/activemodel/test/cases/serializers/xml_serialization_test.rb
+++ b/activemodel/test/cases/serializers/xml_serialization_test.rb
@@ -4,15 +4,14 @@ require 'active_support/core_ext/object/instance_variables'
require 'ostruct'
class Contact
- extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
- attr_accessor :address, :friends
+ attr_accessor :address, :friends, :contact
remove_method :attributes if method_defined?(:attributes)
def attributes
- instance_values.except("address", "friends")
+ instance_values.except("address", "friends", "contact")
end
end
@@ -25,7 +24,6 @@ class Customer < Struct.new(:name)
end
class Address
- extend ActiveModel::Naming
include ActiveModel::Serializers::Xml
attr_accessor :street, :city, :state, :zip, :apt_number
@@ -37,7 +35,7 @@ end
class SerializableContact < Contact
def serializable_hash(options={})
- super(options.merge(:only => [:name, :age]))
+ super(options.merge(only: [:name, :age]))
end
end
@@ -52,12 +50,10 @@ class XmlSerializationTest < ActiveModel::TestCase
customer.name = "John"
@contact.preferences = customer
@contact.address = Address.new
- @contact.address.street = "123 Lane"
@contact.address.city = "Springfield"
- @contact.address.state = "CA"
- @contact.address.zip = 11111
@contact.address.apt_number = 35
@contact.friends = [Contact.new, Contact.new]
+ @contact.contact = SerializableContact.new
end
test "should serialize default root" do
@@ -73,33 +69,33 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should serialize default root with namespace" do
- @xml = @contact.to_xml :namespace => "http://xml.rubyonrails.org/contact"
+ @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'
+ @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
+ @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
+ @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
+ @xml = @contact.to_xml root: 'xml_contact', camelize: :lower
assert_match %r{^<xmlContact>}, @xml
assert_match %r{</xmlContact>$}, @xml
assert_match %r{<createdAt}, @xml
@@ -117,7 +113,7 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should allow skipped types" do
- @xml = @contact.to_xml :skip_types => true
+ @xml = @contact.to_xml skip_types: true
assert_match %r{<age>25</age>}, @xml
end
@@ -133,7 +129,7 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should serialize nil" do
- assert_match %r{<pseudonyms nil=\"true\"/>}, @contact.to_xml(:methods => :pseudonyms)
+ assert_match %r{<pseudonyms nil="true"/>}, @contact.to_xml(methods: :pseudonyms)
end
test "should serialize integer" do
@@ -141,50 +137,50 @@ class XmlSerializationTest < ActiveModel::TestCase
end
test "should serialize datetime" do
- assert_match %r{<created-at type=\"dateTime\">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
+ assert_match %r{<created-at type="dateTime">2006-08-01T00:00:00Z</created-at>}, @contact.to_xml
end
test "should serialize boolean" do
- assert_match %r{<awesome type=\"boolean\">false</awesome>}, @contact.to_xml
+ assert_match %r{<awesome type="boolean">false</awesome>}, @contact.to_xml
end
test "should serialize array" do
- assert_match %r{<social type=\"array\">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(:methods => :social)
+ assert_match %r{<social type="array">\s*<social>twitter</social>\s*<social>github</social>\s*</social>}, @contact.to_xml(methods: :social)
end
test "should serialize hash" do
- assert_match %r{<network>\s*<git type=\"symbol\">github</git>\s*</network>}, @contact.to_xml(:methods => :network)
+ assert_match %r{<network>\s*<git type="symbol">github</git>\s*</network>}, @contact.to_xml(methods: :network)
end
test "should serialize yaml" do
- assert_match %r{<preferences type=\"yaml\">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
+ assert_match %r{<preferences type="yaml">--- !ruby/struct:Customer(\s*)\nname: John\n</preferences>}, @contact.to_xml
end
test "should call proc on object" do
proc = Proc.new { |options| options[:builder].tag!('nationality', 'unknown') }
- xml = @contact.to_xml(:procs => [ proc ])
+ xml = @contact.to_xml(procs: [ proc ])
assert_match %r{<nationality>unknown</nationality>}, xml
end
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 ])
+ xml = @contact.to_xml(procs: [ proc ])
assert_match %r{<name-reverse>kcats noraa</name-reverse>}, xml
end
test "should serialize string correctly when type passed" do
- xml = @contact.to_xml :type => 'Contact'
+ xml = @contact.to_xml type: 'Contact'
assert_match %r{<contact type="Contact">}, xml
assert_match %r{<name>aaron stack</name>}, xml
end
test "include option with singular association" do
- xml = @contact.to_xml :include => :address, :indent => 0
- assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true))
+ xml = @contact.to_xml include: :address, indent: 0
+ assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
end
test "include option with plural association" do
- xml = @contact.to_xml :include => :friends, :indent => 0
+ xml = @contact.to_xml include: :friends, indent: 0
assert_match %r{<friends type="array">}, xml
assert_match %r{<friend type="Contact">}, xml
end
@@ -201,61 +197,66 @@ class XmlSerializationTest < ActiveModel::TestCase
test "include option with ary" do
@contact.friends = FriendList.new(@contact.friends)
- xml = @contact.to_xml :include => :friends, :indent => 0
+ xml = @contact.to_xml include: :friends, indent: 0
assert_match %r{<friends type="array">}, xml
assert_match %r{<friend type="Contact">}, xml
end
test "multiple includes" do
- xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => [ :address, :friends ]
- assert xml.include?(@contact.address.to_xml(:indent => 0, :skip_instruct => true))
+ xml = @contact.to_xml indent: 0, skip_instruct: true, include: [ :address, :friends ]
+ assert xml.include?(@contact.address.to_xml(indent: 0, skip_instruct: true))
assert_match %r{<friends type="array">}, xml
assert_match %r{<friend type="Contact">}, xml
end
test "include with options" do
- xml = @contact.to_xml :indent => 0, :skip_instruct => true, :include => { :address => { :only => :city } }
+ xml = @contact.to_xml indent: 0, skip_instruct: true, include: { address: { only: :city } }
assert xml.include?(%(><address><city>Springfield</city></address>))
end
test "propagates skip_types option to included associations" do
- xml = @contact.to_xml :include => :friends, :indent => 0, :skip_types => true
+ xml = @contact.to_xml include: :friends, indent: 0, skip_types: true
assert_match %r{<friends>}, xml
assert_match %r{<friend>}, xml
end
test "propagates skip-types option to included associations and attributes" do
- xml = @contact.to_xml :skip_types => true, :include => :address, :indent => 0
+ xml = @contact.to_xml skip_types: true, include: :address, indent: 0
assert_match %r{<address>}, xml
assert_match %r{<apt-number>}, xml
end
test "propagates camelize option to included associations and attributes" do
- xml = @contact.to_xml :camelize => true, :include => :address, :indent => 0
+ xml = @contact.to_xml camelize: true, include: :address, indent: 0
assert_match %r{<Address>}, xml
assert_match %r{<AptNumber type="integer">}, xml
end
test "propagates dasherize option to included associations and attributes" do
- xml = @contact.to_xml :dasherize => false, :include => :address, :indent => 0
+ xml = @contact.to_xml dasherize: false, include: :address, indent: 0
assert_match %r{<apt_number type="integer">}, xml
end
test "don't propagate skip_types if skip_types is defined at the included association level" do
- xml = @contact.to_xml :skip_types => true, :include => { :address => { :skip_types => false } }, :indent => 0
+ xml = @contact.to_xml skip_types: true, include: { address: { skip_types: false } }, indent: 0
assert_match %r{<address>}, xml
assert_match %r{<apt-number type="integer">}, xml
end
test "don't propagate camelize if camelize is defined at the included association level" do
- xml = @contact.to_xml :camelize => true, :include => { :address => { :camelize => false } }, :indent => 0
+ xml = @contact.to_xml camelize: true, include: { address: { camelize: false } }, indent: 0
assert_match %r{<address>}, xml
assert_match %r{<apt-number type="integer">}, xml
end
test "don't propagate dasherize if dasherize is defined at the included association level" do
- xml = @contact.to_xml :dasherize => false, :include => { :address => { :dasherize => true } }, :indent => 0
+ xml = @contact.to_xml dasherize: false, include: { address: { dasherize: true } }, indent: 0
assert_match %r{<address>}, xml
assert_match %r{<apt-number type="integer">}, xml
end
+
+ test "association with sti" do
+ xml = @contact.to_xml(include: :contact)
+ assert xml.include?(%(<contact type="SerializableContact">))
+ end
end
diff --git a/activemodel/test/cases/translation_test.rb b/activemodel/test/cases/translation_test.rb
index fd833cdd06..deb4e1ed0a 100644
--- a/activemodel/test/cases/translation_test.rb
+++ b/activemodel/test/cases/translation_test.rb
@@ -8,22 +8,22 @@ class ActiveModelI18nTests < ActiveModel::TestCase
end
def test_translated_model_attributes
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute' } } }
assert_equal 'person name attribute', Person.human_attribute_name('name')
end
def test_translated_model_attributes_with_default
- I18n.backend.store_translations 'en', :attributes => { :name => 'name default attribute' }
+ I18n.backend.store_translations 'en', attributes: { name: 'name default attribute' }
assert_equal 'name default attribute', Person.human_attribute_name('name')
end
def test_translated_model_attributes_using_default_option
- assert_equal 'name default attribute', Person.human_attribute_name('name', :default => "name default attribute")
+ assert_equal 'name default attribute', Person.human_attribute_name('name', default: "name default attribute")
end
def test_translated_model_attributes_using_default_option_as_symbol
- I18n.backend.store_translations 'en', :default_name => 'name default attribute'
- assert_equal 'name default attribute', Person.human_attribute_name('name', :default => :default_name)
+ I18n.backend.store_translations 'en', default_name: 'name default attribute'
+ assert_equal 'name default attribute', Person.human_attribute_name('name', default: :default_name)
end
def test_translated_model_attributes_falling_back_to_default
@@ -31,71 +31,74 @@ class ActiveModelI18nTests < ActiveModel::TestCase
end
def test_translated_model_attributes_using_default_option_as_symbol_and_falling_back_to_default
- assert_equal 'Name', Person.human_attribute_name('name', :default => :default_name)
+ assert_equal 'Name', Person.human_attribute_name('name', default: :default_name)
end
def test_translated_model_attributes_with_symbols
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
assert_equal 'person name attribute', Person.human_attribute_name(:name)
end
def test_translated_model_attributes_with_ancestor
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:child => {:name => 'child name attribute'} } }
+ I18n.backend.store_translations 'en', activemodel: { attributes: { child: { name: 'child name attribute'} } }
assert_equal 'child name attribute', Child.human_attribute_name('name')
end
def test_translated_model_attributes_with_ancestors_fallback
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:name => 'person name attribute'} } }
+ I18n.backend.store_translations 'en', activemodel: { attributes: { person: { name: 'person name attribute'} } }
assert_equal 'person name attribute', Child.human_attribute_name('name')
end
def test_translated_model_attributes_with_attribute_matching_namespaced_model_name
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:person => {:gender => 'person gender'}, :"person/gender" => {:attribute => 'person gender attribute'}}}
+ I18n.backend.store_translations 'en', activemodel: { attributes: {
+ person: { gender: 'person gender'},
+ :"person/gender" => { attribute: 'person gender attribute' }
+ } }
assert_equal 'person gender', Person.human_attribute_name('gender')
assert_equal 'person gender attribute', Person::Gender.human_attribute_name('attribute')
end
def test_translated_deeply_nested_model_attributes
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/contacts/addresses" => {:street => 'Deeply Nested Address Street'}}}
+ I18n.backend.store_translations 'en', activemodel: { attributes: { :"person/contacts/addresses" => { street: 'Deeply Nested Address Street' } } }
assert_equal 'Deeply Nested Address Street', Person.human_attribute_name('contacts.addresses.street')
end
def test_translated_nested_model_attributes
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:"person/addresses" => {:street => 'Person Address Street'}}}
+ I18n.backend.store_translations 'en', activemodel: { attributes: { :"person/addresses" => { street: 'Person Address Street' } } }
assert_equal 'Person Address Street', Person.human_attribute_name('addresses.street')
end
def test_translated_nested_model_attributes_with_namespace_fallback
- I18n.backend.store_translations 'en', :activemodel => {:attributes => {:addresses => {:street => 'Cool Address Street'}}}
+ I18n.backend.store_translations 'en', activemodel: { attributes: { addresses: { street: 'Cool Address Street' } } }
assert_equal 'Cool Address Street', Person.human_attribute_name('addresses.street')
end
def test_translated_model_names
- I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
+ I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
assert_equal 'person model', Person.model_name.human
end
def test_translated_model_names_with_sti
- I18n.backend.store_translations 'en', :activemodel => {:models => {:child => 'child model'} }
+ I18n.backend.store_translations 'en', activemodel: { models: { child: 'child model' } }
assert_equal 'child model', Child.model_name.human
end
def test_translated_model_names_with_ancestors_fallback
- I18n.backend.store_translations 'en', :activemodel => {:models => {:person => 'person model'} }
+ I18n.backend.store_translations 'en', activemodel: { models: { person: 'person model' } }
assert_equal 'person model', Child.model_name.human
end
def test_human_does_not_modify_options
- options = { :default => 'person model' }
+ options = { default: 'person model' }
Person.model_name.human(options)
- assert_equal({ :default => 'person model' }, options)
+ assert_equal({ default: 'person model' }, options)
end
def test_human_attribute_name_does_not_modify_options
- options = { :default => 'Cool gender' }
+ options = { default: 'Cool gender' }
Person.human_attribute_name('gender', options)
- assert_equal({ :default => 'Cool gender' }, options)
+ assert_equal({ default: 'Cool gender' }, options)
end
end
diff --git a/activemodel/test/cases/validations/absence_validation_test.rb b/activemodel/test/cases/validations/absence_validation_test.rb
new file mode 100644
index 0000000000..c05d71de5a
--- /dev/null
+++ b/activemodel/test/cases/validations/absence_validation_test.rb
@@ -0,0 +1,67 @@
+# encoding: utf-8
+require 'cases/helper'
+require 'models/topic'
+require 'models/person'
+require 'models/custom_reader'
+
+class AbsenceValidationTest < ActiveModel::TestCase
+ teardown do
+ Topic.reset_callbacks(:validate)
+ Person.reset_callbacks(:validate)
+ CustomReader.reset_callbacks(:validate)
+ end
+
+ def test_validate_absences
+ Topic.validates_absence_of(:title, :content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ t.title = ""
+ t.content = "something"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:content]
+ t.content = ""
+ assert t.valid?
+ end
+
+ def test_accepts_array_arguments
+ Topic.validates_absence_of %w(title content)
+ t = Topic.new
+ t.title = "foo"
+ t.content = "bar"
+ assert t.invalid?
+ assert_equal ["must be blank"], t.errors[:title]
+ assert_equal ["must be blank"], t.errors[:content]
+ end
+
+ def test_validates_acceptance_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"
+ assert p.invalid?
+ assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
+ end
+
+ def test_validates_absence_of_for_ruby_class
+ Person.validates_absence_of :karma
+ p = Person.new
+ p.karma = "good"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p.karma = nil
+ assert p.valid?
+ end
+
+ def test_validates_absence_of_for_ruby_class_with_custom_reader
+ CustomReader.validates_absence_of :karma
+ p = CustomReader.new
+ p[:karma] = "excellent"
+ assert p.invalid?
+ assert_equal ["must be blank"], p.errors[:karma]
+ p[:karma] = ""
+ assert p.valid?
+ end
+end
diff --git a/activemodel/test/cases/validations/acceptance_validation_test.rb b/activemodel/test/cases/validations/acceptance_validation_test.rb
index de04e11258..dc413bef30 100644
--- a/activemodel/test/cases/validations/acceptance_validation_test.rb
+++ b/activemodel/test/cases/validations/acceptance_validation_test.rb
@@ -30,7 +30,7 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_eula
- Topic.validates_acceptance_of(:eula, :message => "must be abided")
+ Topic.validates_acceptance_of(:eula, message: "must be abided")
t = Topic.new("title" => "We should be confirmed","eula" => "")
assert t.invalid?
@@ -41,7 +41,7 @@ class AcceptanceValidationTest < ActiveModel::TestCase
end
def test_terms_of_service_agreement_with_accept_value
- Topic.validates_acceptance_of(:terms_of_service, :accept => "I agree.")
+ Topic.validates_acceptance_of(:terms_of_service, accept: "I agree.")
t = Topic.new("title" => "We should be confirmed", "terms_of_service" => "")
assert t.invalid?
diff --git a/activemodel/test/cases/validations/callbacks_test.rb b/activemodel/test/cases/validations/callbacks_test.rb
index 0015b3c196..6cd0f4ed4d 100644
--- a/activemodel/test/cases/validations/callbacks_test.rb
+++ b/activemodel/test/cases/validations/callbacks_test.rb
@@ -40,8 +40,29 @@ class DogWithMissingName < Dog
validates_presence_of :name
end
+class DogValidatorWithIfCondition < Dog
+ before_validation :set_before_validation_marker1, if: -> { true }
+ before_validation :set_before_validation_marker2, if: -> { false }
+
+ after_validation :set_after_validation_marker1, if: -> { true }
+ after_validation :set_after_validation_marker2, if: -> { false }
+
+ def set_before_validation_marker1; self.history << 'before_validation_marker1'; end
+ def set_before_validation_marker2; self.history << 'before_validation_marker2' ; end
+
+ def set_after_validation_marker1; self.history << 'after_validation_marker1'; end
+ def set_after_validation_marker2; self.history << 'after_validation_marker2' ; end
+end
+
+
class CallbacksWithMethodNamesShouldBeCalled < ActiveModel::TestCase
+ def test_if_condition_is_respected_for_before_validation
+ d = DogValidatorWithIfCondition.new
+ d.valid?
+ assert_equal ["before_validation_marker1", "after_validation_marker1"], d.history
+ end
+
def test_before_validation_and_after_validation_callbacks_should_be_called
d = DogWithMethodCallbacks.new
d.valid?
diff --git a/activemodel/test/cases/validations/conditional_validation_test.rb b/activemodel/test/cases/validations/conditional_validation_test.rb
index e06b04af19..5049d6dd61 100644
--- a/activemodel/test/cases/validations/conditional_validation_test.rb
+++ b/activemodel/test/cases/validations/conditional_validation_test.rb
@@ -11,7 +11,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_if_validation_using_method_true
# When the method returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => :condition_is_true )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -20,23 +20,23 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_unless_validation_using_method_true
# When the method returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => :condition_is_true )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_method_false
# When the method returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => :condition_is_true_but_its_not )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: :condition_is_true_but_its_not)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_method_false
# When the method returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => :condition_is_true_but_its_not )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: :condition_is_true_but_its_not)
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -45,7 +45,7 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_if_validation_using_string_true
# When the evaluated string returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => "a = 1; a == 1" )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "a = 1; a == 1")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -54,23 +54,23 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_unless_validation_using_string_true
# When the evaluated string returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => "a = 1; a == 1" )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "a = 1; a == 1")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_string_false
# When the evaluated string returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :if => "false")
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", if: "false")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_string_false
# When the evaluated string returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}", :unless => "false")
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}", unless: "false")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -79,8 +79,8 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_if_validation_using_block_true
# When the block returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}",
- :if => Proc.new { |r| r.content.size > 4 } )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ if: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -89,26 +89,26 @@ class ConditionalValidationTest < ActiveModel::TestCase
def test_unless_validation_using_block_true
# When the block returns true
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}",
- :unless => Proc.new { |r| r.content.size > 4 } )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ unless: Proc.new { |r| r.content.size > 4 })
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_if_validation_using_block_false
# When the block returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}",
- :if => Proc.new { |r| r.title != "uhohuhoh"} )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ if: Proc.new { |r| r.title != "uhohuhoh"})
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.valid?
- assert t.errors[:title].empty?
+ assert_empty t.errors[:title]
end
def test_unless_validation_using_block_false
# When the block returns false
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}",
- :unless => Proc.new { |r| r.title != "uhohuhoh"} )
+ Topic.validates_length_of(:title, maximum: 5, too_long: "hoo %{count}",
+ unless: Proc.new { |r| r.title != "uhohuhoh"} )
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -120,11 +120,11 @@ class ConditionalValidationTest < ActiveModel::TestCase
# ensure that it works correctly
def test_validation_with_if_as_string
Topic.validates_presence_of(:title)
- Topic.validates_presence_of(:author_name, :if => "title.to_s.match('important')")
+ Topic.validates_presence_of(:author_name, if: "title.to_s.match('important')")
t = Topic.new
assert t.invalid?, "A topic without a title should not be valid"
- assert t.errors[:author_name].empty?, "A topic without an 'important' title should not require an author"
+ assert_empty t.errors[:author_name], "A topic without an 'important' title should not require an author"
t.title = "Just a title"
assert t.valid?, "A topic with a basic title should be valid"
diff --git a/activemodel/test/cases/validations/confirmation_validation_test.rb b/activemodel/test/cases/validations/confirmation_validation_test.rb
index f7556a249f..f03de2c24a 100644
--- a/activemodel/test/cases/validations/confirmation_validation_test.rb
+++ b/activemodel/test/cases/validations/confirmation_validation_test.rb
@@ -13,7 +13,7 @@ class ConfirmationValidationTest < ActiveModel::TestCase
def test_no_title_confirmation
Topic.validates_confirmation_of(:title)
- t = Topic.new(:author_name => "Plutarch")
+ t = Topic.new(author_name: "Plutarch")
assert t.valid?
t.title_confirmation = "Parallel Lives"
@@ -57,8 +57,8 @@ class ConfirmationValidationTest < ActiveModel::TestCase
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'}}}
+ errors: { messages: { confirmation: "doesn't match %{attribute}" } },
+ activemodel: { attributes: { topic: { title: 'Test Title'} } }
})
Topic.validates_confirmation_of(:title)
@@ -71,4 +71,35 @@ class ConfirmationValidationTest < ActiveModel::TestCase
I18n.backend = @old_backend
end
+ test "does not override confirmation reader if present" do
+ klass = Class.new do
+ include ActiveModel::Validations
+
+ def title_confirmation
+ "expected title"
+ end
+
+ validates_confirmation_of :title
+ end
+
+ assert_equal "expected title", klass.new.title_confirmation,
+ "confirmation validation should not override the reader"
+ end
+
+ test "does not override confirmation writer if present" do
+ klass = Class.new do
+ include ActiveModel::Validations
+
+ def title_confirmation=(value)
+ @title_confirmation = "expected title"
+ end
+
+ validates_confirmation_of :title
+ end
+
+ model = klass.new
+ model.title_confirmation = "new title"
+ assert_equal "expected title", model.title_confirmation,
+ "confirmation validation should not override the writer"
+ end
end
diff --git a/activemodel/test/cases/validations/exclusion_validation_test.rb b/activemodel/test/cases/validations/exclusion_validation_test.rb
index 7d5af27f3d..81455ba519 100644
--- a/activemodel/test/cases/validations/exclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/exclusion_validation_test.rb
@@ -11,14 +11,14 @@ class ExclusionValidationTest < ActiveModel::TestCase
end
def test_validates_exclusion_of
- Topic.validates_exclusion_of( :title, :in => %w( abe monkey ) )
+ Topic.validates_exclusion_of(:title, in: %w( abe monkey ))
assert Topic.new("title" => "something", "content" => "abc").valid?
assert Topic.new("title" => "monkey", "content" => "abc").invalid?
end
def test_validates_exclusion_of_with_formatted_message
- Topic.validates_exclusion_of( :title, :in => %w( abe monkey ), :message => "option %{value} is restricted" )
+ Topic.validates_exclusion_of(:title, in: %w( abe monkey ), message: "option %{value} is restricted")
assert Topic.new("title" => "something", "content" => "abc")
@@ -29,7 +29,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
end
def test_validates_exclusion_of_with_within_option
- Topic.validates_exclusion_of( :title, :within => %w( abe monkey ) )
+ Topic.validates_exclusion_of(:title, within: %w( abe monkey ))
assert Topic.new("title" => "something", "content" => "abc")
@@ -39,7 +39,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
end
def test_validates_exclusion_of_for_ruby_class
- Person.validates_exclusion_of :karma, :in => %w( abe monkey )
+ Person.validates_exclusion_of :karma, in: %w( abe monkey )
p = Person.new
p.karma = "abe"
@@ -54,7 +54,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
end
def test_validates_exclusion_of_with_lambda
- Topic.validates_exclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
+ Topic.validates_exclusion_of :title, in: lambda { |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
t = Topic.new
t.title = "elephant"
@@ -66,7 +66,7 @@ class ExclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_with_symbol
- Person.validates_exclusion_of :karma, :in => :reserved_karmas
+ Person.validates_exclusion_of :karma, in: :reserved_karmas
p = Person.new
p.karma = "abe"
diff --git a/activemodel/test/cases/validations/format_validation_test.rb b/activemodel/test/cases/validations/format_validation_test.rb
index 308a3c6cef..26e8dbf19c 100644
--- a/activemodel/test/cases/validations/format_validation_test.rb
+++ b/activemodel/test/cases/validations/format_validation_test.rb
@@ -11,7 +11,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validate_format
- Topic.validates_format_of(:title, :content, :with => /\AValidation\smacros \w+!\z/, :message => "is bad data")
+ Topic.validates_format_of(:title, :content, with: /\AValidation\smacros \w+!\z/, message: "is bad data")
t = Topic.new("title" => "i'm incorrect", "content" => "Validation macros rule!")
assert t.invalid?, "Shouldn't be valid"
@@ -27,7 +27,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validate_format_with_allow_blank
- Topic.validates_format_of(:title, :with => /\AValidation\smacros \w+!\z/, :allow_blank => true)
+ Topic.validates_format_of(:title, with: /\AValidation\smacros \w+!\z/, allow_blank: true)
assert Topic.new("title" => "Shouldn't be valid").invalid?
assert Topic.new("title" => "").valid?
assert Topic.new("title" => nil).valid?
@@ -36,7 +36,7 @@ class PresenceValidationTest < ActiveModel::TestCase
# testing ticket #3142
def test_validate_format_numeric
- Topic.validates_format_of(:title, :content, :with => /\A[1-9][0-9]*\z/, :message => "is bad data")
+ Topic.validates_format_of(:title, :content, with: /\A[1-9][0-9]*\z/, message: "is bad data")
t = Topic.new("title" => "72x", "content" => "6789")
assert t.invalid?, "Shouldn't be valid"
@@ -63,24 +63,24 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validate_format_with_formatted_message
- Topic.validates_format_of(:title, :with => /\AValid Title\z/, :message => "can't be %{value}")
- t = Topic.new(:title => 'Invalid title')
+ Topic.validates_format_of(:title, with: /\AValid Title\z/, message: "can't be %{value}")
+ t = Topic.new(title: 'Invalid title')
assert t.invalid?
assert_equal ["can't be Invalid title"], t.errors[:title]
end
def test_validate_format_of_with_multiline_regexp_should_raise_error
- assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /^Valid Title$/) }
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: /^Valid Title$/) }
end
def test_validate_format_of_with_multiline_regexp_and_option
assert_nothing_raised(ArgumentError) do
- Topic.validates_format_of(:title, :with => /^Valid Title$/, :multiline => true)
+ Topic.validates_format_of(:title, with: /^Valid Title$/, multiline: true)
end
end
def test_validate_format_with_not_option
- Topic.validates_format_of(:title, :without => /foo/, :message => "should not contain foo")
+ Topic.validates_format_of(:title, without: /foo/, message: "should not contain foo")
t = Topic.new
t.title = "foobar"
@@ -97,19 +97,19 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_with_both_regexps_should_raise_error
- assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => /this/, :without => /that/) }
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: /this/, without: /that/) }
end
def test_validates_format_of_when_with_isnt_a_regexp_should_raise_error
- assert_raise(ArgumentError) { Topic.validates_format_of(:title, :with => "clearly not a regexp") }
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, with: "clearly not a regexp") }
end
def test_validates_format_of_when_not_isnt_a_regexp_should_raise_error
- assert_raise(ArgumentError) { Topic.validates_format_of(:title, :without => "clearly not a regexp") }
+ assert_raise(ArgumentError) { Topic.validates_format_of(:title, without: "clearly not a regexp") }
end
def test_validates_format_of_with_lambda
- Topic.validates_format_of :content, :with => lambda{ |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, with: lambda { |topic| topic.title == "digit" ? /\A\d+\Z/ : /\A\S+\Z/ }
t = Topic.new
t.title = "digit"
@@ -121,7 +121,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_without_lambda
- Topic.validates_format_of :content, :without => lambda{ |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
+ Topic.validates_format_of :content, without: lambda { |topic| topic.title == "characters" ? /\A\d+\Z/ : /\A\S+\Z/ }
t = Topic.new
t.title = "characters"
@@ -133,7 +133,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_format_of_for_ruby_class
- Person.validates_format_of :karma, :with => /\A\d+\Z/
+ Person.validates_format_of :karma, with: /\A\d+\Z/
p = Person.new
p.karma = "Pixies"
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 df0fcd243a..40a5aee997 100644
--- a/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -8,131 +8,131 @@ class I18nGenerateMessageValidationTest < ActiveModel::TestCase
@person = Person.new
end
- # validates_inclusion_of: generate_message(attr_name, :inclusion, :message => custom_message, :value => value)
+ # validates_inclusion_of: generate_message(attr_name, :inclusion, message: custom_message, value: value)
def test_generate_message_inclusion_with_default_message
- assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, :value => 'title')
+ assert_equal 'is not included in the list', @person.errors.generate_message(:title, :inclusion, value: 'title')
end
def test_generate_message_inclusion_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, :message => 'custom message %{value}', :value => 'title')
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :inclusion, message: 'custom message %{value}', value: 'title')
end
- # validates_exclusion_of: generate_message(attr_name, :exclusion, :message => custom_message, :value => value)
+ # validates_exclusion_of: generate_message(attr_name, :exclusion, message: custom_message, value: value)
def test_generate_message_exclusion_with_default_message
- assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, :value => 'title')
+ assert_equal 'is reserved', @person.errors.generate_message(:title, :exclusion, value: 'title')
end
def test_generate_message_exclusion_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, :message => 'custom message %{value}', :value => 'title')
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :exclusion, message: 'custom message %{value}', value: 'title')
end
- # validates_format_of: generate_message(attr_name, :invalid, :message => custom_message, :value => value)
+ # validates_format_of: generate_message(attr_name, :invalid, message: custom_message, value: value)
def test_generate_message_invalid_with_default_message
- assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, :value => 'title')
+ assert_equal 'is invalid', @person.errors.generate_message(:title, :invalid, value: 'title')
end
def test_generate_message_invalid_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, :message => 'custom message %{value}', :value => 'title')
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :invalid, message: 'custom message %{value}', value: 'title')
end
- # validates_confirmation_of: generate_message(attr_name, :confirmation, :message => custom_message)
+ # validates_confirmation_of: generate_message(attr_name, :confirmation, message: custom_message)
def test_generate_message_confirmation_with_default_message
assert_equal "doesn't match Title", @person.errors.generate_message(:title, :confirmation)
end
def test_generate_message_confirmation_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, :message => 'custom message')
+ assert_equal 'custom message', @person.errors.generate_message(:title, :confirmation, message: 'custom message')
end
- # validates_acceptance_of: generate_message(attr_name, :accepted, :message => custom_message)
+ # validates_acceptance_of: generate_message(attr_name, :accepted, message: custom_message)
def test_generate_message_accepted_with_default_message
assert_equal "must be accepted", @person.errors.generate_message(:title, :accepted)
end
def test_generate_message_accepted_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, :message => 'custom message')
+ assert_equal 'custom message', @person.errors.generate_message(:title, :accepted, message: 'custom message')
end
- # add_on_empty: generate_message(attr, :empty, :message => custom_message)
+ # add_on_empty: generate_message(attr, :empty, message: custom_message)
def test_generate_message_empty_with_default_message
assert_equal "can't be empty", @person.errors.generate_message(:title, :empty)
end
def test_generate_message_empty_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :empty, :message => 'custom message')
+ assert_equal 'custom message', @person.errors.generate_message(:title, :empty, message: 'custom message')
end
- # add_on_blank: generate_message(attr, :blank, :message => custom_message)
+ # add_on_blank: generate_message(attr, :blank, message: custom_message)
def test_generate_message_blank_with_default_message
assert_equal "can't be blank", @person.errors.generate_message(:title, :blank)
end
def test_generate_message_blank_with_custom_message
- assert_equal 'custom message', @person.errors.generate_message(:title, :blank, :message => 'custom message')
+ assert_equal 'custom message', @person.errors.generate_message(:title, :blank, message: 'custom message')
end
- # validates_length_of: generate_message(attr, :too_long, :message => custom_message, :count => option_value.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
- assert_equal "is too long (maximum is 10 characters)", @person.errors.generate_message(:title, :too_long, :count => 10)
+ 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_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_long, :message => 'custom message %{count}', :count => 10)
+ 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)
+ # validates_length_of: generate_message(attr, :too_short, default: custom_message, count: option_value.begin)
def test_generate_message_too_short_with_default_message
- assert_equal "is too short (minimum is 10 characters)", @person.errors.generate_message(:title, :too_short, :count => 10)
+ 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_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :too_short, :message => 'custom message %{count}', :count => 10)
+ 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)
+ # validates_length_of: generate_message(attr, :wrong_length, message: custom_message, count: option_value)
def test_generate_message_wrong_length_with_default_message
- assert_equal "is the wrong length (should be 10 characters)", @person.errors.generate_message(:title, :wrong_length, :count => 10)
+ 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_custom_message
- assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, :message => 'custom message %{count}', :count => 10)
+ assert_equal 'custom message 10', @person.errors.generate_message(:title, :wrong_length, message: 'custom message %{count}', count: 10)
end
- # validates_numericality_of: generate_message(attr_name, :not_a_number, :value => raw_value, :message => custom_message)
+ # validates_numericality_of: generate_message(attr_name, :not_a_number, value: raw_value, message: custom_message)
def test_generate_message_not_a_number_with_default_message
- assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, :value => 'title')
+ assert_equal "is not a number", @person.errors.generate_message(:title, :not_a_number, value: 'title')
end
def test_generate_message_not_a_number_with_custom_message
- assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, :message => 'custom message %{value}', :value => 'title')
+ assert_equal 'custom message title', @person.errors.generate_message(:title, :not_a_number, message: 'custom message %{value}', value: 'title')
end
- # validates_numericality_of: generate_message(attr_name, option, :value => raw_value, :default => custom_message)
+ # validates_numericality_of: generate_message(attr_name, option, value: raw_value, default: custom_message)
def test_generate_message_greater_than_with_default_message
- assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, :value => 'title', :count => 10)
+ assert_equal "must be greater than 10", @person.errors.generate_message(:title, :greater_than, value: 'title', count: 10)
end
def test_generate_message_greater_than_or_equal_to_with_default_message
- assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, :value => 'title', :count => 10)
+ assert_equal "must be greater than or equal to 10", @person.errors.generate_message(:title, :greater_than_or_equal_to, value: 'title', count: 10)
end
def test_generate_message_equal_to_with_default_message
- assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, :value => 'title', :count => 10)
+ assert_equal "must be equal to 10", @person.errors.generate_message(:title, :equal_to, value: 'title', count: 10)
end
def test_generate_message_less_than_with_default_message
- assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, :value => 'title', :count => 10)
+ assert_equal "must be less than 10", @person.errors.generate_message(:title, :less_than, value: 'title', count: 10)
end
def test_generate_message_less_than_or_equal_to_with_default_message
- assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, :value => 'title', :count => 10)
+ assert_equal "must be less than or equal to 10", @person.errors.generate_message(:title, :less_than_or_equal_to, value: 'title', count: 10)
end
def test_generate_message_odd_with_default_message
- assert_equal "must be odd", @person.errors.generate_message(:title, :odd, :value => 'title', :count => 10)
+ assert_equal "must be odd", @person.errors.generate_message(:title, :odd, value: 'title', count: 10)
end
def test_generate_message_even_with_default_message
- assert_equal "must be even", @person.errors.generate_message(:title, :even, :value => 'title', :count => 10)
+ assert_equal "must be even", @person.errors.generate_message(:title, :even, value: 'title', count: 10)
end
end
diff --git a/activemodel/test/cases/validations/i18n_validation_test.rb b/activemodel/test/cases/validations/i18n_validation_test.rb
index 4f8b7327c0..e29771d6b7 100644
--- a/activemodel/test/cases/validations/i18n_validation_test.rb
+++ b/activemodel/test/cases/validations/i18n_validation_test.rb
@@ -12,7 +12,7 @@ class I18nValidationTest < ActiveModel::TestCase
@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 => {:custom => nil}})
+ I18n.backend.store_translations('en', errors: { messages: { custom: nil } })
end
def teardown
@@ -21,42 +21,22 @@ class I18nValidationTest < ActiveModel::TestCase
I18n.backend = @old_backend
end
- def test_errors_add_on_empty_generates_message
- @person.errors.expects(:generate_message).with(:title, :empty, {})
- @person.errors.add_on_empty :title
- end
-
- def test_errors_add_on_empty_generates_message_with_custom_default_message
- @person.errors.expects(:generate_message).with(:title, :empty, {:message => 'custom'})
- @person.errors.add_on_empty :title, :message => 'custom'
- end
-
- def test_errors_add_on_blank_generates_message
- @person.errors.expects(:generate_message).with(:title, :blank, {})
- @person.errors.add_on_blank :title
- end
-
- def test_errors_add_on_blank_generates_message_with_custom_default_message
- @person.errors.expects(:generate_message).with(:title, :blank, {:message => 'custom'})
- @person.errors.add_on_blank :title, :message => 'custom'
- end
-
def test_full_message_encoding
- I18n.backend.store_translations('en', :errors => {
- :messages => { :too_short => '猫舌' }})
- Person.validates_length_of :title, :within => 3..5
+ I18n.backend.store_translations('en', errors: {
+ messages: { too_short: '猫舌' } })
+ Person.validates_length_of :title, within: 3..5
@person.valid?
assert_equal ['Title 猫舌'], @person.errors.full_messages
end
def test_errors_full_messages_translates_human_attribute_name_for_model_attributes
@person.errors.add(:name, 'not found')
- Person.expects(:human_attribute_name).with(:name, :default => 'Name').returns("Person's name")
+ Person.expects(:human_attribute_name).with(:name, default: 'Name').returns("Person's name")
assert_equal ["Person's name not found"], @person.errors.full_messages
end
def test_errors_full_messages_uses_format
- I18n.backend.store_translations('en', :errors => {:format => "Field %{attribute} %{message}"})
+ I18n.backend.store_translations('en', errors: { format: "Field %{attribute} %{message}" })
@person.errors.add('name', 'empty')
assert_equal ["Field Name empty"], @person.errors.full_messages
end
@@ -69,10 +49,10 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES = [
# [ case, validation_options, generate_message_options]
[ "given no options", {}, {}],
- [ "given custom message", {:message => "custom"}, {:message => "custom"}],
- [ "given if condition", {:if => lambda { true }}, {}],
- [ "given unless condition", {:unless => lambda { false }}, {}],
- [ "given option that is not reserved", {:format => "jpg"}, {:format => "jpg" }]
+ [ "given custom message", { message: "custom" }, { message: "custom" }],
+ [ "given if condition", { if: lambda { true }}, {}],
+ [ "given unless condition", { unless: lambda { false }}, {}],
+ [ "given option that is not reserved", { format: "jpg" }, { format: "jpg" }]
]
# validates_confirmation_of w/ mocha
@@ -81,7 +61,7 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_confirmation_of on generated message #{name}" do
Person.validates_confirmation_of :title, validation_options
@person.title_confirmation = 'foo'
- @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(:attribute => 'Title'))
+ @person.errors.expects(:generate_message).with(:title_confirmation, :confirmation, generate_message_options.merge(attribute: 'Title'))
@person.valid?
end
end
@@ -90,7 +70,7 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_acceptance_of on generated message #{name}" do
- Person.validates_acceptance_of :title, validation_options.merge(:allow_nil => false)
+ Person.validates_acceptance_of :title, validation_options.merge(allow_nil: false)
@person.errors.expects(:generate_message).with(:title, :accepted, generate_message_options)
@person.valid?
end
@@ -110,8 +90,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :withing on generated message when too short #{name}" do
- Person.validates_length_of :title, validation_options.merge(:within => 3..5)
- @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(:count => 3))
+ Person.validates_length_of :title, validation_options.merge(within: 3..5)
+ @person.errors.expects(:generate_message).with(:title, :too_short, generate_message_options.merge(count: 3))
@person.valid?
end
end
@@ -120,9 +100,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :too_long generated message #{name}" do
- Person.validates_length_of :title, validation_options.merge(:within => 3..5)
+ Person.validates_length_of :title, validation_options.merge(within: 3..5)
@person.title = 'this title is too long'
- @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(:count => 5))
+ @person.errors.expects(:generate_message).with(:title, :too_long, generate_message_options.merge(count: 5))
@person.valid?
end
end
@@ -131,8 +111,8 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_length_of for :is on generated message #{name}" do
- Person.validates_length_of :title, validation_options.merge(:is => 5)
- @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(:count => 5))
+ Person.validates_length_of :title, validation_options.merge(is: 5)
+ @person.errors.expects(:generate_message).with(:title, :wrong_length, generate_message_options.merge(count: 5))
@person.valid?
end
end
@@ -141,9 +121,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_format_of on generated message #{name}" do
- Person.validates_format_of :title, validation_options.merge(:with => /\A[1-9][0-9]*\z/)
+ Person.validates_format_of :title, validation_options.merge(with: /\A[1-9][0-9]*\z/)
@person.title = '72x'
- @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(:value => '72x'))
+ @person.errors.expects(:generate_message).with(:title, :invalid, generate_message_options.merge(value: '72x'))
@person.valid?
end
end
@@ -152,9 +132,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of on generated message #{name}" do
- Person.validates_inclusion_of :title, validation_options.merge(:in => %w(a b c))
+ Person.validates_inclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = 'z'
- @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(:value => 'z'))
+ @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z'))
@person.valid?
end
end
@@ -163,9 +143,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_inclusion_of using :within on generated message #{name}" do
- Person.validates_inclusion_of :title, validation_options.merge(:within => %w(a b c))
+ Person.validates_inclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = 'z'
- @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(:value => 'z'))
+ @person.errors.expects(:generate_message).with(:title, :inclusion, generate_message_options.merge(value: 'z'))
@person.valid?
end
end
@@ -174,9 +154,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of generated message #{name}" do
- Person.validates_exclusion_of :title, validation_options.merge(:in => %w(a b c))
+ Person.validates_exclusion_of :title, validation_options.merge(in: %w(a b c))
@person.title = 'a'
- @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(:value => 'a'))
+ @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a'))
@person.valid?
end
end
@@ -185,9 +165,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_exclusion_of using :within generated message #{name}" do
- Person.validates_exclusion_of :title, validation_options.merge(:within => %w(a b c))
+ Person.validates_exclusion_of :title, validation_options.merge(within: %w(a b c))
@person.title = 'a'
- @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(:value => 'a'))
+ @person.errors.expects(:generate_message).with(:title, :exclusion, generate_message_options.merge(value: 'a'))
@person.valid?
end
end
@@ -198,7 +178,7 @@ class I18nValidationTest < ActiveModel::TestCase
test "validates_numericality_of generated message #{name}" do
Person.validates_numericality_of :title, validation_options
@person.title = 'a'
- @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(:value => 'a'))
+ @person.errors.expects(:generate_message).with(:title, :not_a_number, generate_message_options.merge(value: 'a'))
@person.valid?
end
end
@@ -207,9 +187,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :only_integer on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(:only_integer => true)
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true)
@person.title = '0.0'
- @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(:value => '0.0'))
+ @person.errors.expects(:generate_message).with(:title, :not_an_integer, generate_message_options.merge(value: '0.0'))
@person.valid?
end
end
@@ -218,9 +198,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :odd on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(:only_integer => true, :odd => true)
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true, odd: true)
@person.title = 0
- @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(:value => 0))
+ @person.errors.expects(:generate_message).with(:title, :odd, generate_message_options.merge(value: 0))
@person.valid?
end
end
@@ -229,9 +209,9 @@ class I18nValidationTest < ActiveModel::TestCase
COMMON_CASES.each do |name, validation_options, generate_message_options|
test "validates_numericality_of for :less_than on generated message #{name}" do
- Person.validates_numericality_of :title, validation_options.merge(:only_integer => true, :less_than => 0)
+ Person.validates_numericality_of :title, validation_options.merge(only_integer: true, less_than: 0)
@person.title = 1
- @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(:value => 1, :count => 0))
+ @person.errors.expects(:generate_message).with(:title, :less_than, generate_message_options.merge(value: 1, count: 0))
@person.valid?
end
end
@@ -246,8 +226,8 @@ class I18nValidationTest < ActiveModel::TestCase
end
# test "validates_confirmation_of finds custom model key translation when blank"
test "#{validation} finds custom model key translation when #{error_type}" do
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message'}}}}}}
- I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}}
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message' } } } } } }
+ I18n.backend.store_translations 'en', errors: { messages: { error_type => 'global message'}}
yield(@person, {})
@person.valid?
@@ -256,17 +236,17 @@ class I18nValidationTest < ActiveModel::TestCase
# test "validates_confirmation_of finds custom model key translation with interpolation when blank"
test "#{validation} finds custom model key translation with interpolation when #{error_type}" do
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {attribute => {error_type => 'custom message with %{extra}'}}}}}}
- I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}}
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { attribute => { error_type => 'custom message with %{extra}' } } } } } }
+ I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} }
- yield(@person, {:extra => "extra information"})
+ yield(@person, { extra: "extra information" })
@person.valid?
assert_equal ['custom message with extra information'], @person.errors[attribute]
end
# test "validates_confirmation_of finds global default key translation when blank"
test "#{validation} finds global default key translation when #{error_type}" do
- I18n.backend.store_translations 'en', :errors => {:messages => {error_type => 'global message'}}
+ I18n.backend.store_translations 'en', errors: { messages: {error_type => 'global message'} }
yield(@person, {})
@person.valid?
@@ -284,7 +264,7 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_acceptance_of w/o mocha
set_expectations_for_validation "validates_acceptance_of", :accepted do |person, options_to_merge|
- Person.validates_acceptance_of :title, options_to_merge.merge(:allow_nil => false)
+ Person.validates_acceptance_of :title, options_to_merge.merge(allow_nil: false)
end
# validates_presence_of w/o mocha
@@ -296,36 +276,36 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_length_of :within w/o mocha
set_expectations_for_validation "validates_length_of", :too_short do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(:within => 3..5)
+ Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
end
set_expectations_for_validation "validates_length_of", :too_long do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(:within => 3..5)
+ Person.validates_length_of :title, options_to_merge.merge(within: 3..5)
person.title = "too long"
end
# validates_length_of :is w/o mocha
set_expectations_for_validation "validates_length_of", :wrong_length do |person, options_to_merge|
- Person.validates_length_of :title, options_to_merge.merge(:is => 5)
+ Person.validates_length_of :title, options_to_merge.merge(is: 5)
end
# validates_format_of w/o mocha
set_expectations_for_validation "validates_format_of", :invalid do |person, options_to_merge|
- Person.validates_format_of :title, options_to_merge.merge(:with => /\A[1-9][0-9]*\z/)
+ Person.validates_format_of :title, options_to_merge.merge(with: /\A[1-9][0-9]*\z/)
end
# validates_inclusion_of w/o mocha
set_expectations_for_validation "validates_inclusion_of", :inclusion do |person, options_to_merge|
- Person.validates_inclusion_of :title, options_to_merge.merge(:in => %w(a b c))
+ Person.validates_inclusion_of :title, options_to_merge.merge(in: %w(a b c))
end
# validates_exclusion_of w/o mocha
set_expectations_for_validation "validates_exclusion_of", :exclusion do |person, options_to_merge|
- Person.validates_exclusion_of :title, options_to_merge.merge(:in => %w(a b c))
+ Person.validates_exclusion_of :title, options_to_merge.merge(in: %w(a b c))
person.title = 'a'
end
@@ -339,55 +319,54 @@ class I18nValidationTest < ActiveModel::TestCase
# validates_numericality_of with :only_integer w/o mocha
set_expectations_for_validation "validates_numericality_of", :not_an_integer do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true)
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true)
person.title = '1.0'
end
# validates_numericality_of :odd w/o mocha
set_expectations_for_validation "validates_numericality_of", :odd do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true, :odd => true)
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, odd: true)
person.title = 0
end
# validates_numericality_of :less_than w/o mocha
set_expectations_for_validation "validates_numericality_of", :less_than do |person, options_to_merge|
- Person.validates_numericality_of :title, options_to_merge.merge(:only_integer => true, :less_than => 0)
+ Person.validates_numericality_of :title, options_to_merge.merge(only_integer: true, less_than: 0)
person.title = 1
end
# test with validates_with
def test_validations_with_message_symbol_must_translate
- I18n.backend.store_translations 'en', :errors => {:messages => {:custom_error => "I am a custom error"}}
- Person.validates_presence_of :title, :message => :custom_error
+ I18n.backend.store_translations 'en', errors: { messages: { custom_error: "I am a custom error" } }
+ Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
def test_validates_with_message_symbol_must_translate_per_attribute
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:attributes => {:title => {:custom_error => "I am a custom error"}}}}}}
- Person.validates_presence_of :title, :message => :custom_error
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { attributes: { title: { custom_error: "I am a custom error" } } } } } }
+ Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
def test_validates_with_message_symbol_must_translate_per_model
- I18n.backend.store_translations 'en', :activemodel => {:errors => {:models => {:person => {:custom_error => "I am a custom error"}}}}
- Person.validates_presence_of :title, :message => :custom_error
+ I18n.backend.store_translations 'en', activemodel: { errors: { models: { person: { custom_error: "I am a custom error" } } } }
+ Person.validates_presence_of :title, message: :custom_error
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
def test_validates_with_message_string
- Person.validates_presence_of :title, :message => "I am a custom error"
+ Person.validates_presence_of :title, message: "I am a custom error"
@person.title = nil
@person.valid?
assert_equal ["I am a custom error"], @person.errors[:title]
end
-
end
diff --git a/activemodel/test/cases/validations/inclusion_validation_test.rb b/activemodel/test/cases/validations/inclusion_validation_test.rb
index 117e9109fc..8b90856869 100644
--- a/activemodel/test/cases/validations/inclusion_validation_test.rb
+++ b/activemodel/test/cases/validations/inclusion_validation_test.rb
@@ -1,5 +1,6 @@
# encoding: utf-8
require 'cases/helper'
+require 'active_support/all'
require 'models/topic'
require 'models/person'
@@ -11,16 +12,38 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_range
- Topic.validates_inclusion_of( :title, :in => 'aaa'..'bbb' )
+ Topic.validates_inclusion_of(:title, in: 'aaa'..'bbb')
assert Topic.new("title" => "bbc", "content" => "abc").invalid?
assert Topic.new("title" => "aa", "content" => "abc").invalid?
+ assert Topic.new("title" => "aaab", "content" => "abc").invalid?
assert Topic.new("title" => "aaa", "content" => "abc").valid?
assert Topic.new("title" => "abc", "content" => "abc").valid?
assert Topic.new("title" => "bbb", "content" => "abc").valid?
end
+ def test_validates_inclusion_of_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.ago..Time.now)
+ assert Topic.new(title: 'aaa', created_at: 2.years.ago).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.ago).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.from_now).invalid?
+ end
+
+ def test_validates_inclusion_of_date_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(Date.today)..Date.today)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(Date.today)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(Date.today)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(Date.today)).invalid?
+ end
+
+ def test_validates_inclusion_of_date_time_range
+ Topic.validates_inclusion_of(:created_at, in: 1.year.until(DateTime.current)..DateTime.current)
+ assert Topic.new(title: 'aaa', created_at: 2.years.until(DateTime.current)).invalid?
+ assert Topic.new(title: 'aaa', created_at: 3.months.until(DateTime.current)).valid?
+ assert Topic.new(title: 'aaa', created_at: 37.weeks.since(DateTime.current)).invalid?
+ end
+
def test_validates_inclusion_of
- Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ) )
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ))
assert Topic.new("title" => "a!", "content" => "abc").invalid?
assert Topic.new("title" => "a b", "content" => "abc").invalid?
@@ -33,16 +56,16 @@ class InclusionValidationTest < ActiveModel::TestCase
assert t.errors[:title].any?
assert_equal ["is not included in the list"], t.errors[:title]
- assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => nil ) }
- assert_raise(ArgumentError) { Topic.validates_inclusion_of( :title, :in => 0) }
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: nil) }
+ assert_raise(ArgumentError) { Topic.validates_inclusion_of(:title, in: 0) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => "hi!" ) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => {} ) }
- assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of( :title, :in => [] ) }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: "hi!") }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: {}) }
+ assert_nothing_raised(ArgumentError) { Topic.validates_inclusion_of(:title, in: []) }
end
def test_validates_inclusion_of_with_allow_nil
- Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :allow_nil => true )
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), allow_nil: true)
assert Topic.new("title" => "a!", "content" => "abc").invalid?
assert Topic.new("title" => "", "content" => "abc").invalid?
@@ -50,7 +73,7 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_with_formatted_message
- Topic.validates_inclusion_of( :title, :in => %w( a b c d e f g ), :message => "option %{value} is not in the list" )
+ Topic.validates_inclusion_of(:title, in: %w( a b c d e f g ), message: "option %{value} is not in the list")
assert Topic.new("title" => "a", "content" => "abc").valid?
@@ -61,7 +84,7 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_with_within_option
- Topic.validates_inclusion_of( :title, :within => %w( a b c d e f g ) )
+ Topic.validates_inclusion_of(:title, within: %w( a b c d e f g ))
assert Topic.new("title" => "a", "content" => "abc").valid?
@@ -71,7 +94,7 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_for_ruby_class
- Person.validates_inclusion_of :karma, :in => %w( abe monkey )
+ Person.validates_inclusion_of :karma, in: %w( abe monkey )
p = Person.new
p.karma = "Lifo"
@@ -86,7 +109,7 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_with_lambda
- Topic.validates_inclusion_of :title, :in => lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
+ Topic.validates_inclusion_of :title, in: lambda{ |topic| topic.author_name == "sikachu" ? %w( monkey elephant ) : %w( abe wasabi ) }
t = Topic.new
t.title = "wasabi"
@@ -98,7 +121,7 @@ class InclusionValidationTest < ActiveModel::TestCase
end
def test_validates_inclusion_of_with_symbol
- Person.validates_inclusion_of :karma, :in => :available_karmas
+ Person.validates_inclusion_of :karma, in: :available_karmas
p = Person.new
p.karma = "Lifo"
diff --git a/activemodel/test/cases/validations/length_validation_test.rb b/activemodel/test/cases/validations/length_validation_test.rb
index 113bfd6337..8b2f886cc4 100644
--- a/activemodel/test/cases/validations/length_validation_test.rb
+++ b/activemodel/test/cases/validations/length_validation_test.rb
@@ -5,13 +5,12 @@ require 'models/topic'
require 'models/person'
class LengthValidationTest < ActiveModel::TestCase
-
def teardown
Topic.reset_callbacks(:validate)
end
def test_validates_length_of_with_allow_nil
- Topic.validates_length_of( :title, :is => 5, :allow_nil => true )
+ Topic.validates_length_of( :title, is: 5, allow_nil: true )
assert Topic.new("title" => "ab").invalid?
assert Topic.new("title" => "").invalid?
@@ -20,7 +19,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_with_allow_blank
- Topic.validates_length_of( :title, :is => 5, :allow_blank => true )
+ Topic.validates_length_of( :title, is: 5, allow_blank: true )
assert Topic.new("title" => "ab").invalid?
assert Topic.new("title" => "").valid?
@@ -29,7 +28,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_minimum
- Topic.validates_length_of :title, :minimum => 5
+ Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -51,13 +50,13 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_maximum_should_allow_nil
- Topic.validates_length_of :title, :maximum => 10
+ Topic.validates_length_of :title, maximum: 10
t = Topic.new
assert t.valid?
end
def test_optionally_validates_length_of_using_minimum
- Topic.validates_length_of :title, :minimum => 5, :allow_nil => true
+ Topic.validates_length_of :title, minimum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -67,7 +66,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_maximum
- Topic.validates_length_of :title, :maximum => 5
+ Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -82,7 +81,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_optionally_validates_length_of_using_maximum
- Topic.validates_length_of :title, :maximum => 5, :allow_nil => true
+ Topic.validates_length_of :title, maximum: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -92,7 +91,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_within
- Topic.validates_length_of(:title, :content, :within => 3..5)
+ Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "a!", "content" => "I'm ooooooooh so very long")
assert t.invalid?
@@ -111,7 +110,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_within_with_exclusive_range
- Topic.validates_length_of(:title, :within => 4...10)
+ Topic.validates_length_of(:title, within: 4...10)
t = Topic.new("title" => "9 chars!!")
assert t.valid?
@@ -125,7 +124,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_optionally_validates_length_of_using_within
- Topic.validates_length_of :title, :content, :within => 3..5, :allow_nil => true
+ Topic.validates_length_of :title, :content, within: 3..5, allow_nil: true
t = Topic.new('title' => 'abc', 'content' => 'abcd')
assert t.valid?
@@ -135,7 +134,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_is
- Topic.validates_length_of :title, :is => 5
+ Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -153,7 +152,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_optionally_validates_length_of_using_is
- Topic.validates_length_of :title, :is => 5, :allow_nil => true
+ Topic.validates_length_of :title, is: 5, allow_nil: true
t = Topic.new("title" => "valid", "content" => "whatever")
assert t.valid?
@@ -167,25 +166,25 @@ class LengthValidationTest < ActiveModel::TestCase
bigmax = 2 ** 32
bigrange = bigmin...bigmax
assert_nothing_raised do
- Topic.validates_length_of :title, :is => bigmin + 5
- Topic.validates_length_of :title, :within => bigrange
- Topic.validates_length_of :title, :in => bigrange
- Topic.validates_length_of :title, :minimum => bigmin
- Topic.validates_length_of :title, :maximum => bigmax
+ Topic.validates_length_of :title, is: bigmin + 5
+ Topic.validates_length_of :title, within: bigrange
+ Topic.validates_length_of :title, in: bigrange
+ Topic.validates_length_of :title, minimum: bigmin
+ Topic.validates_length_of :title, maximum: bigmax
end
end
def test_validates_length_of_nasty_params
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => -6) }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => 6) }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :minimum => "a") }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :maximum => "a") }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :within => "a") }
- assert_raise(ArgumentError) { Topic.validates_length_of(:title, :is => "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: -6) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: 6) }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, minimum: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, maximum: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, within: "a") }
+ assert_raise(ArgumentError) { Topic.validates_length_of(:title, is: "a") }
end
def test_validates_length_of_custom_errors_for_minimum_with_message
- Topic.validates_length_of( :title, :minimum => 5, :message => "boo %{count}" )
+ Topic.validates_length_of( :title, minimum: 5, message: "boo %{count}" )
t = Topic.new("title" => "uhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -193,7 +192,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_minimum_with_too_short
- Topic.validates_length_of( :title, :minimum => 5, :too_short => "hoo %{count}" )
+ Topic.validates_length_of( :title, minimum: 5, too_short: "hoo %{count}" )
t = Topic.new("title" => "uhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -201,7 +200,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_maximum_with_message
- Topic.validates_length_of( :title, :maximum => 5, :message => "boo %{count}" )
+ Topic.validates_length_of( :title, maximum: 5, message: "boo %{count}" )
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -209,7 +208,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_in
- Topic.validates_length_of(:title, :in => 10..20, :message => "hoo %{count}")
+ Topic.validates_length_of(:title, in: 10..20, message: "hoo %{count}")
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -222,7 +221,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_maximum_with_too_long
- Topic.validates_length_of( :title, :maximum => 5, :too_long => "hoo %{count}" )
+ Topic.validates_length_of( :title, maximum: 5, too_long: "hoo %{count}" )
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -230,21 +229,21 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_both_too_short_and_too_long
- Topic.validates_length_of :title, :minimum => 3, :maximum => 5, :too_short => 'too short', :too_long => 'too long'
+ Topic.validates_length_of :title, minimum: 3, maximum: 5, too_short: 'too short', too_long: 'too long'
- t = Topic.new(:title => 'a')
+ t = Topic.new(title: 'a')
assert t.invalid?
assert t.errors[:title].any?
assert_equal ['too short'], t.errors['title']
- t = Topic.new(:title => 'aaaaaa')
+ t = Topic.new(title: 'aaaaaa')
assert t.invalid?
assert t.errors[:title].any?
assert_equal ['too long'], t.errors['title']
end
def test_validates_length_of_custom_errors_for_is_with_message
- Topic.validates_length_of( :title, :is => 5, :message => "boo %{count}" )
+ Topic.validates_length_of( :title, is: 5, message: "boo %{count}" )
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -252,7 +251,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_custom_errors_for_is_with_wrong_length
- Topic.validates_length_of( :title, :is => 5, :wrong_length => "hoo %{count}" )
+ Topic.validates_length_of( :title, is: 5, wrong_length: "hoo %{count}" )
t = Topic.new("title" => "uhohuhoh", "content" => "whatever")
assert t.invalid?
assert t.errors[:title].any?
@@ -260,7 +259,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_minimum_utf8
- Topic.validates_length_of :title, :minimum => 5
+ Topic.validates_length_of :title, minimum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
assert t.valid?
@@ -272,7 +271,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_maximum_utf8
- Topic.validates_length_of :title, :maximum => 5
+ Topic.validates_length_of :title, maximum: 5
t = Topic.new("title" => "一二三四五", "content" => "whatever")
assert t.valid?
@@ -284,7 +283,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_within_utf8
- Topic.validates_length_of(:title, :content, :within => 3..5)
+ Topic.validates_length_of(:title, :content, within: 3..5)
t = Topic.new("title" => "一二", "content" => "12三四五六七")
assert t.invalid?
@@ -296,12 +295,12 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_optionally_validates_length_of_using_within_utf8
- Topic.validates_length_of :title, :within => 3..5, :allow_nil => true
+ Topic.validates_length_of :title, within: 3..5, allow_nil: true
- t = Topic.new(:title => "一二三四五")
+ t = Topic.new(title: "一二三四五")
assert t.valid?, t.errors.inspect
- t = Topic.new(:title => "一二三")
+ t = Topic.new(title: "一二三")
assert t.valid?, t.errors.inspect
t.title = nil
@@ -309,7 +308,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_using_is_utf8
- Topic.validates_length_of :title, :is => 5
+ Topic.validates_length_of :title, is: 5
t = Topic.new("title" => "一二345", "content" => "whatever")
assert t.valid?
@@ -321,9 +320,9 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_with_block
- Topic.validates_length_of :content, :minimum => 5, :too_short => "Your essay must be at least %{count} words.",
- :tokenizer => lambda {|str| str.scan(/\w+/) }
- t = Topic.new(:content => "this content should be long enough")
+ Topic.validates_length_of :content, minimum: 5, too_short: "Your essay must be at least %{count} words.",
+ tokenizer: lambda {|str| str.scan(/\w+/) }
+ t = Topic.new(content: "this content should be long enough")
assert t.valid?
t.content = "not long enough"
@@ -333,18 +332,18 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_for_fixnum
- Topic.validates_length_of(:approved, :is => 4)
+ Topic.validates_length_of(:approved, is: 4)
- t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1)
assert t.invalid?
assert t.errors[:approved].any?
- t = Topic.new("title" => "uhohuhoh", "content" => "whatever", :approved => 1234)
+ t = Topic.new("title" => "uhohuhoh", "content" => "whatever", approved: 1234)
assert t.valid?
end
def test_validates_length_of_for_ruby_class
- Person.validates_length_of :karma, :minimum => 5
+ Person.validates_length_of :karma, minimum: 5
p = Person.new
p.karma = "Pix"
@@ -359,7 +358,7 @@ class LengthValidationTest < ActiveModel::TestCase
end
def test_validates_length_of_for_infinite_maxima
- Topic.validates_length_of(:title, :within => 5..Float::INFINITY)
+ Topic.validates_length_of(:title, within: 5..Float::INFINITY)
t = Topic.new("title" => "1234")
assert t.invalid?
@@ -368,11 +367,58 @@ class LengthValidationTest < ActiveModel::TestCase
t.title = "12345"
assert t.valid?
- Topic.validates_length_of(:author_name, :maximum => Float::INFINITY)
+ Topic.validates_length_of(:author_name, maximum: Float::INFINITY)
assert t.valid?
t.author_name = "A very long author name that should still be valid." * 100
assert t.valid?
end
+
+ def test_validates_length_of_using_maximum_should_not_allow_nil_when_nil_not_allowed
+ Topic.validates_length_of :title, maximum: 10, allow_nil: false
+ t = Topic.new
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_maximum_should_not_allow_nil_and_empty_string_when_blank_not_allowed
+ Topic.validates_length_of :title, maximum: 10, allow_blank: false
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_both_minimum_and_maximum_should_not_allow_nil
+ Topic.validates_length_of :title, minimum: 5, maximum: 10
+ t = Topic.new
+ assert t.invalid?
+ end
+
+ def test_validates_length_of_using_minimum_0_should_not_allow_nil
+ Topic.validates_length_of :title, minimum: 0
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_validates_length_of_using_is_0_should_not_allow_nil
+ Topic.validates_length_of :title, is: 0
+ t = Topic.new
+ assert t.invalid?
+
+ t.title = ""
+ assert t.valid?
+ end
+
+ def test_validates_with_diff_in_option
+ Topic.validates_length_of(:title, is: 5)
+ Topic.validates_length_of(:title, is: 5, if: Proc.new { false } )
+
+ assert Topic.new("title" => "david").valid?
+ assert Topic.new("title" => "david2").invalid?
+ end
end
diff --git a/activemodel/test/cases/validations/numericality_validation_test.rb b/activemodel/test/cases/validations/numericality_validation_test.rb
index 6742a4bab0..84332ed014 100644
--- a/activemodel/test/cases/validations/numericality_validation_test.rb
+++ b/activemodel/test/cases/validations/numericality_validation_test.rb
@@ -30,84 +30,84 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_with_nil_allowed
- Topic.validates_numericality_of :approved, :allow_nil => true
+ Topic.validates_numericality_of :approved, allow_nil: true
invalid!(JUNK + BLANK)
valid!(NIL + FLOATS + INTEGERS + BIGDECIMAL + INFINITY)
end
def test_validates_numericality_of_with_integer_only
- Topic.validates_numericality_of :approved, :only_integer => true
+ Topic.validates_numericality_of :approved, only_integer: true
invalid!(NIL + BLANK + JUNK + FLOATS + BIGDECIMAL + INFINITY)
valid!(INTEGERS)
end
def test_validates_numericality_of_with_integer_only_and_nil_allowed
- Topic.validates_numericality_of :approved, :only_integer => true, :allow_nil => true
+ Topic.validates_numericality_of :approved, only_integer: true, allow_nil: true
invalid!(JUNK + BLANK + FLOATS + BIGDECIMAL + INFINITY)
valid!(NIL + INTEGERS)
end
def test_validates_numericality_with_greater_than
- Topic.validates_numericality_of :approved, :greater_than => 10
+ Topic.validates_numericality_of :approved, greater_than: 10
invalid!([-10, 10], 'must be greater than 10')
valid!([11])
end
def test_validates_numericality_with_greater_than_or_equal
- Topic.validates_numericality_of :approved, :greater_than_or_equal_to => 10
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: 10
invalid!([-9, 9], 'must be greater than or equal to 10')
valid!([10])
end
def test_validates_numericality_with_equal_to
- Topic.validates_numericality_of :approved, :equal_to => 10
+ Topic.validates_numericality_of :approved, equal_to: 10
invalid!([-10, 11] + INFINITY, 'must be equal to 10')
valid!([10])
end
def test_validates_numericality_with_less_than
- Topic.validates_numericality_of :approved, :less_than => 10
+ Topic.validates_numericality_of :approved, less_than: 10
invalid!([10], 'must be less than 10')
valid!([-9, 9])
end
def test_validates_numericality_with_less_than_or_equal_to
- Topic.validates_numericality_of :approved, :less_than_or_equal_to => 10
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: 10
invalid!([11], 'must be less than or equal to 10')
valid!([-10, 10])
end
def test_validates_numericality_with_odd
- Topic.validates_numericality_of :approved, :odd => true
+ Topic.validates_numericality_of :approved, odd: true
invalid!([-2, 2], 'must be odd')
valid!([-1, 1])
end
def test_validates_numericality_with_even
- Topic.validates_numericality_of :approved, :even => true
+ Topic.validates_numericality_of :approved, even: true
invalid!([-1, 1], 'must be even')
valid!([-2, 2])
end
def test_validates_numericality_with_greater_than_less_than_and_even
- Topic.validates_numericality_of :approved, :greater_than => 1, :less_than => 4, :even => true
+ Topic.validates_numericality_of :approved, greater_than: 1, less_than: 4, even: true
invalid!([1, 3, 4])
valid!([2])
end
def test_validates_numericality_with_other_than
- Topic.validates_numericality_of :approved, :other_than => 0
+ Topic.validates_numericality_of :approved, other_than: 0
invalid!([0, 0.0])
valid!([-1, 42])
@@ -115,7 +115,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
def test_validates_numericality_with_proc
Topic.send(:define_method, :min_approved, lambda { 5 })
- Topic.validates_numericality_of :approved, :greater_than_or_equal_to => Proc.new {|topic| topic.min_approved }
+ Topic.validates_numericality_of :approved, greater_than_or_equal_to: Proc.new {|topic| topic.min_approved }
invalid!([3, 4])
valid!([5, 6])
@@ -124,7 +124,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
def test_validates_numericality_with_symbol
Topic.send(:define_method, :max_approved, lambda { 5 })
- Topic.validates_numericality_of :approved, :less_than_or_equal_to => :max_approved
+ Topic.validates_numericality_of :approved, less_than_or_equal_to: :max_approved
invalid!([6])
valid!([4, 5])
@@ -132,13 +132,13 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_numeric_message
- Topic.validates_numericality_of :approved, :less_than => 4, :message => "smaller than %{count}"
+ Topic.validates_numericality_of :approved, less_than: 4, message: "smaller than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 10)
assert !topic.valid?
assert_equal ["smaller than 4"], topic.errors[:approved]
- Topic.validates_numericality_of :approved, :greater_than => 4, :message => "greater than %{count}"
+ Topic.validates_numericality_of :approved, greater_than: 4, message: "greater than %{count}"
topic = Topic.new("title" => "numeric test", "approved" => 1)
assert !topic.valid?
@@ -146,7 +146,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_of_for_ruby_class
- Person.validates_numericality_of :karma, :allow_nil => false
+ Person.validates_numericality_of :karma, allow_nil: false
p = Person.new
p.karma = "Pix"
@@ -161,11 +161,11 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def test_validates_numericality_with_invalid_args
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :greater_than_or_equal_to => "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :less_than_or_equal_to => "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :greater_than => "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :less_than => "foo" }
- assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, :equal_to => "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, greater_than_or_equal_to: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, less_than_or_equal_to: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, greater_than: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, less_than: "foo" }
+ assert_raise(ArgumentError){ Topic.validates_numericality_of :approved, equal_to: "foo" }
end
private
@@ -185,7 +185,7 @@ class NumericalityValidationTest < ActiveModel::TestCase
end
def with_each_topic_approved_value(values)
- topic = Topic.new(:title => "numeric test", :content => "whatever")
+ topic = Topic.new(title: "numeric test", content: "whatever")
values.each do |value|
topic.approved = value
yield topic, value
diff --git a/activemodel/test/cases/validations/presence_validation_test.rb b/activemodel/test/cases/validations/presence_validation_test.rb
index 510c13a7c3..2f228cfa83 100644
--- a/activemodel/test/cases/validations/presence_validation_test.rb
+++ b/activemodel/test/cases/validations/presence_validation_test.rb
@@ -41,7 +41,7 @@ class PresenceValidationTest < ActiveModel::TestCase
end
def test_validates_acceptance_of_with_custom_error_using_quotes
- Person.validates_presence_of :karma, :message => "This string contains 'single' and \"double\" quotes"
+ Person.validates_presence_of :karma, message: "This string contains 'single' and \"double\" quotes"
p = Person.new
assert p.invalid?
assert_equal "This string contains 'single' and \"double\" quotes", p.errors[:karma].last
@@ -70,4 +70,38 @@ class PresenceValidationTest < ActiveModel::TestCase
p[:karma] = "Cold"
assert p.valid?
end
+
+ def test_validates_presence_of_with_allow_nil_option
+ Topic.validates_presence_of(:title, allow_nil: true)
+
+ t = Topic.new(title: "something")
+ assert t.valid?, t.errors.full_messages
+
+ t.title = ""
+ assert t.invalid?
+ assert_equal ["can't be blank"], t.errors[:title]
+
+ t.title = " "
+ assert t.invalid?, t.errors.full_messages
+ assert_equal ["can't be blank"], t.errors[:title]
+
+ t.title = nil
+ assert t.valid?, t.errors.full_messages
+ end
+
+ def test_validates_presence_of_with_allow_blank_option
+ Topic.validates_presence_of(:title, allow_blank: true)
+
+ t = Topic.new(title: "something")
+ assert t.valid?, t.errors.full_messages
+
+ t.title = ""
+ assert t.valid?, t.errors.full_messages
+
+ t.title = " "
+ assert t.valid?, t.errors.full_messages
+
+ t.title = nil
+ assert t.valid?, t.errors.full_messages
+ end
end
diff --git a/activemodel/test/cases/validations/validates_test.rb b/activemodel/test/cases/validations/validates_test.rb
index 90bc018ae1..c1914b32bc 100644
--- a/activemodel/test/cases/validations/validates_test.rb
+++ b/activemodel/test/cases/validations/validates_test.rb
@@ -17,20 +17,20 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_messages_empty
- Person.validates :title, :presence => {:message => "" }
+ Person.validates :title, presence: { message: "" }
person = Person.new
assert !person.valid?, 'person should not be valid.'
end
def test_validates_with_built_in_validation
- Person.validates :title, :numericality => true
+ Person.validates :title, numericality: true
person = Person.new
person.valid?
assert_equal ['is not a number'], person.errors[:title]
end
def test_validates_with_attribute_specified_as_string
- Person.validates "title", :numericality => true
+ Person.validates "title", numericality: true
person = Person.new
person.valid?
assert_equal ['is not a number'], person.errors[:title]
@@ -41,14 +41,14 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_built_in_validation_and_options
- Person.validates :salary, :numericality => { :message => 'my custom message' }
+ Person.validates :salary, numericality: { message: 'my custom message' }
person = Person.new
person.valid?
assert_equal ['my custom message'], person.errors[:salary]
end
def test_validates_with_validator_class
- Person.validates :karma, :email => true
+ Person.validates :karma, email: true
person = Person.new
person.valid?
assert_equal ['is not an email'], person.errors[:karma]
@@ -62,33 +62,33 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_if_as_local_conditions
- Person.validates :karma, :presence => true, :email => { :unless => :condition_is_true }
+ Person.validates :karma, presence: true, email: { unless: :condition_is_true }
person = Person.new
person.valid?
assert_equal ["can't be blank"], person.errors[:karma]
end
def test_validates_with_if_as_shared_conditions
- Person.validates :karma, :presence => true, :email => true, :if => :condition_is_true
+ Person.validates :karma, presence: true, email: true, if: :condition_is_true
person = Person.new
person.valid?
assert_equal ["can't be blank", "is not an email"], person.errors[:karma].sort
end
def test_validates_with_unless_shared_conditions
- Person.validates :karma, :presence => true, :email => true, :unless => :condition_is_true
+ Person.validates :karma, presence: true, email: true, unless: :condition_is_true
person = Person.new
assert person.valid?
end
def test_validates_with_allow_nil_shared_conditions
- Person.validates :karma, :length => { :minimum => 20 }, :email => true, :allow_nil => true
+ Person.validates :karma, length: { minimum: 20 }, email: true, allow_nil: true
person = Person.new
assert person.valid?
end
def test_validates_with_regexp
- Person.validates :karma, :format => /positive|negative/
+ Person.validates :karma, format: /positive|negative/
person = Person.new
assert person.invalid?
assert_equal ['is invalid'], person.errors[:karma]
@@ -97,7 +97,7 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_array
- Person.validates :gender, :inclusion => %w(m f)
+ Person.validates :gender, inclusion: %w(m f)
person = Person.new
assert person.invalid?
assert_equal ['is not included in the list'], person.errors[:gender]
@@ -106,7 +106,7 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_range
- Person.validates :karma, :length => 6..20
+ Person.validates :karma, length: 6..20
person = Person.new
assert person.invalid?
assert_equal ['is too short (minimum is 6 characters)'], person.errors[:karma]
@@ -115,33 +115,33 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_validates_with_validator_class_and_options
- Person.validates :karma, :email => { :message => 'my custom message' }
+ Person.validates :karma, email: { message: 'my custom message' }
person = Person.new
person.valid?
assert_equal ['my custom message'], person.errors[:karma]
end
def test_validates_with_unknown_validator
- assert_raise(ArgumentError) { Person.validates :karma, :unknown => true }
+ assert_raise(ArgumentError) { Person.validates :karma, unknown: true }
end
def test_validates_with_included_validator
- PersonWithValidator.validates :title, :presence => true
+ PersonWithValidator.validates :title, presence: true
person = PersonWithValidator.new
person.valid?
assert_equal ['Local validator'], person.errors[:title]
end
def test_validates_with_included_validator_and_options
- PersonWithValidator.validates :title, :presence => { :custom => ' please' }
+ PersonWithValidator.validates :title, presence: { custom: ' please' }
person = PersonWithValidator.new
person.valid?
assert_equal ['Local validator please'], person.errors[:title]
end
def test_validates_with_included_validator_and_wildcard_shortcut
- # Shortcut for PersonWithValidator.validates :title, :like => { :with => "Mr." }
- PersonWithValidator.validates :title, :like => "Mr."
+ # Shortcut for PersonWithValidator.validates :title, like: { with: "Mr." }
+ PersonWithValidator.validates :title, like: "Mr."
person = PersonWithValidator.new
person.title = "Ms. Pacman"
person.valid?
@@ -149,7 +149,7 @@ class ValidatesTest < ActiveModel::TestCase
end
def test_defining_extra_default_keys_for_validates
- Topic.validates :title, :confirmation => true, :message => 'Y U NO CONFIRM'
+ Topic.validates :title, confirmation: true, message: 'Y U NO CONFIRM'
topic = Topic.new
topic.title = "What's happening"
topic.title_confirmation = "Not this"
diff --git a/activemodel/test/cases/validations/validations_context_test.rb b/activemodel/test/cases/validations/validations_context_test.rb
index 15a49e38dd..5f99b320a6 100644
--- a/activemodel/test/cases/validations/validations_context_test.rb
+++ b/activemodel/test/cases/validations/validations_context_test.rb
@@ -18,22 +18,22 @@ class ValidationsContextTest < ActiveModel::TestCase
end
end
- test "with a class that adds errors on update and validating a new model with no arguments" do
- Topic.validates_with(ValidatorThatAddsErrors, :on => :create)
+ test "with a class that adds errors on create and validating a new model with no arguments" do
+ Topic.validates_with(ValidatorThatAddsErrors, on: :create)
topic = Topic.new
- assert topic.valid?, "Validation doesn't run on create if 'on' is set to update"
+ assert topic.valid?, "Validation doesn't run on valid? if 'on' is set to create"
end
test "with a class that adds errors on update and validating a new model" do
- Topic.validates_with(ValidatorThatAddsErrors, :on => :update)
+ Topic.validates_with(ValidatorThatAddsErrors, on: :update)
topic = Topic.new
assert topic.valid?(:create), "Validation doesn't run on create if 'on' is set to update"
end
test "with a class that adds errors on create and validating a new model" do
- Topic.validates_with(ValidatorThatAddsErrors, :on => :create)
+ Topic.validates_with(ValidatorThatAddsErrors, on: :create)
topic = Topic.new
assert topic.invalid?(:create), "Validation does run on create if 'on' is set to create"
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
-end \ No newline at end of file
+end
diff --git a/activemodel/test/cases/validations/with_validation_test.rb b/activemodel/test/cases/validations/with_validation_test.rb
index 07c1bd0533..93716f1433 100644
--- a/activemodel/test/cases/validations/with_validation_test.rb
+++ b/activemodel/test/cases/validations/with_validation_test.rb
@@ -50,7 +50,7 @@ class ValidatesWithTest < ActiveModel::TestCase
end
end
- test "vaidation with class that adds errors" do
+ test "validation with class that adds errors" do
Topic.validates_with(ValidatorThatAddsErrors)
topic = Topic.new
assert topic.invalid?, "A class that adds errors causes the record to be invalid"
@@ -72,26 +72,26 @@ class ValidatesWithTest < ActiveModel::TestCase
end
test "with if statements that return false" do
- Topic.validates_with(ValidatorThatAddsErrors, :if => "1 == 2")
+ Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 2")
topic = Topic.new
assert topic.valid?
end
test "with if statements that return true" do
- Topic.validates_with(ValidatorThatAddsErrors, :if => "1 == 1")
+ Topic.validates_with(ValidatorThatAddsErrors, if: "1 == 1")
topic = Topic.new
assert topic.invalid?
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
test "with unless statements that return true" do
- Topic.validates_with(ValidatorThatAddsErrors, :unless => "1 == 1")
+ Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 1")
topic = Topic.new
assert topic.valid?
end
test "with unless statements that returns false" do
- Topic.validates_with(ValidatorThatAddsErrors, :unless => "1 == 2")
+ Topic.validates_with(ValidatorThatAddsErrors, unless: "1 == 2")
topic = Topic.new
assert topic.invalid?
assert topic.errors[:base].include?(ERROR_MESSAGE)
@@ -100,45 +100,23 @@ class ValidatesWithTest < ActiveModel::TestCase
test "passes all configuration options to the validator class" do
topic = Topic.new
validator = mock()
- validator.expects(:new).with(:foo => :bar, :if => "1 == 1").returns(validator)
+ validator.expects(:new).with(foo: :bar, if: "1 == 1", class: Topic).returns(validator)
validator.expects(:validate).with(topic)
- Topic.validates_with(validator, :if => "1 == 1", :foo => :bar)
- assert topic.valid?
- end
-
- test "calls setup method of validator passing in self when validator has setup method" do
- topic = Topic.new
- validator = stub_everything
- validator.stubs(:new).returns(validator)
- validator.stubs(:validate)
- validator.stubs(:respond_to?).with(:setup).returns(true)
- validator.expects(:setup).with(Topic).once
- Topic.validates_with(validator)
- assert topic.valid?
- end
-
- test "doesn't call setup method of validator when validator has no setup method" do
- topic = Topic.new
- validator = stub_everything
- validator.stubs(:new).returns(validator)
- validator.stubs(:validate)
- validator.stubs(:respond_to?).with(:setup).returns(false)
- validator.expects(:setup).with(Topic).never
- Topic.validates_with(validator)
+ Topic.validates_with(validator, if: "1 == 1", foo: :bar)
assert topic.valid?
end
test "validates_with with options" do
- Topic.validates_with(ValidatorThatValidatesOptions, :field => :first_name)
+ Topic.validates_with(ValidatorThatValidatesOptions, field: :first_name)
topic = Topic.new
assert topic.invalid?
assert topic.errors[:base].include?(ERROR_MESSAGE)
end
test "validates_with each validator" do
- Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content])
- topic = Topic.new :title => "Title", :content => "Content"
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content])
+ topic = Topic.new title: "Title", content: "Content"
assert topic.invalid?
assert_equal ["Value is Title"], topic.errors[:title]
assert_equal ["Value is Content"], topic.errors[:content]
@@ -146,36 +124,36 @@ class ValidatesWithTest < ActiveModel::TestCase
test "each validator checks validity" do
assert_raise RuntimeError do
- Topic.validates_with(ValidatorCheckValidity, :attributes => [:title])
+ Topic.validates_with(ValidatorCheckValidity, attributes: [:title])
end
end
test "each validator expects attributes to be given" do
- assert_raise RuntimeError do
+ assert_raise ArgumentError do
Topic.validates_with(ValidatorPerEachAttribute)
end
end
test "each validator skip nil values if :allow_nil is set to true" do
- Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_nil => true)
- topic = Topic.new :content => ""
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_nil: true)
+ topic = Topic.new content: ""
assert topic.invalid?
assert topic.errors[:title].empty?
assert_equal ["Value is "], topic.errors[:content]
end
test "each validator skip blank values if :allow_blank is set to true" do
- Topic.validates_with(ValidatorPerEachAttribute, :attributes => [:title, :content], :allow_blank => true)
- topic = Topic.new :content => ""
+ Topic.validates_with(ValidatorPerEachAttribute, attributes: [:title, :content], allow_blank: true)
+ topic = Topic.new content: ""
assert topic.valid?
assert topic.errors[:title].empty?
assert topic.errors[:content].empty?
end
test "validates_with can validate with an instance method" do
- Topic.validates :title, :with => :my_validation
+ Topic.validates :title, with: :my_validation
- topic = Topic.new :title => "foo"
+ topic = Topic.new title: "foo"
assert topic.valid?
assert topic.errors[:title].empty?
@@ -185,9 +163,9 @@ class ValidatesWithTest < ActiveModel::TestCase
end
test "optionally pass in the attribute being validated when validating with an instance method" do
- Topic.validates :title, :content, :with => :my_validation_with_arg
+ Topic.validates :title, :content, with: :my_validation_with_arg
- topic = Topic.new :title => "foo"
+ topic = Topic.new title: "foo"
assert !topic.valid?
assert topic.errors[:title].empty?
assert_equal ['is missing'], topic.errors[:content]
diff --git a/activemodel/test/cases/validations_test.rb b/activemodel/test/cases/validations_test.rb
index a9d32808da..039b6b8872 100644
--- a/activemodel/test/cases/validations_test.rb
+++ b/activemodel/test/cases/validations_test.rb
@@ -26,11 +26,11 @@ class ValidationsTest < ActiveModel::TestCase
def test_single_field_validation
r = Reply.new
r.title = "There's no content!"
- assert r.invalid?, "A reply without content shouldn't be saveable"
+ assert r.invalid?, "A reply without content shouldn't be savable"
assert r.after_validation_performed, "after_validation callback should be called"
r.content = "Messa content!"
- assert r.valid?, "A reply with content should be saveable"
+ assert r.valid?, "A reply with content should be savable"
assert r.after_validation_performed, "after_validation callback should be called"
end
@@ -166,7 +166,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_invalid_validator
Topic.validate :i_dont_exist
- assert_raise(NameError) do
+ assert_raises(NoMethodError) do
t = Topic.new
t.valid?
end
@@ -190,16 +190,16 @@ class ValidationsTest < ActiveModel::TestCase
def test_validation_order
Topic.validates_presence_of :title
- Topic.validates_length_of :title, :minimum => 2
+ Topic.validates_length_of :title, minimum: 2
t = Topic.new("title" => "")
assert t.invalid?
assert_equal "can't be blank", t.errors["title"].first
Topic.validates_presence_of :title, :author_name
Topic.validate {errors.add('author_email_address', 'will never be valid')}
- Topic.validates_length_of :title, :content, :minimum => 2
+ Topic.validates_length_of :title, :content, minimum: 2
- t = Topic.new :title => ''
+ t = Topic.new title: ''
assert t.invalid?
assert_equal :title, key = t.errors.keys[0]
@@ -213,10 +213,10 @@ class ValidationsTest < ActiveModel::TestCase
assert_equal 'is too short (minimum is 2 characters)', t.errors[key][0]
end
- def test_validaton_with_if_and_on
- Topic.validates_presence_of :title, :if => Proc.new{|x| x.author_name = "bad"; true }, :on => :update
+ def test_validation_with_if_and_on
+ Topic.validates_presence_of :title, if: Proc.new{|x| x.author_name = "bad"; true }, on: :update
- t = Topic.new(:title => "")
+ t = Topic.new(title: "")
# If block should not fire
assert t.valid?
@@ -239,7 +239,7 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_validation_with_message_as_proc
- Topic.validates_presence_of(:title, :message => proc { "no blanks here".upcase })
+ Topic.validates_presence_of(:title, message: proc { "no blanks here".upcase })
t = Topic.new
assert t.invalid?
@@ -248,7 +248,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_list_of_validators_for_model
Topic.validates_presence_of :title
- Topic.validates_length_of :title, :minimum => 2
+ Topic.validates_length_of :title, minimum: 2
assert_equal 2, Topic.validators.count
assert_equal [:presence, :length], Topic.validators.map(&:kind)
@@ -256,7 +256,7 @@ class ValidationsTest < ActiveModel::TestCase
def test_list_of_validators_on_an_attribute
Topic.validates_presence_of :title, :content
- Topic.validates_length_of :title, :minimum => 2
+ Topic.validates_length_of :title, minimum: 2
assert_equal 2, Topic.validators_on(:title).count
assert_equal [:presence, :length], Topic.validators_on(:title).map(&:kind)
@@ -265,13 +265,13 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_accessing_instance_of_validator_on_an_attribute
- Topic.validates_length_of :title, :minimum => 10
+ Topic.validates_length_of :title, minimum: 10
assert_equal 10, Topic.validators_on(:title).first.options[:minimum]
end
def test_list_of_validators_on_multiple_attributes
- Topic.validates :title, :length => { :minimum => 10 }
- Topic.validates :author_name, :presence => true, :format => /a/
+ Topic.validates :title, length: { minimum: 10 }
+ Topic.validates :author_name, presence: true, format: /a/
validators = Topic.validators_on(:title, :author_name)
@@ -283,7 +283,7 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_list_of_validators_will_be_empty_when_empty
- Topic.validates :title, :length => { :minimum => 10 }
+ Topic.validates :title, length: { minimum: 10 }
assert_equal [], Topic.validators_on(:author_name)
end
@@ -300,52 +300,52 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_strict_validation_in_validates
- Topic.validates :title, :strict => true, :presence => true
+ Topic.validates :title, strict: true, presence: true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_strict_validation_not_fails
- Topic.validates :title, :strict => true, :presence => true
- assert Topic.new(:title => "hello").valid?
+ Topic.validates :title, strict: true, presence: true
+ assert Topic.new(title: "hello").valid?
end
def test_strict_validation_particular_validator
- Topic.validates :title, :presence => { :strict => true }
+ Topic.validates :title, presence: { strict: true }
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_strict_validation_in_custom_validator_helper
- Topic.validates_presence_of :title, :strict => true
+ Topic.validates_presence_of :title, strict: true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_strict_validation_custom_exception
- Topic.validates_presence_of :title, :strict => CustomStrictValidationException
+ Topic.validates_presence_of :title, strict: CustomStrictValidationException
assert_raises CustomStrictValidationException do
Topic.new.valid?
end
end
def test_validates_with_bang
- Topic.validates! :title, :presence => true
+ Topic.validates! :title, presence: true
assert_raises ActiveModel::StrictValidationFailed do
Topic.new.valid?
end
end
def test_validates_with_false_hash_value
- Topic.validates :title, :presence => false
+ Topic.validates :title, presence: false
assert Topic.new.valid?
end
def test_strict_validation_error_message
- Topic.validates :title, :strict => true, :presence => true
+ Topic.validates :title, strict: true, presence: true
exception = assert_raises(ActiveModel::StrictValidationFailed) do
Topic.new.valid?
@@ -354,14 +354,14 @@ class ValidationsTest < ActiveModel::TestCase
end
def test_does_not_modify_options_argument
- options = { :presence => true }
+ options = { presence: true }
Topic.validates :title, options
- assert_equal({ :presence => true }, options)
+ assert_equal({ presence: true }, options)
end
def test_dup_validity_is_independent
Topic.validates_presence_of :title
- topic = Topic.new("title" => "Litterature")
+ topic = Topic.new("title" => "Literature")
topic.valid?
duped = topic.dup
@@ -373,4 +373,25 @@ 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
index 021ea61c80..ece644c40c 100644
--- a/activemodel/test/models/automobile.rb
+++ b/activemodel/test/models/automobile.rb
@@ -7,6 +7,6 @@ class Automobile
def validations
validates_presence_of :make
- validates_length_of :model, :within => 2..10
+ validates_length_of :model, within: 2..10
end
-end \ No newline at end of file
+end
diff --git a/activemodel/test/models/contact.rb b/activemodel/test/models/contact.rb
index 7bfc542afb..c25be28e1d 100644
--- a/activemodel/test/models/contact.rb
+++ b/activemodel/test/models/contact.rb
@@ -9,7 +9,7 @@ class Contact
end
def network
- {:git => :github}
+ { git: :github }
end
def initialize(options = {})
diff --git a/activemodel/test/models/helicopter.rb b/activemodel/test/models/helicopter.rb
index a52b6fb4dd..933f3c463a 100644
--- a/activemodel/test/models/helicopter.rb
+++ b/activemodel/test/models/helicopter.rb
@@ -1,3 +1,7 @@
class Helicopter
include ActiveModel::Conversion
end
+
+class Helicopter::Comanche
+ include ActiveModel::Conversion
+end
diff --git a/activemodel/test/models/observers.rb b/activemodel/test/models/observers.rb
deleted file mode 100644
index 3729b3435e..0000000000
--- a/activemodel/test/models/observers.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-class ORM
- include ActiveModel::Observing
-
- def save
- notify_observers :before_save
- end
-
- class Observer < ActiveModel::Observer
- def before_save_invocations
- @before_save_invocations ||= []
- end
-
- def before_save(record)
- before_save_invocations << record
- end
- end
-end
-
-class Widget < ORM; end
-class Budget < ORM; end
-class WidgetObserver < ORM::Observer; end
-class BudgetObserver < ORM::Observer; end
-class AuditTrail < ORM::Observer
- observe :widget, :budget
-end
-
-ORM.instantiate_observers
diff --git a/activemodel/test/models/reply.rb b/activemodel/test/models/reply.rb
index ec1efeac19..b77910e671 100644
--- a/activemodel/test/models/reply.rb
+++ b/activemodel/test/models/reply.rb
@@ -2,11 +2,11 @@ require 'models/topic'
class Reply < Topic
validate :errors_on_empty_content
- validate :title_is_wrong_create, :on => :create
+ validate :title_is_wrong_create, on: :create
validate :check_empty_title
- validate :check_content_mismatch, :on => :create
- validate :check_wrong_update, :on => :update
+ validate :check_content_mismatch, on: :create
+ validate :check_wrong_update, on: :update
def check_empty_title
errors[:title] << "is Empty" unless title && title.size > 0
diff --git a/activemodel/test/models/topic.rb b/activemodel/test/models/topic.rb
index c9af78f595..1411a093e9 100644
--- a/activemodel/test/models/topic.rb
+++ b/activemodel/test/models/topic.rb
@@ -6,7 +6,7 @@ class Topic
super | [ :message ]
end
- attr_accessor :title, :author_name, :content, :approved
+ attr_accessor :title, :author_name, :content, :approved, :created_at
attr_accessor :after_validation_performed
after_validation :perform_after_validation
diff --git a/activerecord/CHANGELOG.md b/activerecord/CHANGELOG.md
index f3362f81a3..7e6ef27964 100644
--- a/activerecord/CHANGELOG.md
+++ b/activerecord/CHANGELOG.md
@@ -1,908 +1,933 @@
-## Rails 4.0.0 (unreleased) ##
+* ActiveRecord::Base#attribute_for_inspect now truncates long arrays (more than 10 elements)
-* PostgreSQL adapter correctly fetches default values when using multiple schemas and domains in a db. Fixes #7914
+ *Jan Bernacki*
+
+* Allow for the name of the schema_migrations table to be configured.
+
+ *Jerad Phelps*
+
+* Do not add to scope includes values from through associations.
+ Fixed bug when providing `includes` in through association scope, and fetching targets.
+
+ Example:
+ class Vendor < ActiveRecord::Base
+ has_many :relationships, -> { includes(:user) }
+ has_many :users, through: :relationships
+ end
+
+ vendor = Vendor.first
+
+ # Before
+
+ vendor.users.to_a # => Raises exception: not found `:user` for `User`
+
+ # After
- *Arturo Pie*
+ vendor.users.to_a # => No exception is raised
-* Learn ActiveRecord::QueryMethods#order work with hash arguments
- When symbol or hash passed we convert it to Arel::Nodes::Ordering.
- If we pass invalid direction(like name: :DeSc) ActiveRecord::QueryMethods#order will raise an exception
-
- User.order(:name, email: :desc)
- # SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
+ Fixes #12242, #9517, #10240.
- *Tima Maslyuchenko*
+ *Paul Nikitochkin*
-* Rename `ActiveRecord::Fixtures` class to `ActiveRecord::FixtureSet`.
- Instances of this class normally hold a collection of fixtures (records)
- loaded either from a single YAML file, or from a file and a folder
- with the same name. This change make the class name singular and makes
- the class easier to distinguish from the modules like
- `ActiveRecord::TestFixtures`, which operates on multiple fixture sets,
- or `DelegatingFixtures`, `::Fixtures`, etc.,
- and from the class `ActiveRecord::Fixture`, which corresponds to a single
- fixture.
+* Type cast json values on write, so that the value is consistent
+ with reading from the database.
- *Alexey Muranov*
+ Example:
+
+ x = JsonDataType.new tags: {"string" => "foo", :symbol => :bar}
+
+ # Before:
+ x.tags # => {"string" => "foo", :symbol => :bar}
-* The postgres adapter now supports tables with capital letters.
- Fix #5920
+ # After:
+ x.tags # => {"string" => "foo", "symbol" => "bar"}
+
+ *Severin Schoepke*
+
+* `ActiveRecord::Store` works together with PG `hstore` columns.
+ Fixes #12452.
*Yves Senn*
-* `CollectionAssociation#count` returns `0` without querying if the
- parent record is not persisted.
+* 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`.
- Before:
+ *Yves Senn*
- person.pets.count
- # SELECT COUNT(*) FROM "pets" WHERE "pets"."person_id" IS NULL
- # => 0
+* Save `has_one` association when primary key is manually set.
- After:
+ Fixes #12302.
- person.pets.count
- # fires without sql query
- # => 0
+ *Lauro Caetano*
- *Francesco Rodriguez*
+* Allow any version of BCrypt when using `has_secure_password`.
-* Fix `reset_counters` crashing on `has_many :through` associations.
- Fix #7822.
+ *Mike Perham*
- *lulalala*
+* Sub-query generated for `Relation` passed as array condition did not take in account
+ bind values and have invalid syntax.
-* Support for partial inserts.
+ Generate sub-query with inline bind values.
- When inserting new records, only the fields which have been changed
- from the defaults will actually be included in the INSERT statement.
- The other fields will be populated by the database.
+ Fixes #12586.
- This is more efficient, and also means that it will be safe to
- remove database columns without getting subsequent errors in running
- app processes (so long as the code in those processes doesn't
- contain any references to the removed column).
+ *Paul Nikitochkin*
- *Jon Leighton*
+* Fix a bug where rake db:structure:load crashed when the path contained
+ spaces.
-* Allow before and after validations to take an array of lifecycle events
+ *Kevin Mook*
- *John Foley*
+* `ActiveRecord::QueryMethods#unscope` unscopes negative equality
-* Support for specifying transaction isolation level
+ 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`.
- If your database supports setting the isolation level for a transaction, you can set
- it like so:
+ *Eric Hankins*
- Post.transaction(isolation: :serializable) do
- # ...
- end
+* Raise an exception when model without primary key calls `.find_with_ids`.
- Valid isolation levels are:
+ *Shimpei Makimoto*
- * `:read_uncommitted`
- * `:read_committed`
- * `:repeatable_read`
- * `:serializable`
+* Make `Relation#empty?` use `exists?` instead of `count`.
- You should consult the documentation for your database to understand the
- semantics of these different levels:
+ *Szymon Nowak*
- * http://www.postgresql.org/docs/9.1/static/transaction-iso.html
- * https://dev.mysql.com/doc/refman/5.0/en/set-transaction.html
+* `rake db:structure:dump` no longer crashes when the port was specified as `Fixnum`.
- An `ActiveRecord::TransactionIsolationError` will be raised if:
+ *Kenta Okamoto*
- * The adapter does not support setting the isolation level
- * You are joining an existing open transaction
- * You are creating a nested (savepoint) transaction
+* `NullRelation#pluck` takes a list of columns
- The mysql, mysql2 and postgresql adapters support setting the transaction
- 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.
+ The method signature in `NullRelation` was updated to mimic that in
+ `Calculations`.
- *Jon Leighton*
+ *Derek Prior*
-* `ActiveModel::ForbiddenAttributesProtection` is included by default
- in Active Record models. Check the docs of `ActiveModel::ForbiddenAttributesProtection`
- for more details.
+* `scope_chain` should not be mutated for other reflections.
- *Guillermo Iguaran*
+ 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.
-* Remove integration between Active Record and
- `ActiveModel::MassAssignmentSecurity`, `protected_attributes` gem
- should be added to use `attr_accessible`/`attr_protected`. Mass
- assignment options has been removed from all the AR methods that
- used it (ex. `AR::Base.new`, `AR::Base.create`, `AR::Base#update_attributes`, etc).
+ Fix is to dup the value before adding to the `scope_chain`.
- *Guillermo Iguaran*
+ Fixes #3882.
-* Fix the return of querying with an empty hash.
- Fix #6971.
+ *Neeraj Singh*
- User.where(token: {})
+* Prevent the inversed association from being reloaded on save.
- Before:
+ Fixes #9499.
- #=> SELECT * FROM users;
+ *Dmitry Polushkin*
- After:
+* Generate subquery for `Relation` if it passed as array condition for `where`
+ method.
- #=> SELECT * FROM users WHERE 1 = 2;
+ Example:
- *Damien Mathieu*
+ # 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))
-* Fix creation of through association models when using `collection=[]`
- on a `has_many :through` association from an unsaved model.
- Fix #7661.
+ # 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)
- *Ernie Miller*
+ Fixes #12415.
-* Explain only normal CRUD sql (select / update / insert / delete).
- Fix problem that explains unexplainable sql.
- Closes #7544 #6458.
+ *Paul Nikitochkin*
- *kennyj*
+* 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.
+
+ *Paul Nikitochkin*
+
+* `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.
+
+* `create_savepoint`, `rollback_to_savepoint` and `release_savepoint` accept
+ a savepoint name.
+
+ *Yves Senn*
-* You can now override the generated accessor methods for stored attributes
- and reuse the original behavior with `read_store_attribute` and `write_store_attribute`,
- which are counterparts to `read_attribute` and `write_attribute`.
+* Make `next_migration_number` accessible for third party generators.
- *Matt Jones*
+ *Yves Senn*
+
+* Objects instantiated using a null relationship will now retain the
+ attributes of the where clause.
+
+ Fixes #11676, #11675, #11376.
-* Accept belongs_to (including polymorphic) association keys in queries.
+ *Paul Nikitochkin*, *Peter Brown*, *Nthalk*
- The following queries are now equivalent:
+* 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.
- Post.where(author: author)
- Post.where(author_id: author)
+ *arthurnn*
- PriceEstimate.where(estimate_of: treasure)
- PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
+* Callbacks on has_many should access the in memory parent if a inverse_of is set.
- *Peter Brown*
+ *arthurnn*
-* Use native `mysqldump` command instead of `structure_dump` method
- when dumping the database structure to a sql file. Fixes #5547.
+* `ActiveRecord::ConnectionAdapters.string_to_time` respects
+ string with timezone (e.g. Wed, 04 Sep 2013 20:30:00 JST).
+
+ Fixes #12278.
*kennyj*
-* PostgreSQL inet and cidr types are converted to `IPAddr` objects.
+* 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`.
- *Dan McClain*
+ Example:
-* PostgreSQL array type support. Any datatype can be used to create an
- array column, with full migration and schema dumper support.
+ @my_comment.update_attributes(nil) # => raises ArgumentError
- To declare an array column, use the following syntax:
+ *John Wang*
- create_table :table_with_arrays do |t|
- t.integer :int_array, array: true
- # integer[]
- t.integer :int_array, array: true, length: 2
- # smallint[]
- t.string :string_array, array: true, length: 30
- # char varying(30)[]
- end
+* Deprecate `quoted_locking_column` method, which isn't used anywhere.
- This respects any other migration detail (limits, defaults, etc).
- Active Record will serialize and deserialize the array columns on
- their way to and from the database.
+ *kennyj*
- One thing to note: PostgreSQL does not enforce any limits on the
- number of elements, and any array can be multi-dimensional. Any
- array that is multi-dimensional must be rectangular (each sub array
- must have the same number of elements as its siblings).
+* Migration dump UUID default functions to schema.rb.
- If the `pg_array_parser` gem is available, it will be used when
- parsing PostgreSQL's array representation.
+ Fixes #10751.
- *Dan McClain*
+ *kennyj*
-* Attribute predicate methods, such as `article.title?`, will now raise
- `ActiveModel::MissingAttributeError` if the attribute being queried for
- truthiness was not read from the database, instead of just returning `false`.
+* Fixed a bug in `ActiveRecord::Associations::CollectionAssociation#find_by_scan`
+ when using `has_many` association with `:inverse_of` option and UUID primary key.
- *Ernie Miller*
+ Fixes #10450.
-* `ActiveRecord::SchemaDumper` uses Ruby 1.9 style hash, which means that the
- schema.rb file will be generated using this new syntax from now on.
+ *kennyj*
- *Konstantin Shabanov*
+* ActiveRecord::Base#<=> has been removed. Primary keys may not be in order,
+ or even be numbers, so sorting by id doesn't make sense. Please use `sort_by`
+ and specify the attribute you wish to sort with. For example, change:
-* Map interval with precision to string datatype in PostgreSQL. Fixes #7518.
+ Post.all.to_a.sort
- *Yves Senn*
+ to:
+
+ Post.all.to_a.sort_by(&:id)
-* Fix eagerly loading associations without primary keys. Fixes #4976.
+ *Aaron Patterson*
- *Kelley Reynolds*
+* 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.
-* Rails now raise an exception when you're trying to run a migration that has an invalid
- file name. Only lower case letters, numbers, and '_' are allowed in migration's file name.
- Please see #7419 for more details.
+ Fixes #11963.
- *Jan Bernacki*
+ *Paul Nikitochkin*
+
+* Deprecate the delegation of Array bang methods for associations.
+ To use them, instead first call `#to_a` on the association to access the
+ array to be acted on.
-* Fix bug when calling `store_accessor` multiple times.
- Fixes #7532.
+ *Ben Woosley*
- *Matt Jones*
+* `CollectionAssociation#first`/`#last` (e.g. `has_many`) use a `LIMIT`ed
+ query to fetch results rather than loading the entire collection.
-* Fix store attributes that show the changes incorrectly.
- Fixes #7532.
+ *Lann Martin*
+
+* Make possible to run SQLite rake tasks without the `Rails` constant defined.
+
+ *Damien Mathieu*
- *Matt Jones*
+* Allow Relation#from to accept other relations with bind values.
+
+ *Ryan Wallace*
+
+* Fix inserts with prepared statements disabled.
+
+ Fixes #12023.
+
+ *Rafael Mendonça França*
-* Fix `ActiveRecord::Relation#pluck` when columns or tables are reserved words.
+* Setting a has_one association on a new record no longer causes an empty
+ transaction.
- *Ian Lesperance*
+ *Dylan Thacker-Smith*
-* Allow JSON columns to be created in PostgreSQL and properly encoded/decoded.
- to/from database.
+* Fix `AR::Relation#merge` sometimes failing to preserve `readonly(false)` flag.
- *Dickson S. Guedes*
+ *thedarkone*
-* Fix time column type casting for invalid time string values to correctly return `nil`.
+* Re-use `order` argument pre-processing for `reorder`.
- *Adam Meehan*
+ *Paul Nikitochkin*
-* Allow to pass Symbol or Proc into `:limit` option of #accepts_nested_attributes_for.
+* Fix PredicateBuilder so polymorphic association keys in `where` clause can
+ accept objects other than direct descendants of `ActiveRecord::Base` (decorated
+ models, for example).
*Mikhail Dieterle*
-* ActiveRecord::SessionStore has been extracted from Active Record as `activerecord-session_store`
- gem. Please read the `README.md` file on the gem for the usage.
+* PostgreSQL adapter recognizes negative money values formatted with
+ parentheses (eg. `($1.25) # => -1.25`)).
+ Fixes #11899.
- *Prem Sichanugrist*
+ *Yves Senn*
-* Fix `reset_counters` when there are multiple `belongs_to` association with the
- same foreign key and one of them have a counter cache.
- Fixes #5200.
+* Stop interpreting SQL 'string' columns as :string type because there is no
+ common STRING datatype in SQL.
- *Dave Desrochers*
+ *Ben Woosley*
-* `serialized_attributes` and `_attr_readonly` become class method only. Instance reader methods are deprecated.
+* `ActiveRecord::FinderMethods#exists?` returns `true`/`false` in all cases.
- *kennyj*
+ *Xavier Noria*
-* Round usec when comparing timestamp attributes in the dirty tracking.
- Fixes #6975.
+* Assign inet/cidr attribute with `nil` value for invalid address.
- *kennyj*
+ Example:
-* Use inversed parent for first and last child of has_many association.
+ record = User.new
+ record.logged_in_from_ip # is type of an inet or a cidr
- *Ravil Bayramgalin*
+ # Before:
+ record.logged_in_from_ip = 'bad ip address' # raise exception
-* Fix Column.microseconds and Column.fast_string_to_date to avoid converting
- timestamp seconds to a float, since it occasionally results in inaccuracies
- with microsecond-precision times. Fixes #7352.
+ # 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'
- *Ari Pollak*
+ *Paul Nikitochkin*
-* Raise `ArgumentError` if list of attributes to change is empty in `update_all`.
+* `add_to_target` now accepts a second optional `skip_callbacks` argument
- *Roman Shatsov*
+ If truthy, it will skip the :before_add and :after_add callbacks.
-* Fix AR#create to return an unsaved record when AR::RecordInvalid is
- raised. Fixes #3217.
+ *Ben Woosley*
- *Dave Yeu*
+* Fix interactions between `:before_add` callbacks and nested attributes
+ assignment of `has_many` associations, when the association was not
+ yet loaded:
-* Fixed table name prefix that is generated in engines for namespaced models.
+ - A `:before_add` callback was being called when a nested attributes
+ assignment assigned to an existing record.
- *Wojciech Wnętrzak*
+ - Nested Attributes assignment did not affect the record in the
+ association target when a `:before_add` callback triggered the
+ loading of the association
-* Make sure `:environment` task is executed before `db:schema:load` or `db:structure:load`.
- Fixes #4772.
+ *Jörg Schray*
- *Seamus Abshere*
+* Allow enable_extension migration method to be revertible.
-* Allow Relation#merge to take a proc.
+ *Eric Tipton*
- This was requested by DHH to allow creating of one's own custom
- association macros.
+* Type cast hstore values on write, so that the value is consistent
+ with reading from the database.
- For example:
+ Example:
- module Commentable
- def has_many_comments(extra)
- has_many :comments, -> { where(:foo).merge(extra) }
- end
- end
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
- class Post < ActiveRecord::Base
- extend Commentable
- has_many_comments -> { where(:bar) }
- end
+ # Before:
+ x.tags # => {"bool" => true, "number" => 5}
- *Jon Leighton*
+ # After:
+ x.tags # => {"bool" => "true", "number" => "5"}
+
+ *Yves Senn* , *Severin Schoepke*
-* Add CollectionProxy#scope.
+* Fix multidimensional PG arrays containing non-string items.
- This can be used to get a Relation from an association.
+ *Yves Senn*
- Previously we had a #scoped method, but we're deprecating that for
- AR::Base, so it doesn't make sense to have it here.
+* Fixes bug when using includes combined with select, the select statement was overwritten.
- This was requested by DHH, to facilitate code like this:
+ Fixes #11773.
- Project.scope.order('created_at DESC').page(current_page).tagged_with(@tag).limit(5).scoping do
- @topics = @project.topics.scope
- @todolists = @project.todolists.scope
- @attachments = @project.attachments.scope
- @documents = @project.documents.scope
- end
+ *Edo Balvers*
- *Jon Leighton*
+* Load fixtures from linked folders.
-* Add `Relation#load`.
+ *Kassio Borges*
- This method explicitly loads the records and then returns `self`.
+* Create a directory for sqlite3 file if not present on the system.
- Rather than deciding between "do I want an array or a relation?",
- most people are actually asking themselves "do I want to eager load
- or lazy load?" Therefore, this method provides a way to explicitly
- eager-load without having to switch from a `Relation` to an array.
+ *Richard Schneeman*
- Example:
+* Removed redundant override of `xml` column definition for PG,
+ in order to use `xml` column type instead of `text`.
- @posts = Post.where(published: true).load
+ *Paul Nikitochkin*, *Michael Nikitochkin*
- *Jon Leighton*
+* Revert `ActiveRecord::Relation#order` change that make new order
+ prepend the old one.
-* `Relation#order`: make new order prepend old one.
+ Before:
User.order("name asc").order("created_at desc")
# SELECT * FROM users ORDER BY created_at desc, name asc
+ After:
+
+ User.order("name asc").order("created_at desc")
+ # SELECT * FROM users ORDER BY name asc, created_at desc
+
This also affects order defined in `default_scope` or any kind of associations.
- *Bogdan Gusiev*
+* Add ability to define how a class is converted to Arel predicates.
+ For example, adding a very vendor specific regex implementation:
-* `Model.all` now returns an `ActiveRecord::Relation`, rather than an
- array of records. Use `Relation#to_a` if you really want an array.
+ regex_handler = proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, value.source)
+ end
+ ActiveRecord::PredicateBuilder.register_handler(Regexp, regex_handler)
- In some specific cases, this may cause breakage when upgrading.
- However in most cases the `ActiveRecord::Relation` will just act as a
- lazy-loaded array and there will be no problems.
+ *Sean Griffin & @joannecheng*
- Note that calling `Model.all` with options (e.g.
- `Model.all(conditions: '...')` was already deprecated, but it will
- still return an array in order to make the transition easier.
+* Don't allow `quote_value` to be called without a column.
- `Model.scoped` is deprecated in favour of `Model.all`.
+ 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.
- `Relation#all` still returns an array, but is deprecated (since it
- would serve no purpose if we made it return a `Relation`).
+ *Ben Woosley*
- *Jon Leighton*
+* 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.
-* `:finder_sql` and `:counter_sql` options on collection associations
- are deprecated. Please transition to using scopes.
+ Fixes #6763.
- *Jon Leighton*
+ *Alfred Wong*
-* `:insert_sql` and `:delete_sql` options on `has_and_belongs_to_many`
- associations are deprecated. Please transition to using `has_many
- :through`.
+* rescue from all exceptions in `ConnectionManagement#call`
- *Jon Leighton*
+ Fixes #11497.
-* Added `#update_columns` method which updates the attributes from
- the passed-in hash without calling save, hence skipping validations and
- callbacks. `ActiveRecordError` will be raised when called on new objects
- or when at least one of the attributes is marked as read only.
+ 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.
- post.attributes # => {"id"=>2, "title"=>"My title", "body"=>"My content", "author"=>"Peter"}
- post.update_columns(title: 'New title', author: 'Sebastian') # => true
- post.attributes # => {"id"=>2, "title"=>"New title", "body"=>"My content", "author"=>"Sebastian"}
+ Rescuing from all exceptions and not just StandardError, fixes this
+ behaviour.
- *Sebastian Martinez + Rafael Mendonça França*
+ *Vipul A M*
-* The migration generator now creates a join table with (commented) indexes every time
- the migration name contains the word `join_table`:
+* `change_column` for PostgreSQL adapter respects the `:array` option.
- rails g migration create_join_table_for_artists_and_musics artist_id:index music_id
+ *Yves Senn*
- *Aleksey Magusev*
+* Remove deprecation warning from `attribute_missing` for attributes that are columns.
-* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to`
- and `remove_belongs_to` are acceptable. References are reversible.
+ *Arun Agrawal*
- Examples:
+* Remove extra decrement of transaction deep level.
- # Create a user_id column
- add_reference(:products, :user)
- # Create a supplier_id, supplier_type columns and appropriate index
- add_reference(:products, :supplier, polymorphic: true, index: true)
- # Remove polymorphic reference
- remove_reference(:products, :supplier, polymorphic: true)
+ Fixes #4566.
- *Aleksey Magusev*
+ *Paul Nikitochkin*
-* Add `:default` and `:null` options to `column_exists?`.
+* Reset @column_defaults when assigning `locking_column`.
+ We had a potential problem. For example:
- column_exists?(:testings, :taggable_id, :integer, null: false)
- column_exists?(:testings, :taggable_type, :string, default: 'Photo')
+ class Post < ActiveRecord::Base
+ self.column_defaults # if we call this unintentionally before setting locking_column ...
+ self.locking_column = 'my_locking_column'
+ end
- *Aleksey Magusev*
+ Post.column_defaults["my_locking_column"]
+ => nil # expected value is 0 !
-* `ActiveRecord::Relation#inspect` now makes it clear that you are
- dealing with a `Relation` object rather than an array:.
+ *kennyj*
- User.where(age: 30).inspect
- # => <ActiveRecord::Relation [#<User ...>, #<User ...>, ...]>
+* Remove extra select and update queries on save/touch/destroy ActiveRecord model
+ with belongs to reflection with option `touch: true`.
- User.where(age: 30).to_a.inspect
- # => [#<User ...>, #<User ...>]
+ Fixes #11288.
- The number of records displayed will be limited to 10.
+ *Paul Nikitochkin*
- *Brian Cardarella, Jon Leighton & Damien Mathieu*
+* Remove deprecated nil-passing to the following `SchemaCache` methods:
+ `primary_keys`, `tables`, `columns` and `columns_hash`.
-* Add `collation` and `ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later.
- Example:
+ *Yves Senn*
- development:
- adapter: postgresql
- host: localhost
- database: rails_development
- username: foo
- password: bar
- encoding: UTF8
- collation: ja_JP.UTF8
- ctype: ja_JP.UTF8
+* Remove deprecated block filter from `ActiveRecord::Migrator#migrate`.
- *kennyj*
+ *Yves Senn*
-* Changed validates_presence_of on an association so that children objects
- do not validate as being present if they are marked for destruction. This
- prevents you from saving the parent successfully and thus putting the parent
- in an invalid state.
+* Remove deprecated String constructor from `ActiveRecord::Migrator`.
- *Nick Monje & Brent Wheeldon*
+ *Yves Senn*
-* `FinderMethods#exists?` now returns `false` with the `false` argument.
+* Remove deprecated `scope` use without passing a callable object.
- *Egor Lynko*
+ *Arun Agrawal*
-* Added support for specifying the precision of a timestamp in the postgresql
- adapter. So, instead of having to incorrectly specify the precision using the
- `:limit` option, you may use `:precision`, as intended. For example, in a migration:
+* Remove deprecated `transaction_joinable=` in favor of `begin_transaction`
+ with `:joinable` option.
- def change
- create_table :foobars do |t|
- t.timestamps :precision => 0
- end
- end
+ *Arun Agrawal*
- *Tony Schneider*
+* Remove deprecated `decrement_open_transactions`.
-* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an
- array of arrays containing the typecasted values:
+ *Arun Agrawal*
- Person.pluck(:id, :name)
- # SELECT people.id, people.name FROM people
- # [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
+* Remove deprecated `increment_open_transactions`.
- *Jeroen van Ingen & Carlos Antonio da Silva*
+ *Arun Agrawal*
-* Improve the derivation of HABTM join table name to take account of nesting.
- It now takes the table names of the two models, sorts them lexically and
- then joins them, stripping any common prefix from the second table name.
+* Remove deprecated `PostgreSQLAdapter#outside_transaction?`
+ method. You can use `#transaction_open?` instead.
- Some examples:
+ *Yves Senn*
- Top level models (Category <=> Product)
- Old: categories_products
- New: categories_products
+* Remove deprecated `ActiveRecord::Fixtures.find_table_name` in favor of
+ `ActiveRecord::Fixtures.default_fixture_model_name`.
- Top level models with a global table_name_prefix (Category <=> Product)
- Old: site_categories_products
- New: site_categories_products
+ *Vipul A M*
- Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: categories_products
+* Removed deprecated `columns_for_remove` from `SchemaStatements`.
- Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: admin_categories_products
+ *Neeraj Singh*
- Nested models in a parent model (Catalog::Category <=> Catalog::Product)
- Old: categories_products
- New: catalog_categories_products
+* Remove deprecated `SchemaStatements#distinct`.
- Nested models in different parent models (Catalog::Category <=> Content::Page)
- Old: categories_pages
- New: catalog_categories_content_pages
+ *Francesco Rodriguez*
- *Andrew White*
+* Move deprecated `ActiveRecord::TestCase` into the rails test
+ suite. The class is no longer public and is only used for internal
+ Rails tests.
-* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of
- this is to move when the exceptions are raised from the point of declaration
- to when the association is built. This is consistant with other association
- validity checks.
+ *Yves Senn*
- *Andrew White*
+* Removed support for deprecated option `:restrict` for `:dependent`
+ in associations.
-* Added `stored_attributes` hash which contains the attributes stored using
- `ActiveRecord::Store`. This allows you to retrieve the list of attributes
- you've defined.
+ *Neeraj Singh*
- class User < ActiveRecord::Base
- store :settings, accessors: [:color, :homepage]
- end
+* Removed support for deprecated `delete_sql` in associations.
- User.stored_attributes[:settings] # [:color, :homepage]
+ *Neeraj Singh*
- *Joost Baaij & Carlos Antonio da Silva*
+* Removed support for deprecated `insert_sql` in associations.
-* PostgreSQL default log level is now 'warning', to bypass the noisy notice
- messages. You can change the log level using the `min_messages` option
- available in your config/database.yml.
+ *Neeraj Singh*
- *kennyj*
+* Removed support for deprecated `finder_sql` in associations.
-* Add uuid datatype support to PostgreSQL adapter.
+ *Neeraj Singh*
- *Konstantin Shabanov*
+* Support array as root element in JSON fields.
-* Added `ActiveRecord::Migration.check_pending!` that raises an error if
- migrations are pending.
+ *Alexey Noskov & Francesco Rodriguez*
- *Richard Schneeman*
+* Removed support for deprecated `counter_sql` in associations.
-* Added `#destroy!` which acts like `#destroy` but will raise an
- `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`.
+ *Neeraj Singh*
- *Marc-André Lafortune*
+* Do not invoke callbacks when `delete_all` is called on collection.
-* Allow blocks for `count` with `ActiveRecord::Relation`, to work similar as
- `Array#count`:
+ 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.
- Person.where("age > 26").count { |person| person.gender == 'female' }
+ User can also force a deletion strategy by passing parameter to
+ `delete_all`. For example you can do `@post.comments.delete_all(:nullify)`.
- *Chris Finne & Carlos Antonio da Silva*
+ *Neeraj Singh*
-* Added support to `CollectionAssociation#delete` for passing `fixnum`
- or `string` values as record ids. This finds the records responding
- to the `id` and executes delete on them.
+* Calling default_scope without a proc will now raise `ArgumentError`.
- class Person < ActiveRecord::Base
- has_many :pets
- end
+ *Neeraj Singh*
- person.pets.delete("1") # => [#<Pet id: 1>]
- person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>]
+* Removed deprecated method `type_cast_code` from Column.
- *Francesco Rodriguez*
+ *Neeraj Singh*
-* Deprecated most of the 'dynamic finder' methods. All dynamic methods
- except for `find_by_...` and `find_by_...!` are deprecated. Here's
- how you can rewrite the code:
+* Removed deprecated options `delete_sql` and `insert_sql` from HABTM
+ association.
- * `find_all_by_...` can be rewritten using `where(...)`
- * `find_last_by_...` can be rewritten using `where(...).last`
- * `scoped_by_...` can be rewritten using `where(...)`
- * `find_or_initialize_by_...` can be rewritten using
- `where(...).first_or_initialize`
- * `find_or_create_by_...` can be rewritten using
- `where(...).first_or_create`
- * `find_or_create_by_...!` can be rewritten using
- `where(...).first_or_create!`
+ Removed deprecated options `finder_sql` and `counter_sql` from
+ collection association.
- The implementation of the deprecated dynamic finders has been moved
- to the `activerecord-deprecated_finders` gem. See below for details.
+ *Neeraj Singh*
- *Jon Leighton*
+* Remove deprecated `ActiveRecord::Base#connection` method.
+ Make sure to access it via the class.
+
+ *Yves Senn*
+
+* Remove deprecation warning for `auto_explain_threshold_in_seconds`.
+
+ *Yves Senn*
+
+* Remove deprecated `:distinct` option from `Relation#count`.
+
+ *Yves Senn*
+
+* Removed deprecated methods `partial_updates`, `partial_updates?` and
+ `partial_updates=`.
+
+ *Neeraj Singh*
-* Deprecated the old-style hash based finder API. This means that
- methods which previously accepted "finder options" no longer do. For
- example this:
+* Removed deprecated method `scoped`
- Post.find(:all, conditions: { comments_count: 10 }, limit: 5)
+ *Neeraj Singh*
- Should be rewritten in the new style which has existed since Rails 3:
+* Removed deprecated method `default_scopes?`
- Post.where(comments_count: 10).limit(5)
+ *Neeraj Singh*
- Note that as an interim step, it is possible to rewrite the above as:
+* Remove implicit join references that were deprecated in 4.0.
- Post.all.merge(where: { comments_count: 10 }, limit: 5)
+ Example:
+
+ # before with implicit joins
+ Comment.where('posts.author_id' => 7)
- This could save you a lot of work if there is a lot of old-style
- finder usage in your application.
+ # after
+ Comment.references(:posts).where('posts.author_id' => 7)
- `Relation#merge` now accepts a hash of
- options, but they must be identical to the names of the equivalent
- finder method. These are mostly identical to the old-style finder
- option names, except in the following cases:
+ *Yves Senn*
- * `:conditions` becomes `:where`.
- * `:include` becomes `:includes`.
- * `:extend` becomes `:extending`.
+* Apply default scope when joining associations. For example:
- The code to implement the deprecated features has been moved out to
- the `activerecord-deprecated_finders` gem. This gem is a dependency
- of Active Record in Rails 4.0. It will no longer be a dependency
- from Rails 4.1, but if your app relies on the deprecated features
- then you can add it to your own Gemfile. It will be maintained by
- the Rails core team until Rails 5.0 is released.
+ class Post < ActiveRecord::Base
+ default_scope -> { where published: true }
+ end
+
+ class Comment
+ belongs_to :post
+ end
+
+ When calling `Comment.joins(:post)`, we expect to receive only
+ comments on published posts, since that is the default scope for
+ posts.
+
+ Before this change, the default scope from `Post` was not applied,
+ so we'd get comments on unpublished posts.
*Jon Leighton*
-* It's not possible anymore to destroy a model marked as read only.
+* Remove `activerecord-deprecated_finders` as a dependency
- *Johannes Barre*
+ *Åukasz StrzaÅ‚kowski*
-* Added ability to ActiveRecord::Relation#from to accept other ActiveRecord::Relation objects.
+* Remove Oracle / Sqlserver / Firebird database tasks that were deprecated in 4.0.
- Record.from(subquery)
- Record.from(subquery, :a)
+ *kennyj*
- *Radoslav Stankov*
+* `find_each` now returns an `Enumerator` when called without a block, so that it
+ can be chained with other `Enumerable` methods.
-* Added custom coders support for ActiveRecord::Store. Now you can set
- your custom coder like this:
+ *Ben Woosley*
- store :settings, accessors: [ :color, :homepage ], coder: JSON
+* `ActiveRecord::Result.each` now returns an `Enumerator` when called without
+ a block, so that it can be chained with other `Enumerable` methods.
- *Andrey Voronkov*
+ *Ben Woosley*
-* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by
- default to avoid silent data loss. This can be disabled by specifying
- `strict: false` in your `database.yml`.
+* Flatten merged join_values before building the joins.
- *Michael Pearson*
+ 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.
-* Added default order to `first` to assure consistent results among
- different database engines. Introduced `take` as a replacement to
- the old behavior of `first`.
+ Fixes #10669.
- *Marcelo Silveira*
+ *Neeraj Singh and iwiznia*
-* Added an `:index` option to automatically create indexes for references
- and belongs_to statements in migrations.
+* Do not load all child records for inverse case.
- The `references` and `belongs_to` methods now support an `index`
- option that receives either a boolean value or an options hash
- that is identical to options available to the add_index method:
+ currently `post.comments.find(Comment.first.id)` would load all
+ comments for the given post to set the inverse association.
- create_table :messages do |t|
- t.references :person, index: true
- end
+ 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.
- Is the same as:
+ Fix is to use in-memory records only if loaded? is true. Otherwise
+ load the records using full sql.
- create_table :messages do |t|
- t.references :person
- end
- add_index :messages, :person_id
+ Fixes #10509.
- Generators have also been updated to use the new syntax.
+ *Neeraj Singh*
- *Joshua Wood*
+* `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.
-* Added bang methods for mutating `ActiveRecord::Relation` objects.
- For example, while `foo.where(:bar)` will return a new object
- leaving `foo` unchanged, `foo.where!(:bar)` will mutate the foo
- object
+ Example:
- *Jon Leighton*
+ Author.inspect # => "Author(no database connection)"
-* Added `#find_by` and `#find_by!` to mirror the functionality
- provided by dynamic finders in a way that allows dynamic input more
- easily:
+ *Yves Senn*
- Post.find_by name: 'Spartacus', rating: 4
- Post.find_by "published_at < ?", 2.weeks.ago
- Post.find_by! name: 'Spartacus'
+* Handle single quotes in PostgreSQL default column values.
+ Fixes #10881.
- *Jon Leighton*
+ *Dylan Markow*
+
+* Log the sql that is actually sent to the database.
+
+ 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'`.
+
+ Do not squeeze whitespace out of sql queries. Fixes #10982.
+
+ *Neeraj Singh*
+
+* Fixture setup no longer depends on `ActiveRecord::Base.configurations`.
+ This is relevant when `ENV["DATABASE_URL"]` is used in place of a `database.yml`.
+
+ *Yves Senn*
-* Added ActiveRecord::Base#slice to return a hash of the given methods with
- their names as keys and returned values as values.
+* Fix mysql2 adapter raises the correct exception when executing a query on a
+ closed connection.
- *Guillermo Iguaran*
+ *Yves Senn*
-* Deprecate eager-evaluated scopes.
+* Ambiguous reflections are on :through relationships are no longer supported.
+ For example, you need to change this:
- Don't use this:
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, :through => :posts
+ end
- scope :red, where(color: 'red')
- default_scope where(color: 'red')
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
- Use this:
+ class Tagging < ActiveRecord::Base
+ end
- scope :red, -> { where(color: 'red') }
- default_scope { where(color: 'red') }
+ To this:
- The former has numerous issues. It is a common newbie gotcha to do
- the following:
+ class Author < ActiveRecord::Base
+ has_many :posts
+ has_many :taggings, :through => :posts, :source => :tagging
+ end
- scope :recent, where(published_at: Time.now - 2.weeks)
+ class Post < ActiveRecord::Base
+ has_one :tagging
+ has_many :taggings
+ end
- Or a more subtle variant:
+ class Tagging < ActiveRecord::Base
+ end
- scope :recent, -> { where(published_at: Time.now - 2.weeks) }
- scope :recent_red, recent.where(color: 'red')
+ *Aaron Patterson*
- Eager scopes are also very complex to implement within Active
- Record, and there are still bugs. For example, the following does
- not do what you expect:
+* 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.
- scope :remove_conditions, except(:where)
- where(...).remove_conditions # => still has conditions
+ Example:
- *Jon Leighton*
+ User.select("name, username").count
+ # Before => SELECT count(*) FROM users
+ # After => ActiveRecord::StatementInvalid
-* Remove IdentityMap
+ # 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
- IdentityMap has never graduated to be an "enabled-by-default" feature, due
- to some inconsistencies with associations, as described in this commit:
+ *Yves Senn*
- https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6
+* 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.
- Hence the removal from the codebase, until such issues are fixed.
+ 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.
- *Carlos Antonio da Silva*
+ 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.
-* Added the schema cache dump feature.
+ You can turn off the automatic detection of inverse associations by setting
+ the `:inverse_of` option to `false` like so:
- `Schema cache dump` feature was implemetend. This feature can dump/load internal state of `SchemaCache` instance
- because we want to boot rails more quickly when we have many models.
+ class Taggable < ActiveRecord::Base
+ belongs_to :tag, inverse_of: false
+ end
- Usage notes:
+ *John Wang*
- 1) execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:dump
- => generate db/schema_cache.dump
+* Fix `add_column` with `array` option when using PostgreSQL. Fixes #10432
- 2) add config.active_record.use_schema_cache_dump = true in config/production.rb. BTW, true is default.
+ *Adam Anderson*
- 3) boot rails.
- RAILS_ENV=production bundle exec rails server
- => use db/schema_cache.dump
+* Usage of `implicit_readonly` is being removed`. Please use `readonly` method
+ explicitly to mark records as `readonly.
+ Fixes #10615.
- 4) If you remove clear dumped cache, execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:clear
- => remove db/schema_cache.dump
+ Example:
- *kennyj*
+ user = User.joins(:todos).select("users.*, todos.title as todos_title").readonly(true).first
+ user.todos_title = 'clean pet'
+ user.save! # will raise error
-* Added support for partial indices to PostgreSQL adapter.
+ *Yves Senn*
- The `add_index` method now supports a `where` option that receives a
- string with the partial index criteria.
+* Fix the `:primary_key` option for `has_many` associations.
+ Fixes #10693.
- add_index(:accounts, :code, where: 'active')
+ *Yves Senn*
- Generates
+* Fix bug where tiny types are incorrectly coerced as boolean when the length is more than 1.
- CREATE INDEX index_accounts_on_code ON accounts(code) WHERE active
+ Fixes #10620.
- *Marcelo Silveira*
+ *Aaron Patterson*
-* Implemented ActiveRecord::Relation#none method.
+* Also support extensions in PostgreSQL 9.1. This feature has been supported since 9.1.
- The `none` method returns a chainable relation with zero records
- (an instance of the NullRelation class).
+ *kennyj*
- Any subsequent condition chained to the returned relation will continue
- generating an empty relation and will not fire any query to the database.
+* Deprecate `ConnectionAdapters::SchemaStatements#distinct`,
+ as it is no longer used by internals.
- *Juanjo Bazán*
+ *Ben Woosley*
-* Added the `ActiveRecord::NullRelation` class implementing the null
- object pattern for the Relation class.
+* Fix pending migrations error when loading schema and `ActiveRecord::Base.table_name_prefix`
+ is not blank.
- *Juanjo Bazán*
+ Call `assume_migrated_upto_version` on connection to prevent it from first
+ being picked up in `method_missing`.
-* Added new `dependent: :restrict_with_error` option. This will add
- an error to the model, rather than raising an exception.
+ 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`.
- The `:restrict` option is renamed to `:restrict_with_exception` to
- make this distinction explicit.
+ Fixes #10411.
- *Manoj Kumar & Jon Leighton*
+ *Kyle Stevens*
-* Added `create_join_table` migration helper to create HABTM join tables.
+* Method `read_attribute_before_type_cast` should accept input as symbol.
- create_join_table :products, :categories
- # =>
- # create_table :categories_products, id: false do |td|
- # td.integer :product_id, null: false
- # td.integer :category_id, null: false
- # end
+ *Neeraj Singh*
- *Rafael Mendonça França*
+* Confirm a record has not already been destroyed before decrementing counter cache.
-* The primary key is always initialized in the @attributes hash to `nil` (unless
- another value has been specified).
+ *Ben Tucker*
- *Aaron Paterson*
+* 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`.
-* In previous releases, the following would generate a single query with
- an `OUTER JOIN comments`, rather than two separate queries:
+ *Zach Ohlgren*
- Post.includes(:comments)
- .where("comments.name = 'foo'")
+* While removing index if column option is missing then raise IrreversibleMigration exception.
- This behaviour relies on matching SQL string, which is an inherently
- flawed idea unless we write an SQL parser, which we do not wish to
- do.
+ Following code should raise `IrreversibleMigration`. But the code was
+ failing since options is an array and not a hash.
- Therefore, it is now deprecated.
+ def change
+ change_table :users do |t|
+ t.remove_index [:name, :email]
+ end
+ end
- To avoid deprecation warnings and for future compatibility, you must
- explicitly state which tables you reference, when using SQL snippets:
+ Fix was to check if the options is a Hash before operating on it.
- Post.includes(:comments)
- .where("comments.name = 'foo'")
- .references(:comments)
+ Fixes #10419.
- Note that you do not need to explicitly specify references in the
- following cases, as they can be automatically inferred:
+ *Neeraj Singh*
- Post.where(comments: { name: 'foo' })
- Post.where('comments.name' => 'foo')
- Post.order('comments.name')
+* Do not overwrite manually built records during one-to-one nested attribute assignment
- You also do not need to worry about this unless you are doing eager
- loading. Basically, don't worry unless you see a deprecation warning
- or (in future releases) an SQL error due to a missing JOIN.
+ 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.:
- *Jon Leighton*
+ class Member < ActiveRecord::Base
+ has_one :avatar
+ accepts_nested_attributes_for :avatar
-* Support for the `schema_info` table has been dropped. Please
- switch to `schema_migrations`.
+ def avatar
+ super || build_avatar(width: 200)
+ end
+ end
- *Aaron Patterson*
+ member = Member.new
+ member.avatar_attributes = {icon: 'sad'}
+ member.avatar.width # => 200
-* Connections *must* be closed at the end of a thread. If not, your
- connection pool can fill and an exception will be raised.
+ *Olek Janiszewski*
- *Aaron Patterson*
+* 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.
+
+ *Johnny Holton*
+
+* Handle aliased attributes in ActiveRecord::Relation.
+
+ When using symbol keys, ActiveRecord will now translate aliased attribute names to the actual column name used in the database:
-* Added the `ActiveRecord::Model` module which can be included in a
- class as an alternative to inheriting from `ActiveRecord::Base`:
+ With the model
- class Post
- include ActiveRecord::Model
+ class Topic
+ alias_attribute :heading, :title
end
- Please note:
+ The call
- * Up until now it has been safe to assume that all AR models are
- descendants of `ActiveRecord::Base`. This is no longer a safe
- assumption, but it may transpire that there are areas of the
- code which still make this assumption. So there may be
- 'teething difficulties' with this feature. (But please do try it
- and report bugs.)
+ Topic.where(heading: 'The First Topic')
- * Plugins & libraries etc that add methods to `ActiveRecord::Base`
- will not be compatible with `ActiveRecord::Model`. Those libraries
- should add to `ActiveRecord::Model` instead (which is included in
- `Base`), or better still, avoid monkey-patching AR and instead
- provide a module that users can include where they need it.
+ should yield the same result as
- * To minimise the risk of conflicts with other code, it is
- advisable to include `ActiveRecord::Model` early in your class
- definition.
+ Topic.where(title: 'The First Topic')
- *Jon Leighton*
+ This also applies to ActiveRecord::Relation::Calculations calls such as `Model.sum(:aliased)` and `Model.pluck(:aliased)`.
-* PostgreSQL hstore records can be created.
+ This will not work with SQL fragment strings like `Model.sum('DISTINCT aliased')`.
- *Aaron Patterson*
+ *Godfrey Chan*
-* PostgreSQL hstore types are automatically deserialized from the database.
+* Mute `psql` output when running rake db:schema:load.
- *Aaron Patterson*
+ *Godfrey Chan*
+
+* Trigger a save on `has_one association=(associate)` when the associate contents have changed.
+
+ Fix #8856.
+
+ *Chris Thompson*
+
+* Abort a rake task when missing db/structure.sql like `db:schema:load` task.
+
+ *kennyj*
+
+* rake:db:test:prepare falls back to original environment after execution.
+
+ *Slava Markevich*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activerecord/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for previous changes.
diff --git a/activerecord/MIT-LICENSE b/activerecord/MIT-LICENSE
index 03bde18130..0d7fb865e2 100644
--- a/activerecord/MIT-LICENSE
+++ b/activerecord/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activerecord/README.rdoc b/activerecord/README.rdoc
index cc8942809c..e04abe9b37 100644
--- a/activerecord/README.rdoc
+++ b/activerecord/README.rdoc
@@ -80,17 +80,6 @@ A short rundown of some of the major features:
{Learn more}[link:classes/ActiveRecord/Callbacks.html]
-* Observers that react to changes in a model.
-
- class CommentObserver < ActiveRecord::Observer
- def after_create(comment) # is called just after Comment#save
- CommentMailer.new_comment_email('david@loudthinking.com', comment).deliver
- end
- end
-
- {Learn more}[link:classes/ActiveRecord/Observer.html]
-
-
* Inheritance hierarchies.
class Company < ActiveRecord::Base; end
@@ -141,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.sourceforge.net] and Logger[http://www.ruby-doc.org/stdlib/libdoc/logger/rdoc].
+* Logging support for Log4r[http://log4r.rubyforge.org] 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')
@@ -186,7 +175,7 @@ by relying on a number of conventions that make it easy for Active Record to inf
complex relations and structures from a minimal amount of explicit direction.
Convention over Configuration:
-* No XML-files!
+* No XML files!
* Lots of reflection and run-time extension
* Magic is not inherently a bad word
@@ -201,7 +190,7 @@ The latest version of Active Record can be installed with RubyGems:
% [sudo] gem install activerecord
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activerecord
@@ -215,7 +204,7 @@ Active Record is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
diff --git a/activerecord/RUNNING_UNIT_TESTS b/activerecord/RUNNING_UNIT_TESTS
deleted file mode 100644
index bdd8834dcb..0000000000
--- a/activerecord/RUNNING_UNIT_TESTS
+++ /dev/null
@@ -1,31 +0,0 @@
-== Setup
-
-If you don't have the environment set make sure to read
-
- http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#testing-active-record.
-
-== Running the Tests
-
-You can run a particular test file from the command line, e.g.
-
- $ ruby -Itest test/cases/base_test.rb
-
-To run a specific test:
-
- $ ruby -Itest test/cases/base_test.rb -n test_something_works
-
-You can run with a database other than the default you set in test/config.yml, using the ARCONN
-environment variable:
-
- $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
-
-You can run all the tests for a given database via rake:
-
- $ rake test_mysql
-
-The 'rake test' task will run all the tests for mysql, mysql2, sqlite3 and postgresql.
-
-== Custom Config file
-
-By default, the config file is expected to be at the path test/config.yml. You can specify a
-custom location with the ARCONFIG environment variable.
diff --git a/activerecord/RUNNING_UNIT_TESTS.rdoc b/activerecord/RUNNING_UNIT_TESTS.rdoc
new file mode 100644
index 0000000000..ca1f2fd665
--- /dev/null
+++ b/activerecord/RUNNING_UNIT_TESTS.rdoc
@@ -0,0 +1,44 @@
+== Setup
+
+If you don't have an environment for running tests, read
+http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#setting-up-a-development-environment
+
+== Running the Tests
+
+To run a specific test:
+
+ $ ruby -Itest test/cases/base_test.rb -n method_name
+
+To run a set of tests:
+
+ $ ruby -Itest test/cases/base_test.rb
+
+You can also run tests that depend upon a specific database backend. For
+example:
+
+ $ bundle exec rake test_sqlite3
+
+Simply executing <tt>bundle exec rake test</tt> is equivalent to the following:
+
+ $ bundle exec rake test_mysql
+ $ bundle exec rake test_mysql2
+ $ bundle exec rake test_postgresql
+ $ bundle exec rake test_sqlite3
+ $ bundle exec rake test_sqlite3_mem
+
+There should be tests available for each database backend listed in the {Config
+File}[rdoc-label:label-Config+File]. (the exact set of available tests is
+defined in +Rakefile+)
+
+== Config File
+
+If +test/config.yml+ is present, it's parameters are obeyed. Otherwise, the
+parameters in +test/config.example.yml+ are obeyed.
+
+You can override the +connections:+ parameter in either file using the +ARCONN+
+(Active Record CONNection) environment variable:
+
+ $ ARCONN=postgresql ruby -Itest test/cases/base_test.rb
+
+You can specify a custom location for the config file using the +ARCONFIG+
+environment variable.
diff --git a/activerecord/Rakefile b/activerecord/Rakefile
index 53ddff420e..cee1dd5aeb 100644
--- a/activerecord/Rakefile
+++ b/activerecord/Rakefile
@@ -1,5 +1,4 @@
require 'rake/testtask'
-require 'rake/packagetask'
require 'rubygems/package_task'
require File.expand_path(File.dirname(__FILE__)) + "/test/config"
@@ -39,6 +38,11 @@ namespace :test do
end
end
+namespace :db do
+ task :create => ['mysql:build_databases', 'postgresql:build_databases']
+ task :drop => ['mysql:drop_databases', 'postgresql:drop_databases']
+end
+
%w( mysql mysql2 postgresql sqlite3 sqlite3_mem firebird db2 oracle sybase openbase frontbase jdbcmysql jdbcpostgresql jdbcsqlite3 jdbcderby jdbch2 jdbchsqldb ).each do |adapter|
Rake::TestTask.new("test_#{adapter}") { |t|
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
@@ -54,11 +58,10 @@ end
task "isolated_test_#{adapter}" do
adapter_short = adapter == 'db2' ? adapter : adapter[/^[a-z0-9]+/]
puts [adapter, adapter_short].inspect
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
(Dir["test/cases/**/*_test.rb"].reject {
|x| x =~ /\/adapters\//
} + Dir["test/cases/adapters/#{adapter_short}/**/*_test.rb"]).all? do |file|
- sh(ruby, "-Itest", file)
+ sh(Gem.ruby, '-w' ,"-Itest", file)
end or raise "Failures"
end
@@ -115,14 +118,9 @@ namespace :postgresql do
%x( createdb -E UTF8 -T template0 #{config['arunit']['database']} )
%x( createdb -E UTF8 -T template0 #{config['arunit2']['database']} )
- # prepare hstore
- version = %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2")
- %w(arunit arunit2).each do |db|
- if version < "9.1.0"
- puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
- else
- %x( psql #{config[db]['database']} -c "CREATE EXTENSION hstore;" )
- end
+ # notify about preparing hstore
+ if %x( createdb --version ).strip.gsub(/(.*)(\d\.\d\.\d)$/, "\\2") < "9.1.0"
+ puts "Please prepare hstore data type. See http://www.postgresql.org/docs/9.0/static/hstore.html"
end
end
@@ -220,7 +218,7 @@ end
# Publishing ------------------------------------------------------
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/activerecord/activerecord.gemspec b/activerecord/activerecord.gemspec
index 53791d96ef..9986ded904 100644
--- a/activerecord/activerecord.gemspec
+++ b/activerecord/activerecord.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -8,21 +8,21 @@ Gem::Specification.new do |s|
s.description = 'Databases on Rails. Build a persistent domain model by mapping database tables to Ruby classes. Strong conventions for associations, validations, aggregations, migrations, and testing come baked-in.'
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'examples/**/*', 'lib/**/*']
s.require_path = 'lib'
- s.extra_rdoc_files = %w( README.rdoc )
+ s.extra_rdoc_files = %w(README.rdoc)
s.rdoc_options.concat ['--main', 'README.rdoc']
- s.add_dependency('activesupport', version)
- s.add_dependency('activemodel', version)
- s.add_dependency('arel', '~> 3.0.2')
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'activemodel', version
- s.add_dependency('activerecord-deprecated_finders', '0.0.1')
+ s.add_dependency 'arel', '~> 4.0.0'
end
diff --git a/activerecord/examples/associations.png b/activerecord/examples/associations.png
deleted file mode 100644
index 661c7a8bbc..0000000000
--- a/activerecord/examples/associations.png
+++ /dev/null
Binary files differ
diff --git a/activerecord/examples/performance.rb b/activerecord/examples/performance.rb
index cd9825b50c..d3546ce948 100644
--- a/activerecord/examples/performance.rb
+++ b/activerecord/examples/performance.rb
@@ -5,12 +5,12 @@ require 'benchmark/ips'
TIME = (ENV['BENCHMARK_TIME'] || 20).to_i
RECORDS = (ENV['BENCHMARK_RECORDS'] || TIME*1000).to_i
-conn = { :adapter => 'sqlite3', :database => ':memory:' }
+conn = { adapter: 'sqlite3', database: ':memory:' }
ActiveRecord::Base.establish_connection(conn)
class User < ActiveRecord::Base
- connection.create_table :users, :force => true do |t|
+ connection.create_table :users, force: true do |t|
t.string :name, :email
t.timestamps
end
@@ -19,7 +19,7 @@ class User < ActiveRecord::Base
end
class Exhibit < ActiveRecord::Base
- connection.create_table :exhibits, :force => true do |t|
+ connection.create_table :exhibits, force: true do |t|
t.belongs_to :user
t.string :name
t.text :notes
@@ -43,6 +43,8 @@ class Exhibit < ActiveRecord::Base
def self.feel(exhibits) exhibits.each { |e| e.feel } end
end
+def progress_bar(int); print "." if (int%100).zero? ; end
+
puts 'Generating data...'
module ActiveRecord
@@ -75,30 +77,32 @@ notes = ActiveRecord::Faker::LOREM.join ' '
today = Date.today
puts "Inserting #{RECORDS} users and exhibits..."
-RECORDS.times do
+RECORDS.times do |record|
user = User.create(
- :created_at => today,
- :name => ActiveRecord::Faker.name,
- :email => ActiveRecord::Faker.email
+ created_at: today,
+ name: ActiveRecord::Faker.name,
+ email: ActiveRecord::Faker.email
)
Exhibit.create(
- :created_at => today,
- :name => ActiveRecord::Faker.name,
- :user => user,
- :notes => notes
+ created_at: today,
+ name: ActiveRecord::Faker.name,
+ user: user,
+ notes: notes
)
+ progress_bar(record)
end
+puts "Done!\n"
Benchmark.ips(TIME) do |x|
ar_obj = Exhibit.find(1)
- attrs = { :name => 'sam' }
- attrs_first = { :name => 'sam' }
- attrs_second = { :name => 'tom' }
+ attrs = { name: 'sam' }
+ attrs_first = { name: 'sam' }
+ attrs_second = { name: 'tom' }
exhibit = {
- :name => ActiveRecord::Faker.name,
- :notes => notes,
- :created_at => Date.today
+ name: ActiveRecord::Faker.name,
+ notes: notes,
+ created_at: Date.today
}
x.report("Model#id") do
@@ -117,10 +121,18 @@ Benchmark.ips(TIME) do |x|
Exhibit.first.look
end
+ x.report 'Model.take' do
+ Exhibit.take
+ end
+
x.report("Model.all limit(100)") do
Exhibit.look Exhibit.limit(100)
end
+ x.report("Model.all take(100)") do
+ Exhibit.look Exhibit.take(100)
+ end
+
x.report "Model.all limit(100) with relationship" do
Exhibit.feel Exhibit.limit(100).includes(:user)
end
@@ -143,7 +155,7 @@ Benchmark.ips(TIME) do |x|
end
x.report 'Resource#update' do
- Exhibit.first.update_attributes(:name => 'bob')
+ Exhibit.first.update(name: 'bob')
end
x.report 'Resource#destroy' do
@@ -167,6 +179,6 @@ Benchmark.ips(TIME) do |x|
end
x.report "AR.execute(query)" do
- ActiveRecord::Base.connection.execute("Select * from exhibits where id = #{(rand * 1000 + 1).to_i}")
+ ActiveRecord::Base.connection.execute("SELECT * FROM exhibits WHERE id = #{(rand * 1000 + 1).to_i}")
end
end
diff --git a/activerecord/examples/simple.rb b/activerecord/examples/simple.rb
index c12f746992..4ed5d80eb2 100644
--- a/activerecord/examples/simple.rb
+++ b/activerecord/examples/simple.rb
@@ -1,14 +1,14 @@
-$LOAD_PATH.unshift "#{File.dirname(__FILE__)}/../lib"
+require File.expand_path('../../../load_paths', __FILE__)
require 'active_record'
class Person < ActiveRecord::Base
- establish_connection :adapter => 'sqlite3', :database => 'foobar.db'
- connection.create_table table_name, :force => true do |t|
+ establish_connection adapter: 'sqlite3', database: 'foobar.db'
+ connection.create_table table_name, force: true do |t|
t.string :name
end
end
-bob = Person.create!(:name => 'bob')
+bob = Person.create!(name: 'bob')
puts Person.all.inspect
bob.destroy
puts Person.all.inspect
diff --git a/activerecord/lib/active_record.rb b/activerecord/lib/active_record.rb
index 1675127ab0..f19f5ecdf9 100644
--- a/activerecord/lib/active_record.rb
+++ b/activerecord/lib/active_record.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2004-2012 David Heinemeier Hansson
+# Copyright (c) 2004-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -25,7 +25,6 @@ require 'active_support'
require 'active_support/rails'
require 'active_model'
require 'arel'
-require 'active_record/deprecated_finders'
require 'active_record/version'
@@ -35,29 +34,29 @@ module ActiveRecord
autoload :Base
autoload :Callbacks
autoload :Core
- autoload :CounterCache
autoload :ConnectionHandling
+ autoload :CounterCache
autoload :DynamicMatchers
autoload :Explain
autoload :Inheritance
autoload :Integration
autoload :Migration
autoload :Migrator, 'active_record/migration'
- autoload :Model
autoload :ModelSchema
autoload :NestedAttributes
- autoload :Observer
autoload :Persistence
autoload :QueryCache
autoload :Querying
autoload :ReadonlyAttributes
autoload :Reflection
+ autoload :RuntimeRegistry
autoload :Sanitization
autoload :Schema
autoload :SchemaDumper
autoload :SchemaMigration
autoload :Scoping
autoload :Serialization
+ autoload :StatementCache
autoload :Store
autoload :Timestamp
autoload :Transactions
@@ -71,11 +70,12 @@ module ActiveRecord
autoload :Aggregations
autoload :Associations
- autoload :AttributeMethods
autoload :AttributeAssignment
+ autoload :AttributeMethods
autoload :AutosaveAssociation
autoload :Relation
+ autoload :AssociationRelation
autoload :NullRelation
autoload_under 'relation' do
@@ -147,7 +147,6 @@ module ActiveRecord
'active_record/tasks/postgresql_database_tasks'
end
- autoload :TestCase
autoload :TestFixtures, 'active_record/fixtures'
def self.eager_load!
diff --git a/activerecord/lib/active_record/aggregations.rb b/activerecord/lib/active_record/aggregations.rb
index 3db8e0716b..0d5313956b 100644
--- a/activerecord/lib/active_record/aggregations.rb
+++ b/activerecord/lib/active_record/aggregations.rb
@@ -16,8 +16,8 @@ module ActiveRecord
# the database).
#
# class Customer < ActiveRecord::Base
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount)
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount)
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# end
#
# The customer class now has the following methods to manipulate the value objects:
@@ -113,7 +113,7 @@ module ActiveRecord
# other than the writer method.
#
# The immutable requirement is enforced by Active Record by freezing any object assigned as a value
- # object. Attempting to change it afterwards will result in a ActiveSupport::FrozenObjectError.
+ # object. Attempting to change it afterwards will result in a RuntimeError.
#
# Read more about value objects on http://c2.com/cgi/wiki?ValueObject and on the dangers of not
# keeping value objects immutable on http://c2.com/cgi/wiki?ValueObjectsShouldBeImmutable
@@ -138,15 +138,15 @@ module ActiveRecord
#
# class NetworkResource < ActiveRecord::Base
# composed_of :cidr,
- # :class_name => 'NetAddr::CIDR',
- # :mapping => [ %w(network_address network), %w(cidr_range bits) ],
- # :allow_nil => true,
- # :constructor => Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
- # :converter => Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
+ # class_name: 'NetAddr::CIDR',
+ # mapping: [ %w(network_address network), %w(cidr_range bits) ],
+ # allow_nil: true,
+ # constructor: Proc.new { |network_address, cidr_range| NetAddr::CIDR.create("#{network_address}/#{cidr_range}") },
+ # converter: Proc.new { |value| NetAddr::CIDR.create(value.is_a?(Array) ? value.join('/') : value) }
# end
#
# # This calls the :constructor
- # network_resource = NetworkResource.new(:network_address => '192.168.0.1', :cidr_range => 24)
+ # network_resource = NetworkResource.new(network_address: '192.168.0.1', cidr_range: 24)
#
# # These assignments will both use the :converter
# network_resource.cidr = [ '192.168.2.1', 8 ]
@@ -165,7 +165,7 @@ module ActiveRecord
# by specifying an instance of the value object in the conditions hash. The following example
# finds all customers with +balance_amount+ equal to 20 and +balance_currency+ equal to "USD":
#
- # Customer.where(:balance => Money.new(20, "USD")).all
+ # Customer.where(balance: Money.new(20, "USD"))
#
module ClassMethods
# Adds reader and writer methods for manipulating a value object:
@@ -197,17 +197,17 @@ module ActiveRecord
# can return nil to skip the assignment.
#
# Option examples:
- # composed_of :temperature, :mapping => %w(reading celsius)
- # composed_of :balance, :class_name => "Money", :mapping => %w(balance amount),
- # :converter => Proc.new { |balance| balance.to_money }
- # composed_of :address, :mapping => [ %w(address_street street), %w(address_city city) ]
+ # composed_of :temperature, mapping: %w(reading celsius)
+ # composed_of :balance, class_name: "Money", mapping: %w(balance amount),
+ # converter: Proc.new { |balance| balance.to_money }
+ # composed_of :address, mapping: [ %w(address_street street), %w(address_city city) ]
# composed_of :gps_location
- # composed_of :gps_location, :allow_nil => true
+ # composed_of :gps_location, allow_nil: true
# composed_of :ip_address,
- # :class_name => 'IPAddr',
- # :mapping => %w(ip to_i),
- # :constructor => Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
- # :converter => Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
+ # class_name: 'IPAddr',
+ # mapping: %w(ip to_i),
+ # constructor: Proc.new { |ip| IPAddr.new(ip, Socket::AF_INET) },
+ # converter: Proc.new { |ip| ip.is_a?(Integer) ? IPAddr.new(ip, Socket::AF_INET) : IPAddr.new(ip.to_s) }
#
def composed_of(part_id, options = {})
options.assert_valid_keys(:class_name, :mapping, :allow_nil, :constructor, :converter)
@@ -223,7 +223,8 @@ module ActiveRecord
reader_method(name, class_name, mapping, allow_nil, constructor)
writer_method(name, class_name, mapping, allow_nil, converter)
- create_reflection(:composed_of, part_id, nil, options, self)
+ reflection = ActiveRecord::Reflection.create(:composed_of, part_id, nil, options, self)
+ Reflection.add_aggregate_reflection self, part_id, reflection
end
private
diff --git a/activerecord/lib/active_record/association_relation.rb b/activerecord/lib/active_record/association_relation.rb
new file mode 100644
index 0000000000..20516bba0c
--- /dev/null
+++ b/activerecord/lib/active_record/association_relation.rb
@@ -0,0 +1,18 @@
+module ActiveRecord
+ class AssociationRelation < Relation
+ def initialize(klass, table, association)
+ super(klass, table)
+ @association = association
+ end
+
+ def proxy_association
+ @association
+ end
+
+ private
+
+ def exec_queries
+ super.each { |r| @association.set_inverse_instance r }
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/associations.rb b/activerecord/lib/active_record/associations.rb
index 69b95f814c..74e2774626 100644
--- a/activerecord/lib/active_record/associations.rb
+++ b/activerecord/lib/active_record/associations.rb
@@ -1,6 +1,7 @@
require 'active_support/core_ext/enumerable'
require 'active_support/core_ext/string/conversions'
require 'active_support/core_ext/module/remove_method'
+require 'active_record/errors'
module ActiveRecord
class InverseOfAssociationNotFoundError < ActiveRecordError #:nodoc:
@@ -72,12 +73,6 @@ module ActiveRecord
end
end
- class HasAndBelongsToManyAssociationForeignKeyNeeded < ActiveRecordError #:nodoc:
- def initialize(reflection)
- super("Cannot create self referential has_and_belongs_to_many association on '#{reflection.class_name rescue nil}##{reflection.name rescue nil}'. :association_foreign_key cannot be the same as the :foreign_key.")
- end
- end
-
class EagerLoadPolymorphicError < ActiveRecordError #:nodoc:
def initialize(reflection)
super("Can not eagerly load the polymorphic association #{reflection.name.inspect}")
@@ -113,7 +108,6 @@ module ActiveRecord
autoload :BelongsToAssociation, 'active_record/associations/belongs_to_association'
autoload :BelongsToPolymorphicAssociation, 'active_record/associations/belongs_to_polymorphic_association'
- autoload :HasAndBelongsToManyAssociation, 'active_record/associations/has_and_belongs_to_many_association'
autoload :HasManyAssociation, 'active_record/associations/has_many_association'
autoload :HasManyThroughAssociation, 'active_record/associations/has_many_through_association'
autoload :HasOneAssociation, 'active_record/associations/has_one_association'
@@ -163,7 +157,7 @@ module ActiveRecord
private
# Returns the specified association instance if it responds to :loaded?, nil otherwise.
def association_instance_get(name)
- @association_cache[name.to_sym]
+ @association_cache[name]
end
# Set the specified association instance.
@@ -171,7 +165,7 @@ module ActiveRecord
@association_cache[name] = association
end
- # Associations are a set of macro-like class methods for tying objects together through
+ # \Associations are a set of macro-like class methods for tying objects together through
# foreign keys. They express relationships like "Project has one Project Manager"
# or "Project belongs to a Portfolio". Each macro adds a number of methods to the
# class which are specialized according to the collection or association symbol and the
@@ -190,8 +184,8 @@ module ActiveRecord
# * <tt>Project#portfolio, Project#portfolio=(portfolio), Project#portfolio.nil?</tt>
# * <tt>Project#project_manager, Project#project_manager=(project_manager), Project#project_manager.nil?,</tt>
# * <tt>Project#milestones.empty?, Project#milestones.size, Project#milestones, Project#milestones<<(milestone),</tt>
- # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(mileston), Project#milestones.find(milestone_id),</tt>
- # <tt>Project#milestones.all(options), Project#milestones.build, Project#milestones.create</tt>
+ # <tt>Project#milestones.delete(milestone), Project#milestones.destroy(milestone), Project#milestones.find(milestone_id),</tt>
+ # <tt>Project#milestones.build, Project#milestones.create</tt>
# * <tt>Project#categories.empty?, Project#categories.size, Project#categories, Project#categories<<(category1),</tt>
# <tt>Project#categories.delete(category1), Project#categories.destroy(category1)</tt>
#
@@ -231,7 +225,7 @@ module ActiveRecord
# others.size | X | X | X
# others.length | X | X | X
# others.count | X | X | X
- # others.sum(args*,&block) | X | X | X
+ # others.sum(*args) | X | X | X
# others.empty? | X | X | X
# others.clear | X | X | X
# others.delete(other,other,...) | X | X | X
@@ -240,6 +234,7 @@ module ActiveRecord
# others.destroy_all | X | X | X
# others.find(*args) | X | X | X
# others.exists? | X | X | X
+ # others.distinct | X | X | X
# others.uniq | X | X | X
# others.reset | X | X | X
#
@@ -305,11 +300,11 @@ module ActiveRecord
# end
# class Programmer < ActiveRecord::Base
# has_many :assignments
- # has_many :projects, :through => :assignments
+ # has_many :projects, through: :assignments
# end
# class Project < ActiveRecord::Base
# has_many :assignments
- # has_many :programmers, :through => :assignments
+ # has_many :programmers, through: :assignments
# end
#
# For the second way, use +has_and_belongs_to_many+ in both models. This requires a join table
@@ -363,11 +358,11 @@ module ActiveRecord
# there is some special behavior you should be aware of, mostly involving the saving of
# associated objects.
#
- # You can set the :autosave option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
+ # You can set the <tt>:autosave</tt> option on a <tt>has_one</tt>, <tt>belongs_to</tt>,
# <tt>has_many</tt>, or <tt>has_and_belongs_to_many</tt> association. Setting it
# to +true+ will _always_ save the members, whereas setting it to +false+ will
- # _never_ save the members. More details about :autosave option is available at
- # autosave_association.rb .
+ # _never_ save the members. More details about <tt>:autosave</tt> option is available at
+ # AutosaveAssociation.
#
# === One-to-one associations
#
@@ -400,7 +395,7 @@ module ActiveRecord
#
# == Customizing the query
#
- # Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
+ # \Associations are built from <tt>Relation</tt>s, and you can use the <tt>Relation</tt> syntax
# to customize them. For example, to add a condition:
#
# class Blog < ActiveRecord::Base
@@ -426,7 +421,7 @@ module ActiveRecord
# object from an association collection.
#
# class Project
- # has_and_belongs_to_many :developers, :after_add => :evaluate_velocity
+ # has_and_belongs_to_many :developers, after_add: :evaluate_velocity
#
# def evaluate_velocity(developer)
# ...
@@ -437,7 +432,7 @@ module ActiveRecord
#
# class Project
# has_and_belongs_to_many :developers,
- # :after_add => [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
+ # after_add: [:evaluate_velocity, Proc.new { |p, d| p.shipping_date = Time.now}]
# end
#
# Possible callbacks are: +before_add+, +after_add+, +before_remove+ and +after_remove+.
@@ -456,7 +451,7 @@ module ActiveRecord
# has_many :people do
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
# end
@@ -471,7 +466,7 @@ module ActiveRecord
# module FindOrCreateByNameExtension
# def find_or_create_by_name(name)
# first_name, last_name = name.split(" ", 2)
- # find_or_create_by_first_name_and_last_name(first_name, last_name)
+ # find_or_create_by(first_name: first_name, last_name: last_name)
# end
# end
#
@@ -507,7 +502,7 @@ module ActiveRecord
#
# class Author < ActiveRecord::Base
# has_many :authorships
- # has_many :books, :through => :authorships
+ # has_many :books, through: :authorships
# end
#
# class Authorship < ActiveRecord::Base
@@ -523,7 +518,7 @@ module ActiveRecord
#
# class Firm < ActiveRecord::Base
# has_many :clients
- # has_many :invoices, :through => :clients
+ # has_many :invoices, through: :clients
# end
#
# class Client < ActiveRecord::Base
@@ -543,7 +538,7 @@ module ActiveRecord
#
# class Group < ActiveRecord::Base
# has_many :users
- # has_many :avatars, :through => :users
+ # has_many :avatars, through: :users
# end
#
# class User < ActiveRecord::Base
@@ -566,12 +561,14 @@ module ActiveRecord
# @group.avatars << Avatar.new # this would work if User belonged_to Avatar rather than the other way around
# @group.avatars.delete(@group.avatars.last) # so would this
#
+ # == Setting Inverses
+ #
# If you are using a +belongs_to+ on the join model, it is a good idea to set the
# <tt>:inverse_of</tt> option on the +belongs_to+, which will mean that the following example
# works correctly (where <tt>tags</tt> is a +has_many+ <tt>:through</tt> association):
#
# @post = Post.first
- # @tag = @post.tags.build :name => "ruby"
+ # @tag = @post.tags.build name: "ruby"
# @tag.save
#
# The last line ought to save the through record (a <tt>Taggable</tt>). This will only work if the
@@ -579,18 +576,38 @@ module ActiveRecord
#
# class Taggable < ActiveRecord::Base
# belongs_to :post
- # belongs_to :tag, :inverse_of => :taggings
+ # belongs_to :tag, inverse_of: :taggings
+ # end
+ #
+ # If you do not set the <tt>:inverse_of</tt> record, the association will
+ # do its best to match itself up with the correct inverse. Automatic
+ # inverse detection only works on <tt>has_many</tt>, <tt>has_one</tt>, and
+ # <tt>belongs_to</tt> associations.
+ #
+ # Extra options on the associations, as defined in the
+ # <tt>AssociationReflection::INVALID_AUTOMATIC_INVERSE_OPTIONS</tt> constant, 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 <tt>:inverse_of</tt> option to <tt>false</tt> like so:
+ #
+ # class Taggable < ActiveRecord::Base
+ # belongs_to :tag, inverse_of: false
# end
#
- # == Nested Associations
+ # == Nested \Associations
#
# You can actually specify *any* association with the <tt>:through</tt> option, including an
# association which has a <tt>:through</tt> option itself. For example:
#
# class Author < ActiveRecord::Base
# has_many :posts
- # has_many :comments, :through => :posts
- # has_many :commenters, :through => :comments
+ # has_many :comments, through: :posts
+ # has_many :commenters, through: :comments
# end
#
# class Post < ActiveRecord::Base
@@ -608,12 +625,12 @@ module ActiveRecord
#
# class Author < ActiveRecord::Base
# has_many :posts
- # has_many :commenters, :through => :posts
+ # has_many :commenters, through: :posts
# end
#
# class Post < ActiveRecord::Base
# has_many :comments
- # has_many :commenters, :through => :comments
+ # has_many :commenters, through: :comments
# end
#
# class Comment < ActiveRecord::Base
@@ -625,18 +642,18 @@ module ActiveRecord
# add a <tt>Commenter</tt> in the example above, there would be no way to tell how to set up the
# intermediate <tt>Post</tt> and <tt>Comment</tt> objects.
#
- # == Polymorphic Associations
+ # == Polymorphic \Associations
#
# Polymorphic associations on models are not restricted on what types of models they
# can be associated with. Rather, they specify an interface that a +has_many+ association
# must adhere to.
#
# class Asset < ActiveRecord::Base
- # belongs_to :attachable, :polymorphic => true
+ # belongs_to :attachable, polymorphic: true
# end
#
# class Post < ActiveRecord::Base
- # has_many :assets, :as => :attachable # The :as option specifies the polymorphic interface to use.
+ # has_many :assets, as: :attachable # The :as option specifies the polymorphic interface to use.
# end
#
# @asset.attachable = @post
@@ -653,7 +670,7 @@ module ActiveRecord
# column in the posts table.
#
# class Asset < ActiveRecord::Base
- # belongs_to :attachable, :polymorphic => true
+ # belongs_to :attachable, polymorphic: true
#
# def attachable_type=(sType)
# super(sType.to_s.classify.constantize.base_class.to_s)
@@ -661,8 +678,8 @@ module ActiveRecord
# end
#
# class Post < ActiveRecord::Base
- # # because we store "Post" in attachable_type now :dependent => :destroy will work
- # has_many :assets, :as => :attachable, :dependent => :destroy
+ # # because we store "Post" in attachable_type now dependent: :destroy will work
+ # has_many :assets, as: :attachable, dependent: :destroy
# end
#
# class GuestPost < Post
@@ -724,7 +741,7 @@ module ActiveRecord
#
# To include a deep hierarchy of associations, use a hash:
#
- # Post.includes(:author, {:comments => {:author => :gravatar}}).each do |post|
+ # Post.includes(:author, {comments: {author: :gravatar}}).each do |post|
#
# That'll grab not only all the comments but all their authors and gravatar pictures.
# You can mix and match symbols, arrays and hashes in any combination to describe the
@@ -740,7 +757,7 @@ module ActiveRecord
# other than the main one. If this is the case Active Record falls back to the previously
# used LEFT OUTER JOIN based strategy. For example
#
- # Post.includes([:author, :comments]).where(['comments.approved = ?', true]).all
+ # Post.includes([:author, :comments]).where(['comments.approved = ?', true])
#
# This will result in a single SQL query with joins along the lines of:
# <tt>LEFT OUTER JOIN comments ON comments.post_id = posts.id</tt> and
@@ -749,13 +766,13 @@ module ActiveRecord
# 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.
+ # <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:
#
# class Post < ActiveRecord::Base
- # has_many :approved_comments, -> { where approved: true }, :class_name => 'Comment'
+ # has_many :approved_comments, -> { where approved: true }, class_name: 'Comment'
# end
#
# Post.includes(:approved_comments)
@@ -767,7 +784,7 @@ module ActiveRecord
# returning all the associated objects:
#
# class Picture < ActiveRecord::Base
- # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, :class_name => 'Comment'
+ # has_many :most_recent_comments, -> { order('id DESC').limit(10) }, class_name: 'Comment'
# end
#
# Picture.includes(:most_recent_comments).first.most_recent_comments # => returns all associated comments.
@@ -775,7 +792,7 @@ module ActiveRecord
# Eager loading is supported with polymorphic associations.
#
# class Address < ActiveRecord::Base
- # belongs_to :addressable, :polymorphic => true
+ # belongs_to :addressable, polymorphic: true
# end
#
# A call that tries to eager load the addressable model
@@ -787,7 +804,7 @@ module ActiveRecord
# For example if all the addressables are either of class Person or Company then a total
# of 3 queries will be executed. The list of addressable types to load is determined on
# the back of the addresses loaded. This is not supported if Active Record has to fallback
- # to the previous implementation of eager loading and will raise ActiveRecord::EagerLoadPolymorphicError.
+ # to the previous implementation of eager loading and will raise <tt>ActiveRecord::EagerLoadPolymorphicError</tt>.
# The reason is that the parent model's type is a column value so its corresponding table
# name cannot be put in the +FROM+/+JOIN+ clauses of that query.
#
@@ -809,10 +826,10 @@ module ActiveRecord
#
# TreeMixin.joins(:children)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
- # TreeMixin.joins(:children => :parent)
+ # TreeMixin.joins(children: :parent)
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
- # TreeMixin.joins(:children => {:parent => :children})
+ # TreeMixin.joins(children: {parent: :children})
# # => SELECT ... FROM mixins INNER JOIN mixins childrens_mixins ...
# INNER JOIN parents_mixins ...
# INNER JOIN mixins childrens_mixins_2
@@ -821,10 +838,10 @@ module ActiveRecord
#
# Post.joins(:categories)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
- # Post.joins(:categories => :posts)
+ # Post.joins(categories: :posts)
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
- # Post.joins(:categories => {:posts => :categories})
+ # Post.joins(categories: {posts: :categories})
# # => SELECT ... FROM posts INNER JOIN categories_posts ... INNER JOIN categories ...
# INNER JOIN categories_posts posts_categories_join INNER JOIN posts posts_categories
# INNER JOIN categories_posts categories_posts_join INNER JOIN categories categories_posts_2
@@ -868,7 +885,7 @@ module ActiveRecord
#
# module Billing
# class Account < ActiveRecord::Base
- # belongs_to :firm, :class_name => "MyApplication::Business::Firm"
+ # belongs_to :firm, class_name: "MyApplication::Business::Firm"
# end
# end
# end
@@ -910,16 +927,16 @@ module ActiveRecord
# example, if we changed our model definitions to:
#
# class Dungeon < ActiveRecord::Base
- # has_many :traps, :inverse_of => :dungeon
- # has_one :evil_wizard, :inverse_of => :dungeon
+ # has_many :traps, inverse_of: :dungeon
+ # has_one :evil_wizard, inverse_of: :dungeon
# end
#
# class Trap < ActiveRecord::Base
- # belongs_to :dungeon, :inverse_of => :traps
+ # belongs_to :dungeon, inverse_of: :traps
# end
#
# class EvilWizard < ActiveRecord::Base
- # belongs_to :dungeon, :inverse_of => :evil_wizard
+ # belongs_to :dungeon, inverse_of: :evil_wizard
# end
#
# Then, from our code snippet above, +d+ and <tt>t.dungeon</tt> are actually the same
@@ -942,13 +959,13 @@ module ActiveRecord
# For example:
#
# class Author
- # has_many :posts, :dependent => :destroy
+ # has_many :posts, dependent: :destroy
# end
# Author.find(1).destroy # => Will destroy all of the author's posts, too
#
# The <tt>:dependent</tt> option can have different values which specify how the deletion
# is done. For more information, see the documentation for this option on the different
- # specific association types. When no option is given, the behaviour is to do nothing
+ # specific association types. When no option is given, the behavior is to do nothing
# with the associated records when destroying a record.
#
# Note that <tt>:dependent</tt> is implemented using Rails' callback
@@ -964,8 +981,8 @@ module ActiveRecord
# For +has_and_belongs_to_many+, <tt>delete</tt> and <tt>destroy</tt> are the same: they
# cause the records in the join table to be removed.
#
- # For +has_many+, <tt>destroy</tt> will always call the <tt>destroy</tt> method of the
- # record(s) being removed so that callbacks are run. However <tt>delete</tt> will either
+ # For +has_many+, <tt>destroy</tt> and <tt>destroy_all</tt> will always call the <tt>destroy</tt> method of the
+ # record(s) being removed so that callbacks are run. However <tt>delete</tt> and <tt>delete_all</tt> will either
# do the deletion according to the strategy specified by the <tt>:dependent</tt> option, or
# if no <tt>:dependent</tt> option is given, then it will follow the default strategy.
# The default strategy is <tt>:nullify</tt> (set the foreign keys to <tt>nil</tt>), except for
@@ -986,7 +1003,7 @@ module ActiveRecord
# associated objects themselves. So with +has_and_belongs_to_many+ and +has_many+
# <tt>:through</tt>, the join records will be deleted, but the associated records won't.
#
- # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by_name('food'))</tt>
+ # This makes sense if you think about it: if you were to call <tt>post.tags.delete(Tag.find_by(name: 'food'))</tt>
# you would want the 'food' tag to be unlinked from the post, rather than for the tag itself
# to be removed from the database.
#
@@ -1022,16 +1039,16 @@ module ActiveRecord
# An empty array is returned if none are found.
# [collection<<(object, ...)]
# Adds one or more objects to the collection by setting their foreign keys to the collection's primary key.
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by setting their foreign keys to +NULL+.
- # Objects will be in addition destroyed if they're associated with <tt>:dependent => :destroy</tt>,
- # and deleted if they're associated with <tt>:dependent => :delete_all</tt>.
+ # Objects will be in addition destroyed if they're associated with <tt>dependent: :destroy</tt>,
+ # and deleted if they're associated with <tt>dependent: :delete_all</tt>.
#
# If the <tt>:through</tt> option is used, then the join records are deleted (rather than
- # nullified) by default, but you can specify <tt>:dependent => :destroy</tt> or
- # <tt>:dependent => :nullify</tt> to override this.
+ # nullified) by default, but you can specify <tt>dependent: :destroy</tt> or
+ # <tt>dependent: :nullify</tt> to override this.
# [collection.destroy(object, ...)]
# Removes one or more objects from the collection by running <tt>destroy</tt> on
# each record, regardless of any dependent option, ensuring callbacks are run.
@@ -1049,8 +1066,8 @@ module ActiveRecord
# method loads the models and calls <tt>collection=</tt>. See above.
# [collection.clear]
# Removes every object from the collection. This destroys the associated objects if they
- # are associated with <tt>:dependent => :destroy</tt>, deletes them directly from the
- # database if <tt>:dependent => :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
+ # are associated with <tt>dependent: :destroy</tt>, deletes them directly from the
+ # database if <tt>dependent: :delete_all</tt>, otherwise sets their foreign keys to +NULL+.
# If the <tt>:through</tt> option is true no destroy callbacks are invoked on the join models.
# Join models are directly deleted.
# [collection.empty?]
@@ -1058,10 +1075,10 @@ module ActiveRecord
# [collection.size]
# Returns the number of associated objects.
# [collection.find(...)]
- # Finds an associated object according to the same rules as ActiveRecord::Base.find.
+ # Finds an associated object according to the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {}, ...)]
# Returns one or more new objects of the collection type that have been instantiated
# with +attributes+ and linked to this object through a foreign key, but have not yet
@@ -1071,14 +1088,17 @@ module ActiveRecord
# with +attributes+, linked to this object through a foreign key, and that has already
# been saved (if it passed the validation). *Note*: This only works if the base model
# already exists in the DB, not if it is a new (unsaved) record!
+ # [collection.create!(attributes = {})]
+ # 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
#
- # Example: A Firm class declares <tt>has_many :clients</tt>, which will add:
- # * <tt>Firm#clients</tt> (similar to <tt>Clients.all :conditions => ["firm_id = ?", id]</tt>)
+ # A <tt>Firm</tt> class declares <tt>has_many :clients</tt>, which will add:
+ # * <tt>Firm#clients</tt> (similar to <tt>Client.where(firm_id: id)</tt>)
# * <tt>Firm#clients<<</tt>
# * <tt>Firm#clients.delete</tt>
# * <tt>Firm#clients.destroy</tt>
@@ -1088,10 +1108,11 @@ module ActiveRecord
# * <tt>Firm#clients.clear</tt>
# * <tt>Firm#clients.empty?</tt> (similar to <tt>firm.clients.size == 0</tt>)
# * <tt>Firm#clients.size</tt> (similar to <tt>Client.count "firm_id = #{id}"</tt>)
- # * <tt>Firm#clients.find</tt> (similar to <tt>Client.find(id, :conditions => "firm_id = #{id}")</tt>)
- # * <tt>Firm#clients.exists?(:name => 'ACME')</tt> (similar to <tt>Client.exists?(:name => 'ACME', :firm_id => firm.id)</tt>)
+ # * <tt>Firm#clients.find</tt> (similar to <tt>Client.where(firm_id: id).find(id)</tt>)
+ # * <tt>Firm#clients.exists?(name: 'ACME')</tt> (similar to <tt>Client.exists?(name: 'ACME', firm_id: firm.id)</tt>)
# * <tt>Firm#clients.build</tt> (similar to <tt>Client.new("firm_id" => id)</tt>)
# * <tt>Firm#clients.create</tt> (similar to <tt>c = Client.new("firm_id" => id); c.save; c</tt>)
+ # * <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.
#
# === Options
@@ -1110,18 +1131,21 @@ module ActiveRecord
# Controls what happens to the associated objects when
# their owner is destroyed. Note that these are implemented as
# callbacks, and Rails executes callbacks in order. Therefore, other
- # similar callbacks may affect the :dependent behavior, and the
- # :dependent behavior may affect other callbacks.
+ # similar callbacks may affect the <tt>:dependent</tt> behavior, and the
+ # <tt>:dependent</tt> behavior may affect other callbacks.
#
- # * <tt>:destroy</tt> causes all the associated objects to also be destroyed
- # * <tt>:delete_all</tt> causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:destroy</tt> causes all the associated objects to also be destroyed.
+ # * <tt>:delete_all</tt> causes all the associated objects to be deleted directly from the database (so callbacks will not be executed).
# * <tt>:nullify</tt> causes the foreign keys to be set to +NULL+. Callbacks are not executed.
- # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records
- # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects
+ # * <tt>:restrict_with_exception</tt> causes an exception to be raised if there are any associated records.
+ # * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there are any associated objects.
#
# If using with the <tt>:through</tt> option, the association on the join model must be
# a +belongs_to+, and the records which get deleted are the join records, rather than
# the associated records.
+ # [:counter_cache]
+ # This option can be used to configure a custom named <tt>:counter_cache.</tt> You only need this option,
+ # when you customized the name of your <tt>:counter_cache</tt> on the <tt>belongs_to</tt> association.
# [:as]
# Specifies a polymorphic interface (See <tt>belongs_to</tt>).
# [:through]
@@ -1143,7 +1167,7 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_many :through</tt> queries.
# Only use it if the name cannot be inferred from the association.
- # <tt>has_many :subscribers, :through => :subscriptions</tt> will look for either <tt>:subscribers</tt> or
+ # <tt>has_many :subscribers, through: :subscriptions</tt> will look for either <tt>:subscribers</tt> or
# <tt>:subscriber</tt> on Subscription, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_many :through</tt> queries where the source
@@ -1154,8 +1178,8 @@ module ActiveRecord
# If true, always save the associated objects or destroy them if marked for destruction,
# when saving the parent object. If false, never save or destroy the associated objects.
# By default, only save associated objects that are new records. This option is implemented as a
- # before_save callback. Because callbacks are run in the order they are defined, associated objects
- # may need to be explicitly saved in any user-defined before_save callbacks.
+ # +before_save+ callback. Because callbacks are run in the order they are defined, associated objects
+ # may need to be explicitly saved in any user-defined +before_save+ callbacks.
#
# Note that <tt>accepts_nested_attributes_for</tt> sets <tt>:autosave</tt> to <tt>true</tt>.
# [:inverse_of]
@@ -1174,13 +1198,14 @@ module ActiveRecord
# has_many :reports, -> { readonly }
# has_many :subscribers, through: :subscriptions, source: :user
def has_many(name, scope = nil, options = {}, &extension)
- Builder::HasMany.build(self, name, scope, options, &extension)
+ reflection = Builder::HasMany.build(self, name, scope, options, &extension)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
# if the other class contains the foreign key. If the current class contains the foreign key,
# then you should use +belongs_to+ instead. See also ActiveRecord::Associations::ClassMethods's overview
- # on when to use has_one and when to use belongs_to.
+ # on when to use +has_one+ and when to use +belongs_to+.
#
# The following methods for retrieval and query of a single associated object will be added:
#
@@ -1207,7 +1232,7 @@ module ActiveRecord
# === Example
#
# An Account class declares <tt>has_one :beneficiary</tt>, which will add:
- # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.first(:conditions => "account_id = #{id}")</tt>)
+ # * <tt>Account#beneficiary</tt> (similar to <tt>Beneficiary.where(account_id: id).first</tt>)
# * <tt>Account#beneficiary=(beneficiary)</tt> (similar to <tt>beneficiary.account_id = account.id; beneficiary.save</tt>)
# * <tt>Account#build_beneficiary</tt> (similar to <tt>Beneficiary.new("account_id" => id)</tt>)
# * <tt>Account#create_beneficiary</tt> (similar to <tt>b = Beneficiary.new("account_id" => id); b.save; b</tt>)
@@ -1227,7 +1252,7 @@ module ActiveRecord
# its owner is destroyed:
#
# * <tt>:destroy</tt> causes the associated object to also be destroyed
- # * <tt>:delete</tt> causes the asssociated object to be deleted directly from the database (so callbacks will not execute)
+ # * <tt>:delete</tt> causes the associated object to be deleted directly from the database (so callbacks will not execute)
# * <tt>:nullify</tt> causes the foreign key to be set to +NULL+. Callbacks are not executed.
# * <tt>:restrict_with_exception</tt> causes an exception to be raised if there is an associated record
# * <tt>:restrict_with_error</tt> causes an error to be added to the owner if there is an associated object
@@ -1247,7 +1272,7 @@ module ActiveRecord
# [:source]
# Specifies the source association name used by <tt>has_one :through</tt> queries.
# Only use it if the name cannot be inferred from the association.
- # <tt>has_one :favorite, :through => :favorites</tt> will look for a
+ # <tt>has_one :favorite, through: :favorites</tt> will look for a
# <tt>:favorite</tt> on Favorite, unless a <tt>:source</tt> is given.
# [:source_type]
# Specifies type of the source association used by <tt>has_one :through</tt> queries where the source
@@ -1267,17 +1292,18 @@ module ActiveRecord
# See ActiveRecord::Associations::ClassMethods's overview on Bi-directional associations for more detail.
#
# Option examples:
- # has_one :credit_card, :dependent => :destroy # destroys the associated credit card
- # has_one :credit_card, :dependent => :nullify # updates the associated records foreign
+ # has_one :credit_card, dependent: :destroy # destroys the associated credit card
+ # has_one :credit_card, dependent: :nullify # updates the associated records foreign
# # key value to NULL rather than destroying it
- # has_one :last_comment, -> { order 'posted_on' }, :class_name => "Comment"
- # has_one :project_manager, -> { where role: 'project_manager' }, :class_name => "Person"
+ # has_one :last_comment, -> { order 'posted_on' }, class_name: "Comment"
+ # has_one :project_manager, -> { where role: 'project_manager' }, class_name: "Person"
# has_one :attachment, as: :attachable
# has_one :boss, readonly: :true
# has_one :club, through: :membership
# has_one :primary_address, -> { where primary: true }, through: :addressables, source: :addressable
def has_one(name, scope = nil, options = {})
- Builder::HasOne.build(self, name, scope, options)
+ reflection = Builder::HasOne.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a one-to-one association with another class. This method should only be used
@@ -1326,12 +1352,12 @@ module ActiveRecord
# Specify the foreign key used for the association. By default this is guessed to be the name
# of the association with an "_id" suffix. So a class that defines a <tt>belongs_to :person</tt>
# association will use "person_id" as the default <tt>:foreign_key</tt>. Similarly,
- # <tt>belongs_to :favorite_person, :class_name => "Person"</tt> will use a foreign key
+ # <tt>belongs_to :favorite_person, class_name: "Person"</tt> will use a foreign key
# of "favorite_person_id".
# [:foreign_type]
# Specify the column used to store the associated object's type, if this is a polymorphic
# association. By default this is guessed to be the name of the association with a "_type"
- # suffix. So a class that defines a <tt>belongs_to :taggable, :polymorphic => true</tt>
+ # suffix. So a class that defines a <tt>belongs_to :taggable, polymorphic: true</tt>
# association will use "taggable_type" as the default <tt>:foreign_type</tt>.
# [:primary_key]
# Specify the method that returns the primary key of associated object used for the association.
@@ -1348,10 +1374,10 @@ module ActiveRecord
# class is created and decremented when it's destroyed. This requires that a column
# named <tt>#{table_name}_count</tt> (such as +comments_count+ for a belonging Comment class)
# is used on the associate class (such as a Post class) - that is the migration for
- # <tt>#{table_name}_count</tt> is created on the associate class (such that Post.comments_count will
+ # <tt>#{table_name}_count</tt> is created on the associate class (such that <tt>Post.comments_count</tt> will
# return the count cached, see note below). You can also specify a custom counter
# cache column by providing a column name instead of a +true+/+false+ value to this
- # option (e.g., <tt>:counter_cache => :my_custom_counter</tt>.)
+ # option (e.g., <tt>counter_cache: :my_custom_counter</tt>.)
# Note: Specifying a counter cache will add it to that model's list of readonly attributes
# using +attr_readonly+.
# [:polymorphic]
@@ -1389,7 +1415,8 @@ module ActiveRecord
# belongs_to :company, touch: true
# belongs_to :company, touch: :employees_last_updated_at
def belongs_to(name, scope = nil, options = {})
- Builder::BelongsTo.build(self, name, scope, options)
+ reflection = Builder::BelongsTo.build(self, name, scope, options)
+ Reflection.add_reflection self, name, reflection
end
# Specifies a many-to-many relationship with another class. This associates two classes via an
@@ -1403,13 +1430,15 @@ module ActiveRecord
# 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". Be aware of this caveat, and use the
# custom <tt>:join_table</tt> option if you need to.
+ # If your tables share a common prefix, it will only appear once at the beginning. For example,
+ # the tables "catalog_categories" and "catalog_products" generate a join table name of "catalog_categories_products".
#
# The join table should not have a primary key or a model associated with it. You must manually generate the
# join table with a migration such as this:
#
# class CreateDevelopersProjectsJoinTable < ActiveRecord::Migration
# def change
- # create_table :developers_projects, :id => false do |t|
+ # create_table :developers_projects, id: false do |t|
# t.integer :developer_id
# t.integer :project_id
# end
@@ -1428,8 +1457,8 @@ module ActiveRecord
# [collection<<(object, ...)]
# Adds one or more objects to the collection by creating associations in the join table
# (<tt>collection.push</tt> and <tt>collection.concat</tt> are aliases to this method).
- # Note that this operation instantly fires update sql without waiting for the save or update call on the
- # parent object.
+ # Note that this operation instantly fires update SQL without waiting for the save or update call on the
+ # parent object, unless the parent object is a new record.
# [collection.delete(object, ...)]
# Removes one or more objects from the collection by removing their associations from the join table.
# This does not destroy the objects.
@@ -1451,10 +1480,10 @@ module ActiveRecord
# [collection.find(id)]
# Finds an associated object responding to the +id+ and that
# meets the condition that it has to be associated with this object.
- # Uses the same rules as ActiveRecord::Base.find.
+ # Uses the same rules as <tt>ActiveRecord::Base.find</tt>.
# [collection.exists?(...)]
# Checks whether an associated object with the given conditions exists.
- # Uses the same rules as ActiveRecord::Base.exists?.
+ # Uses the same rules as <tt>ActiveRecord::Base.exists?</tt>.
# [collection.build(attributes = {})]
# Returns a new object of the collection type that has been instantiated
# with +attributes+ and linked to this object through the join table, but has not yet been saved.
@@ -1524,7 +1553,39 @@ module ActiveRecord
# has_and_belongs_to_many :categories, join_table: "prods_cats"
# has_and_belongs_to_many :categories, -> { readonly }
def has_and_belongs_to_many(name, scope = nil, options = {}, &extension)
- Builder::HasAndBelongsToMany.build(self, name, scope, options, &extension)
+ if scope.is_a?(Hash)
+ options = scope
+ scope = nil
+ end
+
+ builder = Builder::HasAndBelongsToMany.new name, self, options
+
+ join_model = builder.through_model
+
+ middle_reflection = builder.middle_reflection join_model
+
+ Builder::HasMany.define_callbacks self, middle_reflection
+ Reflection.add_reflection self, middle_reflection.name, middle_reflection
+
+ include Module.new {
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def destroy_associations
+ association(:#{middle_reflection.name}).delete_all(:delete_all)
+ association(:#{name}).reset
+ super
+ end
+ RUBY
+ }
+
+ hm_options = {}
+ hm_options[:through] = middle_reflection.name
+ hm_options[:source] = join_model.right_reflection.name
+
+ [:before_add, :after_add, :before_remove, :after_remove].each do |k|
+ hm_options[k] = options[k] if options.key? k
+ end
+
+ has_many name, scope, hm_options, &extension
end
end
end
diff --git a/activerecord/lib/active_record/associations/alias_tracker.rb b/activerecord/lib/active_record/associations/alias_tracker.rb
index 84540a7000..0c23029981 100644
--- a/activerecord/lib/active_record/associations/alias_tracker.rb
+++ b/activerecord/lib/active_record/associations/alias_tracker.rb
@@ -8,7 +8,7 @@ module ActiveRecord
attr_reader :aliases, :table_joins, :connection
# table_joins is an array of arel joins which might conflict with the aliases we assign here
- def initialize(connection = ActiveRecord::Model.connection, table_joins = [])
+ def initialize(connection = Base.connection, table_joins = [])
@aliases = Hash.new { |h,k| h[k] = initial_count_for(k) }
@table_joins = table_joins
@connection = connection
diff --git a/activerecord/lib/active_record/associations/association.rb b/activerecord/lib/active_record/associations/association.rb
index ba75c8be41..e6a45487d0 100644
--- a/activerecord/lib/active_record/associations/association.rb
+++ b/activerecord/lib/active_record/associations/association.rb
@@ -13,11 +13,11 @@ module ActiveRecord
# BelongsToAssociation
# BelongsToPolymorphicAssociation
# CollectionAssociation
- # HasAndBelongsToManyAssociation
# HasManyAssociation
# HasManyThroughAssociation + ThroughAssociation
class Association #:nodoc:
attr_reader :owner, :target, :reflection
+ attr_accessor :inversed
delegate :options, :to => :reflection
@@ -30,7 +30,7 @@ module ActiveRecord
reset_scope
end
- # Returns the name of the table of the related class:
+ # Returns the name of the table of the associated class:
#
# post.comments.aliased_table_name # => "comments"
#
@@ -43,6 +43,7 @@ module ActiveRecord
@loaded = false
@target = nil
@stale_state = nil
+ @inversed = false
end
# Reloads the \target and returns +self+ on success.
@@ -60,8 +61,9 @@ module ActiveRecord
# Asserts the \target has been loaded setting the \loaded flag to +true+.
def loaded!
- @loaded = true
+ @loaded = true
@stale_state = stale_state
+ @inversed = false
end
# The target is stale if the target no longer points to the record(s) that the
@@ -71,7 +73,7 @@ module ActiveRecord
#
# Note that if the target has not been loaded, it is not considered stale.
def stale_target?
- loaded? && @stale_state != stale_state
+ !inversed && loaded? && @stale_state != stale_state
end
# Sets the target of this association to <tt>\target</tt>, and the \loaded flag to +true+.
@@ -84,15 +86,10 @@ module ActiveRecord
target_scope.merge(association_scope)
end
- def scoped
- ActiveSupport::Deprecation.warn("#scoped is deprecated. use #scope instead.")
- scope
- end
-
# The scope for this association.
#
# Note that the association_scope is merged into the target_scope only when the
- # scoped method is called. This is because at that point the call may be surrounded
+ # scope method is called. This is because at that point the call may be surrounded
# by scope.scoping { ... } or with_scope { ... } etc, which affects the scope which
# actually gets built.
def association_scope
@@ -110,10 +107,11 @@ module ActiveRecord
if record && invertible_for?(record)
inverse = record.association(inverse_reflection_for(record).name)
inverse.target = owner
+ inverse.inversed = true
end
end
- # This class of the target. belongs_to polymorphic overrides this to look at the
+ # Returns the class of the target. belongs_to polymorphic overrides this to look at the
# polymorphic_type field on the owner.
def klass
reflection.klass
@@ -122,7 +120,7 @@ module ActiveRecord
# Can be overridden (i.e. in ThroughAssociation) to merge in other scopes (i.e. the
# through association's scope)
def target_scope
- klass.all
+ AssociationRelation.create(klass, klass.arel_table, self).merge!(klass.all)
end
# Loads the \target if needed and returns it.
@@ -146,7 +144,7 @@ module ActiveRecord
def interpolate(sql, record = nil)
if sql.respond_to?(:to_proc)
- owner.send(:instance_exec, record, &sql)
+ owner.instance_exec(record, &sql)
else
sql
end
@@ -164,6 +162,13 @@ module ActiveRecord
@reflection = @owner.class.reflect_on_association(reflection_name)
end
+ def initialize_attributes(record) #:nodoc:
+ skip_assign = [reflection.foreign_key, reflection.type].compact
+ attributes = create_scope.except(*(record.changed - skip_assign))
+ record.assign_attributes(attributes)
+ set_inverse_instance(record)
+ end
+
private
def find_target?
@@ -189,13 +194,14 @@ module ActiveRecord
creation_attributes.each { |key, value| record[key] = value }
end
- # Should be true if there is a foreign key present on the owner which
+ # Returns true if there is a foreign key present on the owner which
# references the target. This is used to determine whether we can load
# the target if the owner is currently a new record (and therefore
- # without a key).
+ # without a key). If the owner is a new record then foreign_key must
+ # be present in order to load target.
#
# Currently implemented by belongs_to (vanilla and polymorphic) and
- # has_one/has_many :through associations which go through a belongs_to
+ # has_one/has_many :through associations which go through a belongs_to.
def foreign_key_present?
false
end
@@ -203,7 +209,7 @@ module ActiveRecord
# Raises ActiveRecord::AssociationTypeMismatch unless +record+ is of
# the kind of the class of the associated objects. Meant to be used as
# a sanity check when you are about to assign an associated record.
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
unless record.is_a?(reflection.klass) || record.is_a?(reflection.class_name.constantize)
message = "#{reflection.class_name}(##{reflection.klass.object_id}) expected, got #{record.class}(##{record.class.object_id})"
raise ActiveRecord::AssociationTypeMismatch, message
@@ -217,13 +223,14 @@ module ActiveRecord
reflection.inverse_of
end
- # Is this association invertible? Can be redefined by subclasses.
+ # Returns true if inverse association on the given record needs to be set.
+ # This method is redefined by subclasses.
def invertible_for?(record)
inverse_reflection_for(record)
end
# This should be implemented to return the values of the relevant key(s) on the owner,
- # so that when state_state is different from the value stored on the last find_target,
+ # so that when stale_state is different from the value stored on the last find_target,
# the target is stale.
#
# This is only relevant to certain associations, which is why it returns nil by default.
@@ -232,8 +239,7 @@ module ActiveRecord
def build_record(attributes)
reflection.build_association(attributes) do |record|
- attributes = create_scope.except(*(record.changed - [reflection.foreign_key]))
- record.assign_attributes(attributes)
+ initialize_attributes(record)
end
end
end
diff --git a/activerecord/lib/active_record/associations/association_scope.rb b/activerecord/lib/active_record/associations/association_scope.rb
index 1303822868..17f056e764 100644
--- a/activerecord/lib/active_record/associations/association_scope.rb
+++ b/activerecord/lib/active_record/associations/association_scope.rb
@@ -15,14 +15,14 @@ module ActiveRecord
def scope
scope = klass.unscoped
- scope.merge! eval_scope(klass, reflection.scope) if reflection.scope
+ scope.extending! Array(options[:extend])
add_constraints(scope)
end
private
def column_for(table_name, column_name)
- columns = alias_tracker.connection.schema_cache.columns_hash[table_name]
+ columns = alias_tracker.connection.schema_cache.columns_hash(table_name)
columns[column_name]
end
@@ -44,21 +44,9 @@ module ActiveRecord
chain.each_with_index do |reflection, i|
table, foreign_table = tables.shift, tables.first
- if reflection.source_macro == :has_and_belongs_to_many
- join_table = tables.shift
-
- scope = scope.joins(join(
- join_table,
- table[reflection.association_primary_key].
- eq(join_table[reflection.association_foreign_key])
- ))
-
- table, foreign_table = join_table, tables.first
- end
-
if reflection.source_macro == :belongs_to
if reflection.options[:polymorphic]
- key = reflection.association_primary_key(klass)
+ key = reflection.association_primary_key(self.klass)
else
key = reflection.association_primary_key
end
@@ -82,20 +70,32 @@ module ActiveRecord
constraint = table[key].eq(foreign_table[foreign_key])
if reflection.type
- type = chain[i + 1].klass.base_class.name
- constraint = constraint.and(table[reflection.type].eq(type))
+ value = chain[i + 1].klass.base_class.name
+ bind_val = bind scope, table.table_name, reflection.type.to_s, value
+ scope = scope.where(table[reflection.type].eq(bind_val))
end
scope = scope.joins(join(foreign_table, constraint))
end
+ is_first_chain = i == 0
+ klass = is_first_chain ? self.klass : reflection.klass
+
# Exclude the scope of the association itself, because that
# was already merged in the #scope method.
- (scope_chain[i] - [self.reflection.scope]).each do |scope_chain_item|
- item = eval_scope(reflection.klass, scope_chain_item)
+ scope_chain[i].each do |scope_chain_item|
+ item = eval_scope(klass, scope_chain_item)
+
+ if scope_chain_item == self.reflection.scope
+ scope.merge! item.except(:where, :includes, :bind)
+ end
+
+ if is_first_chain
+ scope.includes! item.includes_values
+ end
- scope.includes! item.includes_values
scope.where_values += item.where_values
+ scope.order_values |= item.order_values
end
end
@@ -113,7 +113,7 @@ module ActiveRecord
# the owner
klass.table_name
else
- reflection.table_name
+ super
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 75f72c1a46..e1fa5225b5 100644
--- a/activerecord/lib/active_record/associations/belongs_to_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_association.rb
@@ -1,5 +1,5 @@
module ActiveRecord
- # = Active Record Belongs To Associations
+ # = Active Record Belongs To Association
module Associations
class BelongsToAssociation < SingularAssociation #:nodoc:
@@ -8,7 +8,7 @@ module ActiveRecord
end
def replace(record)
- raise_on_type_mismatch(record) if record
+ raise_on_type_mismatch!(record) if record
update_counters(record)
replace_keys(record)
@@ -50,8 +50,11 @@ module ActiveRecord
# Checks whether record is different to the current target, without loading it
def different_target?(record)
- record.nil? && owner[reflection.foreign_key] ||
- record && record.id != owner[reflection.foreign_key]
+ if record.nil?
+ owner[reflection.foreign_key]
+ else
+ record.id != owner[reflection.foreign_key]
+ end
end
def replace_keys(record)
diff --git a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
index 88ce03a3cd..eae5eed3a1 100644
--- a/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
+++ b/activerecord/lib/active_record/associations/belongs_to_polymorphic_association.rb
@@ -22,7 +22,7 @@ module ActiveRecord
reflection.polymorphic_inverse_of(record.class)
end
- def raise_on_type_mismatch(record)
+ def raise_on_type_mismatch!(record)
# A polymorphic association cannot have a type mismatch, by definition
end
diff --git a/activerecord/lib/active_record/associations/builder/association.rb b/activerecord/lib/active_record/associations/builder/association.rb
index 1df876bf62..d8d68eb908 100644
--- a/activerecord/lib/active_record/associations/builder/association.rb
+++ b/activerecord/lib/active_record/associations/builder/association.rb
@@ -1,68 +1,100 @@
+# This is the parent Association class which defines the variables
+# used by all associations.
+#
+# The hierarchy is defined as follows:
+# Association
+# - SingularAssociation
+# - BelongsToAssociation
+# - HasOneAssociation
+# - CollectionAssociation
+# - HasManyAssociation
+
module ActiveRecord::Associations::Builder
class Association #:nodoc:
class << self
- attr_accessor :valid_options
+ attr_accessor :extensions
end
+ self.extensions = []
- self.valid_options = [:class_name, :foreign_key, :validate]
-
- attr_reader :model, :name, :scope, :options, :reflection
+ VALID_OPTIONS = [:class_name, :class, :foreign_key, :validate]
- def self.build(*args, &block)
- new(*args, &block).build
+ def self.build(model, name, scope, options, &block)
+ extension = define_extensions model, name, &block
+ reflection = create_reflection model, name, scope, options, extension
+ define_accessors model, reflection
+ define_callbacks model, reflection
+ reflection
end
- def initialize(model, name, scope, options)
- @model = model
- @name = name
+ def self.create_reflection(model, name, scope, options, extension = nil)
+ raise ArgumentError, "association names must be a Symbol" unless name.kind_of?(Symbol)
if scope.is_a?(Hash)
- @scope = nil
- @options = scope
- else
- @scope = scope
- @options = options
+ options = scope
+ scope = nil
+ end
+
+ validate_options(options)
+
+ scope = build_scope(scope, extension)
+
+ ActiveRecord::Reflection.create(macro, name, scope, options, model)
+ end
+
+ def self.build_scope(scope, extension)
+ new_scope = scope
+
+ if scope && scope.arity == 0
+ new_scope = proc { instance_exec(&scope) }
end
- if @scope && @scope.arity == 0
- prev_scope = @scope
- @scope = proc { instance_exec(&prev_scope) }
+ if extension
+ new_scope = wrap_scope new_scope, extension
end
+
+ new_scope
end
- def mixin
- @model.generated_feature_methods
+ def self.wrap_scope(scope, extension)
+ scope
end
- include Module.new { def build; end }
+ def self.macro
+ raise NotImplementedError
+ end
- def build
- validate_options
- define_accessors
- configure_dependency if options[:dependent]
- @reflection = model.create_reflection(macro, name, scope, options, model)
- super # provides an extension point
- @reflection
+ def self.valid_options(options)
+ VALID_OPTIONS + Association.extensions.flat_map(&:valid_options)
end
- def macro
- raise NotImplementedError
+ def self.validate_options(options)
+ options.assert_valid_keys(valid_options(options))
end
- def valid_options
- Association.valid_options
+ def self.define_extensions(model, name)
end
- def validate_options
- options.assert_valid_keys(valid_options)
+ def self.define_callbacks(model, reflection)
+ add_before_destroy_callbacks(model, reflection) if reflection.options[:dependent]
+ Association.extensions.each do |extension|
+ extension.build model, reflection
+ end
end
- def define_accessors
- define_readers
- define_writers
+ # Defines the setter and getter methods for the association
+ # class Post < ActiveRecord::Base
+ # has_many :comments
+ # end
+ #
+ # Post.first.comments and Post.first.comments= methods are defined by this method...
+ def self.define_accessors(model, reflection)
+ mixin = model.generated_feature_methods
+ name = reflection.name
+ define_readers(mixin, name)
+ define_writers(mixin, name)
end
- def define_readers
+ def self.define_readers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}(*args)
association(:#{name}).reader(*args)
@@ -70,7 +102,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}=(value)
association(:#{name}).writer(value)
@@ -78,29 +110,17 @@ module ActiveRecord::Associations::Builder
CODE
end
- def configure_dependency
- unless valid_dependent_options.include? options[:dependent]
- raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{options[:dependent]}"
- end
+ def self.valid_dependent_options
+ raise NotImplementedError
+ end
- if options[:dependent] == :restrict
- ActiveSupport::Deprecation.warn(
- "The :restrict option is deprecated. Please use :restrict_with_exception instead, which " \
- "provides the same functionality."
- )
+ def self.add_before_destroy_callbacks(model, reflection)
+ unless valid_dependent_options.include? reflection.options[:dependent]
+ raise ArgumentError, "The :dependent option must be one of #{valid_dependent_options}, but is :#{reflection.options[:dependent]}"
end
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def #{macro}_dependent_for_#{name}
- association(:#{name}).handle_dependency
- end
- CODE
-
- model.before_destroy "#{macro}_dependent_for_#{name}"
- end
-
- def valid_dependent_options
- raise NotImplementedError
+ name = reflection.name
+ model.before_destroy lambda { |o| o.association(name).handle_dependency }
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/belongs_to.rb b/activerecord/lib/active_record/associations/builder/belongs_to.rb
index 2f2600b7fb..aa43c34d86 100644
--- a/activerecord/lib/active_record/associations/builder/belongs_to.rb
+++ b/activerecord/lib/active_record/associations/builder/belongs_to.rb
@@ -1,66 +1,130 @@
module ActiveRecord::Associations::Builder
class BelongsTo < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:belongs_to
end
- def valid_options
+ def self.valid_options(options)
super + [:foreign_type, :polymorphic, :touch]
end
- def constructable?
- !options[:polymorphic]
+ def self.valid_dependent_options
+ [:destroy, :delete]
end
- def build
- reflection = super
- add_counter_cache_callbacks(reflection) if options[:counter_cache]
- add_touch_callbacks(reflection) if options[:touch]
- reflection
+ def self.define_callbacks(model, reflection)
+ super
+ add_counter_cache_callbacks(model, reflection) if reflection.options[:counter_cache]
+ add_touch_callbacks(model, reflection) if reflection.options[:touch]
end
- def add_counter_cache_callbacks(reflection)
- cache_column = reflection.counter_cache_column
+ def self.define_accessors(mixin, reflection)
+ super
+ add_counter_cache_methods mixin
+ end
+
+ def self.add_counter_cache_methods(mixin)
+ return if mixin.method_defined? :belongs_to_counter_cache_after_create
+
+ 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
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_counter_cache_after_create_for_#{name}
- record = #{name}
- record.class.increment_counter(:#{cache_column}, record.id) unless record.nil?
+ 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_before_destroy_for_#{name}
- unless marked_for_destruction?
- record = #{name}
- record.class.decrement_counter(:#{cache_column}, record.id) unless record.nil?
+ def belongs_to_counter_cache_after_update(reflection)
+ foreign_key = reflection.foreign_key
+ cache_column = reflection.counter_cache_column
+
+ if (@_after_create_counter_called ||= false)
+ @_after_create_counter_called = false
+ elsif attribute_changed?(foreign_key) && !new_record? && reflection.constructable?
+ model = reflection.klass
+ foreign_key_was = attribute_was foreign_key
+ foreign_key = attribute foreign_key
+
+ if foreign_key && model.respond_to?(:increment_counter)
+ model.increment_counter(cache_column, foreign_key)
+ end
+ if foreign_key_was && model.respond_to?(:decrement_counter)
+ model.decrement_counter(cache_column, foreign_key_was)
+ end
end
end
- CODE
+ end
+ end
+
+ 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_create "belongs_to_counter_cache_after_create_for_#{name}"
- model.before_destroy "belongs_to_counter_cache_before_destroy_for_#{name}"
+ model.after_update lambda { |record|
+ record.belongs_to_counter_cache_after_update(reflection)
+ }
klass = reflection.class_name.safe_constantize
klass.attr_readonly cache_column if klass && klass.respond_to?(:attr_readonly)
end
- def add_touch_callbacks(reflection)
- mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def belongs_to_touch_after_save_or_destroy_for_#{name}
- record = #{name}
+ def self.touch_record(o, foreign_key, name, touch) # :nodoc:
+ old_foreign_id = o.changed_attributes[foreign_key]
- unless record.nil?
- record.touch #{options[:touch].inspect if options[:touch] != true}
+ if old_foreign_id
+ klass = o.association(name).klass
+ old_record = klass.find_by(klass.primary_key => old_foreign_id)
+
+ if old_record
+ if touch != true
+ old_record.touch touch
+ else
+ old_record.touch
end
end
- CODE
+ end
- model.after_save "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_touch "belongs_to_touch_after_save_or_destroy_for_#{name}"
- model.after_destroy "belongs_to_touch_after_save_or_destroy_for_#{name}"
+ record = o.send name
+ unless record.nil? || record.new_record?
+ if touch != true
+ record.touch touch
+ else
+ record.touch
+ end
+ end
end
- def valid_dependent_options
- [:destroy, :delete]
+ def self.add_touch_callbacks(model, reflection)
+ foreign_key = reflection.foreign_key
+ n = reflection.name
+ touch = reflection.options[:touch]
+
+ callback = lambda { |record|
+ BelongsTo.touch_record(record, foreign_key, n, touch)
+ }
+
+ model.after_save callback
+ model.after_touch callback
+ model.after_destroy callback
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/collection_association.rb b/activerecord/lib/active_record/associations/builder/collection_association.rb
index 1b382f7285..2ff67f904d 100644
--- a/activerecord/lib/active_record/associations/builder/collection_association.rb
+++ b/activerecord/lib/active_record/associations/builder/collection_association.rb
@@ -1,68 +1,54 @@
-module ActiveRecord::Associations::Builder
- class CollectionAssociation < Association #:nodoc:
- CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
+# This class is inherited by the has_many and has_many_and_belongs_to_many association classes
- def valid_options
- super + [:table_name, :finder_sql, :counter_sql, :before_add, :after_add, :before_remove, :after_remove]
- end
+require 'active_record/associations'
- attr_reader :block_extension, :extension_module
+module ActiveRecord::Associations::Builder
+ class CollectionAssociation < Association #:nodoc:
- def initialize(*args, &extension)
- super(*args)
- @block_extension = extension
- end
+ CALLBACKS = [:before_add, :after_add, :before_remove, :after_remove]
- def build
- show_deprecation_warnings
- wrap_block_extension
- reflection = super
- CALLBACKS.each { |callback_name| define_callback(callback_name) }
- reflection
+ def self.valid_options(options)
+ super + [:table_name, :before_add,
+ :after_add, :before_remove, :after_remove, :extend]
end
- def writable?
- true
+ def self.define_callbacks(model, reflection)
+ super
+ name = reflection.name
+ options = reflection.options
+ CALLBACKS.each { |callback_name|
+ define_callback(model, callback_name, name, options)
+ }
end
- def show_deprecation_warnings
- [:finder_sql, :counter_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using scopes).")
- end
+ def self.define_extensions(model, name)
+ if block_given?
+ extension_module_name = "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
+ extension = Module.new(&Proc.new)
+ model.parent.const_set(extension_module_name, extension)
end
end
- def wrap_block_extension
- if block_extension
- @extension_module = mod = Module.new(&block_extension)
- silence_warnings do
- model.parent.const_set(extension_module_name, mod)
- end
-
- prev_scope = @scope
+ def self.define_callback(model, callback_name, name, options)
+ full_callback_name = "#{callback_name}_for_#{name}"
- if prev_scope
- @scope = proc { |owner| instance_exec(owner, &prev_scope).extending(mod) }
+ # TODO : why do i need method_defined? I think its because of the inheritance chain
+ model.class_attribute full_callback_name unless model.method_defined?(full_callback_name)
+ callbacks = Array(options[callback_name.to_sym]).map do |callback|
+ case callback
+ when Symbol
+ ->(method, owner, record) { owner.send(callback, record) }
+ when Proc
+ ->(method, owner, record) { callback.call(owner, record) }
else
- @scope = proc { extending(mod) }
+ ->(method, owner, record) { callback.send(method, owner, record) }
end
end
+ model.send "#{full_callback_name}=", callbacks
end
- def extension_module_name
- @extension_module_name ||= "#{model.name.demodulize}#{name.to_s.camelize}AssociationExtension"
- end
-
- def define_callback(callback_name)
- full_callback_name = "#{callback_name}_for_#{name}"
-
- # TODO : why do i need method_defined? I think its because of the inheritance chain
- model.class_attribute full_callback_name.to_sym unless model.method_defined?(full_callback_name)
- model.send("#{full_callback_name}=", Array(options[callback_name.to_sym]))
- end
-
- def define_readers
+ # Defines the setter and getter methods for the collection_singular_ids.
+ def self.define_readers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -72,7 +58,7 @@ module ActiveRecord::Associations::Builder
CODE
end
- def define_writers
+ def self.define_writers(mixin, name)
super
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
@@ -81,5 +67,13 @@ module ActiveRecord::Associations::Builder
end
CODE
end
+
+ def self.wrap_scope(scope, mod)
+ if scope
+ proc { |owner| instance_exec(owner, &scope).extending(mod) }
+ else
+ proc { extending(mod) }
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
index bdac02b5bf..1c9c04b044 100644
--- a/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_and_belongs_to_many.rb
@@ -1,39 +1,121 @@
module ActiveRecord::Associations::Builder
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- def macro
- :has_and_belongs_to_many
- end
+ class HasAndBelongsToMany # :nodoc:
+ class JoinTableResolver
+ KnownTable = Struct.new :join_table
+
+ class KnownClass
+ def initialize(lhs_class, rhs_class_name)
+ @lhs_class = lhs_class
+ @rhs_class_name = rhs_class_name
+ @join_table = nil
+ end
+
+ def join_table
+ @join_table ||= [@lhs_class.table_name, klass.table_name].sort.join("\0").gsub(/^(.*_)(.+)\0\1(.+)/, '\1\2_\3').gsub("\0", "_")
+ end
+
+ private
+ def klass; @rhs_class_name.constantize; end
+ end
- def valid_options
- super + [:join_table, :association_foreign_key, :delete_sql, :insert_sql]
+ def self.build(lhs_class, name, options)
+ if options[:join_table]
+ KnownTable.new options[:join_table]
+ else
+ class_name = options.fetch(:class_name) {
+ name.to_s.camelize.singularize
+ }
+ KnownClass.new lhs_class, class_name
+ end
+ end
end
- def build
- reflection = super
- define_destroy_hook
- reflection
+ attr_reader :lhs_model, :association_name, :options
+
+ def initialize(association_name, lhs_model, options)
+ @association_name = association_name
+ @lhs_model = lhs_model
+ @options = options
end
- def show_deprecation_warnings
- super
+ def through_model
+ habtm = JoinTableResolver.build lhs_model, association_name, options
+
+ join_model = Class.new(ActiveRecord::Base) {
+ class << self;
+ attr_accessor :class_resolver
+ attr_accessor :name
+ attr_accessor :table_name_resolver
+ attr_accessor :left_reflection
+ attr_accessor :right_reflection
+ end
+
+ def self.table_name
+ table_name_resolver.join_table
+ end
+
+ def self.compute_type(class_name)
+ class_resolver.compute_type class_name
+ end
+
+ def self.add_left_association(name, options)
+ belongs_to name, options
+ self.left_reflection = reflect_on_association(name)
+ end
- [:delete_sql, :insert_sql].each do |name|
- if options.include? name
- ActiveSupport::Deprecation.warn("The :#{name} association option is deprecated. Please find an alternative (such as using has_many :through).")
+ def self.add_right_association(name, options)
+ rhs_name = name.to_s.singularize.to_sym
+ belongs_to rhs_name, options
+ self.right_reflection = reflect_on_association(rhs_name)
end
+
+ }
+
+ join_model.name = "HABTM_#{association_name.to_s.camelize}"
+ join_model.table_name_resolver = habtm
+ join_model.class_resolver = lhs_model
+
+ join_model.add_left_association :left_side, class: lhs_model
+ join_model.add_right_association association_name, belongs_to_options(options)
+ join_model
+ end
+
+ def middle_reflection(join_model)
+ middle_name = [lhs_model.name.downcase.pluralize,
+ association_name].join('_').gsub(/::/, '_').to_sym
+ middle_options = middle_options join_model
+
+ HasMany.create_reflection(lhs_model,
+ middle_name,
+ nil,
+ middle_options)
+ end
+
+ private
+
+ def middle_options(join_model)
+ middle_options = {}
+ middle_options[:class] = join_model
+ middle_options[:source] = join_model.left_reflection.name
+ if options.key? :foreign_key
+ middle_options[:foreign_key] = options[:foreign_key]
end
+ middle_options
end
- def define_destroy_hook
- name = self.name
- model.send(:include, Module.new {
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def destroy_associations
- association(:#{name}).delete_all
- super
- end
- RUBY
- })
+ def belongs_to_options(options)
+ rhs_options = {}
+
+ if options.key? :class_name
+ rhs_options[:foreign_key] = options[:class_name].foreign_key
+ rhs_options[:class_name] = options[:class_name]
+ end
+
+ if options.key? :association_foreign_key
+ rhs_options[:foreign_key] = options[:association_foreign_key]
+ end
+
+ rhs_options
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_many.rb b/activerecord/lib/active_record/associations/builder/has_many.rb
index ab8225460a..227184cd19 100644
--- a/activerecord/lib/active_record/associations/builder/has_many.rb
+++ b/activerecord/lib/active_record/associations/builder/has_many.rb
@@ -1,15 +1,15 @@
module ActiveRecord::Associations::Builder
class HasMany < CollectionAssociation #:nodoc:
- def macro
+ def self.macro
:has_many
end
- def valid_options
- super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of]
+ def self.valid_options(options)
+ super + [:primary_key, :dependent, :as, :through, :source, :source_type, :inverse_of, :counter_cache]
end
- def valid_dependent_options
- [:destroy, :delete_all, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.valid_dependent_options
+ [:destroy, :delete_all, :nullify, :restrict_with_error, :restrict_with_exception]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/has_one.rb b/activerecord/lib/active_record/associations/builder/has_one.rb
index 0da564f402..064a3c8b51 100644
--- a/activerecord/lib/active_record/associations/builder/has_one.rb
+++ b/activerecord/lib/active_record/associations/builder/has_one.rb
@@ -1,25 +1,21 @@
module ActiveRecord::Associations::Builder
class HasOne < SingularAssociation #:nodoc:
- def macro
+ def self.macro
:has_one
end
- def valid_options
+ def self.valid_options(options)
valid = super + [:order, :as]
valid += [:through, :source, :source_type] if options[:through]
valid
end
- def constructable?
- !options[:through]
+ def self.valid_dependent_options
+ [:destroy, :delete, :nullify, :restrict_with_error, :restrict_with_exception]
end
- def configure_dependency
- super unless options[:through]
- end
-
- def valid_dependent_options
- [:destroy, :delete, :nullify, :restrict, :restrict_with_error, :restrict_with_exception]
+ def self.add_before_destroy_callbacks(model, reflection)
+ super unless reflection.options[:through]
end
end
end
diff --git a/activerecord/lib/active_record/associations/builder/singular_association.rb b/activerecord/lib/active_record/associations/builder/singular_association.rb
index 6a5830e57f..2a4b1c441f 100644
--- a/activerecord/lib/active_record/associations/builder/singular_association.rb
+++ b/activerecord/lib/active_record/associations/builder/singular_association.rb
@@ -1,19 +1,18 @@
+# This class is inherited by the has_one and belongs_to association classes
+
module ActiveRecord::Associations::Builder
class SingularAssociation < Association #:nodoc:
- def valid_options
+ def self.valid_options(options)
super + [:remote, :dependent, :counter_cache, :primary_key, :inverse_of]
end
- def constructable?
- true
- end
-
- def define_accessors
+ def self.define_accessors(model, reflection)
super
- define_constructors if constructable?
+ define_constructors(model.generated_feature_methods, reflection.name) if reflection.constructable?
end
- def define_constructors
+ # Defines the (build|create)_association methods for belongs_to or has_one association
+ def self.define_constructors(mixin, name)
mixin.class_eval <<-CODE, __FILE__, __LINE__ + 1
def build_#{name}(*args, &block)
association(:#{name}).build(*args, &block)
diff --git a/activerecord/lib/active_record/associations/collection_association.rb b/activerecord/lib/active_record/associations/collection_association.rb
index 7f39d3083e..62f23f54f9 100644
--- a/activerecord/lib/active_record/associations/collection_association.rb
+++ b/activerecord/lib/active_record/associations/collection_association.rb
@@ -7,7 +7,6 @@ module ActiveRecord
# collections. See the class hierarchy in AssociationProxy.
#
# CollectionAssociation:
- # HasAndBelongsToManyAssociation => has_and_belongs_to_many
# HasManyAssociation => has_many
# HasManyThroughAssociation + ThroughAssociation => has_many :through
#
@@ -34,7 +33,7 @@ module ActiveRecord
reload
end
- CollectionProxy.new(self)
+ @proxy ||= CollectionProxy.create(klass, self)
end
# Implements the writer method, e.g. foo.items= for Foo.has_many :items
@@ -44,7 +43,7 @@ module ActiveRecord
# Implements the ids reader method, e.g. foo.item_ids for Foo.has_many :items
def ids_reader
- if loaded? || options[:finder_sql]
+ if loaded?
load_target.map do |record|
record.send(reflection.association_primary_key)
end
@@ -79,8 +78,17 @@ module ActiveRecord
if block_given?
load_target.find(*args) { |*block_args| yield(*block_args) }
else
- if options[:finder_sql]
- find_by_scan(*args)
+ if options[:inverse_of] && loaded?
+ args_flatten = args.flatten
+ raise RecordNotFound, "Couldn't find #{scope.klass.name} without an ID" if args_flatten.blank?
+ result = find_by_scan(*args)
+
+ result_size = Array(result).size
+ if !result || result_size != args_flatten.size
+ scope.raise_record_not_found_exception!(args_flatten, result_size, args_flatten.size)
+ else
+ result
+ end
else
scope.find(*args)
end
@@ -141,11 +149,33 @@ module ActiveRecord
end
end
- # Remove all records from this association.
+ # Removes all records from the association without calling callbacks
+ # on the associated records. It honors the `:dependent` option. However
+ # if the `:dependent` value is `:destroy` then in that case the `:delete_all`
+ # deletion strategy for the association is applied.
+ #
+ # You can force a particular deletion strategy by passing a parameter.
+ #
+ # Example:
+ #
+ # @author.books.delete_all(:nullify)
+ # @author.books.delete_all(:delete_all)
#
# See delete for more info.
- def delete_all
- delete(:all).tap do
+ def delete_all(dependent = nil)
+ if dependent.present? && ![:nullify, :delete_all].include?(dependent)
+ raise ArgumentError, "Valid values are :nullify or :delete_all"
+ end
+
+ dependent = if dependent.present?
+ dependent
+ elsif options[:dependent] == :destroy
+ :delete_all
+ else
+ options[:dependent]
+ end
+
+ delete(:all, dependent: dependent).tap do
reset
loaded!
end
@@ -161,46 +191,25 @@ module ActiveRecord
end
end
- # Calculate sum using SQL, not Enumerable.
- def sum(*args)
- if block_given?
- scope.sum(*args) { |*block_args| yield(*block_args) }
- else
- scope.sum(*args)
- end
- end
-
- # Count all records using SQL. If the +:counter_sql+ or +:finder_sql+ option is set for the
- # association, it will be used for the query. Otherwise, construct options and pass them with
+ # Count all records using SQL. Construct options and pass them with
# scope to the target class's +count+.
- def count(column_name = nil, count_options = {})
- return 0 if owner.new_record?
+ def count(column_name = nil)
+ relation = scope
+ if association_scope.distinct_value
+ # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
+ column_name ||= reflection.klass.primary_key
+ relation = relation.distinct
+ end
- column_name, count_options = nil, column_name if column_name.is_a?(Hash)
+ value = relation.count(column_name)
- if options[:counter_sql] || options[:finder_sql]
- unless count_options.blank?
- raise ArgumentError, "If finder_sql/counter_sql is used then options cannot be passed"
- end
+ limit = options[:limit]
+ offset = options[:offset]
- reflection.klass.count_by_sql(custom_counter_sql)
+ if limit || offset
+ [ [value - offset.to_i, 0].max, limit.to_i ].min
else
- if association_scope.uniq_value
- # This is needed because 'SELECT count(DISTINCT *)..' is not valid SQL.
- column_name ||= reflection.klass.primary_key
- count_options[:distinct] = true
- end
-
- value = scope.count(column_name, count_options)
-
- limit = options[:limit]
- offset = options[:offset]
-
- if limit || offset
- [ [value - offset.to_i, 0].max, limit.to_i ].min
- else
- value
- end
+ value
end
end
@@ -212,7 +221,8 @@ module ActiveRecord
# are actually removed from the database, that depends precisely on
# +delete_records+. They are in any case removed from the collection.
def delete(*records)
- dependent = options[:dependent]
+ _options = records.extract_options!
+ dependent = _options[:dependent] || options[:dependent]
if records.first == :all
if loaded? || dependent == :destroy
@@ -226,11 +236,11 @@ module ActiveRecord
end
end
- # Destroy +records+ and remove them from this association calling
- # +before_remove+ and +after_remove+ callbacks.
+ # Deletes the +records+ and removes them from this association calling
+ # +before_remove+ , +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
- # Note that this method will _always_ remove records from the database
- # ignoring the +:dependent+ option.
+ # Note that this method removes records from the database ignoring the
+ # +:dependent+ option.
def destroy(*records)
records = find(records) if records.any? { |record| record.kind_of?(Fixnum) || record.kind_of?(String) }
delete_or_destroy(records, :destroy)
@@ -248,14 +258,14 @@ module ActiveRecord
# +count_records+, which is a method descendants have to provide.
def size
if !find_target? || loaded?
- if association_scope.uniq_value
+ if association_scope.distinct_value
target.uniq.size
else
target.size
end
elsif !loaded? && !association_scope.group_values.empty?
load_target.size
- elsif !loaded? && !association_scope.uniq_value && target.is_a?(Array)
+ elsif !loaded? && !association_scope.distinct_value && target.is_a?(Array)
unsaved_records = target.select { |r| r.new_record? }
unsaved_records.size + count_records
else
@@ -274,17 +284,17 @@ module ActiveRecord
# Returns true if the collection is empty.
#
- # If the collection has been loaded or the <tt>:counter_sql</tt> option
- # is provided, it is equivalent to <tt>collection.size.zero?</tt>. If the
+ # If the collection has been loaded
+ # it is equivalent to <tt>collection.size.zero?</tt>. If the
# collection has not been loaded, it is equivalent to
# <tt>collection.exists?</tt>. If the collection has not already been
# loaded and you are going to fetch the records anyway it is better to
# check <tt>collection.length.zero?</tt>.
def empty?
- if loaded? || options[:counter_sql]
+ if loaded?
size.zero?
else
- !scope.exists?
+ @target.blank? && !scope.exists?
end
end
@@ -308,17 +318,18 @@ module ActiveRecord
end
end
- def uniq
+ def distinct
seen = {}
load_target.find_all do |record|
seen[record.id] = true unless seen.key?(record.id)
end
end
+ alias uniq distinct
# Replace this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
def replace(other_array)
- other_array.each { |val| raise_on_type_mismatch(val) }
+ other_array.each { |val| raise_on_type_mismatch!(val) }
original_target = load_target.dup
if owner.new_record?
@@ -333,7 +344,6 @@ module ActiveRecord
if record.new_record?
include_in_memory?(record)
else
- load_target if options[:finder_sql]
loaded? ? target.include?(record) : scope.exists?(record)
end
else
@@ -350,49 +360,36 @@ module ActiveRecord
target
end
- def add_to_target(record)
- callback(:before_add, record)
+ def add_to_target(record, skip_callbacks = false)
+ callback(:before_add, record) unless skip_callbacks
yield(record) if block_given?
- if association_scope.uniq_value && index = @target.index(record)
+ if association_scope.distinct_value && index = @target.index(record)
@target[index] = record
else
@target << record
end
- callback(:after_add, record)
+ callback(:after_add, record) unless skip_callbacks
set_inverse_instance(record)
record
end
- private
+ def scope(opts = {})
+ scope = super()
+ scope.none! if opts.fetch(:nullify, true) && null_scope?
+ scope
+ end
- def custom_counter_sql
- if options[:counter_sql]
- interpolate(options[:counter_sql])
- else
- # replace the SELECT clause with COUNT(SELECTS), preserving any hints within /* ... */
- interpolate(options[:finder_sql]).sub(/SELECT\b(\/\*.*?\*\/ )?(.*)\bFROM\b/im) do
- count_with = $2.to_s
- count_with = '*' if count_with.blank? || count_with =~ /,/ || count_with =~ /\.\*/
- "SELECT #{$1}COUNT(#{count_with}) FROM"
- end
- end
- end
+ def null_scope?
+ owner.new_record? && !foreign_key_present?
+ end
- def custom_finder_sql
- interpolate(options[:finder_sql])
- end
+ private
def find_target
- records =
- if options[:finder_sql]
- reflection.klass.find_by_sql(custom_finder_sql)
- else
- scope.to_a
- end
-
+ records = scope.to_a
records.each { |record| set_inverse_instance(record) }
records
end
@@ -455,7 +452,7 @@ module ActiveRecord
def delete_or_destroy(records, method)
records = records.flatten
- records.each { |record| raise_on_type_mismatch(record) }
+ records.each { |record| raise_on_type_mismatch!(record) }
existing_records = records.reject { |r| r.new_record? }
if existing_records.empty?
@@ -496,9 +493,9 @@ module ActiveRecord
result = true
records.flatten.each do |record|
- raise_on_type_mismatch(record)
- add_to_target(record) do |r|
- result &&= insert_record(record) unless owner.new_record?
+ raise_on_type_mismatch!(record)
+ add_to_target(record) do |rec|
+ result &&= insert_record(rec) unless owner.new_record?
end
end
@@ -507,20 +504,13 @@ module ActiveRecord
def callback(method, record)
callbacks_for(method).each do |callback|
- case callback
- when Symbol
- owner.send(callback, record)
- when Proc
- callback.call(owner, record)
- else
- callback.send(method, owner, record)
- end
+ callback.call(method, owner, record)
end
end
def callbacks_for(callback_name)
full_callback_name = "#{callback_name}_for_#{reflection.name}"
- owner.class.send(full_callback_name.to_sym) || []
+ owner.class.send(full_callback_name)
end
# Should we deal with assoc.first or assoc.last by issuing an independent query to
@@ -531,24 +521,21 @@ module ActiveRecord
# Otherwise, go to the database only if none of the following are true:
# * target already loaded
# * owner is new record
- # * custom :finder_sql exists
# * target contains new or changed record(s)
- # * the first arg is an integer (which indicates the number of records to be returned)
def fetch_first_or_last_using_find?(args)
if args.first.is_a?(Hash)
true
else
!(loaded? ||
owner.new_record? ||
- options[:finder_sql] ||
- target.any? { |record| record.new_record? || record.changed? } ||
- args.first.kind_of?(Integer))
+ target.any? { |record| record.new_record? || record.changed? })
end
end
def include_in_memory?(record)
if reflection.is_a?(ActiveRecord::Reflection::ThroughReflection)
- owner.send(reflection.through_reflection.name).any? { |source|
+ assoc = owner.association(reflection.through_reflection.name)
+ assoc.reader.any? { |source|
target = source.send(reflection.source_reflection.name)
target.respond_to?(:include?) ? target.include?(record) : target == record
} || target.include?(record)
@@ -557,17 +544,18 @@ module ActiveRecord
end
end
- # If using a custom finder_sql, #find scans the entire collection.
+ # If the :inverse_of option has been
+ # specified, then #find scans the entire collection.
def find_by_scan(*args)
expects_array = args.first.kind_of?(Array)
- ids = args.flatten.compact.map{ |arg| arg.to_i }.uniq
+ ids = args.flatten.compact.map{ |arg| arg.to_s }.uniq
if ids.size == 1
id = ids.first
- record = load_target.detect { |r| id == r.id }
+ record = load_target.detect { |r| id == r.id.to_s }
expects_array ? [ record ] : record
else
- load_target.select { |r| ids.include?(r.id) }
+ load_target.select { |r| ids.include?(r.id.to_s) }
end
end
@@ -576,7 +564,9 @@ module ActiveRecord
args.shift if args.first.is_a?(Hash) && args.first.empty?
collection = fetch_first_or_last_using_find?(args) ? scope : load_target
- collection.send(type, *args).tap {|it| set_inverse_instance it }
+ collection.send(type, *args).tap do |record|
+ set_inverse_instance record if record.is_a? ActiveRecord::Base
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/collection_proxy.rb b/activerecord/lib/active_record/associations/collection_proxy.rb
index e73f940334..2e70a07962 100644
--- a/activerecord/lib/active_record/associations/collection_proxy.rb
+++ b/activerecord/lib/active_record/associations/collection_proxy.rb
@@ -28,10 +28,12 @@ module ActiveRecord
# is computed directly through SQL and does not trigger by itself the
# instantiation of the actual post records.
class CollectionProxy < Relation
- def initialize(association) #:nodoc:
+ delegate(*(ActiveRecord::Calculations.public_instance_methods - [:count]), to: :scope)
+
+ def initialize(klass, association) #:nodoc:
@association = association
- super association.klass, association.klass.arel_table
- merge! association.scope
+ super klass, klass.arel_table
+ merge! association.scope(nullify: false)
end
def target
@@ -80,16 +82,16 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo">
# # ]
#
- # Be careful because this also means you’re initializing a model
- # object with only the fields that you’ve selected. If you attempt
- # to access a field that is not in the initialized record you’ll
+ # Be careful because this also means you're initializing a model
+ # object with only the fields that you've selected. If you attempt
+ # to access a field that is not in the initialized record you'll
# receive:
#
# person.pets.select(:name).first.person_id
# # => ActiveModel::MissingAttributeError: missing attribute: person_id
#
# *Second:* You can pass a block so it can be used just like Array#select.
- # This build an array of objects from the database for the scope,
+ # This builds an array of objects from the database for the scope,
# converting them into an array and iterating through them using
# Array#select.
#
@@ -99,7 +101,7 @@ module ActiveRecord
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
#
- # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
+ # person.pets.select(:name) { |pet| pet.name =~ /oo/ }
# # => [
# # #<Pet id: 2, name: "Spook">,
# # #<Pet id: 3, name: "Choo-Choo">
@@ -225,6 +227,7 @@ module ActiveRecord
def build(attributes = {}, &block)
@association.build(attributes, &block)
end
+ alias_method :new, :build
# Returns a new object of the collection type that has been instantiated with
# attributes, linked to this object and that has already been saved (if it
@@ -278,7 +281,7 @@ module ActiveRecord
# so method calls may be chained.
#
# class Person < ActiveRecord::Base
- # pets :has_many
+ # has_many :pets
# end
#
# person.pets.size # => 0
@@ -300,7 +303,7 @@ module ActiveRecord
@association.concat(*records)
end
- # Replace this collection with +other_array+. This will perform a diff
+ # Replaces this collection with +other_array+. This will perform a diff
# and delete/add only records that have changed.
#
# class Person < ActiveRecord::Base
@@ -414,13 +417,13 @@ module ActiveRecord
#
# Pet.find(1, 2, 3)
# # => ActiveRecord::RecordNotFound
- def delete_all
- @association.delete_all
+ def delete_all(dependent = nil)
+ @association.delete_all(dependent)
end
- # Deletes the records of the collection directly from the database.
- # This will _always_ remove the records ignoring the +:dependent+
- # option.
+ # Deletes the records of the collection directly from the database
+ # ignoring the +:dependent+ option. It invokes +before_remove+,
+ # +after_remove+ , +before_destroy+ and +after_destroy+ callbacks.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -646,11 +649,12 @@ module ActiveRecord
# # #<Pet name: "Fancy-Fancy">
# # ]
#
- # person.pets.select(:name).uniq
+ # person.pets.select(:name).distinct
# # => [#<Pet name: "Fancy-Fancy">]
- def uniq
- @association.uniq
+ def distinct
+ @association.distinct
end
+ alias uniq distinct
# Count all records using SQL.
#
@@ -665,12 +669,16 @@ module ActiveRecord
# # #<Pet id: 2, name: "Spook", person_id: 1>,
# # #<Pet id: 3, name: "Choo-Choo", person_id: 1>
# # ]
- def count(column_name = nil, options = {})
- @association.count(column_name, options)
+ def count(column_name = nil)
+ @association.count(column_name)
end
# Returns the size of the collection. If the collection hasn't been loaded,
- # it executes a <tt>SELECT COUNT(*)</tt> query.
+ # it executes a <tt>SELECT COUNT(*)</tt> query. Else it calls <tt>collection.size</tt>.
+ #
+ # If the collection has been already loaded +size+ and +length+ are
+ # equivalent. If not and you are going to need the records anyway
+ # +length+ will take one less query. Otherwise +size+ is more efficient.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -695,7 +703,8 @@ module ActiveRecord
# Returns the size of the collection calling +size+ on the target.
# If the collection has been already loaded, +length+ and +size+ are
- # equivalent.
+ # equivalent. If not and you are going to need the records anyway this
+ # method will take one less query. Otherwise +size+ is more efficient.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -716,7 +725,12 @@ module ActiveRecord
@association.length
end
- # Returns +true+ if the collection is empty.
+ # Returns +true+ if the collection is empty. If the collection has been
+ # loaded it is equivalent
+ # to <tt>collection.size.zero?</tt>. If the collection has not been loaded,
+ # it is equivalent to <tt>collection.exists?</tt>. If the collection has
+ # not already been loaded and you are going to fetch the records anyway it
+ # is better to check <tt>collection.length.zero?</tt>.
#
# class Person < ActiveRecord::Base
# has_many :pets
@@ -746,7 +760,7 @@ module ActiveRecord
# person.pets.count # => 0
# person.pets.any? # => true
#
- # You can also pass a block to define criteria. The behaviour
+ # You can also pass a block to define criteria. The behavior
# is the same, it returns true if the collection based on the
# criteria is not empty.
#
@@ -781,7 +795,7 @@ module ActiveRecord
# person.pets.many? #=> true
#
# You can also pass a block to define criteria. The
- # behaviour is the same, it returns true if the collection
+ # behavior is the same, it returns true if the collection
# based on the criteria has more than one record.
#
# person.pets
@@ -812,14 +826,12 @@ module ActiveRecord
#
# person.pets # => [#<Pet id: 20, name: "Snoop">]
#
- # person.pets.include?(Pet.find(20)) # => true
+ # person.pets.include?(Pet.find(20)) # => true
# person.pets.include?(Pet.find(21)) # => false
def include?(record)
- @association.include?(record)
+ !!@association.include?(record)
end
- alias_method :new, :build
-
def proxy_association
@association
end
@@ -834,14 +846,8 @@ module ActiveRecord
# Returns a <tt>Relation</tt> object for the records in this association
def scope
- association = @association
-
- @association.scope.extending! do
- define_method(:proxy_association) { association }
- end
+ @association.scope
end
-
- # :nodoc:
alias spawn scope
# Equivalent to <tt>Array#==</tt>. Returns +true+ if the two arrays
@@ -911,7 +917,7 @@ module ActiveRecord
alias_method :to_a, :to_ary
# Adds one or more +records+ to the collection by setting their foreign keys
- # to the association‘s primary key. Returns +self+, so several appends may be
+ # to the association's primary key. Returns +self+, so several appends may be
# chained together.
#
# class Person < ActiveRecord::Base
@@ -934,6 +940,11 @@ module ActiveRecord
proxy_association.concat(records) && self
end
alias_method :push, :<<
+ alias_method :append, :<<
+
+ def prepend(*args)
+ raise NoMethodError, "prepend on association is not defined. Please use << or append"
+ end
# Equivalent to +delete_all+. The difference is that returns +self+, instead
# of an array with the deleted objects, so methods can be chained. See
@@ -959,7 +970,7 @@ module ActiveRecord
# person.pets.reload # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
#
- # person.pets(true)  # fetches pets from the database
+ # person.pets(true) # fetches pets from the database
# # => [#<Pet id: 1, name: "Snoop", group: "dogs", person_id: 1>]
def reload
proxy_association.reload
diff --git a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb b/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
deleted file mode 100644
index 93618721bb..0000000000
--- a/activerecord/lib/active_record/associations/has_and_belongs_to_many_association.rb
+++ /dev/null
@@ -1,65 +0,0 @@
-module ActiveRecord
- # = Active Record Has And Belongs To Many Association
- module Associations
- class HasAndBelongsToManyAssociation < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(owner, reflection)
- @join_table = Arel::Table.new(reflection.join_table)
- super
- end
-
- def insert_record(record, validate = true, raise = false)
- if record.new_record?
- if raise
- record.save!(:validate => validate)
- else
- return unless record.save(:validate => validate)
- end
- end
-
- if options[:insert_sql]
- owner.connection.insert(interpolate(options[:insert_sql], record))
- else
- stmt = join_table.compile_insert(
- join_table[reflection.foreign_key] => owner.id,
- join_table[reflection.association_foreign_key] => record.id
- )
-
- owner.connection.insert stmt
- end
-
- record
- end
-
- private
-
- def count_records
- load_target.size
- end
-
- def delete_records(records, method)
- if sql = options[:delete_sql]
- records = load_target if records == :all
- records.each { |record| owner.connection.delete(interpolate(sql, record)) }
- else
- relation = join_table
- condition = relation[reflection.foreign_key].eq(owner.id)
-
- unless records == :all
- condition = condition.and(
- relation[reflection.association_foreign_key]
- .in(records.map { |x| x.id }.compact)
- )
- end
-
- owner.connection.delete(relation.where(condition).compile_delete)
- end
- end
-
- def invertible_for?(record)
- false
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/has_many_association.rb b/activerecord/lib/active_record/associations/has_many_association.rb
index 74864d271f..0a23109b9b 100644
--- a/activerecord/lib/active_record/associations/has_many_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_association.rb
@@ -9,7 +9,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) unless empty?
when :restrict_with_error
@@ -22,15 +22,17 @@ module ActiveRecord
else
if options[:dependent] == :destroy
# No point in executing the counter update since we're going to destroy the parent anyway
- load_target.each(&:mark_for_destruction)
+ load_target.each { |t| t.destroyed_by_association = reflection }
+ destroy_all
+ else
+ delete_all
end
-
- delete_all
end
end
def insert_record(record, validate = true, raise = false)
set_owner_attributes(record)
+ set_inverse_instance(record)
if raise
record.save!(:validate => validate)
@@ -57,8 +59,6 @@ module ActiveRecord
def count_records
count = if has_cached_counter?
owner.send(:read_attribute, cached_counter_attribute_name)
- elsif options[:counter_sql] || options[:finder_sql]
- reflection.klass.count_by_sql(custom_counter_sql)
else
scope.count
end
@@ -76,7 +76,7 @@ module ActiveRecord
end
def cached_counter_attribute_name(reflection = reflection)
- "#{reflection.name}_count"
+ options[:counter_cache] || "#{reflection.name}_count"
end
def update_counter(difference, reflection = reflection)
@@ -114,8 +114,7 @@ module ActiveRecord
if records == :all
scope = self.scope
else
- keys = records.map { |r| r[reflection.association_primary_key] }
- scope = self.scope.where(reflection.association_primary_key => keys)
+ scope = self.scope.where(reflection.klass.primary_key => records)
end
if method == :delete_all
@@ -127,7 +126,11 @@ module ActiveRecord
end
def foreign_key_present?
- owner.attribute_present?(reflection.association_primary_key)
+ if reflection.klass.primary_key
+ owner.attribute_present?(reflection.association_primary_key)
+ else
+ false
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_many_through_association.rb b/activerecord/lib/active_record/associations/has_many_through_association.rb
index c7d8a84a7e..56331bbb0b 100644
--- a/activerecord/lib/active_record/associations/has_many_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_many_through_association.rb
@@ -29,7 +29,7 @@ module ActiveRecord
def concat(*records)
unless owner.new_record?
records.flatten.each do |record|
- raise_on_type_mismatch(record)
+ raise_on_type_mismatch!(record)
record.save! if record.new_record?
end
end
@@ -114,11 +114,7 @@ module ActiveRecord
end
def target_reflection_has_associated_record?
- if through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?
- false
- else
- true
- end
+ !(through_reflection.macro == :belongs_to && owner[through_reflection.foreign_key].blank?)
end
def update_through_counter?(method)
@@ -144,7 +140,21 @@ module ActiveRecord
case method
when :destroy
- count = scope.destroy_all.length
+ if scope.klass.primary_key
+ count = scope.destroy_all.length
+ else
+ scope.to_a.each do |record|
+ record.run_callbacks :destroy
+ end
+
+ arel = scope.arel
+
+ stmt = Arel::DeleteManager.new arel.engine
+ stmt.from scope.klass.arel_table
+ stmt.wheres = arel.constraints
+
+ count = scope.klass.connection.delete(stmt, 'SQL', scope.bind_values)
+ end
when :nullify
count = scope.update_all(source_reflection.foreign_key => nil)
else
@@ -153,6 +163,11 @@ module ActiveRecord
delete_through_records(records)
+ if source_reflection.options[:counter_cache]
+ counter = source_reflection.counter_cache_column
+ klass.decrement_counter counter, records.map(&:id)
+ end
+
if through_reflection.macro == :has_many && update_through_counter?(method)
update_counter(-count, through_reflection)
end
diff --git a/activerecord/lib/active_record/associations/has_one_association.rb b/activerecord/lib/active_record/associations/has_one_association.rb
index 06bead41de..0008600418 100644
--- a/activerecord/lib/active_record/associations/has_one_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_association.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def handle_dependency
case options[:dependent]
- when :restrict, :restrict_with_exception
+ when :restrict_with_exception
raise ActiveRecord::DeleteRestrictionError.new(reflection.name) if load_target
when :restrict_with_error
@@ -22,20 +22,21 @@ module ActiveRecord
end
def replace(record, save = true)
- raise_on_type_mismatch(record) if record
+ raise_on_type_mismatch!(record) if record
load_target
- # If target and record are nil, or target is equal to record,
- # we don't need to have transaction.
- if (target || record) && target != record
- reflection.klass.transaction do
+ return self.target if !(target || record)
+ if (target != record) || record.changed?
+ save &&= owner.persisted?
+
+ transaction_if(save) do
remove_target!(options[:dependent]) if target && !target.destroyed?
if record
set_owner_attributes(record)
set_inverse_instance(record)
- if owner.persisted? && save && !record.save
+ if save && !record.save
nullify_owner_attributes(record)
set_owner_attributes(target) if target
raise RecordNotSaved, "Failed to save the new associated #{reflection.name}."
@@ -90,6 +91,14 @@ module ActiveRecord
def nullify_owner_attributes(record)
record[reflection.foreign_key] = nil
end
+
+ def transaction_if(value)
+ if value
+ reflection.klass.transaction { yield }
+ else
+ yield
+ end
+ end
end
end
end
diff --git a/activerecord/lib/active_record/associations/has_one_through_association.rb b/activerecord/lib/active_record/associations/has_one_through_association.rb
index fdf8ae1453..08e0ec691f 100644
--- a/activerecord/lib/active_record/associations/has_one_through_association.rb
+++ b/activerecord/lib/active_record/associations/has_one_through_association.rb
@@ -23,7 +23,7 @@ module ActiveRecord
attributes = construct_join_attributes(record)
if through_record
- through_record.update_attributes(attributes)
+ through_record.update(attributes)
elsif owner.new_record?
through_proxy.build(attributes)
else
diff --git a/activerecord/lib/active_record/associations/join_dependency.rb b/activerecord/lib/active_record/associations/join_dependency.rb
index cd366ac8b7..c3ac0680ea 100644
--- a/activerecord/lib/active_record/associations/join_dependency.rb
+++ b/activerecord/lib/active_record/associations/join_dependency.rb
@@ -1,213 +1,273 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- autoload :JoinPart, 'active_record/associations/join_dependency/join_part'
autoload :JoinBase, 'active_record/associations/join_dependency/join_base'
autoload :JoinAssociation, 'active_record/associations/join_dependency/join_association'
- attr_reader :join_parts, :reflections, :alias_tracker, :active_record
+ class Aliases # :nodoc:
+ def initialize(tables)
+ @tables = tables
+ @alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.each_with_object({}) { |column,i|
+ i[column.name] = column.alias
+ }
+ }
+ @name_and_alias_cache = tables.each_with_object({}) { |table,h|
+ h[table.node] = table.columns.map { |column|
+ [column.name, column.alias]
+ }
+ }
+ end
- def initialize(base, associations, joins)
- @active_record = base
- @table_joins = joins
- @join_parts = [JoinBase.new(base)]
- @associations = {}
- @reflections = []
- @alias_tracker = AliasTracker.new(base.connection, joins)
- @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
- build(associations)
- end
+ def columns
+ @tables.flat_map { |t| t.column_aliases }
+ end
- def graft(*associations)
- associations.each do |association|
- join_associations.detect {|a| association == a} ||
- build(association.reflection.name, association.find_parent_in(self) || join_base, association.join_type)
+ # An array of [column_name, alias] pairs for the table
+ def column_aliases(node)
+ @name_and_alias_cache[node]
end
- self
- end
- def join_associations
- join_parts.last(join_parts.length - 1)
- end
+ def column_alias(node, column)
+ @alias_cache[node][column]
+ end
- def join_base
- join_parts.first
- end
+ class Table < Struct.new(:node, :columns)
+ def table
+ Arel::Nodes::TableAlias.new node.table, node.aliased_table_name
+ end
- def columns
- join_parts.collect { |join_part|
- table = join_part.aliased_table
- join_part.column_names_with_alias.collect{ |column_name, aliased_name|
- table[column_name].as Arel.sql(aliased_name)
- }
- }.flatten
+ def column_aliases
+ t = table
+ columns.map { |column| t[column.name].as Arel.sql column.alias }
+ end
+ end
+ Column = Struct.new(:name, :alias)
end
- def instantiate(rows)
- primary_key = join_base.aliased_primary_key
- parents = {}
-
- records = rows.map { |model|
- primary_id = model[primary_key]
- parent = parents[primary_id] ||= join_base.instantiate(model)
- construct(parent, @associations, join_associations, model)
- parent
- }.uniq
+ attr_reader :alias_tracker, :base_klass, :join_root
- remove_duplicate_results!(active_record, records, @associations)
- records
+ def self.make_tree(associations)
+ hash = {}
+ walk_tree associations, hash
+ hash
end
- def remove_duplicate_results!(base, records, associations)
+ def self.walk_tree(associations, hash)
case associations
when Symbol, String
- reflection = base.reflections[associations]
- remove_uniq_by_reflection(reflection, records)
+ hash[associations.to_sym] ||= {}
when Array
- associations.each do |association|
- remove_duplicate_results!(base, records, association)
+ associations.each do |assoc|
+ walk_tree assoc, hash
end
when Hash
- associations.keys.each do |name|
- reflection = base.reflections[name]
- remove_uniq_by_reflection(reflection, records)
-
- parent_records = []
- records.each do |record|
- if descendant = record.send(reflection.name)
- if reflection.collection?
- parent_records.concat descendant.target.uniq
- else
- parent_records << descendant
- end
- end
- end
-
- remove_duplicate_results!(reflection.klass, parent_records, associations[name]) unless parent_records.empty?
+ associations.each do |k,v|
+ cache = hash[k] ||= {}
+ walk_tree v, cache
end
+ else
+ raise ConfigurationError, associations.inspect
end
end
- protected
+ # base is the base class on which operation is taking place.
+ # associations is the list of associations which are joined using hash, symbol or array.
+ # joins is the list of all string join commnads and arel nodes.
+ #
+ # Example :
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # has_many :patients, through: :appointments
+ # end
+ #
+ # If I execute `@physician.patients.to_a` then
+ # base #=> Physician
+ # associations #=> []
+ # joins #=> [#<Arel::Nodes::InnerJoin: ...]
+ #
+ # However if I execute `Physician.joins(:appointments).to_a` then
+ # base #=> Physician
+ # associations #=> [:appointments]
+ # joins #=> []
+ #
+ def initialize(base, associations, joins)
+ @alias_tracker = AliasTracker.new(base.connection, joins)
+ @alias_tracker.aliased_name_for(base.table_name) # Updates the count for base.table_name to 1
+ tree = self.class.make_tree associations
+ @join_root = JoinBase.new base, build(tree, base)
+ @join_root.children.each { |child| construct_tables! @join_root, child }
+ end
- def cache_joined_association(association)
- associations = []
- parent = association.parent
- while parent != join_base
- associations.unshift(parent.reflection.name)
- parent = parent.parent
- end
- ref = @associations
- associations.each do |key|
- ref = ref[key]
- end
- ref[association.reflection.name] ||= {}
+ def reflections
+ join_root.drop(1).map!(&:reflection)
end
- def build(associations, parent = nil, join_type = Arel::InnerJoin)
- parent ||= join_parts.last
- case associations
- when Symbol, String
- reflection = parent.reflections[associations.to_s.intern] or
- raise ConfigurationError, "Association named '#{ associations }' was not found; perhaps you misspelled it?"
- unless join_association = find_join_association(reflection, parent)
- @reflections << reflection
- join_association = build_join_association(reflection, parent)
- join_association.join_type = join_type
- @join_parts << join_association
- cache_joined_association(join_association)
- end
- join_association
- when Array
- associations.each do |association|
- build(association, parent, join_type)
- end
- when Hash
- associations.keys.sort_by { |a| a.to_s }.each do |name|
- join_association = build(name, parent, join_type)
- build(associations[name], join_association, join_type)
+ def join_constraints(outer_joins)
+ joins = join_root.children.flat_map { |child|
+ make_inner_joins join_root, child
+ }
+
+ joins.concat outer_joins.flat_map { |oj|
+ if join_root.match? oj.join_root
+ walk join_root, oj.join_root
+ else
+ oj.join_root.children.flat_map { |child|
+ make_outer_joins join_root, child
+ }
end
- else
- raise ConfigurationError, associations.inspect
- end
+ }
end
- def find_join_association(name_or_reflection, parent)
- if String === name_or_reflection
- name_or_reflection = name_or_reflection.to_sym
- end
+ def aliases
+ Aliases.new join_root.each_with_index.map { |join_part,i|
+ columns = join_part.column_names.each_with_index.map { |column_name,j|
+ Aliases::Column.new column_name, "t#{i}_r#{j}"
+ }
+ Aliases::Table.new(join_part, columns)
+ }
+ end
+
+ 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|
+ i[parent_id] = Hash.new { |j,child_klass| j[child_klass] = {} }
+ }
+ }
+
+ model_cache = Hash.new { |h,klass| h[klass] = {} }
+ parents = model_cache[join_root]
+ column_aliases = aliases.column_aliases join_root
- join_associations.detect { |j|
- j.reflection == name_or_reflection && j.parent == parent
+ result_set.each { |row_hash|
+ primary_id = type_caster.type_cast row_hash[primary_key]
+ parent = parents[primary_id] ||= join_root.instantiate(row_hash, column_aliases)
+ construct(parent, join_root, row_hash, result_set, seen, model_cache, aliases)
}
+
+ parents.values
end
- def remove_uniq_by_reflection(reflection, records)
- if reflection && reflection.collection?
- records.each { |record| record.send(reflection.name).target.uniq! }
- end
+ private
+
+ def make_constraints(parent, child, tables, join_type)
+ chain = child.reflection.chain
+ foreign_table = parent.table
+ foreign_klass = parent.base_klass
+ child.join_constraints(foreign_table, foreign_klass, child, join_type, tables, child.reflection.scope_chain, chain)
end
- def build_join_association(reflection, parent)
- JoinAssociation.new(reflection, self, parent)
+ def make_outer_joins(parent, child)
+ tables = table_aliases_for(parent, child)
+ join_type = Arel::OuterJoin
+ joins = make_constraints parent, child, tables, join_type
+
+ joins.concat child.children.flat_map { |c| make_outer_joins(child, c) }
end
- def construct(parent, associations, join_parts, row)
- case associations
- when Symbol, String
- name = associations.to_s
+ def make_inner_joins(parent, child)
+ tables = child.tables
+ join_type = Arel::InnerJoin
+ joins = make_constraints parent, child, tables, join_type
- join_part = join_parts.detect { |j|
- j.reflection.name.to_s == name &&
- j.parent_table_name == parent.class.table_name }
+ joins.concat child.children.flat_map { |c| make_inner_joins(child, c) }
+ end
- raise(ConfigurationError, "No such association") unless join_part
+ def table_aliases_for(parent, node)
+ node.reflection.chain.map { |reflection|
+ alias_tracker.aliased_table_for(
+ reflection.table_name,
+ table_alias_for(reflection, parent, reflection != node.reflection)
+ )
+ }
+ end
- join_parts.delete(join_part)
- construct_association(parent, join_part, row)
- when Array
- associations.each do |association|
- construct(parent, association, join_parts, row)
- end
- when Hash
- associations.sort_by { |k,_| k.to_s }.each do |association_name, assoc|
- association = construct(parent, association_name, join_parts, row)
- construct(association, assoc, join_parts, row) if association
+ def construct_tables!(parent, node)
+ node.tables = table_aliases_for(parent, node)
+ node.children.each { |child| construct_tables! node, child }
+ end
+
+ def table_alias_for(reflection, parent, join)
+ name = "#{reflection.plural_name}_#{parent.table_name}"
+ name << "_join" if join
+ name
+ end
+
+ def walk(left, right)
+ intersection, missing = right.children.map { |node1|
+ [left.children.find { |node2| node1.match? node2 }, node1]
+ }.partition(&:first)
+
+ ojs = missing.flat_map { |_,n| make_outer_joins left, n }
+ intersection.flat_map { |l,r| walk l, r }.concat ojs
+ end
+
+ def find_reflection(klass, name)
+ klass.reflect_on_association(name) or
+ raise ConfigurationError, "Association named '#{ name }' was not found on #{ klass.name }; perhaps you misspelled it?"
+ end
+
+ def build(associations, base_klass)
+ associations.map do |name, right|
+ reflection = find_reflection base_klass, name
+ reflection.check_validity!
+
+ if reflection.options[:polymorphic]
+ raise EagerLoadPolymorphicError.new(reflection)
end
- else
- raise ConfigurationError, associations.inspect
+
+ JoinAssociation.new reflection, build(right, reflection.klass)
end
end
- def construct_association(record, join_part, row)
- return if record.id.to_s != join_part.parent.record_id(row).to_s
+ def construct(ar_parent, parent, row, rs, seen, model_cache, aliases)
+ primary_id = ar_parent.id
- macro = join_part.reflection.macro
- if macro == :has_one
- return record.association(join_part.reflection.name).target if record.association_cache.key?(join_part.reflection.name)
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- set_target_and_inverse(join_part, association, record)
- else
- association = join_part.instantiate(row) unless row[join_part.aliased_primary_key].nil?
- case macro
- when :has_many, :has_and_belongs_to_many
- other = record.association(join_part.reflection.name)
+ parent.children.each do |node|
+ if node.reflection.collection?
+ other = ar_parent.association(node.reflection.name)
other.loaded!
- other.target.push(association) if association
- other.set_inverse_instance(association)
- when :belongs_to
- set_target_and_inverse(join_part, association, record)
else
- raise ConfigurationError, "unknown macro: #{join_part.reflection.macro}"
+ if ar_parent.association_cache.key?(node.reflection.name)
+ model = ar_parent.association(node.reflection.name).target
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ next
+ end
+ end
+
+ key = aliases.column_alias(node, node.primary_key)
+ id = row[key]
+ next if id.nil?
+
+ model = seen[parent.base_klass][primary_id][node.base_klass][id]
+
+ if model
+ construct(model, node, row, rs, seen, model_cache, aliases)
+ else
+ model = construct_model(ar_parent, node, row, model_cache, id, aliases)
+ seen[parent.base_klass][primary_id][node.base_klass][id] = model
+ construct(model, node, row, rs, seen, model_cache, aliases)
end
end
- association
end
- def set_target_and_inverse(join_part, association, record)
- other = record.association(join_part.reflection.name)
- other.target = association
- other.set_inverse_instance(association)
+ def construct_model(record, node, row, model_cache, id, aliases)
+ model = model_cache[node][id] ||= node.instantiate(row,
+ aliases.column_aliases(node))
+ other = record.association(node.reflection.name)
+
+ if node.reflection.collection?
+ other.target.push(model)
+ else
+ other.target = model
+ end
+
+ other.set_inverse_instance(model)
+ model
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_association.rb b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
index 0d3b4dbab1..191d430636 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_association.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_association.rb
@@ -1,153 +1,117 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinAssociation < JoinPart # :nodoc:
- include JoinHelper
-
# The reflection of the association represented
attr_reader :reflection
- # The JoinDependency object which this JoinAssociation exists within. This is mainly
- # relevant for generating aliases which do not conflict with other joins which are
- # part of the query.
- attr_reader :join_dependency
-
- # A JoinBase instance representing the active record we are joining onto.
- # (So in Author.has_many :posts, the Author would be that base record.)
- attr_reader :parent
-
- # What type of join will be generated, either Arel::InnerJoin (default) or Arel::OuterJoin
- attr_accessor :join_type
-
- # These implement abstract methods from the superclass
- attr_reader :aliased_prefix
-
- attr_reader :tables
-
- delegate :options, :through_reflection, :source_reflection, :chain, :to => :reflection
- delegate :table, :table_name, :to => :parent, :prefix => :parent
- delegate :alias_tracker, :to => :join_dependency
+ attr_accessor :tables
- alias :alias_suffix :parent_table_name
-
- def initialize(reflection, join_dependency, parent = nil)
- reflection.check_validity!
-
- if reflection.options[:polymorphic]
- raise EagerLoadPolymorphicError.new(reflection)
- end
-
- super(reflection.klass)
+ def initialize(reflection, children)
+ super(reflection.klass, children)
@reflection = reflection
- @join_dependency = join_dependency
- @parent = parent
- @join_type = Arel::InnerJoin
- @aliased_prefix = "t#{ join_dependency.join_parts.size }"
- @tables = construct_tables.reverse
+ @tables = nil
end
- def ==(other)
- other.class == self.class &&
- other.reflection == reflection &&
- other.parent == parent
+ def match?(other)
+ return true if self == other
+ super && reflection == other.reflection
end
- def find_parent_in(other_join_dependency)
- other_join_dependency.join_parts.detect do |join_part|
- parent == join_part
- end
- end
+ def join_constraints(foreign_table, foreign_klass, node, join_type, tables, scope_chain, chain)
+ joins = []
+ tables = tables.reverse
- def join_to(relation)
- tables = @tables.dup
- foreign_table = parent_table
- foreign_klass = parent.active_record
+ scope_chain_iter = scope_chain.reverse_each
# The chain starts with the target table, but we want to end with it here (makes
# more sense in this context), so we reverse
- chain.reverse.each_with_index do |reflection, i|
+ chain.reverse_each do |reflection|
table = tables.shift
+ klass = reflection.klass
case reflection.source_macro
when :belongs_to
key = reflection.association_primary_key
foreign_key = reflection.foreign_key
- when :has_and_belongs_to_many
- # Join the join table first...
- relation.from(join(
- table,
- table[reflection.foreign_key].
- eq(foreign_table[reflection.active_record_primary_key])
- ))
-
- foreign_table, table = table, tables.shift
-
- key = reflection.association_primary_key
- foreign_key = reflection.association_foreign_key
else
key = reflection.foreign_key
foreign_key = reflection.active_record_primary_key
end
- constraint = build_constraint(reflection, table, key, foreign_table, foreign_key)
+ constraint = build_constraint(klass, table, key, foreign_table, foreign_key)
- scope_chain_items = scope_chain[i]
+ scope_chain_items = scope_chain_iter.next.map do |item|
+ if item.is_a?(Relation)
+ item
+ else
+ ActiveRecord::Relation.create(klass, table).instance_exec(node, &item)
+ end
+ end
if reflection.type
- scope_chain_items += [
- ActiveRecord::Relation.new(reflection.klass, table)
+ scope_chain_items <<
+ ActiveRecord::Relation.create(klass, table)
.where(reflection.type => foreign_klass.base_class.name)
- ]
end
- scope_chain_items.each do |item|
- unless item.is_a?(Relation)
- item = ActiveRecord::Relation.new(reflection.klass, table).instance_exec(self, &item)
- end
+ scope_chain_items.concat [klass.send(:build_default_scope)].compact
- constraint = constraint.and(item.arel.constraints) unless item.arel.constraints.empty?
+ rel = scope_chain_items.inject(scope_chain_items.shift) do |left, right|
+ left.merge right
end
- relation.from(join(table, constraint))
+ if rel && !rel.arel.constraints.empty?
+ constraint = constraint.and rel.arel.constraints
+ 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, reflection.klass
+ foreign_table, foreign_klass = table, klass
end
- relation
+ joins
end
- def build_constraint(reflection, table, key, foreign_table, foreign_key)
+ # Builds equality condition.
+ #
+ # Example:
+ #
+ # class Physician < ActiveRecord::Base
+ # has_many :appointments
+ # end
+ #
+ # If I execute `Physician.joins(:appointments).to_a` then
+ # reflection #=> #<ActiveRecord::Reflection::AssociationReflection @macro=:has_many ...>
+ # table #=> #<Arel::Table @name="appointments" ...>
+ # key #=> physician_id
+ # foreign_table #=> #<Arel::Table @name="physicians" ...>
+ # foreign_key #=> id
+ #
+ def build_constraint(klass, table, key, foreign_table, foreign_key)
constraint = table[key].eq(foreign_table[foreign_key])
- if reflection.klass.finder_needs_type_condition?
+ if klass.finder_needs_type_condition?
constraint = table.create_and([
constraint,
- reflection.klass.send(:type_condition, table)
+ klass.send(:type_condition, table)
])
end
constraint
end
- def join_relation(joining_relation)
- self.join_type = Arel::OuterJoin
- joining_relation.joins(self)
- end
-
def table
- tables.last
+ tables.first
end
def aliased_table_name
table.table_alias || table.name
end
-
- def scope_chain
- @scope_chain ||= reflection.scope_chain.reverse
- end
-
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_base.rb b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
index 3920e84976..3a26c25737 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_base.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_base.rb
@@ -1,22 +1,20 @@
+require 'active_record/associations/join_dependency/join_part'
+
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
class JoinBase < JoinPart # :nodoc:
- def ==(other)
- other.class == self.class &&
- other.active_record == active_record
- end
-
- def aliased_prefix
- "t0"
+ def match?(other)
+ return true if self == other
+ super && base_klass == other.base_klass
end
def table
- Arel::Table.new(table_name, arel_engine)
+ base_klass.arel_table
end
def aliased_table_name
- active_record.table_name
+ base_klass.table_name
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_dependency/join_part.rb b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
index 711f7b3ce1..91e1c6a9d7 100644
--- a/activerecord/lib/active_record/associations/join_dependency/join_part.rb
+++ b/activerecord/lib/active_record/associations/join_dependency/join_part.rb
@@ -1,41 +1,43 @@
module ActiveRecord
module Associations
class JoinDependency # :nodoc:
- # A JoinPart represents a part of a JoinDependency. It is an abstract class, inherited
+ # A JoinPart represents a part of a JoinDependency. It is inherited
# by JoinBase and JoinAssociation. A JoinBase represents the Active Record which
# everything else is being joined onto. A JoinAssociation represents an association which
# is joining to the base. A JoinAssociation may result in more than one actual join
# operations (for example a has_and_belongs_to_many JoinAssociation would result in
# two; one for the join table and one for the target table).
class JoinPart # :nodoc:
+ include Enumerable
+
# The Active Record class which this join part is associated 'about'; for a JoinBase
# this is the actual base model, for a JoinAssociation this is the target model of the
# association.
- attr_reader :active_record
+ attr_reader :base_klass, :children
- delegate :table_name, :column_names, :primary_key, :reflections, :arel_engine, :to => :active_record
+ delegate :table_name, :column_names, :primary_key, :to => :base_klass
- def initialize(active_record)
- @active_record = active_record
- @cached_record = {}
+ def initialize(base_klass, children)
+ @base_klass = base_klass
@column_names_with_alias = nil
+ @children = children
end
- def aliased_table
- Arel::Nodes::TableAlias.new table, aliased_table_name
+ def name
+ reflection.name
end
- def ==(other)
- raise NotImplementedError
+ def match?(other)
+ self.class == other.class
end
- # An Arel::Table for the active_record
- def table
- raise NotImplementedError
+ def each(&block)
+ yield self
+ children.each { |child| child.each(&block) }
end
- # The prefix to be used when aliasing columns in the active_record's table
- def aliased_prefix
+ # An Arel::Table for the active_record
+ def table
raise NotImplementedError
end
@@ -44,33 +46,25 @@ module ActiveRecord
raise NotImplementedError
end
- # The alias for the primary key of the active_record's table
- def aliased_primary_key
- "#{aliased_prefix}_r0"
- end
+ def extract_record(row, column_names_with_alias)
+ # This code is performance critical as it is called per row.
+ # see: https://github.com/rails/rails/pull/12185
+ hash = {}
- # An array of [column_name, alias] pairs for the table
- def column_names_with_alias
- unless @column_names_with_alias
- @column_names_with_alias = []
+ index = 0
+ length = column_names_with_alias.length
- ([primary_key] + (column_names - [primary_key])).compact.each_with_index do |column_name, i|
- @column_names_with_alias << [column_name, "#{aliased_prefix}_r#{i}"]
- end
+ while index < length
+ column_name, alias_name = column_names_with_alias[index]
+ hash[column_name] = row[alias_name]
+ index += 1
end
- @column_names_with_alias
- end
-
- def extract_record(row)
- Hash[column_names_with_alias.map{|cn, an| [cn, row[an]]}]
- end
- def record_id(row)
- row[aliased_primary_key]
+ hash
end
- def instantiate(row)
- @cached_record[record_id(row)] ||= active_record.send(:instantiate, extract_record(row))
+ def instantiate(row, aliases)
+ base_klass.instantiate(extract_record(row, aliases))
end
end
end
diff --git a/activerecord/lib/active_record/associations/join_helper.rb b/activerecord/lib/active_record/associations/join_helper.rb
index 5a41b40c8f..f345d16841 100644
--- a/activerecord/lib/active_record/associations/join_helper.rb
+++ b/activerecord/lib/active_record/associations/join_helper.rb
@@ -10,21 +10,12 @@ module ActiveRecord
private
def construct_tables
- tables = []
- chain.each do |reflection|
- tables << alias_tracker.aliased_table_for(
+ chain.map do |reflection|
+ alias_tracker.aliased_table_for(
table_name_for(reflection),
table_alias_for(reflection, reflection != self.reflection)
)
-
- if reflection.source_macro == :has_and_belongs_to_many
- tables << alias_tracker.aliased_table_for(
- (reflection.source_reflection || reflection).join_table,
- table_alias_for(reflection, true)
- )
- end
end
- tables
end
def table_name_for(reflection)
diff --git a/activerecord/lib/active_record/associations/preloader.rb b/activerecord/lib/active_record/associations/preloader.rb
index c1cd3a4ae3..2393667ac8 100644
--- a/activerecord/lib/active_record/associations/preloader.rb
+++ b/activerecord/lib/active_record/associations/preloader.rb
@@ -42,12 +42,9 @@ module ActiveRecord
autoload :HasManyThrough, 'active_record/associations/preloader/has_many_through'
autoload :HasOne, 'active_record/associations/preloader/has_one'
autoload :HasOneThrough, 'active_record/associations/preloader/has_one_through'
- autoload :HasAndBelongsToMany, 'active_record/associations/preloader/has_and_belongs_to_many'
autoload :BelongsTo, 'active_record/associations/preloader/belongs_to'
end
- attr_reader :records, :associations, :preload_scope, :model
-
# Eager loads the named associations for the given Active Record record(s).
#
# In this description, 'association name' shall refer to the name passed
@@ -72,7 +69,7 @@ module ActiveRecord
# books.
# - a Hash which specifies multiple association names, as well as
# association names for the to-be-preloaded association objects. For
- # example, specifying <tt>{ :author => :avatar }</tt> will preload a
+ # example, specifying <tt>{ author: :avatar }</tt> will preload a
# book's author, as well as that author's avatar.
#
# +:associations+ has the same format as the +:include+ option for
@@ -80,38 +77,49 @@ module ActiveRecord
#
# :books
# [ :books, :author ]
- # { :author => :avatar }
- # [ :books, { :author => :avatar } ]
- def initialize(records, associations, preload_scope = nil)
- @records = Array.wrap(records).compact.uniq
- @associations = Array.wrap(associations)
- @preload_scope = preload_scope || Relation.new(nil, nil)
- end
+ # { author: :avatar }
+ # [ :books, { author: :avatar } ]
+
+ NULL_RELATION = Struct.new(:values).new({})
+
+ def preload(records, associations, preload_scope = nil)
+ records = Array.wrap(records).compact.uniq
+ associations = Array.wrap(associations)
+ preload_scope = preload_scope || NULL_RELATION
- def run
- unless records.empty?
- associations.each { |association| preload(association) }
+ if records.empty?
+ []
+ else
+ associations.flat_map { |association|
+ preloaders_on association, records, preload_scope
+ }
end
end
private
- def preload(association)
+ def preloaders_on(association, records, scope)
case association
when Hash
- preload_hash(association)
- when String, Symbol
- preload_one(association.to_sym)
+ preloaders_for_hash(association, records, scope)
+ when Symbol
+ preloaders_for_one(association, records, scope)
+ when String
+ preloaders_for_one(association.to_sym, records, scope)
else
raise ArgumentError, "#{association.inspect} was not recognised for preload"
end
end
- def preload_hash(association)
- association.each do |parent, child|
- Preloader.new(records, parent, preload_scope).run
- Preloader.new(records.map { |record| record.send(parent) }.flatten, child).run
- end
+ def preloaders_for_hash(association, records, scope)
+ parent, child = association.to_a.first # hash should only be of length 1
+
+ loaders = preloaders_for_one parent, records, scope
+
+ recs = loaders.flat_map(&:preloaded_records).uniq
+ loaders.concat Array.wrap(child).flat_map { |assoc|
+ preloaders_on assoc, recs, scope
+ }
end
# Not all records have the same class, so group then preload group on the reflection
@@ -121,52 +129,81 @@ module ActiveRecord
# Additionally, polymorphic belongs_to associations can have multiple associated
# classes, depending on the polymorphic_type field. So we group by the classes as
# well.
- def preload_one(association)
- grouped_records(association).each do |reflection, klasses|
- klasses.each do |klass, records|
- preloader_for(reflection).new(klass, records, reflection, preload_scope).run
+ def preloaders_for_one(association, records, scope)
+ grouped_records(association, records).flat_map do |reflection, klasses|
+ klasses.map do |rhs_klass, rs|
+ loader = preloader_for(reflection, rs, rhs_klass).new(rhs_klass, rs, reflection, scope)
+ loader.run self
+ loader
end
end
end
- def grouped_records(association)
- Hash[
- records_by_reflection(association).map do |reflection, records|
- [reflection, records.group_by { |record| association_klass(reflection, record) }]
- end
- ]
+ def grouped_records(association, records)
+ 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)
+ def records_by_reflection(association, records)
records.group_by do |record|
- reflection = record.class.reflections[association]
+ reflection = record.class.reflect_on_association(association)
- unless reflection
- raise ActiveRecord::ConfigurationError, "Association named '#{association}' was not found; " \
- "perhaps you misspelled it?"
- end
-
- reflection
+ 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.send(reflection.foreign_type)
+ klass = record.read_attribute(reflection.foreign_type.to_s)
klass && klass.constantize
else
reflection.klass
end
end
- def preloader_for(reflection)
+ class AlreadyLoaded
+ attr_reader :owners, :reflection
+
+ def initialize(klass, owners, reflection, preload_scope)
+ @owners = owners
+ @reflection = reflection
+ end
+
+ def run(preloader); end
+
+ def preloaded_records
+ owners.flat_map { |owner| owner.read_attribute reflection.name }
+ end
+ end
+
+ class NullPreloader
+ def self.new(klass, owners, reflection, preload_scope); self; end
+ def self.run(preloader); end
+ end
+
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader unless rhs_klass
+
+ if owners.first.association(reflection.name).loaded?
+ return AlreadyLoaded
+ end
+
case reflection.macro
when :has_many
reflection.options[:through] ? HasManyThrough : HasMany
when :has_one
reflection.options[:through] ? HasOneThrough : HasOne
- when :has_and_belongs_to_many
- HasAndBelongsToMany
when :belongs_to
BelongsTo
end
diff --git a/activerecord/lib/active_record/associations/preloader/association.rb b/activerecord/lib/active_record/associations/preloader/association.rb
index cbf5e734ea..69b65982b3 100644
--- a/activerecord/lib/active_record/associations/preloader/association.rb
+++ b/activerecord/lib/active_record/associations/preloader/association.rb
@@ -3,6 +3,7 @@ module ActiveRecord
class Preloader
class Association #:nodoc:
attr_reader :owners, :reflection, :preload_scope, :model, :klass
+ attr_reader :preloaded_records
def initialize(klass, owners, reflection, preload_scope)
@klass = klass
@@ -12,15 +13,14 @@ module ActiveRecord
@model = owners.first && owners.first.class
@scope = nil
@owners_by_key = nil
+ @preloaded_records = []
end
- def run
- unless owners.first.association(reflection.name).loaded?
- preload
- end
+ def run(preloader)
+ preload(preloader)
end
- def preload
+ def preload(preloader)
raise NotImplementedError
end
@@ -29,6 +29,10 @@ module ActiveRecord
end
def records_for(ids)
+ query_scope(ids)
+ end
+
+ def query_scope(ids)
scope.where(association_key.in(ids))
end
@@ -52,12 +56,9 @@ module ActiveRecord
raise NotImplementedError
end
- # We're converting to a string here because postgres will return the aliased association
- # key in a habtm as a string (for whatever reason)
def owners_by_key
@owners_by_key ||= owners.group_by do |owner|
- key = owner[owner_key_name]
- key && key.to_s
+ owner[owner_key_name]
end
end
@@ -67,38 +68,47 @@ module ActiveRecord
private
- def associated_records_by_owner
+ def associated_records_by_owner(preloader)
owners_map = owners_by_key
owner_keys = owners_map.keys.compact
- if klass.nil? || owner_keys.empty?
- records = []
- else
- # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
- # Make several smaller queries if necessary or make one query if the adapter supports it
- sliced = owner_keys.each_slice(model.connection.in_clause_length || owner_keys.size)
- records = sliced.map { |slice| records_for(slice).to_a }.flatten
+ # Each record may have multiple owners, and vice-versa
+ records_by_owner = owners.each_with_object({}) do |owner,h|
+ h[owner] = []
end
- # Each record may have multiple owners, and vice-versa
- records_by_owner = Hash[owners.map { |owner| [owner, []] }]
- records.each do |record|
- owner_key = record[association_key_name].to_s
+ if owner_keys.any?
+ # Some databases impose a limit on the number of ids in a list (in Oracle it's 1000)
+ # Make several smaller queries if necessary or make one query if the adapter supports it
+ sliced = owner_keys.each_slice(klass.connection.in_clause_length || owner_keys.size)
- owners_map[owner_key].each do |owner|
- records_by_owner[owner] << record
+ records = load_slices sliced
+ records.each do |record, owner_key|
+ owners_map[owner_key].each do |owner|
+ records_by_owner[owner] << record
+ end
end
end
+
records_by_owner
end
+ def load_slices(slices)
+ @preloaded_records = slices.flat_map { |slice|
+ records_for(slice)
+ }
+
+ @preloaded_records.map { |record|
+ [record, record[association_key_name]]
+ }
+ end
+
def reflection_scope
@reflection_scope ||= reflection.scope ? klass.unscoped.instance_exec(nil, &reflection.scope) : klass.unscoped
end
def build_scope
scope = klass.unscoped
- scope.default_scoped = true
values = reflection_scope.values
preload_values = preload_scope.values
@@ -109,11 +119,19 @@ module ActiveRecord
scope.select! preload_values[:select] || values[:select] || table[Arel.star]
scope.includes! preload_values[:includes] || values[:includes]
+ if preload_values.key? :order
+ scope.order! preload_values[:order]
+ else
+ if values.key? :order
+ scope.order! values[:order]
+ end
+ end
+
if options[:as]
scope.where!(klass.table_name => { reflection.type => model.base_class.sti_name })
end
- scope
+ klass.default_scoped.merge(scope)
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/collection_association.rb b/activerecord/lib/active_record/associations/preloader/collection_association.rb
index e6cd35e7a1..5adffcd831 100644
--- a/activerecord/lib/active_record/associations/preloader/collection_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/collection_association.rb
@@ -9,8 +9,8 @@ module ActiveRecord
super.order(preload_scope.values[:order] || reflection_scope.values[:order])
end
- def preload
- associated_records_by_owner.each do |owner, records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, records|
association = owner.association(reflection.name)
association.loaded!
association.target.concat(records)
diff --git a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb b/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
deleted file mode 100644
index 8e8925f0a9..0000000000
--- a/activerecord/lib/active_record/associations/preloader/has_and_belongs_to_many.rb
+++ /dev/null
@@ -1,60 +0,0 @@
-module ActiveRecord
- module Associations
- class Preloader
- class HasAndBelongsToMany < CollectionAssociation #:nodoc:
- attr_reader :join_table
-
- def initialize(klass, records, reflection, preload_options)
- super
- @join_table = Arel::Table.new(reflection.join_table).alias('t0')
- end
-
- # Unlike the other associations, we want to get a raw array of rows so that we can
- # access the aliased column on the join table
- def records_for(ids)
- scope = super
- klass.connection.select_all(scope.arel, 'SQL', scope.bind_values)
- end
-
- def owner_key_name
- reflection.active_record_primary_key
- end
-
- def association_key_name
- 'ar_association_key_name'
- end
-
- def association_key
- join_table[reflection.foreign_key]
- end
-
- private
-
- # Once we have used the join table column (in super), we manually instantiate the
- # actual records, ensuring that we don't create more than one instances of the same
- # record
- def associated_records_by_owner
- records = {}
- super.each do |owner_key, rows|
- rows.map! { |row| records[row[klass.primary_key]] ||= klass.instantiate(row) }
- end
- end
-
- def build_scope
- super.joins(join).select(join_select)
- end
-
- def join_select
- association_key.as(Arel.sql(association_key_name))
- end
-
- def join
- condition = table[reflection.association_primary_key].eq(
- join_table[reflection.association_foreign_key])
-
- table.create_join(join_table, table.create_on(condition))
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/associations/preloader/has_many_through.rb b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
index 9a662d3f53..7b37b5942d 100644
--- a/activerecord/lib/active_record/associations/preloader/has_many_through.rb
+++ b/activerecord/lib/active_record/associations/preloader/has_many_through.rb
@@ -4,10 +4,14 @@ module ActiveRecord
class HasManyThrough < CollectionAssociation #:nodoc:
include ThroughAssociation
- def associated_records_by_owner
- super.each do |owner, records|
- records.uniq! if reflection_scope.uniq_value
+ def associated_records_by_owner(preloader)
+ records_by_owner = super
+
+ if reflection_scope.distinct_value
+ records_by_owner.each_value { |records| records.uniq! }
end
+
+ records_by_owner
end
end
end
diff --git a/activerecord/lib/active_record/associations/preloader/singular_association.rb b/activerecord/lib/active_record/associations/preloader/singular_association.rb
index 44e804d785..2b5cfda8ce 100644
--- a/activerecord/lib/active_record/associations/preloader/singular_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/singular_association.rb
@@ -5,8 +5,8 @@ module ActiveRecord
private
- def preload
- associated_records_by_owner.each do |owner, associated_records|
+ def preload(preloader)
+ associated_records_by_owner(preloader).each do |owner, associated_records|
record = associated_records.first
association = owner.association(reflection.name)
diff --git a/activerecord/lib/active_record/associations/preloader/through_association.rb b/activerecord/lib/active_record/associations/preloader/through_association.rb
index 1c1ba11c44..2a8530af62 100644
--- a/activerecord/lib/active_record/associations/preloader/through_association.rb
+++ b/activerecord/lib/active_record/associations/preloader/through_association.rb
@@ -2,7 +2,6 @@ module ActiveRecord
module Associations
class Preloader
module ThroughAssociation #:nodoc:
-
def through_reflection
reflection.through_reflection
end
@@ -11,50 +10,84 @@ module ActiveRecord
reflection.source_reflection
end
- def associated_records_by_owner
- through_records = through_records_by_owner
+ def associated_records_by_owner(preloader)
+ preloader.preload(owners,
+ through_reflection.name,
+ through_scope)
- Preloader.new(through_records.values.flatten, source_reflection.name, reflection_scope).run
+ through_records = owners.map do |owner|
+ association = owner.association through_reflection.name
- through_records.each do |owner, records|
- records.map! { |r| r.send(source_reflection.name) }.flatten!
- records.compact!
+ [owner, Array(association.reader)]
end
- end
- private
+ reset_association owners, through_reflection.name
+
+ middle_records = through_records.map { |(_,rec)| rec }.flatten
+
+ preloaders = preloader.preload(middle_records,
+ source_reflection.name,
+ reflection_scope)
- def through_records_by_owner
- Preloader.new(owners, through_reflection.name, through_scope).run
+ @preloaded_records = preloaders.flat_map(&:preloaded_records)
+
+ middle_to_pl = preloaders.each_with_object({}) do |pl,h|
+ pl.owners.each { |middle|
+ h[middle] = pl
+ }
+ end
+
+ record_offset = {}
+ @preloaded_records.each_with_index do |record,i|
+ record_offset[record] = i
+ end
- Hash[owners.map do |owner|
- through_records = Array.wrap(owner.send(through_reflection.name))
+ through_records.each_with_object({}) { |(lhs,center),records_by_owner|
+ pl_to_middle = center.group_by { |record| middle_to_pl[record] }
- # Dont cache the association - we would only be caching a subset
- if reflection.options[:source_type] && through_reflection.collection?
- owner.association(through_reflection.name).reset
+ records_by_owner[lhs] = pl_to_middle.flat_map do |pl, middles|
+ rhs_records = middles.flat_map { |r|
+ association = r.association source_reflection.name
+
+ association.reader
+ }.compact
+
+ rhs_records.sort_by { |rhs| record_offset[rhs] }
end
+ }
+ end
+
+ private
+
+ def reset_association(owners, association_name)
+ should_reset = (through_scope != through_reflection.klass.unscoped) ||
+ (reflection.options[:source_type] && through_reflection.collection?)
- [owner, through_records]
- end]
+ # Dont cache the association - we would only be caching a subset
+ if should_reset
+ owners.each { |owner|
+ owner.association(association_name).reset
+ }
+ end
end
+
def through_scope
- through_scope = through_reflection.klass.unscoped
+ scope = through_reflection.klass.unscoped
if options[:source_type]
- through_scope.where! reflection.foreign_type => options[:source_type]
+ scope.where! reflection.foreign_type => options[:source_type]
else
unless reflection_scope.where_values.empty?
- through_scope.includes_values = reflection_scope.values[:includes] || options[:source]
- through_scope.where_values = reflection_scope.values[:where]
+ scope.includes_values = Array(reflection_scope.values[:includes] || options[:source])
+ scope.where_values = reflection_scope.values[:where]
end
- through_scope.order! reflection_scope.values[:order]
- through_scope.references! reflection_scope.values[:references]
+ scope.references! reflection_scope.values[:references]
+ scope.order! reflection_scope.values[:order] if scope.eager_loading?
end
- through_scope
+ scope
end
end
end
diff --git a/activerecord/lib/active_record/associations/singular_association.rb b/activerecord/lib/active_record/associations/singular_association.rb
index 32f4557c28..02dc464536 100644
--- a/activerecord/lib/active_record/associations/singular_association.rb
+++ b/activerecord/lib/active_record/associations/singular_association.rb
@@ -12,7 +12,7 @@ module ActiveRecord
target
end
- # Implements the writer method, e.g. foo.items= for Foo.has_many :items
+ # Implements the writer method, e.g. foo.bar= for Foo.belongs_to :bar
def writer(record)
replace(record)
end
@@ -42,7 +42,6 @@ module ActiveRecord
scope.first.tap { |record| set_inverse_instance(record) }
end
- # Implemented by subclasses
def replace(record)
raise NotImplementedError, "Subclasses must implement a replace(record) method"
end
diff --git a/activerecord/lib/active_record/associations/through_association.rb b/activerecord/lib/active_record/associations/through_association.rb
index b9e014735b..ba7d2a3782 100644
--- a/activerecord/lib/active_record/associations/through_association.rb
+++ b/activerecord/lib/active_record/associations/through_association.rb
@@ -13,9 +13,9 @@ module ActiveRecord
# 2. To get the type conditions for any STI models in the chain
def target_scope
scope = super
- chain[1..-1].each do |reflection|
- scope = scope.merge(
- reflection.klass.all.with_default_scope.
+ chain.drop(1).each do |reflection|
+ scope.merge!(
+ reflection.klass.all.
except(:select, :create_with, :includes, :preload, :joins, :eager_load)
)
end
@@ -28,7 +28,7 @@ module ActiveRecord
# methods which create and delete records on the association.
#
# We only support indirectly modifying through associations which has a belongs_to source.
- # This is the "has_many :tags, :through => :taggings" situation, where the join model
+ # This is the "has_many :tags, through: :taggings" situation, where the join model
# typically has a belongs_to on both side. In other words, associations which could also
# be represented as has_and_belongs_to_many associations.
#
diff --git a/activerecord/lib/active_record/attribute_assignment.rb b/activerecord/lib/active_record/attribute_assignment.rb
index af13b75a9d..30fa2c8ba5 100644
--- a/activerecord/lib/active_record/attribute_assignment.rb
+++ b/activerecord/lib/active_record/attribute_assignment.rb
@@ -1,8 +1,8 @@
+require 'active_model/forbidden_attributes_protection'
module ActiveRecord
module AttributeAssignment
extend ActiveSupport::Concern
- include ActiveModel::DeprecatedMassAssignmentSecurity
include ActiveModel::ForbiddenAttributesProtection
# Allows you to set all the attributes by passing in a hash of attributes with
@@ -12,6 +12,9 @@ module ActiveRecord
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
# exception is raised.
def assign_attributes(new_attributes)
+ if !new_attributes.respond_to?(:stringify_keys)
+ raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
+ end
return if new_attributes.blank?
attributes = new_attributes.stringify_keys
@@ -44,7 +47,7 @@ module ActiveRecord
if respond_to?("#{k}=")
raise
else
- raise UnknownAttributeError, "unknown attribute: #{k}"
+ raise UnknownAttributeError.new(self, k)
end
end
@@ -57,9 +60,8 @@ module ActiveRecord
# by calling new on the column type or aggregation type (through composed_of) object with these parameters.
# So having the pairs written_on(1) = "2004", written_on(2) = "6", written_on(3) = "24", will instantiate
# written_on (a date type) with Date.new("2004", "6", "24"). You can also specify a typecast character in the
- # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum,
- # f for Float, s for String, and a for Array. If all the values for a given attribute are empty, the
- # attribute will be set to +nil+.
+ # parentheses to have the parameters typecasted before they're used in the constructor. Use i for Fixnum and
+ # f for Float. If all the values for a given attribute are empty, the attribute will be set to +nil+.
def assign_multiparameter_attributes(pairs)
execute_callstack_for_multiparameter_attributes(
extract_callstack_for_multiparameter_attributes(pairs)
@@ -82,7 +84,7 @@ module ActiveRecord
end
def extract_callstack_for_multiparameter_attributes(pairs)
- attributes = { }
+ attributes = {}
pairs.each do |(multiparameter_name, value)|
attribute_name = multiparameter_name.split("(").first
@@ -133,7 +135,7 @@ module ActiveRecord
if object.class.send(:create_time_zone_conversion_attribute?, name, column)
Time.zone.local(*set_values)
else
- Time.time_with_datetime_fallback(object.class.default_timezone, *set_values)
+ Time.send(object.class.default_timezone, *set_values)
end
end
@@ -147,7 +149,7 @@ module ActiveRecord
end
else
# else column is a timestamp, so if Date bits were not provided, error
- validate_missing_parameters!([1,2,3])
+ validate_required_parameters!([1,2,3])
# If Date bits were provided but blank, then return nil
return if blank_date_parameter?
@@ -173,14 +175,14 @@ module ActiveRecord
def read_other(klass)
max_position = extract_max_param
positions = (1..max_position)
- validate_missing_parameters!(positions)
+ validate_required_parameters!(positions)
set_values = values.values_at(*positions)
klass.new(*set_values)
end
# Checks whether some blank date parameter exists. Note that this is different
- # than the validate_missing_parameters! method, since it just checks for blank
+ # than the validate_required_parameters! method, since it just checks for blank
# positions instead of missing ones, and does not raise in case one blank position
# exists. The caller is responsible to handle the case of this returning true.
def blank_date_parameter?
@@ -188,7 +190,7 @@ module ActiveRecord
end
# If some position is not provided, it errors out a missing parameter exception.
- def validate_missing_parameters!(positions)
+ def validate_required_parameters!(positions)
if missing_parameter = positions.detect { |position| !values.key?(position) }
raise ArgumentError.new("Missing Parameter - #{name}(#{missing_parameter})")
end
diff --git a/activerecord/lib/active_record/attribute_methods.rb b/activerecord/lib/active_record/attribute_methods.rb
index 0aff2562b8..3924eec872 100644
--- a/activerecord/lib/active_record/attribute_methods.rb
+++ b/activerecord/lib/active_record/attribute_methods.rb
@@ -1,12 +1,15 @@
require 'active_support/core_ext/enumerable'
+require 'mutex_m'
+require 'thread_safe'
module ActiveRecord
# = Active Record Attribute Methods
- module AttributeMethods #:nodoc:
+ module AttributeMethods
extend ActiveSupport::Concern
include ActiveModel::AttributeMethods
included do
+ initialize_generated_modules
include Read
include Write
include BeforeTypeCast
@@ -17,35 +20,88 @@ module ActiveRecord
include Serialization
end
+ AttrNames = Module.new {
+ def self.set_name_cache(name, value)
+ const_name = "ATTR_#{name}"
+ unless const_defined? const_name
+ const_set const_name, value.dup.freeze
+ end
+ end
+ }
+
+ class AttributeMethodCache
+ def initialize
+ @module = Module.new
+ @method_cache = ThreadSafe::Cache.new
+ end
+
+ def [](name)
+ @method_cache.compute_if_absent(name) do
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+ @module.module_eval method_body(temp_method, safe_name), __FILE__, __LINE__
+ @module.instance_method temp_method
+ end
+ end
+
+ private
+ def method_body; raise NotImplementedError; end
+ end
+
module ClassMethods
+ def inherited(child_class) #:nodoc:
+ child_class.initialize_generated_modules
+ super
+ end
+
+ def initialize_generated_modules # :nodoc:
+ @generated_attribute_methods = Module.new { extend Mutex_m }
+ @attribute_methods_generated = false
+ include @generated_attribute_methods
+ end
+
# Generates all the attribute related methods for columns in the database
# accessors, mutators and query methods.
- def define_attribute_methods
- # Use a mutex; we don't want two thread simaltaneously trying to define
+ def define_attribute_methods # :nodoc:
+ # Use a mutex; we don't want two thread simultaneously trying to define
# attribute methods.
- @attribute_methods_mutex.synchronize do
- return if attribute_methods_generated?
+ generated_attribute_methods.synchronize do
+ return false if @attribute_methods_generated
superclass.define_attribute_methods unless self == base_class
super(column_names)
@attribute_methods_generated = true
end
+ true
end
- def attribute_methods_generated?
- @attribute_methods_generated ||= false
- end
-
- def undefine_attribute_methods
- super if attribute_methods_generated?
- @attribute_methods_generated = false
+ def undefine_attribute_methods # :nodoc:
+ generated_attribute_methods.synchronize do
+ super if @attribute_methods_generated
+ @attribute_methods_generated = false
+ end
end
+ # Raises a <tt>ActiveRecord::DangerousAttributeError</tt> exception when an
+ # \Active \Record method is defined in the model, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # def save
+ # 'already defined by Active Record'
+ # end
+ # end
+ #
+ # Person.instance_method_already_implemented?(:save)
+ # # => ActiveRecord::DangerousAttributeError: save is defined by ActiveRecord
+ #
+ # Person.instance_method_already_implemented?(:name)
+ # # => false
def instance_method_already_implemented?(method_name)
if dangerous_attribute_method?(method_name)
- raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord"
+ raise DangerousAttributeError, "#{method_name} is defined by Active Record"
end
- if [Base, Model].include?(active_record_super)
+ if superclass == Base
super
else
# If B < A and A defines its own attribute method, then we don't want to overwrite that.
@@ -56,11 +112,11 @@ module ActiveRecord
# A method name is 'dangerous' if it is already defined by Active Record, but
# not by any ancestors. (So 'puts' is not dangerous but 'save' is.)
- def dangerous_attribute_method?(name)
+ def dangerous_attribute_method?(name) # :nodoc:
method_defined_within?(name, Base)
end
- def method_defined_within?(name, klass, sup = klass.superclass)
+ def method_defined_within?(name, klass, sup = klass.superclass) # :nodoc:
if klass.method_defined?(name) || klass.private_method_defined?(name)
if sup.method_defined?(name) || sup.private_method_defined?(name)
klass.instance_method(name).owner != sup.instance_method(name).owner
@@ -72,13 +128,27 @@ module ActiveRecord
end
end
+ # Returns +true+ if +attribute+ is an attribute method and table exists,
+ # +false+ otherwise.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_method?('name') # => true
+ # Person.attribute_method?(:age=) # => true
+ # Person.attribute_method?(:nothing) # => false
def attribute_method?(attribute)
super || (table_exists? && column_names.include?(attribute.to_s.sub(/=$/, '')))
end
- # Returns an array of column names as strings if it's not
- # an abstract class and table exists.
- # Otherwise it returns an empty array.
+ # Returns an array of column names as strings if it's not an abstract class and
+ # table exists. Otherwise it returns an empty array.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # Person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attribute_names ||= if !abstract_class? && table_exists?
column_names
@@ -90,50 +160,83 @@ module ActiveRecord
# If we haven't generated any methods yet, generate them, then
# see if we've created the method we're looking for.
- def method_missing(method, *args, &block)
- unless self.class.attribute_methods_generated?
- self.class.define_attribute_methods
-
- if respond_to_without_attributes?(method)
- send(method, *args, &block)
- else
- super
- end
+ def method_missing(method, *args, &block) # :nodoc:
+ self.class.define_attribute_methods
+ if respond_to_without_attributes?(method)
+ send(method, *args, &block)
else
super
end
end
- def attribute_missing(match, *args, &block)
- if self.class.columns_hash[match.attr_name]
- ActiveSupport::Deprecation.warn(
- "The method `#{match.method_name}', matching the attribute `#{match.attr_name}' has " \
- "dispatched through method_missing. This shouldn't happen, because `#{match.attr_name}' " \
- "is a column of the table. If this error has happened through normal usage of Active " \
- "Record (rather than through your own code or external libraries), please report it as " \
- "a bug."
- )
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
+ # which will all return +true+. It also define the attribute methods if they have
+ # not been generated.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.respond_to(:name) # => true
+ # person.respond_to(:name=) # => true
+ # person.respond_to(:name?) # => true
+ # person.respond_to('age') # => true
+ # person.respond_to('age=') # => true
+ # person.respond_to('age?') # => true
+ # person.respond_to(:nothing) # => false
+ def respond_to?(name, include_private = false)
+ 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)
+ return has_attribute?(name)
end
- super
+ return true
end
- def respond_to?(name, include_private = false)
- self.class.define_attribute_methods unless self.class.attribute_methods_generated?
- super
- end
-
- # Returns true if the given attribute is in the attributes hash
+ # Returns +true+ if the given attribute is in the attributes hash, otherwise +false+.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.has_attribute?(:name) # => true
+ # person.has_attribute?('age') # => true
+ # person.has_attribute?(:nothing) # => false
def has_attribute?(attr_name)
@attributes.has_key?(attr_name.to_s)
end
# Returns an array of names for the attributes available on this object.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person.attribute_names
+ # # => ["id", "created_at", "updated_at", "name", "age"]
def attribute_names
@attributes.keys
end
# Returns a hash of all the attributes with their names as keys and the values of the attributes as values.
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.create(name: 'Francesco', age: 22)
+ # 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)
@@ -142,87 +245,141 @@ module ActiveRecord
# Returns an <tt>#inspect</tt>-like string for the value of the
# attribute +attr_name+. String attributes are truncated upto 50
- # characters, and Date and Time attributes are returned in the
- # <tt>:db</tt> format. Other attributes return the value of
- # <tt>#inspect</tt> without modification.
+ # characters, Date and Time attributes are returned in the
+ # <tt>:db</tt> format, Array attributes are truncated upto 10 values.
+ # Other attributes return the value of <tt>#inspect</tt> without
+ # modification.
#
- # person = Person.create!(:name => "David Heinemeier Hansson " * 3)
+ # person = Person.create!(name: 'David Heinemeier Hansson ' * 3)
#
# person.attribute_for_inspect(:name)
- # # => '"David Heinemeier Hansson David Heinemeier Hansson D..."'
+ # # => "\"David Heinemeier Hansson David Heinemeier Hansson ...\""
#
# person.attribute_for_inspect(:created_at)
- # # => '"2009-01-12 04:48:57"'
+ # # => "\"2012-10-22 00:15:07\""
+ #
+ # person.attribute_for_inspect(:tag_ids)
+ # # => "[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]"
def attribute_for_inspect(attr_name)
value = read_attribute(attr_name)
if value.is_a?(String) && value.length > 50
- "#{value[0..50]}...".inspect
+ "#{value[0, 50]}...".inspect
elsif value.is_a?(Date) || value.is_a?(Time)
%("#{value.to_s(:db)}")
+ elsif value.is_a?(Array) && value.size > 10
+ inspected = value.first(10).inspect
+ %(#{inspected[0...-1]}, ...])
else
value.inspect
end
end
- # Returns true if the specified +attribute+ has been set by the user or by a database load and is neither
- # nil nor empty? (the latter only applies to objects that respond to empty?, most notably Strings).
+ # Returns +true+ if the specified +attribute+ has been set by the user or by a
+ # database load and is neither +nil+ nor <tt>empty?</tt> (the latter only applies
+ # to objects that respond to <tt>empty?</tt>, most notably Strings). Otherwise, +false+.
+ # Note that it always returns +true+ with boolean attributes.
+ #
+ # 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
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 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 data column is cast to a date object, like Date.new(2004, 12, 12)).
- # (Alias for the protected read_attribute method).
+ # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)). It raises
+ # <tt>ActiveModel::MissingAttributeError</tt> if the identified attribute is missing.
+ #
+ # Alias for the <tt>read_attribute</tt> method.
+ #
+ # class Person < ActiveRecord::Base
+ # belongs_to :organization
+ # end
+ #
+ # person = Person.new(name: 'Francesco', age: '22')
+ # person[:name] # => "Francesco"
+ # person[:age] # => 22
+ #
+ # person = Person.select('id').first
+ # person[:name] # => ActiveModel::MissingAttributeError: missing attribute: name
+ # person[:organization_id] # => ActiveModel::MissingAttributeError: missing attribute: organization_id
def [](attr_name)
- read_attribute(attr_name)
+ read_attribute(attr_name) { |n| missing_attribute(n, caller) }
end
# Updates the attribute identified by <tt>attr_name</tt> with the specified +value+.
- # (Alias for the protected write_attribute method).
+ # (Alias for the protected <tt>write_attribute</tt> method).
+ #
+ # class Person < ActiveRecord::Base
+ # end
+ #
+ # person = Person.new
+ # person[:age] = '22'
+ # person[:age] # => 22
+ # person[:age] # => Fixnum
def []=(attr_name, value)
write_attribute(attr_name, value)
end
protected
- def clone_attributes(reader_method = :read_attribute, attributes = {})
+ 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)
+ def clone_attribute_value(reader_method, attribute_name) # :nodoc:
value = send(reader_method, attribute_name)
value.duplicable? ? value.clone : value
rescue TypeError, NoMethodError
value
end
- def arel_attributes_with_values_for_create(attribute_names)
+ def arel_attributes_with_values_for_create(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_create(attribute_names))
end
- def arel_attributes_with_values_for_update(attribute_names)
+ def arel_attributes_with_values_for_update(attribute_names) # :nodoc:
arel_attributes_with_values(attributes_for_update(attribute_names))
end
- def attribute_method?(attr_name)
+ def attribute_method?(attr_name) # :nodoc:
+ # We check defined? because Syck calls respond_to? before actually calling initialize.
defined?(@attributes) && @attributes.include?(attr_name)
end
private
# Returns a Hash of the Arel::Attributes and attribute values that have been
- # type casted for use in an Arel insert/update method.
+ # typecasted for use in an Arel insert/update method.
def arel_attributes_with_values(attribute_names)
attrs = {}
arel_table = self.class.arel_table
@@ -236,7 +393,7 @@ 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) && !pk_attribute?(name) && !readonly_attribute?(name)
+ column_for_attribute(name) && !readonly_attribute?(name)
end
end
@@ -257,14 +414,10 @@ module ActiveRecord
end
def typecasted_attribute_value(name)
- if self.class.serialized_attributes.include?(name)
- @attributes[name].serialized_value
- else
- # 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
+ # 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
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 d4f529acbf..f596a8b02e 100644
--- a/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
+++ b/activerecord/lib/active_record/attribute_methods/before_type_cast.rb
@@ -1,5 +1,28 @@
module ActiveRecord
module AttributeMethods
+ # = Active Record Attribute Methods Before Type Cast
+ #
+ # <tt>ActiveRecord::AttributeMethods::BeforeTypeCast</tt> provides a way to
+ # read the value of the attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.id # => 1
+ # task.completed_on # => Sun, 21 Oct 2012
+ #
+ # task.attributes_before_type_cast
+ # # => {"id"=>"1", "completed_on"=>"2012-10-21", ... }
+ # task.read_attribute_before_type_cast('id') # => "1"
+ # task.read_attribute_before_type_cast('completed_on') # => "2012-10-21"
+ #
+ # In addition to #read_attribute_before_type_cast and #attributes_before_type_cast,
+ # it declares a method for all attributes with the <tt>*_before_type_cast</tt>
+ # suffix.
+ #
+ # task.id_before_type_cast # => "1"
+ # task.completed_on_before_type_cast # => "2012-10-21"
module BeforeTypeCast
extend ActiveSupport::Concern
@@ -7,11 +30,32 @@ module ActiveRecord
attribute_method_suffix "_before_type_cast"
end
+ # Returns the value of the attribute identified by +attr_name+ before
+ # typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(id: '1', completed_on: '2012-10-21')
+ # task.read_attribute('id') # => 1
+ # task.read_attribute_before_type_cast('id') # => '1'
+ # task.read_attribute('completed_on') # => Sun, 21 Oct 2012
+ # 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]
+ @attributes[attr_name.to_s]
end
# Returns a hash of attributes before typecasting and deserialization.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # task = Task.new(title: nil, is_done: true, completed_on: '2012-10-21')
+ # task.attributes
+ # # => {"id"=>nil, "title"=>nil, "is_done"=>true, "completed_on"=>Sun, 21 Oct 2012, "created_at"=>nil, "updated_at"=>nil}
+ # 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
end
diff --git a/activerecord/lib/active_record/attribute_methods/dirty.rb b/activerecord/lib/active_record/attribute_methods/dirty.rb
index 7a5bb9e863..19e81abba5 100644
--- a/activerecord/lib/active_record/attribute_methods/dirty.rb
+++ b/activerecord/lib/active_record/attribute_methods/dirty.rb
@@ -1,11 +1,6 @@
require 'active_support/core_ext/module/attribute_accessors'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :partial_updates, instance_accessor: false
- self.partial_updates = true
- end
-
module AttributeMethods
module Dirty # :nodoc:
extend ActiveSupport::Concern
@@ -17,14 +12,14 @@ module ActiveRecord
raise "You cannot include Dirty after Timestamp"
end
- config_attribute :partial_updates
+ class_attribute :partial_writes, instance_writer: false
+ self.partial_writes = true
end
# Attempts to +save+ the record and clears changed attributes if successful.
def save(*)
if status = super
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
status
end
@@ -32,16 +27,14 @@ module ActiveRecord
# Attempts to <tt>save!</tt> the record and clears changed attributes if successful.
def save!(*)
super.tap do
- @previously_changed = changes
- @changed_attributes.clear
+ changes_applied
end
end
# <tt>reload</tt> the record and clears changed attributes.
def reload(*)
super.tap do
- @previously_changed.clear
- @changed_attributes.clear
+ reset_changes
end
end
@@ -52,38 +45,28 @@ module ActiveRecord
# The attribute already has an unsaved change.
if attribute_changed?(attr)
- old = @changed_attributes[attr]
- @changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
+ old = changed_attributes[attr]
+ changed_attributes.delete(attr) unless _field_changed?(attr, old, value)
else
old = clone_attribute_value(:read_attribute, attr)
- @changed_attributes[attr] = old if _field_changed?(attr, old, value)
+ changed_attributes[attr] = old if _field_changed?(attr, old, value)
end
# Carry on.
super(attr, value)
end
- def update(*)
- partial_updates? ? super(keys_for_partial_update) : super
+ def update_record(*)
+ partial_writes? ? super(keys_for_partial_write) : super
end
- def create(*)
- if partial_updates?
- keys = keys_for_partial_update
-
- # This is an extremely bloody annoying necessity to work around mysql being crap.
- # See test_mysql_text_not_null_defaults
- keys.concat self.class.columns.select(&:explicit_default?).map(&:name)
-
- super keys
- else
- super
- end
+ def create_record(*)
+ partial_writes? ? super(keys_for_partial_write) : super
end
# Serialized attributes should always be written in case they've been
# changed in place.
- def keys_for_partial_update
+ def keys_for_partial_write
changed | (attributes.keys & self.class.serialized_attributes.keys)
end
@@ -110,7 +93,11 @@ module ActiveRecord
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? && value != '0'
+ old == 0 && value.is_a?(String) && value.present? && non_zero?(value)
+ end
+
+ def non_zero?(value)
+ value !~ /\A0+(\.0+)?\z/
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 0857b02c8e..931209b07b 100644
--- a/activerecord/lib/active_record/attribute_methods/primary_key.rb
+++ b/activerecord/lib/active_record/attribute_methods/primary_key.rb
@@ -8,27 +8,32 @@ module ActiveRecord
# Returns this record's primary key value wrapped in an Array if one is
# available.
def to_key
+ sync_with_transaction_state
key = self.id
[key] if key
end
# Returns the primary key value.
def id
+ sync_with_transaction_state
read_attribute(self.class.primary_key)
end
# Sets the primary key value.
def id=(value)
+ sync_with_transaction_state
write_attribute(self.class.primary_key, value) if self.class.primary_key
end
# Queries the primary key value.
def id?
+ sync_with_transaction_state
query_attribute(self.class.primary_key)
end
# Returns the primary key value before type cast.
def id_before_type_cast
+ sync_with_transaction_state
read_attribute_before_type_cast(self.class.primary_key)
end
@@ -76,7 +81,7 @@ module ActiveRecord
end
def get_primary_key(base_name) #:nodoc:
- return 'id' unless base_name && !base_name.blank?
+ return 'id' if base_name.blank?
case primary_key_prefix_type
when :table_name
@@ -85,7 +90,7 @@ module ActiveRecord
base_name.foreign_key
else
if ActiveRecord::Base != self && table_exists?
- connection.schema_cache.primary_keys[table_name]
+ connection.schema_cache.primary_keys(table_name)
else
'id'
end
diff --git a/activerecord/lib/active_record/attribute_methods/read.rb b/activerecord/lib/active_record/attribute_methods/read.rb
index 46fd6ebfb3..c152a246b5 100644
--- a/activerecord/lib/active_record/attribute_methods/read.rb
+++ b/activerecord/lib/active_record/attribute_methods/read.rb
@@ -1,16 +1,45 @@
-module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :attribute_types_cached_by_default, instance_accessor: false
- end
+require 'active_support/core_ext/module/method_transplanting'
+module ActiveRecord
module AttributeMethods
module Read
+ ReaderMethodCache = Class.new(AttributeMethodCache) {
+ private
+ # We want to generate the methods via module_eval rather than
+ # define_method, because define_method is slower on dispatch.
+ # Evaluating many similar methods may use more memory as the instruction
+ # sequences are duplicated and cached (in MRI). define_method may
+ # be slower on dispatch, but if you're careful about the closure
+ # created, then define_method will consume much less memory.
+ #
+ # But sometimes the database might return columns with
+ # characters that are not allowed in normal method names (like
+ # 'my_column(omg)'. So to work around this we first define with
+ # the __temp__ identifier, and then use alias method to rename
+ # it to what we want.
+ #
+ # We are also defining a constant to hold the frozen string of
+ # the attribute name. Using a constant means that we do not have
+ # to allocate an object on each call to the attribute method.
+ # Making it frozen means that it doesn't get duped when used to
+ # key the @attributes_cache in read_attribute.
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
included do
- config_attribute :attribute_types_cached_by_default
+ class_attribute :attribute_types_cached_by_default, instance_writer: false
+ self.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
end
module ClassMethods
@@ -35,37 +64,30 @@ module ActiveRecord
protected
- # We want to generate the methods via module_eval rather than
- # define_method, because define_method is slower on dispatch and
- # uses more memory (because it creates a closure).
- #
- # But sometimes the database might return columns with
- # characters that are not allowed in normal method names (like
- # 'my_column(omg)'. So to work around this we first define with
- # the __temp__ identifier, and then use alias method to rename
- # it to what we want.
- #
- # We are also defining a constant to hold the frozen string of
- # the attribute name. Using a constant means that we do not have
- # to allocate an object on each call to the attribute method.
- # Making it frozen means that it doesn't get duped when used to
- # key the @attributes_cache in read_attribute.
- def define_method_attribute(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- module AttrNames
- unless defined? ATTR_#{safe_name}
- ATTR_#{safe_name} = #{name.inspect}.freeze
+ if Module.methods_transplantable?
+ def define_method_attribute(name)
+ method = ReaderMethodCache[name]
+ generated_attribute_methods.module_eval { define_method name, method }
+ end
+ else
+ def define_method_attribute(name)
+ safe_name = name.unpack('h*').first
+ temp_method = "__temp__#{safe_name}"
+
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def #{temp_method}
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ read_attribute(name) { |n| missing_attribute(n, caller) }
end
- end
+ STR
- def __temp__#{safe_name}
- read_attribute(AttrNames::ATTR_#{safe_name}) { |n| missing_attribute(n, caller) }
+ generated_attribute_methods.module_eval do
+ alias_method name, temp_method
+ undef_method temp_method
end
-
- alias_method #{name.inspect}, :__temp__#{safe_name}
- undef_method :__temp__#{safe_name}
- STR
+ end
end
private
@@ -79,23 +101,22 @@ module ActiveRecord
end
end
- ActiveRecord::Model.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
-
# Returns the value of the attribute identified by <tt>attr_name</tt> after
# it has been typecast (for example, "2004-12-12" in a data column is cast
# to a date object, like Date.new(2004, 12, 12)).
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/3552829.
+ # We use #[] first as a perf optimization for non-nil values. See https://gist.github.com/jonleighton/3552829.
name = attr_name.to_s
@attributes_cache[name] || @attributes_cache.fetch(name) {
- column = @columns_hash.fetch(name) {
- return @attributes.fetch(name) {
- if name == 'id' && self.class.primary_key != name
- read_attribute(self.class.primary_key)
- end
- }
- }
+ column = @column_types_override[name] if @column_types_override
+ column ||= @column_types[name]
+
+ return @attributes.fetch(name) {
+ if name == 'id' && self.class.primary_key != name
+ read_attribute(self.class.primary_key)
+ end
+ } unless column
value = @attributes.fetch(name) {
return block_given? ? yield(name) : nil
diff --git a/activerecord/lib/active_record/attribute_methods/serialization.rb b/activerecord/lib/active_record/attribute_methods/serialization.rb
index 9994a81ede..5701804168 100644
--- a/activerecord/lib/active_record/attribute_methods/serialization.rb
+++ b/activerecord/lib/active_record/attribute_methods/serialization.rb
@@ -5,12 +5,18 @@ module ActiveRecord
included do
# Returns a hash of all the attributes that have been specified for
- # serialization as keys and their class restriction as values.
+ # 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
@@ -44,37 +50,40 @@ module ActiveRecord
end
end
- def serialized_attributes
- ActiveSupport::Deprecation.warn("Instance level serialized_attributes method is deprecated, please use class level method.")
- defined?(@serialized_attributes) ? @serialized_attributes : self.class.serialized_attributes
- end
-
class Type # :nodoc:
def initialize(column)
@column = column
end
def type_cast(value)
- value.unserialized_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
- state == :serialized ? unserialize : value
+ def unserialized_value(v = value)
+ state == :serialized ? unserialize(v) : value
end
def serialized_value
state == :unserialized ? serialize : value
end
- def unserialize
+ def unserialize(v)
self.state = :unserialized
- self.value = coder.load(value)
+ self.value = coder.load(v)
end
def serialize
@@ -85,10 +94,10 @@ module ActiveRecord
# 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:
+ module Behavior # :nodoc:
extend ActiveSupport::Concern
- module ClassMethods
+ module ClassMethods # :nodoc:
def initialize_attributes(attributes, options = {})
serialized = (options.delete(:serialized) { true }) ? :serialized : :unserialized
super(attributes, options)
@@ -111,6 +120,14 @@ module ActiveRecord
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
@@ -118,6 +135,24 @@ module ActiveRecord
super
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
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 f36a5806a9..f168282ea3 100644
--- a/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
+++ b/activerecord/lib/active_record/attribute_methods/time_zone_conversion.rb
@@ -1,13 +1,4 @@
-
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :time_zone_aware_attributes, instance_accessor: false
- self.time_zone_aware_attributes = false
-
- mattr_accessor :skip_time_zone_conversion_for_attributes, instance_accessor: false
- self.skip_time_zone_conversion_for_attributes = []
- end
-
module AttributeMethods
module TimeZoneConversion
class Type # :nodoc:
@@ -28,8 +19,11 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :time_zone_aware_attributes, global: true
- config_attribute :skip_time_zone_conversion_for_attributes
+ mattr_accessor :time_zone_aware_attributes, instance_writer: false
+ self.time_zone_aware_attributes = false
+
+ class_attribute :skip_time_zone_conversion_for_attributes, instance_writer: false
+ self.skip_time_zone_conversion_for_attributes = []
end
module ClassMethods
@@ -39,19 +33,12 @@ module ActiveRecord
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}=(original_time)
- time = original_time
- unless time.acts_like?(:time)
- time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
- end
- zoned_time = time && time.in_time_zone rescue nil
- rounded_time = round_usec(zoned_time)
- rounded_value = round_usec(read_attribute("#{attr_name}"))
- if (rounded_value != rounded_time) || (!rounded_value && original_time)
- write_attribute("#{attr_name}", original_time)
- #{attr_name}_will_change!
- @attributes_cache["#{attr_name}"] = zoned_time
- end
+ 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)
@@ -64,15 +51,9 @@ module ActiveRecord
def create_time_zone_conversion_attribute?(name, column)
time_zone_aware_attributes &&
!self.skip_time_zone_conversion_for_attributes.include?(name.to_sym) &&
- [:datetime, :timestamp].include?(column.type)
+ (:datetime == column.type || :timestamp == column.type)
end
end
-
- private
- def round_usec(value)
- return unless value
- value.change(:usec => 0)
- end
end
end
end
diff --git a/activerecord/lib/active_record/attribute_methods/write.rb b/activerecord/lib/active_record/attribute_methods/write.rb
index cd33494cc3..c853fc0917 100644
--- a/activerecord/lib/active_record/attribute_methods/write.rb
+++ b/activerecord/lib/active_record/attribute_methods/write.rb
@@ -1,6 +1,21 @@
+require 'active_support/core_ext/module/method_transplanting'
+
module ActiveRecord
module AttributeMethods
module Write
+ WriterMethodCache = Class.new(AttributeMethodCache) {
+ private
+
+ def method_body(method_name, const_name)
+ <<-EOMETHOD
+ def #{method_name}(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{const_name}
+ write_attribute(name, value)
+ end
+ EOMETHOD
+ end
+ }.new
+
extend ActiveSupport::Concern
included do
@@ -10,17 +25,29 @@ module ActiveRecord
module ClassMethods
protected
- # See define_method_attribute in read.rb for an explanation of
- # this code.
- def define_method_attribute=(name)
- safe_name = name.unpack('h*').first
- generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
- def __temp__#{safe_name}=(value)
- write_attribute(AttrNames::ATTR_#{safe_name}, value)
- end
- alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
- undef_method :__temp__#{safe_name}=
- STR
+ if Module.methods_transplantable?
+ # See define_method_attribute in read.rb for an explanation of
+ # this code.
+ def define_method_attribute=(name)
+ method = WriterMethodCache[name]
+ generated_attribute_methods.module_eval {
+ define_method "#{name}=", method
+ }
+ end
+ else
+ def define_method_attribute=(name)
+ safe_name = name.unpack('h*').first
+ ActiveRecord::AttributeMethods::AttrNames.set_name_cache safe_name, name
+
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
+ def __temp__#{safe_name}=(value)
+ name = ::ActiveRecord::AttributeMethods::AttrNames::ATTR_#{safe_name}
+ write_attribute(name, value)
+ end
+ alias_method #{(name + '=').inspect}, :__temp__#{safe_name}=
+ undef_method :__temp__#{safe_name}=
+ STR
+ end
end
end
diff --git a/activerecord/lib/active_record/autosave_association.rb b/activerecord/lib/active_record/autosave_association.rb
index a30f888a7a..e9622ca0c1 100644
--- a/activerecord/lib/active_record/autosave_association.rb
+++ b/activerecord/lib/active_record/autosave_association.rb
@@ -16,8 +16,9 @@ module ActiveRecord
# Note that it also means that associations marked for destruction won't
# be destroyed directly. They will however still be marked for destruction.
#
- # Note that <tt>:autosave => false</tt> is not same as not declaring <tt>:autosave</tt>.
- # When the <tt>:autosave</tt> option is not present new associations are saved.
+ # Note that <tt>autosave: false</tt> is not same as not declaring <tt>:autosave</tt>.
+ # When the <tt>:autosave</tt> option is not present then new association records are
+ # saved but the updated association records are not saved.
#
# == Validation
#
@@ -32,12 +33,10 @@ module ActiveRecord
# autosave callbacks are executed. Placing your callbacks after
# associations is usually a good practice.
#
- # == Examples
- #
# === One-to-one Example
#
# class Post
- # has_one :author, :autosave => true
+ # has_one :author, autosave: true
# end
#
# Saving changes to the parent and its associated model can now be performed
@@ -64,14 +63,14 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.author.id
- # Author.find_by_id(id).nil? # => false
+ # Author.find_by(id: id).nil? # => false
#
# post.save
# post.reload.author # => nil
#
# Now it _is_ removed from the database:
#
- # Author.find_by_id(id).nil? # => true
+ # Author.find_by(id: id).nil? # => true
#
# === One-to-many Example
#
@@ -81,27 +80,27 @@ module ActiveRecord
# has_many :comments # :autosave option is not declared
# end
#
- # post = Post.new(:title => 'ruby rocks')
- # post.comments.build(:body => 'hello world')
+ # post = Post.new(title: 'ruby rocks')
+ # post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.build(:body => 'hello world')
+ # post = Post.create(title: 'ruby rocks')
+ # post.comments.build(body: 'hello world')
# post.save # => saves both post and comment
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.create(:body => 'hello world')
+ # post = Post.create(title: 'ruby rocks')
+ # post.comments.create(body: 'hello world')
# post.save # => saves both post and comment
#
# When <tt>:autosave</tt> is true all children are saved, no matter whether they
# are new records or not:
#
# class Post
- # has_many :comments, :autosave => true
+ # has_many :comments, autosave: true
# end
#
- # post = Post.create(:title => 'ruby rocks')
- # post.comments.create(:body => 'hello world')
+ # 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
#
@@ -115,105 +114,106 @@ module ActiveRecord
# Note that the model is _not_ yet removed from the database:
#
# id = post.comments.last.id
- # Comment.find_by_id(id).nil? # => false
+ # Comment.find_by(id: id).nil? # => false
#
# post.save
# post.reload.comments.length # => 1
#
# Now it _is_ removed from the database:
#
- # Comment.find_by_id(id).nil? # => true
+ # Comment.find_by(id: id).nil? # => true
module AutosaveAssociation
extend ActiveSupport::Concern
module AssociationBuilderExtension #:nodoc:
- def build
+ def self.build(model, reflection)
model.send(:add_autosave_association_callbacks, reflection)
- super
+ end
+
+ def self.valid_options
+ [ :autosave ]
end
end
included do
- Associations::Builder::Association.class_eval do
- self.valid_options << :autosave
- include AssociationBuilderExtension
- end
+ Associations::Builder::Association.extensions << AssociationBuilderExtension
end
module ClassMethods
private
- def define_non_cyclic_method(name, reflection, &block)
- define_method(name) do |*args|
- result = true; @_already_called ||= {}
- # Loop prevention for validation of associations
- unless @_already_called[[name, reflection.name]]
- begin
- @_already_called[[name, reflection.name]]=true
- result = instance_eval(&block)
- ensure
- @_already_called[[name, reflection.name]]=false
+ def define_non_cyclic_method(name, &block)
+ define_method(name) do |*args|
+ result = true; @_already_called ||= {}
+ # Loop prevention for validation of associations
+ unless @_already_called[name]
+ begin
+ @_already_called[name]=true
+ result = instance_eval(&block)
+ ensure
+ @_already_called[name]=false
+ end
end
- end
- result
+ result
+ end
end
- end
- # Adds validation and save callbacks for the association as specified by
- # the +reflection+.
- #
- # For performance reasons, we don't check whether to validate at runtime.
- # However the validation and callback methods are lazy and those methods
- # get created when they are invoked for the very first time. However,
- # this can change, for instance, when using nested attributes, which is
- # called _after_ the association has been defined. Since we don't want
- # the callbacks to get defined multiple times, there are guards that
- # check if the save or validation methods have already been defined
- # before actually defining them.
- def add_autosave_association_callbacks(reflection)
- save_method = :"autosave_associated_records_for_#{reflection.name}"
- validation_method = :"validate_associated_records_for_#{reflection.name}"
- collection = reflection.collection?
-
- unless method_defined?(save_method)
- if collection
- before_save :before_save_collection_association
-
- define_non_cyclic_method(save_method, reflection) { save_collection_association(reflection) }
- # Doesn't use after_save as that would save associations added in after_create/after_update twice
- after_create save_method
- after_update save_method
- elsif reflection.macro == :has_one
- define_method(save_method) { save_has_one_association(reflection) }
- # Configures two callbacks instead of a single after_save so that
- # the model may rely on their execution order relative to its
- # own callbacks.
- #
- # For example, given that after_creates run before after_saves, if
- # we configured instead an after_save there would be no way to fire
- # a custom after_create callback after the child association gets
- # created.
- after_create save_method
- after_update save_method
- else
- define_non_cyclic_method(save_method, reflection) { save_belongs_to_association(reflection) }
- before_save save_method
+ # Adds validation and save callbacks for the association as specified by
+ # the +reflection+.
+ #
+ # For performance reasons, we don't check whether to validate at runtime.
+ # However the validation and callback methods are lazy and those methods
+ # get created when they are invoked for the very first time. However,
+ # this can change, for instance, when using nested attributes, which is
+ # called _after_ the association has been defined. Since we don't want
+ # the callbacks to get defined multiple times, there are guards that
+ # check if the save or validation methods have already been defined
+ # before actually defining them.
+ def add_autosave_association_callbacks(reflection)
+ save_method = :"autosave_associated_records_for_#{reflection.name}"
+ validation_method = :"validate_associated_records_for_#{reflection.name}"
+ collection = reflection.collection?
+
+ unless method_defined?(save_method)
+ if collection
+ before_save :before_save_collection_association
+
+ define_non_cyclic_method(save_method) { save_collection_association(reflection) }
+ # Doesn't use after_save as that would save associations added in after_create/after_update twice
+ after_create save_method
+ after_update save_method
+ elsif reflection.macro == :has_one
+ define_method(save_method) { save_has_one_association(reflection) }
+ # Configures two callbacks instead of a single after_save so that
+ # the model may rely on their execution order relative to its
+ # own callbacks.
+ #
+ # For example, given that after_creates run before after_saves, if
+ # we configured instead an after_save there would be no way to fire
+ # a custom after_create callback after the child association gets
+ # created.
+ after_create save_method
+ after_update save_method
+ else
+ define_non_cyclic_method(save_method) { save_belongs_to_association(reflection) }
+ before_save save_method
+ end
end
- end
- if reflection.validate? && !method_defined?(validation_method)
- method = (collection ? :validate_collection_association : :validate_single_association)
- define_non_cyclic_method(validation_method, reflection) { send(method, reflection) }
- validate validation_method
+ if reflection.validate? && !method_defined?(validation_method)
+ method = (collection ? :validate_collection_association : :validate_single_association)
+ define_non_cyclic_method(validation_method) { send(method, reflection) }
+ validate validation_method
+ end
end
- end
end
# Reloads the attributes of the object as usual and clears <tt>marked_for_destruction</tt> flag.
def reload(options = nil)
@marked_for_destruction = false
+ @destroyed_by_association = nil
super
end
@@ -233,6 +233,19 @@ module ActiveRecord
@marked_for_destruction
end
+ # Records the association that is being destroyed and destroying this
+ # record in the process.
+ def destroyed_by_association=(reflection)
+ @destroyed_by_association = reflection
+ end
+
+ # Returns the association for the parent being destroyed.
+ #
+ # Used to avoid updating the counter cache unnecessarily.
+ def destroyed_by_association
+ @destroyed_by_association
+ end
+
# Returns whether or not this record has been changed in any way (including whether
# any of its nested autosave associations are likewise changed)
def changed_for_autosave?
@@ -241,173 +254,179 @@ module ActiveRecord
private
- # Returns the record for an association collection that should be validated
- # or saved. If +autosave+ is +false+ only new records will be returned,
- # unless the parent is/was a new record itself.
- def associated_records_to_validate_or_save(association, new_record, autosave)
- if new_record
- association && association.target
- elsif autosave
- association.target.find_all { |record| record.changed_for_autosave? }
- else
- association.target.find_all { |record| record.new_record? }
+ # Returns the record for an association collection that should be validated
+ # or saved. If +autosave+ is +false+ only new records will be returned,
+ # unless the parent is/was a new record itself.
+ def associated_records_to_validate_or_save(association, new_record, autosave)
+ if new_record
+ association && association.target
+ elsif autosave
+ association.target.find_all { |record| record.changed_for_autosave? }
+ else
+ association.target.find_all { |record| record.new_record? }
+ end
end
- end
- # go through nested autosave associations that are loaded in memory (without loading
- # any new ones), and return true if is changed for autosave
- def nested_records_changed_for_autosave?
- self.class.reflect_on_all_autosave_associations.any? do |reflection|
- association = association_instance_get(reflection.name)
- association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ # go through nested autosave associations that are loaded in memory (without loading
+ # any new ones), and return true if is changed for autosave
+ def nested_records_changed_for_autosave?
+ self.class.reflect_on_all_autosave_associations.any? do |reflection|
+ association = association_instance_get(reflection.name)
+ association && Array.wrap(association.target).any? { |a| a.changed_for_autosave? }
+ end
end
- end
- # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
- # turned on for the association.
- def validate_single_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.reader
- association_valid?(reflection, record) if record
- end
+ # Validate the association if <tt>:validate</tt> or <tt>:autosave</tt> is
+ # turned on for the association.
+ def validate_single_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.reader
+ association_valid?(reflection, record) if record
+ end
- # Validate the associated records if <tt>:validate</tt> or
- # <tt>:autosave</tt> is turned on for the association specified by
- # +reflection+.
- def validate_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
- records.each { |record| association_valid?(reflection, record) }
+ # Validate the associated records if <tt>:validate</tt> or
+ # <tt>:autosave</tt> is turned on for the association specified by
+ # +reflection+.
+ def validate_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ if records = associated_records_to_validate_or_save(association, new_record?, reflection.options[:autosave])
+ records.each { |record| association_valid?(reflection, record) }
+ end
end
end
- end
- # Returns whether or not the association is valid and applies any errors to
- # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
- # enabled records if they're marked_for_destruction? or destroyed.
- def association_valid?(reflection, record)
- return true if record.destroyed? || record.marked_for_destruction?
-
- unless valid = record.valid?(validation_context)
- if reflection.options[:autosave]
- record.errors.each do |attribute, message|
- attribute = "#{reflection.name}.#{attribute}"
- errors[attribute] << message
- errors[attribute].uniq!
+ # Returns whether or not the association is valid and applies any errors to
+ # the parent, <tt>self</tt>, if it wasn't. Skips any <tt>:autosave</tt>
+ # enabled records if they're marked_for_destruction? or destroyed.
+ def association_valid?(reflection, record)
+ return true if record.destroyed? || record.marked_for_destruction?
+
+ unless valid = record.valid?
+ if reflection.options[:autosave]
+ record.errors.each do |attribute, message|
+ attribute = "#{reflection.name}.#{attribute}"
+ errors[attribute] << message
+ errors[attribute].uniq!
+ end
+ else
+ errors.add(reflection.name)
end
- else
- errors.add(reflection.name)
end
+ valid
end
- valid
- end
- # Is used as a before_save callback to check while saving a collection
- # association whether or not the parent was a new record before saving.
- def before_save_collection_association
- @new_record_before_save = new_record?
- true
- end
+ # Is used as a before_save callback to check while saving a collection
+ # association whether or not the parent was a new record before saving.
+ def before_save_collection_association
+ @new_record_before_save = new_record?
+ true
+ end
- # Saves any new associated records, or all loaded autosave associations if
- # <tt>:autosave</tt> is enabled on the association.
- #
- # In addition, it destroys all children that were marked for destruction
- # with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_collection_association(reflection)
- if association = association_instance_get(reflection.name)
- autosave = reflection.options[:autosave]
-
- if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
- records_to_destroy = []
- records.each do |record|
- next if record.destroyed?
-
- saved = true
-
- if autosave && record.marked_for_destruction?
- records_to_destroy << record
- elsif autosave != false && (@new_record_before_save || record.new_record?)
- if autosave
- saved = association.insert_record(record, false)
- else
- association.insert_record(record) unless reflection.nested?
- end
- elsif autosave
- saved = record.save(:validate => false)
+ # Saves any new associated records, or all loaded autosave associations if
+ # <tt>:autosave</tt> is enabled on the association.
+ #
+ # In addition, it destroys all children that were marked for destruction
+ # with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_collection_association(reflection)
+ if association = association_instance_get(reflection.name)
+ autosave = reflection.options[:autosave]
+
+ if records = associated_records_to_validate_or_save(association, @new_record_before_save, autosave)
+
+ if autosave
+ records_to_destroy = records.select(&:marked_for_destruction?)
+ records_to_destroy.each { |record| association.destroy(record) }
+ records -= records_to_destroy
end
- raise ActiveRecord::Rollback unless saved
- end
+ records.each do |record|
+ next if record.destroyed?
+
+ saved = true
+
+ if autosave != false && (@new_record_before_save || record.new_record?)
+ if autosave
+ saved = association.insert_record(record, false)
+ else
+ association.insert_record(record) unless reflection.nested?
+ end
+ elsif autosave
+ saved = record.save(:validate => false)
+ end
- records_to_destroy.each do |record|
- association.destroy(record)
+ raise ActiveRecord::Rollback unless saved
+ end
end
- end
- # reconstruct the scope now that we know the owner's id
- association.send(:reset_scope) if association.respond_to?(:reset_scope)
+ # reconstruct the scope now that we know the owner's id
+ association.reset_scope if association.respond_to?(:reset_scope)
+ end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
- # on the association.
- #
- # In addition, it will destroy the association if it was marked for
- # destruction with mark_for_destruction.
- #
- # This all happens inside a transaction, _if_ the Transactions module is included into
- # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
- def save_has_one_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- record.destroy
- else
- key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
- if autosave != false && (new_record? || record.new_record? || record[reflection.foreign_key] != key || autosave)
- unless reflection.through_reflection
- record[reflection.foreign_key] = key
- end
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled
+ # on the association.
+ #
+ # In addition, it will destroy the association if it was marked for
+ # destruction with mark_for_destruction.
+ #
+ # This all happens inside a transaction, _if_ the Transactions module is included into
+ # ActiveRecord::Base after the AutosaveAssociation module, which it does by default.
+ def save_has_one_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
- saved = record.save(:validate => !autosave)
- raise ActiveRecord::Rollback if !saved && autosave
- saved
+ if autosave && record.marked_for_destruction?
+ record.destroy
+ else
+ key = reflection.options[:primary_key] ? send(reflection.options[:primary_key]) : id
+ if autosave != false && (autosave || new_record? || record_changed?(reflection, record, key))
+
+ unless reflection.through_reflection
+ record[reflection.foreign_key] = key
+ end
+
+ saved = record.save(:validate => !autosave)
+ raise ActiveRecord::Rollback if !saved && autosave
+ saved
+ end
end
end
end
- end
- # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
- #
- # In addition, it will destroy the association if it was marked for destruction.
- def save_belongs_to_association(reflection)
- association = association_instance_get(reflection.name)
- record = association && association.load_target
- if record && !record.destroyed?
- autosave = reflection.options[:autosave]
-
- if autosave && record.marked_for_destruction?
- self[reflection.foreign_key] = nil
- record.destroy
- elsif autosave != false
- saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
-
- if association.updated?
- association_id = record.send(reflection.options[:primary_key] || :id)
- self[reflection.foreign_key] = association_id
- association.loaded!
- end
+ # If the record is new or it has changed, returns true.
+ def record_changed?(reflection, record, key)
+ record.new_record? || record[reflection.foreign_key] != key || record.attribute_changed?(reflection.foreign_key)
+ end
+
+ # Saves the associated record if it's new or <tt>:autosave</tt> is enabled.
+ #
+ # In addition, it will destroy the association if it was marked for destruction.
+ def save_belongs_to_association(reflection)
+ association = association_instance_get(reflection.name)
+ record = association && association.load_target
+ if record && !record.destroyed?
+ autosave = reflection.options[:autosave]
+
+ if autosave && record.marked_for_destruction?
+ self[reflection.foreign_key] = nil
+ record.destroy
+ elsif autosave != false
+ saved = record.save(:validate => !autosave) if record.new_record? || (autosave && record.changed_for_autosave?)
+
+ if association.updated?
+ association_id = record.send(reflection.options[:primary_key] || :id)
+ self[reflection.foreign_key] = association_id
+ association.loaded!
+ end
- saved if autosave
+ saved if autosave
+ end
end
end
- end
end
end
diff --git a/activerecord/lib/active_record/base.rb b/activerecord/lib/active_record/base.rb
index a4705b24ca..04e3dd49e7 100644
--- a/activerecord/lib/active_record/base.rb
+++ b/activerecord/lib/active_record/base.rb
@@ -8,16 +8,17 @@ require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/class/delegating_attributes'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/slice'
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/errors'
require 'active_record/log_subscriber'
require 'active_record/explain_subscriber'
+require 'active_record/relation/delegation'
module ActiveRecord #:nodoc:
# = Active Record
@@ -36,7 +37,7 @@ module ActiveRecord #:nodoc:
# method is especially useful when you're receiving the data from somewhere else, like an
# HTTP request. It works like this:
#
- # user = User.new(:name => "David", :occupation => "Code Artist")
+ # user = User.new(name: "David", occupation: "Code Artist")
# user.name # => "David"
#
# You can also use block initialization:
@@ -69,7 +70,7 @@ module ActiveRecord #:nodoc:
# end
#
# def self.authenticate_safely_simply(user_name, password)
- # where(:user_name => user_name, :password => password).first
+ # where(user_name: user_name, password: password).first
# end
# end
#
@@ -87,27 +88,27 @@ module ActiveRecord #:nodoc:
#
# Company.where(
# "id = :id AND name = :name AND division = :division AND created_at > :accounting_date",
- # { :id => 3, :name => "37signals", :division => "First", :accounting_date => '2005-01-01' }
+ # { id: 3, name: "37signals", division: "First", accounting_date: '2005-01-01' }
# ).first
#
# Similarly, a simple hash without a statement will generate conditions based on equality with the SQL AND
# operator. For instance:
#
- # Student.where(:first_name => "Harvey", :status => 1)
+ # Student.where(first_name: "Harvey", status: 1)
# Student.where(params[:student])
#
# A range may be used in the hash to use the SQL BETWEEN operator:
#
- # Student.where(:grade => 9..12)
+ # Student.where(grade: 9..12)
#
# An array may be used in the hash to use the SQL IN operator:
#
- # Student.where(:grade => [9,11,12])
+ # Student.where(grade: [9,11,12])
#
# When joining tables, nested hashes or keys written in the form 'table_name.column_name'
# can be used to qualify the table name of a particular condition. For instance:
#
- # Student.joins(:schools).where(:schools => { :category => 'public' })
+ # Student.joins(:schools).where(schools: { category: 'public' })
# Student.joins(:schools).where('schools.category' => 'public' )
#
# == Overwriting default accessors
@@ -141,10 +142,10 @@ module ActiveRecord #:nodoc:
# 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:
#
- # user = User.new(:name => "David")
+ # user = User.new(name: "David")
# user.name? # => true
#
- # anonymous = User.new(:name => "")
+ # anonymous = User.new(name: "")
# anonymous.name? # => false
#
# == Accessing attributes before they have been typecasted
@@ -160,14 +161,11 @@ module ActiveRecord #:nodoc:
#
# == Dynamic attribute-based finders
#
- # Dynamic attribute-based finders are a cleaner way of getting (and/or creating) objects
+ # Dynamic attribute-based finders are a mildly deprecated way of getting (and/or creating) objects
# by simple queries without turning to SQL. They work by appending the name of an attribute
- # to <tt>find_by_</tt>, <tt>find_last_by_</tt>, or <tt>find_all_by_</tt> and thus produces finders
- # like <tt>Person.find_by_user_name</tt>, <tt>Person.find_all_by_last_name</tt>, and
- # <tt>Payment.find_by_transaction_id</tt>. Instead of writing
- # <tt>Person.where(:user_name => user_name).first</tt>, you just do <tt>Person.find_by_user_name(user_name)</tt>.
- # And instead of writing <tt>Person.where(:last_name => last_name).all</tt>, you just do
- # <tt>Person.find_all_by_last_name(last_name)</tt>.
+ # to <tt>find_by_</tt> like <tt>Person.find_by_user_name</tt>.
+ # Instead of writing <tt>Person.find_by(user_name: user_name)</tt>, you can use
+ # <tt>Person.find_by_user_name(user_name)</tt>.
#
# It's possible to add an exclamation point (!) on the end of the dynamic finders to get them to raise an
# <tt>ActiveRecord::RecordNotFound</tt> error if they do not return any records,
@@ -175,51 +173,12 @@ module ActiveRecord #:nodoc:
#
# It's also possible to use multiple attributes in the same find by separating them with "_and_".
#
- # Person.where(:user_name => user_name, :password => password).first
+ # Person.find_by(user_name: user_name, password: password)
# Person.find_by_user_name_and_password(user_name, password) # with dynamic finder
#
# It's even possible to call these dynamic finder methods on relations and named scopes.
#
- # Payment.order("created_on").find_all_by_amount(50)
- # Payment.pending.find_last_by_amount(100)
- #
- # The same dynamic finder style can be used to create the object if it doesn't already exist.
- # This dynamic finder is called with <tt>find_or_create_by_</tt> and will return the object if
- # it already exists and otherwise creates it, then returns it. Protected attributes won't be set
- # unless they are given in a block.
- #
- # # No 'Summer' tag exists
- # Tag.find_or_create_by_name("Summer") # equal to Tag.create(:name => "Summer")
- #
- # # Now the 'Summer' tag does exist
- # Tag.find_or_create_by_name("Summer") # equal to Tag.find_by_name("Summer")
- #
- # # Now 'Bob' exist and is an 'admin'
- # User.find_or_create_by_name('Bob', :age => 40) { |u| u.admin = true }
- #
- # Adding an exclamation point (!) on to the end of <tt>find_or_create_by_</tt> will
- # raise an <tt>ActiveRecord::RecordInvalid</tt> error if the new record is invalid.
- #
- # Use the <tt>find_or_initialize_by_</tt> finder if you want to return a new record without
- # saving it first. Protected attributes won't be set unless they are given in a block.
- #
- # # No 'Winter' tag exists
- # winter = Tag.find_or_initialize_by_name("Winter")
- # winter.persisted? # false
- #
- # To find by a subset of the attributes to be used for instantiating a new object, pass a hash instead of
- # a list of parameters.
- #
- # Tag.find_or_create_by_name(:name => "rails", :creator => current_user)
- #
- # That will either find an existing tag named "rails", or create a new one while setting the
- # user that created it.
- #
- # Just like <tt>find_by_*</tt>, you can also use <tt>scoped_by_*</tt> to retrieve data. The good thing about
- # using this feature is that the very first time result is returned using <tt>method_missing</tt> technique
- # but after that the method is declared on the class. Henceforth <tt>method_missing</tt> will not be hit.
- #
- # User.scoped_by_user_name('David')
+ # Payment.order("created_on").find_by_amount(50)
#
# == Saving arrays, hashes, and other non-mappable objects in text columns
#
@@ -232,7 +191,7 @@ module ActiveRecord #:nodoc:
# serialize :preferences
# end
#
- # user = User.create(:preferences => { "background" => "black", "display" => large })
+ # user = User.create(preferences: { "background" => "black", "display" => large })
# User.find(user.id).preferences # => { "background" => "black", "display" => large }
#
# You can also specify a class option as the second parameter that'll raise an exception
@@ -242,7 +201,7 @@ module ActiveRecord #:nodoc:
# serialize :preferences, Hash
# end
#
- # user = User.create(:preferences => %w( one two three ))
+ # user = User.create(preferences: %w( one two three ))
# User.find(user.id).preferences # raises SerializationTypeMismatch
#
# When you specify a class option, the default value for that attribute will be a new
@@ -267,9 +226,9 @@ module ActiveRecord #:nodoc:
# class Client < Company; end
# class PriorityClient < Client; end
#
- # When you do <tt>Firm.create(:name => "37signals")</tt>, this record will be saved in
+ # 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.
+ # <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
@@ -321,8 +280,46 @@ module ActiveRecord #:nodoc:
# So it's possible to assign a logger to the class through <tt>Base.logger=</tt> which will then be used by all
# instances in the current object space.
class Base
- include ActiveRecord::Model
+ extend ActiveModel::Naming
+
+ extend ActiveSupport::Benchmarkable
+ extend ActiveSupport::DescendantsTracker
+
+ extend ConnectionHandling
+ extend QueryCache::ClassMethods
+ extend Querying
+ extend Translation
+ extend DynamicMatchers
+ extend Explain
+ extend Delegation::DelegateCache
+
+ include Persistence
+ include ReadonlyAttributes
+ include ModelSchema
+ include Inheritance
+ include Scoping
+ include Sanitization
+ include AttributeAssignment
+ include ActiveModel::Conversion
+ include Integration
+ include Validations
+ include CounterCache
+ include Locking::Optimistic
+ include Locking::Pessimistic
+ include AttributeMethods
+ include Callbacks
+ include Timestamp
+ include Associations
+ include ActiveModel::SecurePassword
+ include AutosaveAssociation
+ include NestedAttributes
+ include Aggregations
+ include Transactions
+ include Reflection
+ include Serialization
+ include Store
+ include Core
end
-end
-ActiveSupport.run_load_hooks(:active_record, ActiveRecord::Model::DeprecationProxy.new)
+ ActiveSupport.run_load_hooks(:active_record, Base)
+end
diff --git a/activerecord/lib/active_record/callbacks.rb b/activerecord/lib/active_record/callbacks.rb
index 111208d0b9..128a9377c1 100644
--- a/activerecord/lib/active_record/callbacks.rb
+++ b/activerecord/lib/active_record/callbacks.rb
@@ -23,11 +23,14 @@ module ActiveRecord
# Check out <tt>ActiveRecord::Transactions</tt> for more details about <tt>after_commit</tt> and
# <tt>after_rollback</tt>.
#
+ # Additionally, an <tt>after_touch</tt> callback is triggered whenever an
+ # object is touched.
+ #
# Lastly an <tt>after_find</tt> and <tt>after_initialize</tt> callback is triggered for each object that
# is found and instantiated by a finder, with <tt>after_initialize</tt> being triggered after new objects
# are instantiated as well.
#
- # That's a total of twelve callbacks, which gives you immense power to react and prepare for each state in the
+ # There are nineteen callbacks in total, which give you immense power to react and prepare for each state in the
# Active Record life cycle. The sequence for calling <tt>Base#save</tt> for an existing record is similar,
# except that each <tt>_create</tt> callback is replaced by the corresponding <tt>_update</tt> callback.
#
@@ -35,7 +38,7 @@ module ActiveRecord
# class CreditCard < ActiveRecord::Base
# # Strip everything but digits, so the user can specify "555 234 34" or
# # "5552-3434" and both will mean "55523434"
- # before_validation(:on => :create) do
+ # before_validation(on: :create) do
# self.number = number.gsub(/[^0-9]/, "") if attribute_present?("number")
# end
# end
@@ -83,7 +86,7 @@ module ActiveRecord
#
# In that case, <tt>Reply#destroy</tt> would only run +destroy_readers+ and _not_ +destroy_author+.
# So, use the callback macros when you want to ensure that a certain callback is called for the entire
- # hierarchy, and use the regular overwriteable methods when you want to leave it up to each descendant
+ # hierarchy, and use the regular overwritable methods when you want to leave it up to each descendant
# to decide whether they want to call +super+ and trigger the inherited callbacks.
#
# *IMPORTANT:* In order for inheritance to work for the callback queues, you must specify the
@@ -200,6 +203,40 @@ module ActiveRecord
# Callbacks are generally run in the order they are defined, with the exception of callbacks defined as
# methods on the model, which are called last.
#
+ # == Ordering callbacks
+ #
+ # Sometimes the code needs that the callbacks execute in a specific order. For example, a +before_destroy+
+ # callback (+log_children+ in this case) should be executed before the children get destroyed by the +dependent: destroy+ option.
+ #
+ # Let's look at the code below:
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children, dependent: destroy
+ #
+ # before_destroy :log_children
+ #
+ # private
+ # def log_children
+ # # Child processing
+ # end
+ # end
+ #
+ # In this case, the problem is that when the +before_destroy+ callback is executed, the children are not available
+ # because the +destroy+ callback gets executed first. You can use the +prepend+ option on the +before_destroy+ callback to avoid this.
+ #
+ # class Topic < ActiveRecord::Base
+ # has_many :children, dependent: destroy
+ #
+ # before_destroy :log_children, prepend: true
+ #
+ # private
+ # def log_children
+ # # Child processing
+ # end
+ # end
+ #
+ # This way, the +before_destroy+ gets executed before the <tt>dependent: destroy</tt> is called, and the data is still available.
+ #
# == Transactions
#
# The entire callback chain of a +save+, <tt>save!</tt>, or +destroy+ call runs
@@ -265,11 +302,11 @@ module ActiveRecord
run_callbacks(:save) { super }
end
- def create #:nodoc:
+ def create_record #:nodoc:
run_callbacks(:create) { super }
end
- def update(*) #:nodoc:
+ def update_record(*) #:nodoc:
run_callbacks(:update) { super }
end
end
diff --git a/activerecord/lib/active_record/coders/yaml_column.rb b/activerecord/lib/active_record/coders/yaml_column.rb
index f6cdc67b4d..d3d7396c91 100644
--- a/activerecord/lib/active_record/coders/yaml_column.rb
+++ b/activerecord/lib/active_record/coders/yaml_column.rb
@@ -3,7 +3,6 @@ require 'yaml'
module ActiveRecord
module Coders # :nodoc:
class YAMLColumn # :nodoc:
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ]
attr_accessor :object_class
@@ -24,19 +23,15 @@ module ActiveRecord
def load(yaml)
return object_class.new if object_class != Object && yaml.nil?
return yaml unless yaml.is_a?(String) && yaml =~ /^---/
- begin
- obj = YAML.load(yaml)
-
- unless obj.is_a?(object_class) || obj.nil?
- raise SerializationTypeMismatch,
- "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
- end
- obj ||= object_class.new if object_class != Object
-
- obj
- rescue *RESCUE_ERRORS
- yaml
+ obj = YAML.load(yaml)
+
+ unless obj.is_a?(object_class) || obj.nil?
+ raise SerializationTypeMismatch,
+ "Attribute was supposed to be a #{object_class}, but was a #{obj.class}"
end
+ obj ||= object_class.new if object_class != Object
+
+ obj
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 42bd16db80..cfdcae7f63 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/connection_pool.rb
@@ -1,7 +1,7 @@
require 'thread'
+require 'thread_safe'
require 'monitor'
require 'set'
-require 'active_support/core_ext/module/deprecation'
module ActiveRecord
# Raised when a connection could not be obtained within the connection
@@ -236,38 +236,32 @@ module ActiveRecord
@spec = spec
- # The cache of reserved connections mapped to threads
- @reserved_connections = {}
-
@checkout_timeout = spec.config[:checkout_timeout] || 5
- @dead_connection_timeout = spec.config[:dead_connection_timeout]
+ @dead_connection_timeout = spec.config[:dead_connection_timeout] || 5
@reaper = Reaper.new self, spec.config[:reaping_frequency]
@reaper.run
# default max pool size to 5
@size = (spec.config[:pool] && spec.config[:pool].to_i) || 5
+ # The cache of reserved connections mapped to threads
+ @reserved_connections = ThreadSafe::Cache.new(:initial_capacity => @size)
+
@connections = []
@automatic_reconnect = true
@available = Queue.new self
end
- # Hack for tests to be able to add connections. Do not call outside of tests
- def insert_connection_for_test!(c) #:nodoc:
- synchronize do
- @connections << c
- @available.add c
- end
- end
-
# Retrieve the connection associated with the current thread, or call
# #checkout to obtain one if necessary.
#
# #connection can be called any number of times; the connection is
# held in a hash keyed by the thread id.
def connection
- synchronize do
+ # this is correctly done double-checked locking
+ # (ThreadSafe::Cache's lookups have volatile semantics)
+ @reserved_connections[current_connection_id] || synchronize do
@reserved_connections[current_connection_id] ||= checkout
end
end
@@ -310,7 +304,7 @@ module ActiveRecord
# Disconnects all connections in the pool, and clears the pool.
def disconnect!
synchronize do
- @reserved_connections = {}
+ @reserved_connections.clear
@connections.each do |conn|
checkin conn
conn.disconnect!
@@ -323,7 +317,7 @@ module ActiveRecord
# Clears the cache which maps classes.
def clear_reloadable_connections!
synchronize do
- @reserved_connections = {}
+ @reserved_connections.clear
@connections.each do |conn|
checkin conn
conn.disconnect! if conn.requires_reloading?
@@ -338,11 +332,6 @@ module ActiveRecord
end
end
- def clear_stale_cached_connections! # :nodoc:
- reap
- end
- deprecate :clear_stale_cached_connections! => "Please use #reap instead"
-
# Check-out a database connection from the pool, indicating that you want
# to use it. You should call #checkin when you no longer need this.
#
@@ -404,7 +393,9 @@ module ActiveRecord
synchronize do
stale = Time.now - @dead_connection_timeout
connections.dup.each do |conn|
- remove conn if conn.in_use? && stale > conn.last_use && !conn.active?
+ if conn.in_use? && stale > conn.last_use && !conn.active?
+ remove conn
+ end
end
end
end
@@ -441,11 +432,11 @@ module ActiveRecord
end
def new_connection
- ActiveRecord::Model.send(spec.adapter_method, spec.config)
+ Base.send(spec.adapter_method, spec.config)
end
def current_connection_id #:nodoc:
- ActiveRecord::Model.connection_id ||= Thread.current.object_id
+ Base.connection_id ||= Thread.current.object_id
end
def checkout_new_connection
@@ -490,39 +481,55 @@ module ActiveRecord
# determine the connection pool that they should use.
class ConnectionHandler
def initialize
- @owner_to_pool = Hash.new { |h,k| h[k] = {} }
- @class_to_pool = Hash.new { |h,k| h[k] = {} }
+ # These caches are keyed by klass.name, NOT klass. Keying them by klass
+ # alone would lead to memory leaks in development mode as all previous
+ # instances of the class would stay in memory.
+ @owner_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
+ h[k] = ThreadSafe::Cache.new(:initial_capacity => 2)
+ end
+ @class_to_pool = ThreadSafe::Cache.new(:initial_capacity => 2) do |h,k|
+ h[k] = ThreadSafe::Cache.new
+ end
end
- def connection_pools
+ def connection_pool_list
owner_to_pool.values.compact
end
+ def connection_pools
+ ActiveSupport::Deprecation.warn(
+ "In the next release, this will return the same as #connection_pool_list. " \
+ "(An array of pools, rather than a hash mapping specs to pools.)"
+ )
+ Hash[connection_pool_list.map { |pool| [pool.spec, pool] }]
+ end
+
def establish_connection(owner, spec)
@class_to_pool.clear
- owner_to_pool[owner] = ConnectionAdapters::ConnectionPool.new(spec)
+ raise RuntimeError, "Anonymous class is not allowed." unless owner.name
+ owner_to_pool[owner.name] = ConnectionAdapters::ConnectionPool.new(spec)
end
# Returns true if there are any active connections among the connection
# pools that the ConnectionHandler is managing.
def active_connections?
- connection_pools.any?(&:active_connection?)
+ connection_pool_list.any?(&:active_connection?)
end
# Returns any connections in use by the current thread back to the pool,
# and also returns connections to the pool cached by threads that are no
# longer alive.
def clear_active_connections!
- connection_pools.each(&:release_connection)
+ connection_pool_list.each(&:release_connection)
end
# Clears the cache which maps classes.
def clear_reloadable_connections!
- connection_pools.each(&:clear_reloadable_connections!)
+ connection_pool_list.each(&:clear_reloadable_connections!)
end
def clear_all_connections!
- connection_pools.each(&:disconnect!)
+ connection_pool_list.each(&:disconnect!)
end
# Locate the connection of the nearest super class. This can be an
@@ -546,7 +553,7 @@ module ActiveRecord
# can be used as an argument for establish_connection, for easily
# re-establishing the connection.
def remove_connection(owner)
- if pool = owner_to_pool.delete(owner)
+ if pool = owner_to_pool.delete(owner.name)
@class_to_pool.clear
pool.automatic_reconnect = false
pool.disconnect!
@@ -559,18 +566,18 @@ module ActiveRecord
# When a connection is established or removed, we invalidate the cache.
#
# Ideally we would use #fetch here, as class_to_pool[klass] may sometimes be nil.
- # However, benchmarking (https://gist.github.com/3552829) showed that #fetch is
- # significantly slower than #[]. So in the nil case, no caching will take place,
- # but that's ok since the nil case is not the common one that we wish to optimise
- # for.
+ # However, benchmarking (https://gist.github.com/jonleighton/3552829) showed that
+ # #fetch is significantly slower than #[]. So in the nil case, no caching will
+ # take place, but that's ok since the nil case is not the common one that we wish
+ # to optimise for.
def retrieve_connection_pool(klass)
- class_to_pool[klass] ||= begin
+ class_to_pool[klass.name] ||= begin
until pool = pool_for(klass)
klass = klass.superclass
- break unless klass < ActiveRecord::Tag
+ break unless klass <= Base
end
- class_to_pool[klass] = pool || pool_for(ActiveRecord::Model)
+ class_to_pool[klass.name] = pool
end
end
@@ -585,21 +592,21 @@ module ActiveRecord
end
def pool_for(owner)
- owner_to_pool.fetch(owner) {
+ owner_to_pool.fetch(owner.name) {
if ancestor_pool = pool_from_any_process_for(owner)
# A connection was established in an ancestor process that must have
# subsequently forked. We can't reuse the connection, but we can copy
# the specification and establish a new connection with it.
establish_connection owner, ancestor_pool.spec
else
- owner_to_pool[owner] = nil
+ owner_to_pool[owner.name] = nil
end
}
end
def pool_from_any_process_for(owner)
- owner_to_pool = @owner_to_pool.values.find { |v| v[owner] }
- owner_to_pool && owner_to_pool[owner]
+ owner_to_pool = @owner_to_pool.values.find { |v| v[owner.name] }
+ owner_to_pool && owner_to_pool[owner.name]
end
end
@@ -617,7 +624,7 @@ module ActiveRecord
end
response
- rescue
+ rescue Exception
ActiveRecord::Base.clear_active_connections! unless testing
raise
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
index 30ccb8f0a4..c0a2111571 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_limits.rb
@@ -17,6 +17,15 @@ module ActiveRecord
64
end
+ # Returns the maximum allowed length for an index name. This
+ # limit is enforced by rails and Is less than or equal to
+ # <tt>index_name_length</tt>. The gap between
+ # <tt>index_name_length</tt> is to allow internal rails
+ # operations to use prefixes in temporary operations.
+ def allowed_index_name_length
+ index_name_length
+ end
+
# Returns the maximum length of an index name.
def index_name_length
64
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 0d7046a705..e1f29ea03a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/database_statements.rb
@@ -18,8 +18,7 @@ module ActiveRecord
end
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select_all(arel, name = nil, binds = [])
select(to_sql(arel, binds), name, binds)
end
@@ -27,8 +26,7 @@ module ActiveRecord
# Returns a record hash with the column names as keys and column values
# as values.
def select_one(arel, name = nil, binds = [])
- result = select_all(arel, name, binds)
- result.first if result
+ select_all(arel, name, binds).first
end
# Returns a single value from a record
@@ -41,8 +39,8 @@ module ActiveRecord
# Returns an array of the values of the first column in a select:
# select_values("SELECT id FROM companies LIMIT 3") => [1,2,3]
def select_values(arel, name = nil)
- result = select_rows(to_sql(arel, []), name)
- result.map { |v| v[0] }
+ select_rows(to_sql(arel, []), name)
+ .map { |v| v[0] }
end
# Returns an array of arrays containing the field values.
@@ -125,7 +123,8 @@ module ActiveRecord
# In order to get around this problem, #transaction will emulate the effect
# of nested transactions, by using savepoints:
# http://dev.mysql.com/doc/refman/5.0/en/savepoint.html
- # Savepoints are supported by MySQL and PostgreSQL, but not SQLite3.
+ # Savepoints are supported by MySQL and PostgreSQL. SQLite3 version >= '3.6.8'
+ # supports savepoints.
#
# It is safe to call this method if a database transaction is already open,
# i.e. if #transaction is called within another #transaction block. In case
@@ -150,7 +149,7 @@ module ActiveRecord
# already-automatically-released savepoints:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...)
# # active_record_1 now automatically released
# end # RELEASE SAVEPOINT active_record_1 <--- BOOM! database error!
@@ -287,7 +286,7 @@ module ActiveRecord
# Inserts the given fixture into the table. Overridden in adapters that require
# something beyond a simple insert (eg. Oracle).
def insert_fixture(fixture, table_name)
- columns = Hash[columns(table_name).map { |c| [c.name, c] }]
+ columns = schema_cache.columns_hash(table_name)
key_list = []
value_list = fixture.map do |name, value|
@@ -354,8 +353,7 @@ module ActiveRecord
subselect
end
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
end
undef_method :select
@@ -376,14 +374,14 @@ module ActiveRecord
update_sql(sql, name)
end
- def sql_for_insert(sql, pk, id_value, sequence_name, binds)
- [sql, binds]
- end
+ def sql_for_insert(sql, pk, id_value, sequence_name, binds)
+ [sql, binds]
+ end
- def last_inserted_id(result)
- row = result.rows.first
- row && row.first
- end
+ def last_inserted_id(result)
+ row = result.rows.first
+ row && row.first
+ end
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 be6fda95b4..8399232d73 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/query_cache.rb
@@ -9,10 +9,10 @@ module ActiveRecord
def dirties_query_cache(base, *method_names)
method_names.each do |method_name|
base.class_eval <<-end_code, __FILE__, __LINE__ + 1
- def #{method_name}(*) # def update_with_query_dirty(*args)
- clear_query_cache if @query_cache_enabled # clear_query_cache if @query_cache_enabled
- super # update_without_query_dirty(*args)
- end # end
+ def #{method_name}(*)
+ clear_query_cache if @query_cache_enabled
+ super
+ end
end_code
end
end
@@ -20,6 +20,12 @@ module ActiveRecord
attr_reader :query_cache, :query_cache_enabled
+ def initialize(*)
+ super
+ @query_cache = Hash.new { |h,sql| h[sql] = {} }
+ @query_cache_enabled = false
+ end
+
# Enable the query cache within the block.
def cache
old, @query_cache_enabled = @query_cache_enabled, true
@@ -75,16 +81,11 @@ module ActiveRecord
else
@query_cache[sql][binds] = yield
end
-
- # FIXME: we should guarantee that all cached items are Result
- # objects. Then we can avoid this conditional
- if ActiveRecord::Result === result
- result.dup
- else
- result.collect { |row| row.dup }
- end
+ result.dup
end
+ # If arel is locked this is a SELECT ... FOR UPDATE or somesuch. Such
+ # queries should not be cached.
def locked?(arel)
arel.respond_to?(:locked) && arel.locked
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
index 60a9eee7c7..552a22d28a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/quoting.rb
@@ -15,7 +15,6 @@ module ActiveRecord
return "'#{quote_string(value)}'" unless column
case column.type
- when :binary then "'#{quote_string(column.string_to_binary(value))}'"
when :integer then value.to_i.to_s
when :float then value.to_f.to_s
else
@@ -52,7 +51,6 @@ module ActiveRecord
return value unless column
case column.type
- when :binary then value
when :integer then value.to_i
when :float then value.to_f
else
@@ -93,6 +91,18 @@ module ActiveRecord
quote_column_name(table_name)
end
+ # Override to return the quoted table name for assignment. Defaults to
+ # table quoting.
+ #
+ # 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
+ # the column name (as per syntax requirements).
+ def quote_table_name_for_assignment(table, attr)
+ quote_table_name("#{table}.#{attr}")
+ end
+
def quoted_true
"'t'"
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
new file mode 100644
index 0000000000..25c17ce971
--- /dev/null
+++ b/activerecord/lib/active_record/connection_adapters/abstract/savepoints.rb
@@ -0,0 +1,21 @@
+module ActiveRecord
+ module ConnectionAdapters
+ module Savepoints #:nodoc:
+ def supports_savepoints?
+ true
+ end
+
+ def create_savepoint(name = current_savepoint_name)
+ execute("SAVEPOINT #{name}")
+ end
+
+ def rollback_to_savepoint(name = current_savepoint_name)
+ execute("ROLLBACK TO SAVEPOINT #{name}")
+ end
+
+ def release_savepoint(name = current_savepoint_name)
+ execute("RELEASE SAVEPOINT #{name}")
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
index dca355aa93..063b19871a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_definitions.rb
@@ -8,44 +8,28 @@ module ActiveRecord
# Abstract representation of an index definition on a table. Instances of
# this type are typically created and returned by methods in database
# adapters. e.g. ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter#indexes
- class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where) #:nodoc:
+ class IndexDefinition < Struct.new(:table, :name, :unique, :columns, :lengths, :orders, :where, :type, :using) #:nodoc:
end
# Abstract representation of a column definition. Instances of this type
# are typically created by methods in TableDefinition, and added to the
# +columns+ attribute of said TableDefinition object, in order to be used
# for generating a number of table creation or table changing SQL statements.
- class ColumnDefinition < Struct.new(:base, :name, :type, :limit, :precision, :scale, :default, :null) #:nodoc:
+ class ColumnDefinition < Struct.new(:name, :type, :limit, :precision, :scale, :default, :null, :first, :after, :primary_key) #:nodoc:
- def string_to_binary(value)
- value
+ def primary_key?
+ primary_key || type.to_sym == :primary_key
end
+ end
- def sql_type
- base.type_to_sql(type.to_sym, limit, precision, scale)
- end
-
- def to_sql
- column_sql = "#{base.quote_column_name(name)} #{sql_type}"
- column_options = {}
- column_options[:null] = null unless null.nil?
- column_options[:default] = default unless default.nil?
- add_column_options!(column_sql, column_options) unless type.to_sym == :primary_key
- column_sql
- end
-
- private
-
- def add_column_options!(sql, options)
- base.add_column_options!(sql, options.merge(:column => self))
- end
+ class ChangeColumnDefinition < Struct.new(:column, :type, :options) #:nodoc:
end
# Represents the schema of an SQL table in an abstract way. This class
# provides methods for manipulating the schema representation.
#
- # Inside migration files, the +t+ object in +create_table+ and
- # +change_table+ is actually of this type:
+ # Inside migration files, the +t+ object in +create_table+
+ # is actually of this type:
#
# class SomeMigration < ActiveRecord::Migration
# def up
@@ -64,28 +48,24 @@ module ActiveRecord
class TableDefinition
# An array of ColumnDefinition objects, representing the column changes
# that have been defined.
- attr_accessor :columns, :indexes
+ attr_accessor :indexes
+ attr_reader :name, :temporary, :options
- def initialize(base)
- @columns = []
+ def initialize(types, name, temporary, options)
@columns_hash = {}
@indexes = {}
- @base = base
+ @native = types
+ @temporary = temporary
+ @options = options
+ @name = name
end
- def xml(*args)
- raise NotImplementedError unless %w{
- sqlite mysql mysql2
- }.include? @base.adapter_name.downcase
-
- options = args.extract_options!
- column(args[0], :text, options)
- end
+ def columns; @columns_hash.values; end
# Appends a primary key definition to the table definition.
# Can be called multiple times, but this is probably not a good idea.
- def primary_key(name)
- column(name, :primary_key)
+ def primary_key(name, type = :primary_key, options = {})
+ column(name, type, options.merge(:primary_key => true))
end
# Returns a ColumnDefinition for the column with name +name+.
@@ -161,21 +141,21 @@ module ActiveRecord
# td.column(:granted, :boolean)
# # granted BOOLEAN
#
- # td.column(:picture, :binary, :limit => 2.megabytes)
+ # td.column(:picture, :binary, limit: 2.megabytes)
# # => picture BLOB(2097152)
#
- # td.column(:sales_stage, :string, :limit => 20, :default => 'new', :null => false)
+ # td.column(:sales_stage, :string, limit: 20, default: 'new', null: false)
# # => sales_stage VARCHAR(20) DEFAULT 'new' NOT NULL
#
- # td.column(:bill_gates_money, :decimal, :precision => 15, :scale => 2)
+ # td.column(:bill_gates_money, :decimal, precision: 15, scale: 2)
# # => bill_gates_money DECIMAL(15,2)
#
- # td.column(:sensor_reading, :decimal, :precision => 30, :scale => 20)
+ # td.column(:sensor_reading, :decimal, precision: 30, scale: 20)
# # => sensor_reading DECIMAL(30,20)
#
# # While <tt>:scale</tt> defaults to zero on most databases, it
# # probably wouldn't hurt to include it.
- # td.column(:huge_integer, :decimal, :precision => 30)
+ # td.column(:huge_integer, :decimal, precision: 30)
# # => huge_integer DECIMAL(30)
#
# # Defines a column with a database-specific type.
@@ -190,20 +170,20 @@ module ActiveRecord
#
# What can be written like this with the regular calls to column:
#
- # create_table "products", :force => true 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
+ # 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
# end
#
- # Can also be written as follows using the short-hand:
+ # can also be written as follows using the short-hand:
#
# create_table :products do |t|
# t.integer :shop_id, :creator_id
- # t.string :name, :value, :default => "Untitled"
+ # t.string :name, :value, default: "Untitled"
# t.timestamps
# end
#
@@ -218,51 +198,46 @@ module ActiveRecord
# create_table :taggings do |t|
# t.integer :tag_id, :tagger_id, :taggable_id
# t.string :tagger_type
- # t.string :taggable_type, :default => 'Photo'
+ # t.string :taggable_type, default: 'Photo'
# end
- # add_index :taggings, :tag_id, :name => 'index_taggings_on_tag_id'
+ # add_index :taggings, :tag_id, name: 'index_taggings_on_tag_id'
# add_index :taggings, [:tagger_id, :tagger_type]
#
# Can also be written as follows using references:
#
# create_table :taggings do |t|
- # t.references :tag, :index => { :name => 'index_taggings_on_tag_id' }
- # t.references :tagger, :polymorphic => true, :index => true
- # t.references :taggable, :polymorphic => { :default => 'Photo' }
+ # t.references :tag, index: { name: 'index_taggings_on_tag_id' }
+ # t.references :tagger, polymorphic: true, index: true
+ # t.references :taggable, polymorphic: { default: 'Photo' }
# end
def column(name, type, options = {})
name = name.to_s
type = type.to_sym
- column = self[name] || new_column_definition(@base, name, type)
-
- limit = options.fetch(:limit) do
- native[type][:limit] if native[type].is_a?(Hash)
+ if primary_key_column_name == name
+ raise ArgumentError, "you can't redefine the primary key column '#{name}'. To define a custom primary key, pass { id: false } to create_table."
end
- column.limit = limit
- column.precision = options[:precision]
- column.scale = options[:scale]
- column.default = options[:default]
- column.null = options[:null]
+ @columns_hash[name] = new_column_definition(name, type, options)
self
end
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- type = :'#{column_type}' # type = :string
- column_names.each { |name| column(name, type, options) } # column_names.each { |name| column(name, type, options) }
- end # end
- EOV
+ def remove_column(name)
+ @columns_hash.delete name.to_s
+ end
+
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ column_names = args
+ column_names.each { |name| column(name, column_type, options) }
+ end
end
# Adds index options to the indexes hash, keyed by column name
# This is primarily used to track indexes that need to be created after the table
#
- # index(:account_id, :name => 'index_projects_on_account_id')
+ # index(:account_id, name: 'index_projects_on_account_id')
def index(column_name, options = {})
indexes[column_name] = options
end
@@ -282,28 +257,58 @@ module ActiveRecord
args.each do |col|
column("#{col}_id", :integer, 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 : nil) if index_options
+ index(polymorphic ? %w(id type).map { |t| "#{col}_#{t}" } : "#{col}_id", index_options.is_a?(Hash) ? index_options : {}) if index_options
end
end
alias :belongs_to :references
- # Returns a String whose contents are the column definitions
- # concatenated together. This string can then be prepended and appended to
- # to generate the final SQL to create the table.
- def to_sql
- @columns.map { |c| c.to_sql } * ', '
+ def new_column_definition(name, type, options) # :nodoc:
+ column = create_column_definition name, type
+ limit = options.fetch(:limit) do
+ native[type][:limit] if native[type].is_a?(Hash)
+ end
+
+ column.limit = limit
+ column.array = options[:array] if column.respond_to?(:array)
+ column.precision = options[:precision]
+ column.scale = options[:scale]
+ column.default = options[:default]
+ column.null = options[:null]
+ column.first = options[:first]
+ column.after = options[:after]
+ column.primary_key = type == :primary_key || options[:primary_key]
+ column
end
private
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
+
+ def primary_key_column_name
+ primary_key_column = columns.detect { |c| c.primary_key? }
+ primary_key_column && primary_key_column.name
end
def native
- @base.native_database_types
+ @native
+ end
+ end
+
+ class AlterTable # :nodoc:
+ attr_reader :adds
+
+ def initialize(td)
+ @td = td
+ @adds = []
+ end
+
+ def name; @td.name; end
+
+ def add_column(name, type, options)
+ name = name.to_s
+ type = type.to_sym
+ @adds << @td.new_column_definition(name, type, options)
end
end
@@ -315,6 +320,7 @@ module ActiveRecord
# change_table :table do |t|
# t.column
# t.index
+ # t.rename_index
# t.timestamps
# t.change
# t.change_default
@@ -365,9 +371,9 @@ module ActiveRecord
# ====== Creating a simple index
# t.index(:name)
# ====== Creating a unique index
- # t.index([:branch_id, :party_id], :unique => true)
+ # t.index([:branch_id, :party_id], unique: true)
# ====== Creating a named index
- # t.index([:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
+ # t.index([:branch_id, :party_id], unique: true, name: 'by_branch_party')
def index(column_name, options = {})
@base.add_index(@table_name, column_name, options)
end
@@ -377,6 +383,13 @@ module ActiveRecord
@base.index_exists?(@table_name, column_name, options)
end
+ # Renames the given index on the table.
+ #
+ # t.rename_index(:user_id, :account_id)
+ def rename_index(index_name, new_index_name)
+ @base.rename_index(@table_name, index_name, new_index_name)
+ end
+
# Adds timestamps (+created_at+ and +updated_at+) columns to the table. See SchemaStatements#add_timestamps
#
# t.timestamps
@@ -387,7 +400,7 @@ module ActiveRecord
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
#
- # t.change(:name, :string, :limit => 80)
+ # t.change(:name, :string, limit: 80)
# t.change(:description, :text)
def change(column_name, type, options = {})
@base.change_column(@table_name, column_name, type, options)
@@ -406,7 +419,7 @@ module ActiveRecord
# t.remove(:qualification)
# t.remove(:qualification, :experience)
def remove(*column_names)
- @base.remove_column(@table_name, *column_names)
+ @base.remove_columns(@table_name, *column_names)
end
# Removes the given index from the table.
@@ -414,11 +427,11 @@ module ActiveRecord
# ====== Remove the index_table_name_on_column in the table_name table
# t.remove_index :column
# ====== Remove the index named index_table_name_on_branch_id in the table_name table
- # t.remove_index :column => :branch_id
+ # t.remove_index column: :branch_id
# ====== Remove the index named index_table_name_on_branch_id_and_party_id in the table_name table
- # t.remove_index :column => [:branch_id, :party_id]
+ # t.remove_index column: [:branch_id, :party_id]
# ====== Remove the index named by_branch_party in the table_name table
- # t.remove_index :name => :by_branch_party
+ # t.remove_index name: :by_branch_party
def remove_index(options = {})
@base.remove_index(@table_name, options)
end
@@ -469,27 +482,13 @@ module ActiveRecord
#
# t.string(:goat)
# t.string(:goat, :sheep)
- %w( string text integer float decimal datetime timestamp time date binary boolean ).each do |column_type|
- class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{column_type}(*args) # def string(*args)
- options = args.extract_options! # options = args.extract_options!
- column_names = args # column_names = args
- type = :'#{column_type}' # type = :string
- column_names.each do |name| # column_names.each do |name|
- column = ColumnDefinition.new(@base, name.to_s, type) # column = ColumnDefinition.new(@base, name, type)
- if options[:limit] # if options[:limit]
- column.limit = options[:limit] # column.limit = options[:limit]
- elsif native[type].is_a?(Hash) # elsif native[type].is_a?(Hash)
- column.limit = native[type][:limit] # column.limit = native[type][:limit]
- end # end
- column.precision = options[:precision] # column.precision = options[:precision]
- column.scale = options[:scale] # column.scale = options[:scale]
- column.default = options[:default] # column.default = options[:default]
- column.null = options[:null] # column.null = options[:null]
- @base.add_column(@table_name, name, column.sql_type, options) # @base.add_column(@table_name, name, column.sql_type, options)
- end # end
- end # end
- EOV
+ [:string, :text, :integer, :float, :decimal, :datetime, :timestamp, :time, :date, :binary, :boolean].each do |column_type|
+ define_method column_type do |*args|
+ options = args.extract_options!
+ args.each do |name|
+ @base.add_column(@table_name, name, column_type, options)
+ end
+ end
end
private
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 9d6111b51e..cdf0cbe218 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_dumper.rb
@@ -1,10 +1,12 @@
+require 'ipaddr'
+
module ActiveRecord
module ConnectionAdapters # :nodoc:
# The goal of this module is to move Adapter specific column
# definitions to the Adapter instead of having it in the schema
# dumper itself. This code represents the normal case.
- # We can then redefine how certain data types may be handled in the schema dumper on the
- # Adapter level by over-writing this code inside the database spececific adapters
+ # We can then redefine how certain data types may be handled in the schema dumper on the
+ # Adapter level by over-writing this code inside the database specific adapters
module ColumnDumper
def column_spec(column, types)
spec = prepare_column_options(column, types)
@@ -47,6 +49,18 @@ module ActiveRecord
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
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 86d6266af9..4b425494d0 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/schema_statements.rb
@@ -5,7 +5,7 @@ module ActiveRecord
module SchemaStatements
include ActiveRecord::Migration::JoinTable
- # Returns a Hash of mappings from the abstract data types to the native
+ # Returns a hash of mappings from the abstract data types to the native
# database types. See TableDefinition#column for details on the recognized
# abstract data types.
def native_database_types
@@ -20,6 +20,7 @@ module ActiveRecord
# Checks to see if the table +table_name+ exists on the database.
#
# table_exists?(:developers)
+ #
def table_exists?(table_name)
tables.include?(table_name.to_s)
end
@@ -29,17 +30,18 @@ module ActiveRecord
# Checks to see if an index exists on a table for a given index definition.
#
- # # Check an index exists
- # index_exists?(:suppliers, :company_id)
+ # # Check an index exists
+ # index_exists?(:suppliers, :company_id)
+ #
+ # # Check an index on multiple columns exists
+ # index_exists?(:suppliers, [:company_id, :company_type])
#
- # # Check an index on multiple columns exists
- # index_exists?(:suppliers, [:company_id, :company_type])
+ # # Check a unique index exists
+ # index_exists?(:suppliers, :company_id, unique: true)
#
- # # Check a unique index exists
- # index_exists?(:suppliers, :company_id, :unique => true)
+ # # Check an index with a custom name exists
+ # index_exists?(:suppliers, :company_id, name: "idx_company_id"
#
- # # Check an index with a custom name exists
- # 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)
@@ -56,17 +58,18 @@ module ActiveRecord
# Checks to see if a column exists in a given table.
#
- # # Check a column exists
- # column_exists?(:suppliers, :name)
+ # # Check a column exists
+ # column_exists?(:suppliers, :name)
+ #
+ # # Check a column exists of a particular type
+ # column_exists?(:suppliers, :name, :string)
#
- # # Check a column exists of a particular type
- # column_exists?(:suppliers, :name, :string)
+ # # Check a column exists with a specific definition
+ # column_exists?(:suppliers, :name, :string, limit: 100)
+ # column_exists?(:suppliers, :name, :string, default: 'default')
+ # column_exists?(:suppliers, :name, :string, null: false)
+ # column_exists?(:suppliers, :tax, :decimal, precision: 8, scale: 2)
#
- # # Check a column exists with a specific definition
- # column_exists?(:suppliers, :name, :string, limit: 100)
- # column_exists?(:suppliers, :name, :string, default: 'default')
- # column_exists?(:suppliers, :name, :string, null: false)
- # 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 &&
(!type || c.type == type) &&
@@ -84,27 +87,30 @@ module ActiveRecord
# form or the regular form, like this:
#
# === Block form
- # # create_table() passes a TableDefinition object to the block.
- # # This form will not only create the table, but also columns for the
- # # table.
#
- # create_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # # Other fields here
- # end
+ # # create_table() passes a TableDefinition object to the block.
+ # # This form will not only create the table, but also columns for the
+ # # table.
+ #
+ # create_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other fields here
+ # end
#
# === Block form, with shorthand
- # # You can also use the column types as method calls, rather than calling the column method.
- # create_table(:suppliers) do |t|
- # t.string :name, :limit => 60
- # # Other fields here
- # end
+ #
+ # # You can also use the column types as method calls, rather than calling the column method.
+ # create_table(:suppliers) do |t|
+ # t.string :name, limit: 60
+ # # Other fields here
+ # end
#
# === Regular form
- # # Creates a table called 'suppliers' with no columns.
- # create_table(:suppliers)
- # # Add a column to 'suppliers'.
- # add_column(:suppliers, :name, :string, {:limit => 60})
+ #
+ # # Creates a table called 'suppliers' with no columns.
+ # create_table(:suppliers)
+ # # Add a column to 'suppliers'.
+ # add_column(:suppliers, :name, :string, {limit: 60})
#
# The +options+ hash can include the following keys:
# [<tt>:id</tt>]
@@ -127,37 +133,53 @@ module ActiveRecord
# Defaults to false.
#
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_table(:suppliers, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_table(:suppliers, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE suppliers (
- # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE suppliers (
+ # id int(11) DEFAULT NULL auto_increment PRIMARY KEY
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
#
# ====== Rename the primary key column
- # create_table(:objects, :primary_key => 'guid') do |t|
- # t.column :name, :string, :limit => 80
- # end
+ #
+ # create_table(:objects, primary_key: 'guid') do |t|
+ # t.column :name, :string, limit: 80
+ # end
+ #
# generates:
- # CREATE TABLE objects (
- # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
- # name varchar(80)
- # )
+ #
+ # CREATE TABLE objects (
+ # guid int(11) DEFAULT NULL auto_increment PRIMARY KEY,
+ # name varchar(80)
+ # )
#
# ====== Do not add a primary key column
- # create_table(:categories_suppliers, :id => false) do |t|
- # t.column :category_id, :integer
- # t.column :supplier_id, :integer
- # end
+ #
+ # create_table(:categories_suppliers, id: false) do |t|
+ # t.column :category_id, :integer
+ # t.column :supplier_id, :integer
+ # end
+ #
# generates:
- # CREATE TABLE categories_suppliers (
- # category_id int,
- # supplier_id int
- # )
+ #
+ # CREATE TABLE categories_suppliers (
+ # category_id int,
+ # supplier_id int
+ # )
#
# See also TableDefinition#column for details on how to create columns.
def create_table(table_name, options = {})
- td = table_definition
- td.primary_key(options[:primary_key] || Base.get_primary_key(table_name.to_s.singularize)) unless options[:id] == false
+ td = create_table_definition table_name, options[:temporary], options[:options]
+
+ 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
+ end
yield td if block_given?
@@ -165,19 +187,15 @@ module ActiveRecord
drop_table(table_name, options)
end
- create_sql = "CREATE#{' TEMPORARY' if options[:temporary]} TABLE "
- create_sql << "#{quote_table_name(table_name)} ("
- create_sql << td.to_sql
- create_sql << ") #{options[:options]}"
- execute create_sql
+ execute schema_creation.accept td
td.indexes.each_pair { |c,o| add_index table_name, c, o }
end
# Creates a new join table with the name created using the lexical order of the first two
# arguments. These arguments can be a String or a Symbol.
#
- # # Creates a table called 'assemblies_parts' with no id.
- # create_join_table(:assemblies, :parts)
+ # # Creates a table called 'assemblies_parts' with no id.
+ # create_join_table(:assemblies, :parts)
#
# You can pass a +options+ hash can include the following keys:
# [<tt>:table_name</tt>]
@@ -192,13 +210,25 @@ module ActiveRecord
# Set to true to drop the table before creating it.
# Defaults to false.
#
+ # Note that +create_join_table+ does not create any indices by default; you can use
+ # its block form to do so yourself:
+ #
+ # create_join_table :products, :categories do |t|
+ # t.index :product_id
+ # t.index :category_id
+ # end
+ #
# ====== Add a backend specific option to the generated SQL (MySQL)
- # create_join_table(:assemblies, :parts, :options => 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
+ # create_join_table(:assemblies, :parts, options: 'ENGINE=InnoDB DEFAULT CHARSET=utf8')
+ #
# generates:
- # CREATE TABLE assemblies_parts (
- # assembly_id int NOT NULL,
- # part_id int NOT NULL,
- # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
+ # CREATE TABLE assemblies_parts (
+ # assembly_id int NOT NULL,
+ # part_id int NOT NULL,
+ # ) ENGINE=InnoDB DEFAULT CHARSET=utf8
+ #
def create_join_table(table_1, table_2, options = {})
join_table_name = find_join_table_name(table_1, table_2, options)
@@ -214,86 +244,110 @@ module ActiveRecord
end
end
+ # Drops the join table specified by the given arguments.
+ # See +create_join_table+ for details.
+ #
+ # Although this command ignores the block if one is given, it can be helpful
+ # to provide one in a migration's +change+ method so it can be reverted.
+ # In that case, the block will be used by create_join_table.
+ def drop_join_table(table_1, table_2, options = {})
+ join_table_name = find_join_table_name(table_1, table_2, options)
+ drop_table(join_table_name)
+ end
+
# A block for changing columns in +table+.
#
- # # change_table() yields a Table instance
- # change_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # # Other column alterations here
- # end
+ # # change_table() yields a Table instance
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # # Other column alterations here
+ # end
#
# The +options+ hash can include the following keys:
# [<tt>:bulk</tt>]
# Set this to true to make this a bulk alter query, such as
- # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
+ #
+ # ALTER TABLE `users` ADD COLUMN age INT(11), ADD COLUMN birthdate DATETIME ...
#
# Defaults to false.
#
# ====== Add a column
- # change_table(:suppliers) do |t|
- # t.column :name, :string, :limit => 60
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.column :name, :string, limit: 60
+ # end
#
# ====== Add 2 integer columns
- # change_table(:suppliers) do |t|
- # t.integer :width, :height, :null => false, :default => 0
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.integer :width, :height, null: false, default: 0
+ # end
#
# ====== Add created_at/updated_at columns
- # change_table(:suppliers) do |t|
- # t.timestamps
- # end
+ #
+ # change_table(:suppliers) do |t|
+ # t.timestamps
+ # end
#
# ====== Add a foreign key column
- # change_table(:suppliers) do |t|
- # t.references :company
- # end
#
- # Creates a <tt>company_id(integer)</tt> column
+ # change_table(:suppliers) do |t|
+ # t.references :company
+ # end
+ #
+ # Creates a <tt>company_id(integer)</tt> column.
#
# ====== Add a polymorphic foreign key column
+ #
# change_table(:suppliers) do |t|
- # t.belongs_to :company, :polymorphic => true
+ # t.belongs_to :company, polymorphic: true
# end
#
- # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns
+ # Creates <tt>company_type(varchar)</tt> and <tt>company_id(integer)</tt> columns.
#
# ====== Remove a column
+ #
# change_table(:suppliers) do |t|
# t.remove :company
# end
#
# ====== Remove several columns
+ #
# change_table(:suppliers) do |t|
# t.remove :company_id
# t.remove :width, :height
# end
#
# ====== Remove an index
+ #
# change_table(:suppliers) do |t|
# t.remove_index :company_id
# end
#
- # See also Table for details on
- # all of the various column transformation
+ # See also Table for details on all of the various column transformation.
def change_table(table_name, options = {})
if supports_bulk_alter? && options[:bulk]
recorder = ActiveRecord::Migration::CommandRecorder.new(self)
- yield Table.new(table_name, recorder)
+ yield update_table_definition(table_name, recorder)
bulk_change_table(table_name, recorder.commands)
else
- yield Table.new(table_name, self)
+ yield update_table_definition(table_name, self)
end
end
# Renames a table.
#
- # rename_table('octopuses', 'octopi')
+ # rename_table('octopuses', 'octopi')
+ #
def rename_table(table_name, new_name)
raise NotImplementedError, "rename_table is not implemented"
end
# Drops a table from the database.
+ #
+ # Although this command ignores +options+ and the block if one is given, it can be helpful
+ # to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +options+ and the block will be used by create_table.
def drop_table(table_name, options = {})
execute "DROP TABLE #{quote_table_name(table_name)}"
end
@@ -301,41 +355,80 @@ module ActiveRecord
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- execute(add_column_sql)
+ at = create_alter_table table_name
+ at.add_column(column_name, type, options)
+ execute schema_creation.accept at
+ end
+
+ # Removes the given columns from the table definition.
+ #
+ # remove_columns(:suppliers, :qualification, :experience)
+ #
+ def remove_columns(table_name, *column_names)
+ raise ArgumentError.new("You must specify at least one column name. Example: remove_columns(:people, :first_name)") if column_names.empty?
+ column_names.each do |column_name|
+ remove_column(table_name, column_name)
+ end
end
- # Removes the column(s) from the table definition.
+ # Removes the column from the table definition.
+ #
+ # remove_column(:suppliers, :qualification)
#
- # remove_column(:suppliers, :qualification)
- # remove_columns(:suppliers, :qualification, :experience)
- def remove_column(table_name, *column_names)
- columns_for_remove(table_name, *column_names).each {|column_name| execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{column_name}" }
+ # The +type+ and +options+ parameters will be ignored if present. It can be helpful
+ # to provide these in a migration's +change+ method so it can be reverted.
+ # In that case, +type+ and +options+ will be used by add_column.
+ def remove_column(table_name, column_name, type = nil, options = {})
+ execute "ALTER TABLE #{quote_table_name(table_name)} DROP #{quote_column_name(column_name)}"
end
- alias :remove_columns :remove_column
# Changes the column's definition according to the new options.
# See TableDefinition#column for details of the options you can use.
#
- # change_column(:suppliers, :name, :string, :limit => 80)
- # change_column(:accounts, :description, :text)
+ # change_column(:suppliers, :name, :string, limit: 80)
+ # change_column(:accounts, :description, :text)
+ #
def change_column(table_name, column_name, type, options = {})
raise NotImplementedError, "change_column is not implemented"
end
- # Sets a new default value for a column.
+ # Sets a new default value for a column:
+ #
+ # change_column_default(:suppliers, :qualification, 'new')
+ # change_column_default(:accounts, :authorized, 1)
+ #
+ # Setting the default to +nil+ effectively drops the default:
+ #
+ # change_column_default(:users, :email, nil)
#
- # change_column_default(:suppliers, :qualification, 'new')
- # change_column_default(:accounts, :authorized, 1)
- # change_column_default(:users, :email, nil)
def change_column_default(table_name, column_name, default)
raise NotImplementedError, "change_column_default is not implemented"
end
+ # Sets or removes a +NOT NULL+ constraint on a column. The +null+ flag
+ # indicates whether the value can be +NULL+. For example
+ #
+ # change_column_null(:users, :nickname, false)
+ #
+ # says nicknames cannot be +NULL+ (adds the constraint), whereas
+ #
+ # change_column_null(:users, :nickname, true)
+ #
+ # allows them to be +NULL+ (drops the constraint).
+ #
+ # The method accepts an optional fourth argument to replace existing
+ # +NULL+s with some other value. Use that one when enabling the
+ # constraint if needed, since otherwise those rows would not be valid.
+ #
+ # Please note the fourth argument does not set a column's default.
+ def change_column_null(table_name, column_name, null, default = nil)
+ raise NotImplementedError, "change_column_null is not implemented"
+ end
+
# Renames a column.
#
- # rename_column(:suppliers, :description, :name)
+ # rename_column(:suppliers, :description, :name)
+ #
def rename_column(table_name, column_name, new_column_name)
raise NotImplementedError, "rename_column is not implemented"
end
@@ -347,60 +440,106 @@ module ActiveRecord
# you pass <tt>:name</tt> as an option.
#
# ====== Creating a simple index
- # add_index(:suppliers, :name)
- # generates
- # CREATE INDEX suppliers_name_index ON suppliers(name)
+ #
+ # add_index(:suppliers, :name)
+ #
+ # generates:
+ #
+ # CREATE INDEX suppliers_name_index ON suppliers(name)
#
# ====== Creating a unique index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true)
- # generates
- # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true)
+ #
+ # generates:
+ #
+ # CREATE UNIQUE INDEX accounts_branch_id_party_id_index ON accounts(branch_id, party_id)
#
# ====== Creating a named index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true, :name => 'by_branch_party')
- # generates
+ #
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, name: 'by_branch_party')
+ #
+ # generates:
+ #
# CREATE UNIQUE INDEX by_branch_party ON accounts(branch_id, party_id)
#
# ====== Creating an index with specific key length
- # add_index(:accounts, :name, :name => 'by_name', :length => 10)
- # generates
- # CREATE INDEX by_name ON accounts(name(10))
#
- # add_index(:accounts, [:name, :surname], :name => 'by_name_surname', :length => {:name => 10, :surname => 15})
- # generates
- # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ # add_index(:accounts, :name, name: 'by_name', length: 10)
+ #
+ # generates:
#
- # Note: SQLite doesn't support index length
+ # CREATE INDEX by_name ON accounts(name(10))
+ #
+ # add_index(:accounts, [:name, :surname], name: 'by_name_surname', length: {name: 10, surname: 15})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_name_surname ON accounts(name(10), surname(15))
+ #
+ # Note: SQLite doesn't support index length.
#
# ====== Creating an index with a sort order (desc or asc, asc is the default)
- # add_index(:accounts, [:branch_id, :party_id, :surname], :order => {:branch_id => :desc, :party_id => :asc})
- # generates
- # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
#
- # Note: mysql doesn't yet support index order (it accepts the syntax but ignores it)
+ # add_index(:accounts, [:branch_id, :party_id, :surname], order: {branch_id: :desc, party_id: :asc})
+ #
+ # generates:
+ #
+ # CREATE INDEX by_branch_desc_party ON accounts(branch_id DESC, party_id ASC, surname)
+ #
+ # Note: MySQL doesn't yet support index order (it accepts the syntax but ignores it).
#
# ====== Creating a partial index
- # add_index(:accounts, [:branch_id, :party_id], :unique => true, :where => "active")
- # generates
- # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
#
- # Note: only supported by PostgreSQL
+ # add_index(:accounts, [:branch_id, :party_id], unique: true, where: "active")
#
+ # generates:
+ #
+ # CREATE UNIQUE INDEX index_accounts_on_branch_id_and_party_id ON accounts(branch_id, party_id) WHERE active
+ #
+ # ====== Creating an index with a specific method
+ #
+ # add_index(:developers, :name, using: 'btree')
+ #
+ # generates:
+ #
+ # CREATE INDEX index_developers_on_name ON developers USING btree (name) -- PostgreSQL
+ # CREATE INDEX index_developers_on_name USING btree ON developers (name) -- MySQL
+ #
+ # Note: only supported by PostgreSQL and MySQL
+ #
+ # ====== Creating an index with a specific type
+ #
+ # add_index(:developers, :name, type: :fulltext)
+ #
+ # generates:
+ #
+ # CREATE FULLTEXT INDEX index_developers_on_name ON developers (name) -- MySQL
+ #
+ # Note: only supported by MySQL. Supported: <tt>:fulltext</tt> and <tt>:spatial</tt> on MyISAM tables.
def add_index(table_name, column_name, options = {})
index_name, index_type, index_columns, index_options = add_index_options(table_name, column_name, options)
execute "CREATE #{index_type} INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} (#{index_columns})#{index_options}"
end
- # Remove the given index from the table.
+ # Removes the given index from the table.
+ #
+ # Removes the +index_accounts_on_column+ in the +accounts+ table.
#
- # Remove the index_accounts_on_column in the accounts table.
# remove_index :accounts, :column
- # Remove the index named index_accounts_on_branch_id in the accounts table.
- # remove_index :accounts, :column => :branch_id
- # Remove the index named index_accounts_on_branch_id_and_party_id in the accounts table.
- # remove_index :accounts, :column => [:branch_id, :party_id]
- # Remove the index named by_branch_party in the accounts table.
- # remove_index :accounts, :name => :by_branch_party
+ #
+ # Removes the index named +index_accounts_on_branch_id+ in the +accounts+ table.
+ #
+ # remove_index :accounts, column: :branch_id
+ #
+ # Removes the index named +index_accounts_on_branch_id_and_party_id+ in the +accounts+ table.
+ #
+ # remove_index :accounts, column: [:branch_id, :party_id]
+ #
+ # Removes the index named +by_branch_party+ in the +accounts+ table.
+ #
+ # remove_index :accounts, name: :by_branch_party
+ #
def remove_index(table_name, options = {})
remove_index!(table_name, index_name_for_remove(table_name, options))
end
@@ -409,10 +548,12 @@ module ActiveRecord
execute "DROP INDEX #{quote_column_name(index_name)} ON #{quote_table_name(table_name)}"
end
- # Rename an index.
+ # Renames an index.
+ #
+ # Rename the +index_people_on_last_name+ index to +index_users_on_last_name+:
#
- # Rename the index_people_on_last_name index to index_users_on_last_name
# rename_index :people, 'index_people_on_last_name', 'index_users_on_last_name'
+ #
def rename_index(table_name, old_name, new_name)
# this is a naive implementation; some DBs may support this more efficiently (Postgres, for instance)
old_index_def = indexes(table_name).detect { |i| i.name == old_name }
@@ -422,7 +563,7 @@ module ActiveRecord
end
def index_name(table_name, options) #:nodoc:
- if Hash === options # legacy support
+ if Hash === options
if options[:column]
"index_#{table_name}_on_#{Array(options[:column]) * '_and_'}"
elsif options[:name]
@@ -435,7 +576,7 @@ module ActiveRecord
end
end
- # Verify the existence of an index with a given name.
+ # Verifies the existence of an index with a given name.
#
# The default argument is returned if the underlying implementation does not define the indexes method,
# as there's no way to determine the correct answer in that case.
@@ -449,20 +590,23 @@ module ActiveRecord
# <tt>add_reference</tt> and <tt>add_belongs_to</tt> are acceptable.
#
# ====== Create a user_id column
- # add_reference(:products, :user)
+ #
+ # add_reference(:products, :user)
#
# ====== Create a supplier_id and supplier_type columns
- # add_belongs_to(:products, :supplier, polymorphic: true)
+ #
+ # add_belongs_to(:products, :supplier, polymorphic: true)
#
# ====== Create a supplier_id, supplier_type columns and appropriate index
- # add_reference(:products, :supplier, polymorphic: true, index: true)
+ #
+ # add_reference(:products, :supplier, polymorphic: true, index: true)
#
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)
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 : nil) if index_options
+ 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
alias :add_belongs_to :add_reference
@@ -470,10 +614,12 @@ module ActiveRecord
# <tt>remove_reference</tt>, <tt>remove_references</tt> and <tt>remove_belongs_to</tt> are acceptable.
#
# ====== Remove the reference
- # remove_reference(:products, :user, index: true)
+ #
+ # remove_reference(:products, :user, index: true)
#
# ====== Remove polymorphic reference
- # remove_reference(:products, :supplier, polymorphic: true)
+ #
+ # remove_reference(:products, :supplier, polymorphic: true)
#
def remove_reference(table_name, ref_name, options = {})
remove_column(table_name, "#{ref_name}_id")
@@ -481,11 +627,6 @@ module ActiveRecord
end
alias :remove_belongs_to :remove_reference
- # Returns a string of <tt>CREATE TABLE</tt> SQL statement(s) for recreating the
- # entire structure of the database.
- def structure_dump
- end
-
def dump_schema_information #:nodoc:
sm_table = ActiveRecord::Migrator.schema_migrations_table_name
@@ -540,7 +681,7 @@ module ActiveRecord
column_type_sql << "(#{precision})"
end
elsif scale
- raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale if specified"
+ raise ArgumentError, "Error adding decimal column: precision cannot be empty if scale is specified"
end
elsif (type != :primary_key) && (limit ||= native.is_a?(Hash) && native[:limit])
@@ -553,33 +694,28 @@ module ActiveRecord
end
end
- def add_column_options!(sql, options) #:nodoc:
- sql << " DEFAULT #{quote(options[:default], options[:column])}" if options_include_default?(options)
- # must explicitly check for :null to allow change_column to work on migrations
- if options[:null] == false
- sql << " NOT NULL"
- end
- end
-
- # SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax.
+ # Given a set of columns and an ORDER BY clause, returns the columns for a SELECT DISTINCT.
+ # Both PostgreSQL and Oracle overrides this for custom DISTINCT syntax - they
+ # require the order columns appear in the SELECT.
#
- # distinct("posts.id", "posts.created_at desc")
- def distinct(columns, order_by)
- "DISTINCT #{columns}"
+ # columns_for_distinct("posts.id", ["posts.created_at desc"])
+ def columns_for_distinct(columns, orders) # :nodoc:
+ columns
end
- # Adds timestamps (created_at and updated_at) columns to the named table.
+ # Adds timestamps (+created_at+ and +updated_at+) columns to the named table.
+ #
+ # add_timestamps(:suppliers)
#
- # add_timestamps(:suppliers)
def add_timestamps(table_name)
add_column table_name, :created_at, :datetime
add_column table_name, :updated_at, :datetime
end
- # Removes the timestamp columns (created_at and updated_at) from the table definition.
+ # Removes the timestamp columns (+created_at+ and +updated_at+) from the table definition.
#
# remove_timestamps(:suppliers)
+ #
def remove_timestamps(table_name)
remove_column table_name, :updated_at
remove_column table_name, :created_at
@@ -617,47 +753,89 @@ module ActiveRecord
def add_index_options(table_name, column_name, options = {})
column_names = Array(column_name)
- index_name = index_name(table_name, :column => column_names)
+ index_name = index_name(table_name, column: column_names)
- if Hash === options # legacy support, since this param was a string
- index_type = options[:unique] ? "UNIQUE" : ""
- index_name = options[:name].to_s if options.key?(:name)
- if supports_partial_index?
- index_options = options[:where] ? " WHERE #{options[:where]}" : ""
- end
- else
- index_type = options
+ 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 > index_name_length
- raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' is too long; the limit is #{index_name_length} characters"
+ 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]
+ [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)
unless index_name_exists?(table_name, index_name, true)
+ if options.is_a?(Hash) && options.has_key?(:name)
+ options_without_column = options.dup
+ options_without_column.delete :column
+ index_name_without_column = index_name(table_name, options_without_column)
+
+ return index_name_without_column if index_name_exists?(table_name, index_name_without_column, false)
+ end
+
raise ArgumentError, "Index name '#{index_name}' on table '#{table_name}' does not exist"
end
index_name
end
- def columns_for_remove(table_name, *column_names)
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.blank?
- column_names.map {|column_name| quote_column_name(column_name) }
+ def rename_table_indexes(table_name, new_name)
+ indexes(new_name).each do |index|
+ generated_index_name = index_name(table_name, column: index.columns)
+ if generated_index_name == index.name
+ rename_index new_name, generated_index_name, index_name(new_name, column: index.columns)
+ end
+ end
+ end
+
+ def rename_column_indexes(table_name, column_name, new_column_name)
+ column_name, new_column_name = column_name.to_s, new_column_name.to_s
+ indexes(table_name).each do |index|
+ next unless index.columns.include?(new_column_name)
+ old_columns = index.columns.dup
+ old_columns[old_columns.index(new_column_name)] = column_name
+ generated_index_name = index_name(table_name, column: old_columns)
+ if generated_index_name == index.name
+ rename_index table_name, generated_index_name, index_name(table_name, column: index.columns)
+ end
+ end
end
private
- def table_definition
- TableDefinition.new(self)
+ def create_table_definition(name, temporary, options)
+ TableDefinition.new native_database_types, name, temporary, options
+ end
+
+ def create_alter_table(name)
+ AlterTable.new create_table_definition(name, false, {})
+ end
+
+ def update_table_definition(table_name, base)
+ Table.new(table_name, base)
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
index 4cca94e40b..2b6685499a 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract/transaction.rb
@@ -5,6 +5,37 @@ module ActiveRecord
def initialize(connection)
@connection = connection
+ @state = TransactionState.new
+ end
+
+ def state
+ @state
+ end
+ end
+
+ class TransactionState
+ attr_accessor :parent
+
+ VALID_STATES = Set.new([:committed, :rolledback, nil])
+
+ def initialize(state = nil)
+ @state = state
+ @parent = nil
+ end
+
+ def committed?
+ @state == :committed
+ end
+
+ def rolledback?
+ @state == :rolledback
+ end
+
+ def set_state(state)
+ if !VALID_STATES.include?(state)
+ raise ArgumentError, "Invalid transaction state: #{state}"
+ end
+ @state = state
end
end
@@ -47,7 +78,7 @@ module ActiveRecord
@joinable = options.fetch(:joinable, true)
end
- # This state is necesarry so that we correctly handle stuff that might
+ # 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?
@@ -87,10 +118,15 @@ module ActiveRecord
end
def add_record(record)
- records << record
+ if record.has_transactional_callbacks?
+ records << record
+ else
+ record.set_transaction_state(@state)
+ end
end
def rollback_records
+ @state.set_state(:rolledback)
records.uniq.each do |record|
begin
record.rolledback!(parent.closed?)
@@ -101,6 +137,7 @@ module ActiveRecord
end
def commit_records
+ @state.set_state(:committed)
records.uniq.each do |record|
begin
record.committed!
@@ -157,8 +194,9 @@ module ActiveRecord
end
def perform_commit
+ @state.set_state(:committed)
+ @state.parent = parent.state
connection.release_savepoint
- records.each { |r| parent.add_record(r) }
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 0cb219767b..cbe563676b 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_adapter.rb
@@ -3,9 +3,8 @@ require 'bigdecimal'
require 'bigdecimal/util'
require 'active_support/core_ext/benchmark'
require 'active_record/connection_adapters/schema_cache'
-require 'active_record/connection_adapters/abstract/schema_dumper'
+require 'active_record/connection_adapters/abstract/schema_dumper'
require 'monitor'
-require 'active_support/deprecation'
module ActiveRecord
module ConnectionAdapters # :nodoc:
@@ -19,6 +18,7 @@ module ActiveRecord
autoload :ColumnDefinition
autoload :TableDefinition
autoload :Table
+ autoload :AlterTable
end
autoload_at 'active_record/connection_adapters/abstract/connection_pool' do
@@ -33,6 +33,7 @@ module ActiveRecord
autoload :Quoting
autoload :ConnectionPool
autoload :QueryCache
+ autoload :Savepoints
end
autoload_at 'active_record/connection_adapters/abstract/transaction' do
@@ -62,12 +63,30 @@ module ActiveRecord
include MonitorMixin
include ColumnDumper
+ SIMPLE_INT = /\A\d+\z/
+
define_callbacks :checkout, :checkin
attr_accessor :visitor, :pool
attr_reader :schema_cache, :last_use, :in_use, :logger
alias :in_use? :in_use
+ def self.type_cast_config_to_integer(config)
+ if config =~ SIMPLE_INT
+ config.to_i
+ else
+ config
+ end
+ end
+
+ def self.type_cast_config_to_boolean(config)
+ if config == "false"
+ false
+ else
+ config
+ end
+ end
+
def initialize(connection, logger = nil, pool = nil) #:nodoc:
super()
@@ -77,10 +96,95 @@ module ActiveRecord
@last_use = false
@logger = logger
@pool = pool
- @query_cache = Hash.new { |h,sql| h[sql] = {} }
- @query_cache_enabled = false
@schema_cache = SchemaCache.new self
@visitor = nil
+ @prepared_statements = false
+ end
+
+ def valid_type?(type)
+ true
+ end
+
+ class SchemaCreation
+ def initialize(conn)
+ @conn = conn
+ @cache = {}
+ end
+
+ def accept(o)
+ m = @cache[o.class] ||= "visit_#{o.class.name.split('::').last}"
+ send m, o
+ end
+
+ def visit_AddColumn(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ sql = "ADD #{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(sql, column_options(o))
+ end
+
+ private
+
+ def visit_AlterTable(o)
+ sql = "ALTER TABLE #{quote_table_name(o.name)} "
+ sql << o.adds.map { |col| visit_AddColumn col }.join(' ')
+ end
+
+ def visit_ColumnDefinition(o)
+ sql_type = type_to_sql(o.type.to_sym, o.limit, o.precision, o.scale)
+ column_sql = "#{quote_column_name(o.name)} #{sql_type}"
+ add_column_options!(column_sql, column_options(o)) unless o.primary_key?
+ column_sql
+ end
+
+ def visit_TableDefinition(o)
+ create_sql = "CREATE#{' TEMPORARY' if o.temporary} TABLE "
+ create_sql << "#{quote_table_name(o.name)} ("
+ create_sql << o.columns.map { |c| accept c }.join(', ')
+ create_sql << ") #{o.options}"
+ create_sql
+ end
+
+ def column_options(o)
+ column_options = {}
+ column_options[:null] = o.null unless o.null.nil?
+ column_options[:default] = o.default unless o.default.nil?
+ column_options[:column] = o
+ column_options[:first] = o.first
+ column_options[:after] = o.after
+ column_options
+ end
+
+ def quote_column_name(name)
+ @conn.quote_column_name name
+ end
+
+ def quote_table_name(name)
+ @conn.quote_table_name name
+ end
+
+ def type_to_sql(type, limit, precision, scale)
+ @conn.type_to_sql type.to_sym, limit, precision, scale
+ end
+
+ def add_column_options!(sql, options)
+ sql << " DEFAULT #{@conn.quote(options[:default], options[:column])}" if options_include_default?(options)
+ # must explicitly check for :null to allow change_column to work on migrations
+ if options[:null] == false
+ sql << " NOT NULL"
+ end
+ if options[:auto_increment] == true
+ sql << " AUTO_INCREMENT"
+ end
+ sql
+ end
+
+ def options_include_default?(options)
+ options.include?(:default) && !(options[:null] == false && options[:default].nil?)
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
end
def lease
@@ -101,6 +205,18 @@ module ActiveRecord
@in_use = false
end
+ def unprepared_visitor
+ self.class::BindSubstitution.new self
+ 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
+ end
+
# Returns the human-readable name of the adapter. Use mixed case - one
# can always use downcase if needed.
def adapter_name
@@ -172,10 +288,36 @@ module ActiveRecord
false
end
+ # Does this adapter support database extensions? As of this writing only
+ # postgresql does.
+ def supports_extensions?
+ false
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def disable_extension(name)
+ end
+
+ # This is meant to be implemented by the adapters that support extensions
+ def enable_extension(name)
+ end
+
+ # A list of extensions, to be filled in by adapters that support them. At
+ # the moment only postgresql does.
+ def extensions
+ []
+ end
+
+ # A list of index algorithms, to be filled by adapters that support them.
+ # MySQL and PostgreSQL have support for them right now.
+ def index_algorithms
+ {}
+ end
+
# QUOTING ==================================================
- # Returns a bind substitution value given a +column+ and list of current
- # +binds+
+ # Returns a bind substitution value given a bind +index+ and +column+
+ # NOTE: The column param is currently being used by the sqlserver-adapter
def substitute_at(column, index)
Arel::Nodes::BindParam.new '?'
end
@@ -254,26 +396,13 @@ module ActiveRecord
@transaction.number
end
- def increment_open_transactions
- ActiveSupport::Deprecation.warn "#increment_open_transactions is deprecated and has no effect"
+ def create_savepoint(name = nil)
end
- def decrement_open_transactions
- ActiveSupport::Deprecation.warn "#decrement_open_transactions is deprecated and has no effect"
+ def rollback_to_savepoint(name = nil)
end
- def transaction_joinable=(joinable)
- ActiveSupport::Deprecation.warn "#transaction_joinable= is deprecated. Please pass the :joinable option to #begin_transaction instead."
- @transaction.joinable = joinable
- end
-
- def create_savepoint
- end
-
- def rollback_to_savepoint
- end
-
- def release_savepoint
+ def release_savepoint(name = nil)
end
def case_sensitive_modifier(node)
@@ -295,13 +424,14 @@ module ActiveRecord
protected
- def log(sql, name = "SQL", binds = [])
+ def log(sql, name = "SQL", binds = [], statement_name = nil)
@instrumenter.instrument(
"sql.active_record",
- :sql => sql,
- :name => name,
- :connection_id => object_id,
- :binds => binds) { yield }
+ :sql => sql,
+ :name => name,
+ :connection_id => object_id,
+ :statement_name => statement_name,
+ :binds => binds) { yield }
rescue => e
message = "#{e.class.name}: #{e.message}: #{sql}"
@logger.error message if @logger
@@ -312,7 +442,11 @@ module ActiveRecord
def translate_exception(exception, message)
# override in derived class
- ActiveRecord::StatementInvalid.new(message)
+ ActiveRecord::StatementInvalid.new(message, exception)
+ end
+
+ def without_prepared_statement?(binds)
+ !@prepared_statements || binds.empty?
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 8c83c4f5db..138ab811dc 100644
--- a/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb
@@ -3,18 +3,52 @@ require 'arel/visitors/bind_visitor'
module ActiveRecord
module ConnectionAdapters
class AbstractMysqlAdapter < AbstractAdapter
+ include Savepoints
+
+ class SchemaCreation < AbstractAdapter::SchemaCreation
+
+ def visit_AddColumn(o)
+ add_column_position!(super, column_options(o))
+ end
+
+ private
+ 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_position!(change_column_sql, options)
+ end
+
+ def add_column_position!(sql, options)
+ if options[:first]
+ sql << " FIRST"
+ elsif options[:after]
+ sql << " AFTER #{quote_column_name(options[:after])}"
+ end
+ sql
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
class Column < ConnectionAdapters::Column # :nodoc:
- attr_reader :collation
+ attr_reader :collation, :strict, :extra
- def initialize(name, default, sql_type = nil, null = true, collation = nil)
- super(name, default, sql_type, null)
+ def initialize(name, default, sql_type = nil, null = true, collation = nil, strict = false, extra = "")
+ @strict = strict
@collation = collation
+ @extra = extra
+ super(name, default, sql_type, null)
end
def extract_default(default)
- if sql_type =~ /blob/i || type == :text
+ if blob_or_text_column?
if default.blank?
- return null ? nil : ''
+ null || strict ? nil : ''
else
raise ArgumentError, "#{type} columns cannot have a default value: #{default.inspect}"
end
@@ -26,12 +60,12 @@ module ActiveRecord
end
def has_default?
- return false if sql_type =~ /blob/i || type == :text #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
- def explicit_default?
- !null && (sql_type =~ /blob/i || type == :text)
+ def blob_or_text_column?
+ sql_type =~ /blob/i || type == :text
end
# Must return the relevant concrete adapter
@@ -59,6 +93,8 @@ module ActiveRecord
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
@@ -75,8 +111,6 @@ module ActiveRecord
when /^mediumint/i; 3
when /^smallint/i; 2
when /^tinyint/i; 1
- when /^enum\((.+)\)/i
- $1.split(',').map{|enum| enum.strip.length - 2}.max
else
super
end
@@ -128,6 +162,9 @@ module ActiveRecord
:boolean => { :name => "tinyint", :limit => 1 }
}
+ INDEX_TYPES = [:fulltext, :spatial]
+ INDEX_USINGS = [:btree, :hash]
+
class BindSubstitution < Arel::Visitors::MySQL # :nodoc:
include Arel::Visitors::BindVisitor
end
@@ -138,10 +175,11 @@ module ActiveRecord
@connection_options, @config = connection_options, config
@quoted_column_names, @quoted_table_names = {}, {}
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::MySQL.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
end
@@ -158,11 +196,6 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
def supports_bulk_alter? #:nodoc:
true
end
@@ -185,6 +218,10 @@ module ActiveRecord
NATIVE_DATABASE_TYPES
end
+ def index_algorithms
+ { default: 'ALGORITHM = DEFAULT', copy: 'ALGORITHM = COPY', inplace: 'ALGORITHM = INPLACE' }
+ end
+
# HELPER METHODS ===========================================
# The two drivers have slightly different ways of yielding hashes of results, so
@@ -194,8 +231,8 @@ module ActiveRecord
end
# Overridden by the adapters to instantiate their specific Column type.
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ def new_column(field, default, type, null, collation, extra = "") # :nodoc:
+ Column.new(field, default, type, null, collation, extra)
end
# Must return the Mysql error number from the exception, if the exception has an
@@ -207,8 +244,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
elsif value.kind_of?(BigDecimal)
value.to_s("F")
@@ -257,7 +294,7 @@ module ActiveRecord
end
rescue ActiveRecord::StatementInvalid => exception
if exception.message.split(":").first =~ /Packets out of order/
- raise ActiveRecord::StatementInvalid, "'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings."
+ raise ActiveRecord::StatementInvalid.new("'Packets out of order' error was received from the database. Please update your mysql bindings (gem install mysql) and read http://dev.mysql.com/doc/mysql/en/password-hashing.html for more information. If you're on Windows, use the Instant Rails installer to get the updated mysql bindings.", exception.original_exception)
else
raise
end
@@ -300,18 +337,6 @@ module ActiveRecord
# Transactions aren't supported
end
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
# In the simple case, MySQL allows us to place JOINs directly into the UPDATE
# query. However, this does not allow for LIMIT, OFFSET and ORDER. To support
# these, we must use a subquery.
@@ -330,34 +355,22 @@ module ActiveRecord
# SCHEMA STATEMENTS ========================================
- def structure_dump #:nodoc:
- if supports_views?
- sql = "SHOW FULL TABLES WHERE Table_type = 'BASE TABLE'"
- else
- sql = "SHOW TABLES"
- end
-
- select_all(sql, 'SCHEMA').map { |table|
- table.delete('Table_type')
- sql = "SHOW CREATE TABLE #{quote_table_name(table.to_a.first.last)}"
- exec_query(sql, 'SCHEMA').first['Create Table'] + ";\n\n"
- }.join
- end
-
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
def recreate_database(name, options = {})
drop_database(name)
- create_database(name, options)
+ sql = create_database(name, options)
+ reconnect!
+ sql
end
# Create a new MySQL database with optional <tt>:charset</tt> and <tt>:collation</tt>.
# Charset defaults to utf8.
#
# Example:
- # create_database 'charset_test', :charset => 'latin1', :collation => 'latin1_bin'
+ # create_database 'charset_test', charset: 'latin1', collation: 'latin1_bin'
# create_database 'matt_development'
- # create_database 'matt_development', :charset => :big5
+ # create_database 'matt_development', charset: :big5
def create_database(name, options = {})
if options[:collation]
execute "CREATE DATABASE `#{name}` DEFAULT CHARACTER SET `#{options[:charset] || 'utf8'}` COLLATE `#{options[:collation]}`"
@@ -422,7 +435,11 @@ module ActiveRecord
if current_index != row[:Key_name]
next if row[:Key_name] == 'PRIMARY' # skip the primary key
current_index = row[:Key_name]
- indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [])
+
+ mysql_index_type = row[:Index_type].downcase.to_sym
+ index_type = INDEX_TYPES.include?(mysql_index_type) ? mysql_index_type : nil
+ index_using = INDEX_USINGS.include?(mysql_index_type) ? mysql_index_type : nil
+ indexes << IndexDefinition.new(row[:Table], row[:Key_name], row[:Non_unique].to_i == 0, [], [], nil, nil, index_type, index_using)
end
indexes.last.columns << row[:Column_name]
@@ -438,7 +455,8 @@ module ActiveRecord
sql = "SHOW FULL FIELDS FROM #{quote_table_name(table_name)}"
execute_and_free(sql, 'SCHEMA') do |result|
each_hash(result).map do |field|
- new_column(field[:Field], field[:Default], field[:Type], field[:Null] == "YES", field[:Collation])
+ field_name = set_field_encoding(field[:Field])
+ new_column(field_name, field[:Default], field[:Type], field[:Null] == "YES", field[:Collation], field[:Extra])
end
end
end
@@ -468,10 +486,7 @@ module ActiveRecord
# rename_table('octopuses', 'octopi')
def rename_table(table_name, new_name)
execute "RENAME TABLE #{quote_table_name(table_name)} TO #{quote_table_name(new_name)}"
- end
-
- def add_column(table_name, column_name, type, options = {})
- execute("ALTER TABLE #{quote_table_name(table_name)} #{add_column_sql(table_name, column_name, type, options)}")
+ rename_table_indexes(table_name, new_name)
end
def change_column_default(table_name, column_name, default)
@@ -495,11 +510,24 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name) #:nodoc:
execute("ALTER TABLE #{quote_table_name(table_name)} #{rename_column_sql(table_name, column_name, new_column_name)}")
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ 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
# Maps logical Rails types to MySQL-specific data types.
def type_to_sql(type, limit = nil, precision = nil, scale = nil)
case type.to_s
+ when 'binary'
+ case limit
+ when 0..0xfff; "varbinary(#{limit})"
+ when nil; "blob"
+ when 0x1000..0xffffffff; "blob(#{limit})"
+ else raise(ActiveRecordError, "No binary type has character length #{limit}")
+ end
when 'integer'
case limit
when 1; 'tinyint'
@@ -571,6 +599,14 @@ module ActiveRecord
where_sql
end
+ def strict_mode?
+ self.class.type_cast_config_to_boolean(@config.fetch(:strict, true))
+ end
+
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
+
protected
# MySQL is too stupid to create a temporary table for use subquery, so we have
@@ -621,10 +657,9 @@ module ActiveRecord
end
def add_column_sql(table_name, column_name, type, options = {})
- add_column_sql = "ADD #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
- add_column_position!(add_column_sql, options)
- add_column_sql
+ td = create_table_definition table_name, options[:temporary], options[:options]
+ cd = td.new_column_definition(column_name, type, options)
+ schema_creation.visit_AddColumn cd
end
def change_column_sql(table_name, column_name, type, options = {})
@@ -638,32 +673,32 @@ module ActiveRecord
options[:null] = column.null
end
- change_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(change_column_sql, options)
- add_column_position!(change_column_sql, options)
- change_column_sql
+ options[:name] = column.name
+ schema_creation.accept ChangeColumnDefinition.new column, type, options
end
def rename_column_sql(table_name, column_name, new_column_name)
- options = {}
+ 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
current_type = select_one("SHOW COLUMNS FROM #{quote_table_name(table_name)} LIKE '#{column_name}'", 'SCHEMA')["Type"]
- rename_column_sql = "CHANGE #{quote_column_name(column_name)} #{quote_column_name(new_column_name)} #{current_type}"
- add_column_options!(rename_column_sql, options)
- rename_column_sql
+ schema_creation.accept ChangeColumnDefinition.new column, current_type, options
+ end
+
+ def remove_column_sql(table_name, column_name, type = nil, options = {})
+ "DROP #{quote_column_name(column_name)}"
end
- def remove_column_sql(table_name, *column_names)
- columns_for_remove(table_name, *column_names).map {|column_name| "DROP #{column_name}" }
+ def remove_columns_sql(table_name, *column_names)
+ column_names.map {|column_name| remove_column_sql(table_name, column_name) }
end
- alias :remove_columns_sql :remove_column
def add_index_sql(table_name, column_name, options = {})
index_name, index_type, index_columns = add_index_options(table_name, column_name, options)
@@ -695,6 +730,44 @@ module ActiveRecord
end
column
end
+
+ def configure_connection
+ variables = @config[:variables] || {}
+
+ # 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
+
+ # 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)
+
+ # 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'
+ end
+
+ # NAMES does not have an equals sign, see
+ # http://dev.mysql.com/doc/refman/5.0/en/set-statement.html#id944430
+ # (trailing comma because variable_assignments will always have content)
+ encoding = "NAMES #{@config[:encoding]}, " if @config[:encoding]
+
+ # Gather up all of the SET variables...
+ variable_assignments = variables.map do |k, v|
+ if v == ':default' || v == :default
+ "@@SESSION.#{k.to_s} = DEFAULT" # Sets the value to the global or compile default
+ elsif !v.nil?
+ "@@SESSION.#{k.to_s} = #{quote(v)}"
+ end
+ # or else nil; compact to clear nils out
+ end.compact.join(', ')
+
+ # ...and send them all in one query
+ execute("SET #{encoding} #{variable_assignments}", :skip_logging)
+ 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 2028abf6f0..f2fbd5a8f2 100644
--- a/activerecord/lib/active_record/connection_adapters/column.rb
+++ b/activerecord/lib/active_record/connection_adapters/column.rb
@@ -13,7 +13,7 @@ module ActiveRecord
ISO_DATETIME = /\A(\d{4})-(\d\d)-(\d\d) (\d\d):(\d\d):(\d\d)(\.\d+)?\z/
end
- attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale
+ attr_reader :name, :default, :type, :limit, :null, :sql_type, :precision, :scale, :default_function
attr_accessor :primary, :coder
alias :encoded? :coder
@@ -27,16 +27,17 @@ module ActiveRecord
# It will be mapped to one of the standard Rails SQL types in the <tt>type</tt> attribute.
# +null+ determines if this column allows +NULL+ values.
def initialize(name, default, sql_type = nil, null = true)
- @name = name
- @sql_type = sql_type
- @null = null
- @limit = extract_limit(sql_type)
- @precision = extract_precision(sql_type)
- @scale = extract_scale(sql_type)
- @type = simplified_type(sql_type)
- @default = extract_default(default)
- @primary = nil
- @coder = nil
+ @name = name
+ @sql_type = sql_type
+ @null = null
+ @limit = extract_limit(sql_type)
+ @precision = extract_precision(sql_type)
+ @scale = extract_scale(sql_type)
+ @type = simplified_type(sql_type)
+ @default = extract_default(default)
+ @default_function = nil
+ @primary = nil
+ @coder = nil
end
# Returns +true+ if the column is either of type string or text.
@@ -53,10 +54,6 @@ module ActiveRecord
!default.nil?
end
- def explicit_default?
- false
- end
-
# Returns the Ruby class that corresponds to the abstract data type.
def klass
case type
@@ -78,12 +75,13 @@ module ActiveRecord
def type_cast_for_write(value)
return value unless number?
- if value == false
+ case value
+ when FalseClass
0
- elsif value == true
+ when TrueClass
1
- elsif value.is_a?(String) && value.blank?
- nil
+ when String
+ value.presence
else
value
end
@@ -98,7 +96,7 @@ module ActiveRecord
case type
when :string, :text then value
- when :integer then value.to_i
+ 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)
@@ -110,29 +108,6 @@ module ActiveRecord
end
end
- def type_cast_code(var_name)
- ActiveSupport::Deprecation.warn("Column#type_cast_code is deprecated in favor of" \
- "using Column#type_cast only, and it is going to be removed in future Rails versions.")
-
- klass = self.class.name
-
- case type
- when :string, :text then var_name
- when :integer then "(#{var_name}.to_i)"
- when :float then "#{var_name}.to_f"
- when :decimal then "#{klass}.value_to_decimal(#{var_name})"
- when :datetime, :timestamp then "#{klass}.string_to_time(#{var_name})"
- when :time then "#{klass}.string_to_dummy_time(#{var_name})"
- when :date then "#{klass}.value_to_date(#{var_name})"
- when :binary then "#{klass}.binary_to_string(#{var_name})"
- when :boolean then "#{klass}.value_to_boolean(#{var_name})"
- when :hstore then "#{klass}.string_to_hstore(#{var_name})"
- when :inet, :cidr then "#{klass}.string_to_cidr(#{var_name})"
- when :json then "#{klass}.string_to_json(#{var_name})"
- else var_name
- end
- end
-
# Returns the human name of the column name.
#
# ===== Examples
@@ -145,17 +120,7 @@ module ActiveRecord
type_cast(default)
end
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- self.class.string_to_binary(value)
- end
-
class << self
- # Used to convert from Strings to BLOBs
- def string_to_binary(value)
- value
- end
-
# Used to convert from BLOBs to Strings
def binary_to_string(value)
value
@@ -163,7 +128,7 @@ module ActiveRecord
def value_to_date(value)
if value.is_a?(String)
- return nil if value.blank?
+ return nil if value.empty?
fast_string_to_date(value) || fallback_string_to_date(value)
elsif value.respond_to?(:to_date)
value.to_date
@@ -174,14 +139,14 @@ module ActiveRecord
def string_to_time(string)
return string unless string.is_a?(String)
- return nil if string.blank?
+ 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.blank?
+ return nil if string.empty?
dummy_time_string = "2000-01-01 #{string}"
@@ -194,13 +159,24 @@ module ActiveRecord
# convert something to a boolean
def value_to_boolean(value)
- if value.is_a?(String) && value.blank?
+ 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
@@ -228,11 +204,19 @@ module ActiveRecord
end
end
- def new_time(year, mon, mday, hour, min, sec, microsec)
+ def new_time(year, mon, mday, hour, min, sec, microsec, offset = nil)
# Treat 0000-00-00 00:00:00 as nil.
return nil if year.nil? || (year == 0 && mon == 0 && mday == 0)
- Time.time_with_datetime_fallback(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ if offset
+ time = Time.utc(year, mon, mday, hour, min, sec, microsec) rescue nil
+ return nil unless time
+
+ time -= offset
+ Base.default_timezone == :utc ? time : time.getlocal
+ else
+ Time.public_send(Base.default_timezone, year, mon, mday, hour, min, sec, microsec) rescue nil
+ end
end
def fast_string_to_date(string)
@@ -257,7 +241,7 @@ module ActiveRecord
time_hash = Date._parse(string)
time_hash[:sec_fraction] = microseconds(time_hash)
- new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction))
+ new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
end
end
@@ -297,7 +281,7 @@ module ActiveRecord
:text
when /blob/i, /binary/i
:binary
- when /char/i, /string/i
+ when /char/i
:string
when /boolean/i
:boolean
diff --git a/activerecord/lib/active_record/connection_adapters/connection_specification.rb b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
index b9a61f7d91..64fc9e95d8 100644
--- a/activerecord/lib/active_record/connection_adapters/connection_specification.rb
+++ b/activerecord/lib/active_record/connection_adapters/connection_specification.rb
@@ -51,10 +51,13 @@ module ActiveRecord
raise(AdapterNotSpecified, "database configuration does not specify adapter") unless spec.key?(:adapter)
+ path_to_adapter = "active_record/connection_adapters/#{spec[:adapter]}_adapter"
begin
- require "active_record/connection_adapters/#{spec[:adapter]}_adapter"
+ require path_to_adapter
+ rescue Gem::LoadError => e
+ raise Gem::LoadError, "Specified '#{spec[:adapter]}' for database adapter, but the gem is not loaded. Add `gem '#{e.name}'` to your Gemfile (and ensure its version is at the minimum required by ActiveRecord)."
rescue LoadError => e
- raise LoadError, "Please install the #{spec[:adapter]} adapter: `gem install activerecord-#{spec[:adapter]}-adapter` (#{e.message})", e.backtrace
+ raise LoadError, "Could not load '#{path_to_adapter}'. Make sure that the adapter in config/database.yml is valid. If you use an adapter other than 'mysql', 'mysql2', 'postgresql' or 'sqlite3' add the necessary adapter gem to the Gemfile.", e.backtrace
end
adapter_method = "#{spec[:adapter]}_connection"
@@ -72,11 +75,19 @@ module ActiveRecord
:port => config.port,
:database => config.path.sub(%r{^/},""),
:host => config.host }
+
spec.reject!{ |_,value| value.blank? }
+
+ uri_parser = URI::Parser.new
+
+ spec.map { |key,value| spec[key] = uri_parser.unescape(value) if value.is_a?(String) }
+
if config.query
options = Hash[config.query.split("&").map{ |pair| pair.split("=") }].symbolize_keys
+
spec.merge!(options)
end
+
spec
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 328d080687..e790f731ea 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql2_adapter.rb
@@ -1,19 +1,21 @@
require 'active_record/connection_adapters/abstract_mysql_adapter'
-gem 'mysql2', '~> 0.3.10'
+gem 'mysql2', '~> 0.3.13'
require 'mysql2'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
def mysql2_connection(config)
+ config = config.symbolize_keys
+
config[:username] = 'root' if config[:username].nil?
if Mysql2::Client.const_defined? :FOUND_ROWS
config[:flags] = Mysql2::Client::FOUND_ROWS
end
- client = Mysql2::Client.new(config.symbolize_keys)
+ client = Mysql2::Client.new(config)
options = [config[:host], config[:username], config[:password], config[:database], config[:port], config[:socket], 0]
ConnectionAdapters::Mysql2Adapter.new(client, logger, options, config)
end
@@ -36,6 +38,15 @@ module ActiveRecord
configure_connection
end
+ MAX_INDEX_LENGTH_FOR_UTF8MB4 = 191
+ def initialize_schema_migrations_table
+ if @config[:encoding] == 'utf8mb4'
+ ActiveRecord::SchemaMigration.create_table(MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ else
+ ActiveRecord::SchemaMigration.create_table
+ end
+ end
+
def supports_explain?
true
end
@@ -52,8 +63,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ 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)
@@ -175,7 +186,7 @@ module ActiveRecord
# # as values.
# def select_one(sql, name = nil)
# result = execute(sql, name)
- # result.each(:as => :hash) do |r|
+ # result.each(as: :hash) do |r|
# return r
# end
# end
@@ -202,9 +213,11 @@ module ActiveRecord
# Executes the SQL statement in the context of this connection.
def execute(sql, name = nil)
- # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
- # made since we established the connection
- @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ if @connection
+ # make sure we carry over any changes to ActiveRecord::Base.default_timezone that have been
+ # made since we established the connection
+ @connection.query_options[:database_timezone] = ActiveRecord::Base.default_timezone
+ end
super
end
@@ -216,8 +229,7 @@ module ActiveRecord
alias exec_without_stmt exec_query
- # Returns an array of record hashes with the column names as keys and
- # column values as values.
+ # Returns an ActiveRecord::Result instance.
def select(sql, name = nil, binds = [])
exec_query(sql, name)
end
@@ -251,34 +263,16 @@ module ActiveRecord
def configure_connection
@connection.query_options.merge!(:as => :array)
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- variable_assignments = ['SQL_AUTO_IS_NULL=0']
-
- # Make MySQL reject illegal values rather than truncating or
- # blanking them. See
- # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- variable_assignments << "SQL_MODE='STRICT_ALL_TABLES'"
- end
-
- encoding = @config[:encoding]
-
- # make sure we set the encoding
- variable_assignments << "NAMES '#{encoding}'" if encoding
-
- # increase timeout so mysql server doesn't disconnect us
- wait_timeout = @config[:wait_timeout]
- wait_timeout = 2147483 unless wait_timeout.is_a?(Fixnum)
- variable_assignments << "@@wait_timeout = #{wait_timeout}"
-
- execute("SET #{variable_assignments.join(', ')}", :skip_logging)
+ super
end
def version
@version ||= @connection.info[:version].scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
index 0b936bbf39..88c9494fc6 100644
--- a/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/mysql_adapter.rb
@@ -2,7 +2,7 @@ require 'active_record/connection_adapters/abstract_mysql_adapter'
require 'active_record/connection_adapters/statement_pool'
require 'active_support/core_ext/hash/keys'
-gem 'mysql', '~> 2.8.1'
+gem 'mysql', '~> 2.9'
require 'mysql'
class Mysql
@@ -16,9 +16,9 @@ class Mysql
end
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# Establishes a connection to the database that's used by all Active Record objects.
- def mysql_connection(config) # :nodoc:
+ def mysql_connection(config)
config = config.symbolize_keys
host = config[:host]
port = config[:port]
@@ -51,7 +51,8 @@ module ActiveRecord
# * <tt>:database</tt> - The name of the database. No default, must be provided.
# * <tt>:encoding</tt> - (Optional) Sets the client encoding by executing "SET NAMES <encoding>" after connection.
# * <tt>:reconnect</tt> - Defaults to false (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/auto-reconnect.html).
- # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html)
+ # * <tt>:strict</tt> - Defaults to true. Enable STRICT_ALL_TABLES. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/server-sql-mode.html)
+ # * <tt>:variables</tt> - (Optional) A hash session variables to send as `SET @@SESSION.key = value` on each database connection. Use the value `:default` to set a variable to its DEFAULT value. (See MySQL documentation: http://dev.mysql.com/doc/refman/5.0/en/set-statement.html).
# * <tt>:sslca</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslkey</tt> - Necessary to use MySQL with an SSL connection.
# * <tt>:sslcert</tt> - Necessary to use MySQL with an SSL connection.
@@ -125,7 +126,7 @@ module ActiveRecord
def initialize(connection, logger, connection_options, config)
super
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@client_encoding = nil
connect
end
@@ -149,8 +150,8 @@ module ActiveRecord
end
end
- def new_column(field, default, type, null, collation) # :nodoc:
- Column.new(field, default, type, null, collation)
+ 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:
@@ -278,11 +279,7 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- # If the configuration sets prepared_statements:false, binds will
- # always be empty, since the bind variables will have been already
- # substituted and removed from binds by BindVisitor, so this will
- # effectively disable prepared statement usage completely.
- if binds.empty?
+ if without_prepared_statement?(binds)
result_set, affected_rows = exec_without_stmt(sql, name)
else
result_set, affected_rows = exec_stmt(sql, name, binds)
@@ -382,7 +379,7 @@ module ActiveRecord
TYPES = {}
- # Register an MySQL +type_id+ with a typcasting object in
+ # Register an MySQL +type_id+ with a typecasting object in
# +type+.
def self.register_type(type_id, type)
TYPES[type_id] = type
@@ -392,6 +389,14 @@ module ActiveRecord
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 }
+ end
+ end
+
register_type Mysql::Field::TYPE_TINY, Fields::Boolean.new
register_type Mysql::Field::TYPE_LONG, Fields::Integer.new
alias_type Mysql::Field::TYPE_LONGLONG, Mysql::Field::TYPE_LONG
@@ -424,9 +429,7 @@ module ActiveRecord
if field.decimals > 0
types[field.name] = Fields::Decimal.new
else
- types[field.name] = Fields::TYPES.fetch(field.type) {
- Fields::Identity.new
- }
+ types[field.name] = Fields.find_type field
end
}
result_set = ActiveRecord::Result.new(types.keys, result.to_a, types)
@@ -500,12 +503,12 @@ module ActiveRecord
cols = cache[:cols] ||= metadata.fetch_fields.map { |field|
field.name
}
+ metadata.free
end
result_set = ActiveRecord::Result.new(cols, stmt.to_a) if cols
affected_rows = stmt.affected_rows
- stmt.result_metadata.free if cols
stmt.free_result
stmt.close if binds.empty?
@@ -535,20 +538,10 @@ module ActiveRecord
configure_connection
end
+ # Many Rails applications monkey-patch a replacement of the configure_connection method
+ # and don't call 'super', so leave this here even though it looks superfluous.
def configure_connection
- encoding = @config[:encoding]
- execute("SET NAMES '#{encoding}'", :skip_logging) if encoding
-
- # By default, MySQL 'where id is null' selects the last inserted id.
- # Turn this off. http://dev.rubyonrails.org/ticket/6778
- execute("SET SQL_AUTO_IS_NULL=0", :skip_logging)
-
- # Make MySQL reject illegal values rather than truncating or
- # blanking them. See
- # http://dev.mysql.com/doc/refman/5.5/en/server-sql-mode.html#sqlmode_strict_all_tables
- if @config.fetch(:strict, true)
- execute("SET SQL_MODE='STRICT_ALL_TABLES'", :skip_logging)
- end
+ super
end
def select(sql, name = nil, binds = [])
@@ -562,6 +555,14 @@ module ActiveRecord
def version
@version ||= @connection.server_info.scan(/^(\d+)\.(\d+)\.(\d+)/).flatten.map { |v| v.to_i }
end
+
+ def set_field_encoding field_name
+ field_name.force_encoding(client_encoding)
+ if internal_enc = Encoding.default_internal
+ field_name = field_name.encode!(internal_enc)
+ end
+ field_name
+ end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
index b7d24f2bb3..20de8d1982 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/array_parser.rb
@@ -2,6 +2,13 @@ module ActiveRecord
module ConnectionAdapters
class PostgreSQLColumn < Column
module ArrayParser
+
+ DOUBLE_QUOTE = '"'
+ BACKSLASH = "\\"
+ COMMA = ','
+ BRACKET_OPEN = '{'
+ BRACKET_CLOSE = '}'
+
private
# Loads pg_array_parser if available. String parsing can be
# performed quicker by a native extension, which will not create
@@ -12,18 +19,18 @@ module ActiveRecord
include PgArrayParser
rescue LoadError
def parse_pg_array(string)
- parse_data(string, 0)
+ parse_data(string)
end
end
- def parse_data(string, index)
- local_index = index
+ def parse_data(string)
+ local_index = 0
array = []
while(local_index < string.length)
case string[local_index]
- when '{'
+ when BRACKET_OPEN
local_index,array = parse_array_contents(array, string, local_index + 1)
- when '}'
+ when BRACKET_CLOSE
return array
end
local_index += 1
@@ -33,9 +40,9 @@ module ActiveRecord
end
def parse_array_contents(array, string, index)
- is_escaping = false
- is_quoted = false
- was_quoted = false
+ is_escaping = false
+ is_quoted = false
+ was_quoted = false
current_item = ''
local_index = index
@@ -47,29 +54,29 @@ module ActiveRecord
else
if is_quoted
case token
- when '"'
+ when DOUBLE_QUOTE
is_quoted = false
was_quoted = true
- when "\\"
+ when BACKSLASH
is_escaping = true
else
current_item << token
end
else
case token
- when "\\"
+ when BACKSLASH
is_escaping = true
- when ','
+ when COMMA
add_item_to_array(array, current_item, was_quoted)
current_item = ''
was_quoted = false
- when '"'
+ when DOUBLE_QUOTE
is_quoted = true
- when '{'
+ when BRACKET_OPEN
internal_items = []
local_index,internal_items = parse_array_contents(internal_items, string, local_index + 1)
array.push(internal_items)
- when '}'
+ when BRACKET_CLOSE
add_item_to_array(array, current_item, was_quoted)
return local_index,array
else
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
index 62d091357d..ea44e818e5 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/cast.rb
@@ -2,17 +2,39 @@ 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'; 1.0 / 0.0
- when '-infinity'; -1.0 / 0.0
+ when 'infinity'; Float::INFINITY
+ when '-infinity'; -Float::INFINITY
+ when / BC$/
+ super("-" + string.sub(/ BC$/, ""))
else
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|
@@ -28,8 +50,8 @@ module ActiveRecord
nil
elsif String === string
Hash[string.scan(HstorePair).map { |k,v|
- v = v.upcase == 'NULL' ? nil : v.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
- k = k.gsub(/^"(.*)"$/,'\1').gsub(/\\(.)/, '\1')
+ 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
@@ -38,7 +60,7 @@ module ActiveRecord
end
def json_to_string(object)
- if Hash === object
+ if Hash === object || Array === object
ActiveSupport::JSON.encode(object)
else
object
@@ -60,6 +82,12 @@ module ActiveRecord
"{#{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)
@@ -72,7 +100,11 @@ module ActiveRecord
if string.nil?
nil
elsif String === string
- IPAddr.new(string)
+ begin
+ IPAddr.new(string)
+ rescue ArgumentError
+ nil
+ end
else
string
end
@@ -87,7 +119,7 @@ module ActiveRecord
end
def string_to_array(string, oid)
- parse_pg_array(string).map{|val| oid.type_cast val}
+ parse_pg_array(string).map {|val| type_cast_array(oid, val)}
end
private
@@ -118,6 +150,14 @@ module ActiveRecord
"\"#{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
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 553985bd1e..fa173d13a2 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/database_statements.rb
@@ -1,5 +1,3 @@
-require 'active_support/deprecation'
-
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
@@ -136,34 +134,31 @@ module ActiveRecord
end
def exec_query(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
-
- types = {}
- result.fields.each_with_index do |fname, i|
- ftype = result.ftype i
- fmod = result.fmod i
- types[fname] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
- warn "unknown OID: #{fname}(#{oid}) (#{sql})"
- OID::Identity.new
- }
- end
-
- ret = ActiveRecord::Result.new(result.fields, result.values, types)
- result.clear
- return ret
+ 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] = OID::TYPE_MAP.fetch(ftype, fmod) { |oid, mod|
+ warn "unknown OID: #{fname}(#{oid}) (#{sql})"
+ OID::Identity.new
+ }
end
+
+ ret = ActiveRecord::Result.new(fields, result.values, types)
+ result.clear
+ return ret
end
def exec_delete(sql, name = 'SQL', binds = [])
- log(sql, name, binds) do
- result = binds.empty? ? exec_no_cache(sql, binds) :
- exec_cache(sql, binds)
- affected = result.cmd_tuples
- result.clear
- affected
- end
+ result = without_prepared_statement?(binds) ? exec_no_cache(sql, name, binds) :
+ exec_cache(sql, name, binds)
+ affected = result.cmd_tuples
+ result.clear
+ affected
end
alias :exec_update :exec_delete
@@ -219,26 +214,6 @@ module ActiveRecord
def rollback_db_transaction
execute "ROLLBACK"
end
-
- def outside_transaction?
- ActiveSupport::Deprecation.warn(
- "#outside_transaction? is deprecated. This method was only really used " \
- "internally, but you can use #transaction_open? instead."
- )
- @connection.transaction_status == PGconn::PQTRANS_IDLE
- end
-
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
end
end
end
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
index 52344f61c0..6c5792954f 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/oid.rb
@@ -6,20 +6,27 @@ module ActiveRecord
module OID
class Type
def type; end
+ end
- def type_cast_for_write(value)
+ class Identity < Type
+ def type_cast(value)
value
end
end
- class Identity < Type
+ class Bit < Type
def type_cast(value)
- 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
@@ -27,12 +34,17 @@ module ActiveRecord
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.]/, '')
@@ -63,6 +75,16 @@ module ActiveRecord
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)
@@ -78,11 +100,69 @@ module ActiveRecord
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?
- value.to_i rescue value ? 1 : 0
+ ConnectionAdapters::Column.value_to_integer value
end
end
@@ -145,11 +225,19 @@ module ActiveRecord
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
@@ -161,11 +249,19 @@ module ActiveRecord
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
@@ -181,6 +277,10 @@ module ActiveRecord
@mapping[oid]
end
+ def clear
+ @mapping.clear
+ end
+
def key?(oid)
@mapping.key? oid
end
@@ -233,6 +333,13 @@ module ActiveRecord
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'
@@ -243,31 +350,30 @@ module ActiveRecord
# FIXME: why are we keeping these types as strings?
alias_type 'tsvector', 'text'
alias_type 'interval', 'text'
- alias_type 'bit', 'text'
- alias_type 'varbit', 'text'
alias_type 'macaddr', 'text'
alias_type 'uuid', 'text'
- # FIXME: I don't think this is correct. We should probably be returning a parsed date,
- # but the tests pass with a string returned.
- register_type 'timestamptz', OID::Identity.new
-
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'
diff --git a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
index 9d3fa18e3a..e9daa5d7ff 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/quoting.rb
@@ -18,21 +18,34 @@ module ActiveRecord
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 Array
- if column.array
- "'#{PostgreSQLColumn.array_to_string(value, column, self)}'"
+ 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 column.sql_type
+ 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 column.sql_type
+ case sql_type
when 'inet', 'cidr' then super(PostgreSQLColumn.cidr_to_string(value), column)
else super
end
@@ -45,11 +58,14 @@ module ActiveRecord
super
end
when Numeric
- return super unless column.sql_type == 'money'
- # Not truly string input, so doesn't require (or allow) escape string syntax.
- "'#{value}'"
+ 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 column.sql_type
+ case sql_type
when 'bytea' then "'#{escape_bytea(value)}'"
when 'xml' then "xml '#{quote_string(value)}'"
when /^bit/
@@ -69,6 +85,9 @@ module ActiveRecord
return super(value, column) unless column
case value
+ when Range
+ return super(value, column) unless /range$/ =~ column.sql_type
+ PostgreSQLColumn.range_to_string(value)
when NilClass
if column.array && array_member
'NULL'
@@ -78,8 +97,13 @@ module ActiveRecord
super(value, column)
end
when Array
- return super(value, column) unless column.array
- PostgreSQLColumn.array_to_string(value, column, self)
+ case column.sql_type
+ when 'point' then PostgreSQLColumn.point_to_string(value)
+ when 'json' then PostgreSQLColumn.json_to_string(value)
+ else
+ return super(value, column) unless column.array
+ PostgreSQLColumn.array_to_string(value, column, self)
+ end
when String
return super(value, column) unless 'bytea' == column.sql_type
{ :value => value, :format => 1 }
@@ -121,6 +145,10 @@ module ActiveRecord
end
end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
# Quotes column names for use in SQL queries.
def quote_column_name(name) #:nodoc:
PGconn.quote_ident(name.to_s)
@@ -129,11 +157,15 @@ module ActiveRecord
# Quote date/time values for use in SQL input. Includes microseconds
# if the value is a Time responding to usec.
def quoted_date(value) #:nodoc:
+ result = super
if value.acts_like?(:time) && value.respond_to?(:usec)
- "#{super}.#{sprintf("%06d", value.usec)}"
- else
- super
+ result = "#{result}.#{sprintf("%06d", value.usec)}"
+ end
+
+ if value.year < 0
+ result = result.sub(/^-/, "") + " BC"
end
+ result
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 16da3ea732..bc775394a6 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/referential_integrity.rb
@@ -7,13 +7,21 @@ module ActiveRecord
end
def disable_referential_integrity #:nodoc:
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ if supports_disable_referential_integrity?
+ begin
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER ALL" }.join(";"))
+ rescue
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} DISABLE TRIGGER USER" }.join(";"))
+ end
end
yield
ensure
- if supports_disable_referential_integrity? then
- execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ if supports_disable_referential_integrity?
+ begin
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER ALL" }.join(";"))
+ rescue
+ execute(tables.collect { |name| "ALTER TABLE #{quote_table_name(name)} ENABLE TRIGGER USER" }.join(";"))
+ 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 7cad8f94cf..5dc70a5ad1 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql/schema_statements.rb
@@ -1,6 +1,42 @@
module ActiveRecord
module ConnectionAdapters
class PostgreSQLAdapter < AbstractAdapter
+ 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 = "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
+ sql << " PRIMARY KEY "
+ add_column_options!(sql, column_options(o))
+ end
+ sql
+ end
+
+ def add_column_options!(sql, options)
+ if options[:array] || options[:column].try(:array)
+ sql << '[]'
+ end
+
+ column = options.fetch(:column) { return super }
+ if column.type == :uuid && options[:default] =~ /\(\)/
+ sql << " DEFAULT #{options[:default]}"
+ else
+ super
+ end
+ end
+ end
+
+ def schema_creation
+ SchemaCreation.new self
+ end
+
module SchemaStatements
# Drops the database specified on the +name+ attribute
# and creates it again using the provided +options+.
@@ -10,17 +46,17 @@ module ActiveRecord
end
# Create a new PostgreSQL database. Options include <tt>:owner</tt>, <tt>:template</tt>,
- # <tt>:encoding</tt>, <tt>:collation</tt>, <tt>:ctype</tt>,
+ # <tt>:encoding</tt> (defaults to utf8), <tt>:collation</tt>, <tt>:ctype</tt>,
# <tt>:tablespace</tt>, and <tt>:connection_limit</tt> (note that MySQL uses
# <tt>:charset</tt> while PostgreSQL uses <tt>:encoding</tt>).
#
# Example:
# create_database config[:database], config
- # create_database 'foo_development', :encoding => 'unicode'
+ # create_database 'foo_development', encoding: 'unicode'
def create_database(name, options = {})
- options = options.reverse_merge(:encoding => "utf8")
+ options = { encoding: 'utf8' }.merge!(options.symbolize_keys)
- option_string = options.symbolize_keys.sum do |key, value|
+ option_string = options.sum do |key, value|
case key
when :owner
" OWNER = \"#{value}\""
@@ -120,12 +156,15 @@ module ActiveRecord
column_names = columns.values_at(*indkey).compact
- # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
- desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
- orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
- where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ unless column_names.empty?
+ # add info on sort order for columns (only desc order is explicitly specified, asc is the default)
+ desc_order_columns = inddef.scan(/(\w+) DESC/).flatten
+ orders = desc_order_columns.any? ? Hash[desc_order_columns.map {|order_column| [order_column, :desc]}] : {}
+ where = inddef.scan(/WHERE (.+)$/).flatten[0]
+ using = inddef.scan(/USING (.+?) /).flatten[0].to_sym
- column_names.empty? ? nil : IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where)
+ IndexDefinition.new(table_name, index_name, unique, column_names, [], orders, where, nil, using)
+ end
end.compact
end
@@ -267,7 +306,6 @@ module ActiveRecord
FROM pg_class seq,
pg_attribute attr,
pg_depend dep,
- pg_namespace name,
pg_constraint cons
WHERE seq.oid = dep.objid
AND seq.relkind = 'S'
@@ -283,6 +321,7 @@ module ActiveRecord
result = query(<<-end_sql, 'SCHEMA')[0]
SELECT attr.attname,
CASE
+ WHEN pg_get_expr(def.adbin, def.adrelid) !~* 'nextval' THEN NULL
WHEN split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2) ~ '.' THEN
substr(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2),
strpos(split_part(pg_get_expr(def.adbin, def.adrelid), '''', 2), '.')+1)
@@ -294,7 +333,7 @@ module ActiveRecord
JOIN pg_constraint cons ON (conrelid = adrelid AND adnum = conkey[1])
WHERE t.oid = '#{quote_table_name(table)}'::regclass
AND cons.contype = 'p'
- AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval'
+ AND pg_get_expr(def.adbin, def.adrelid) ~* 'nextval|uuid_generate'
end_sql
end
@@ -306,12 +345,11 @@ module ActiveRecord
# Returns just a table's primary key
def primary_key(table)
row = exec_query(<<-end_sql, 'SCHEMA').rows.first
- SELECT DISTINCT(attr.attname)
+ SELECT attr.attname
FROM pg_attribute attr
- INNER JOIN pg_depend dep ON attr.attrelid = dep.refobjid AND attr.attnum = dep.refobjsubid
INNER JOIN pg_constraint cons ON attr.attrelid = cons.conrelid AND attr.attnum = cons.conkey[1]
WHERE cons.contype = 'p'
- AND dep.refobjid = '#{quote_table_name(table)}'::regclass
+ AND cons.conrelid = '#{quote_table_name(table)}'::regclass
end_sql
row && row.first
@@ -323,32 +361,32 @@ module ActiveRecord
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
+ def rename_table(table_name, new_name)
clear_cache!
- execute "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ 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 == "#{name}_#{pk}_seq"
+ if seq == "#{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
+
+ rename_table_indexes(table_name, new_name)
end
# Adds a new column to the named table.
# See TableDefinition#column for details of the options you can use.
def add_column(table_name, column_name, type, options = {})
clear_cache!
- add_column_sql = "ALTER TABLE #{quote_table_name(table_name)} ADD COLUMN #{quote_column_name(column_name)} #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
- add_column_options!(add_column_sql, options)
-
- execute add_column_sql
+ super
end
# Changes the column of a table.
def change_column(table_name, column_name, type, options = {})
clear_cache!
quoted_table_name = quote_table_name(table_name)
-
- execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{type_to_sql(type, options[:limit], options[:precision], options[:scale])}"
+ sql_type = type_to_sql(type, options[:limit], options[:precision], options[:scale])
+ sql_type << "[]" if options[:array]
+ execute "ALTER TABLE #{quoted_table_name} ALTER COLUMN #{quote_column_name(column_name)} TYPE #{sql_type}"
change_column_default(table_name, column_name, options[:default]) if options_include_default?(options)
change_column_null(table_name, column_name, options[:null], options[:default]) if options.key?(:null)
@@ -372,6 +410,12 @@ module ActiveRecord
def rename_column(table_name, column_name, new_column_name)
clear_cache!
execute "ALTER TABLE #{quote_table_name(table_name)} RENAME COLUMN #{quote_column_name(column_name)} TO #{quote_column_name(new_column_name)}"
+ rename_column_indexes(table_name, column_name, new_column_name)
+ end
+
+ def add_index(table_name, column_name, options = {}) #:nodoc:
+ index_name, index_type, index_columns, index_options, index_algorithm, index_using = add_index_options(table_name, column_name, options)
+ execute "CREATE #{index_type} INDEX #{index_algorithm} #{quote_column_name(index_name)} ON #{quote_table_name(table_name)} #{index_using} (#{index_columns})#{index_options}"
end
def remove_index!(table_name, index_name) #:nodoc:
@@ -396,6 +440,13 @@ module ActiveRecord
when nil, 0..0x3fffffff; super(type)
else raise(ActiveRecordError, "No binary type has byte size #{limit}.")
end
+ when 'text'
+ # PostgreSQL doesn't support limits on text columns.
+ # The hard limit is 1Gb, according to section 8.3 in the manual.
+ case limit
+ when nil, 0..0x3fffffff; super(type)
+ else raise(ActiveRecordError, "The limit on text can be at most 1GB - 1byte.")
+ end
when 'integer'
return 'integer' unless limit
@@ -417,25 +468,17 @@ module ActiveRecord
end
end
- # Returns a SELECT DISTINCT clause for a given set of columns and a given ORDER BY clause.
- #
# PostgreSQL requires the ORDER BY columns in the select list for distinct queries, and
# requires that the ORDER BY include the distinct column.
- #
- # distinct("posts.id", "posts.created_at desc")
- def distinct(columns, orders) #:nodoc:
- return "DISTINCT #{columns}" if orders.empty?
-
- # Construct a clean list of column names from the ORDER BY clause, removing
- # any ASC/DESC modifiers
- order_columns = orders.collect do |s|
- s = s.to_sql unless s.is_a?(String)
- s.gsub(/\s+(ASC|DESC)\s*(NULLS\s+(FIRST|LAST)\s*)?/i, '')
- end
- order_columns.delete_if { |c| c.blank? }
- order_columns = order_columns.zip((0...order_columns.size).to_a).map { |s,i| "#{s} AS alias_#{i}" }
-
- "DISTINCT #{columns}, #{order_columns * ', '}"
+ def columns_for_distinct(columns, orders) #:nodoc:
+ order_columns = orders.reject(&:blank?).map{ |s|
+ # 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, '')
+ }.reject(&:blank?).map.with_index { |column, i| "#{column} AS alias_#{i}" }
+
+ [super, *order_columns].join(', ')
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 e18464fa35..3668aecd4b 100644
--- a/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb
@@ -16,23 +16,26 @@ require 'pg'
require 'ipaddr'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
+ VALID_CONN_PARAMS = [:host, :hostaddr, :port, :dbname, :user, :password, :connect_timeout,
+ :client_encoding, :options, :application_name, :fallback_application_name,
+ :keepalives, :keepalives_idle, :keepalives_interval, :keepalives_count,
+ :tty, :sslmode, :requiressl, :sslcompression, :sslcert, :sslkey,
+ :sslrootcert, :sslcrl, :requirepeer, :krbsrvname, :gsslib, :service]
+
# Establishes a connection to the database that's used by all Active Record objects
- def postgresql_connection(config) # :nodoc:
+ def postgresql_connection(config)
conn_params = config.symbolize_keys
- # Forward any unused config params to PGconn.connect.
- [:statement_limit, :encoding, :min_messages, :schema_search_path,
- :schema_order, :adapter, :pool, :checkout_timeout, :template,
- :reaping_frequency, :insert_returning].each do |key|
- conn_params.delete key
- end
- conn_params.delete_if { |k,v| v.nil? }
+ conn_params.delete_if { |_, v| v.nil? }
# Map ActiveRecords param names to PGs.
conn_params[:user] = conn_params.delete(:username) if conn_params[:username]
conn_params[:dbname] = conn_params.delete(:database) if conn_params[:database]
+ # Forward only valid config params to PGconn.connect.
+ conn_params.keep_if { |k, _| VALID_CONN_PARAMS.include?(k) }
+
# The postgres drivers don't allow the creation of an unconnected PGconn object,
# so just pass a nil connection object for the time being.
ConnectionAdapters::PostgreSQLAdapter.new(nil, logger, conn_params, config)
@@ -46,13 +49,17 @@ module ActiveRecord
# Instantiates a new PostgreSQL column definition in a table.
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, self.class.extract_value_from_default(default), sql_type[0..sql_type.length - 3], null)
+ super(name, default_value, sql_type[0..sql_type.length - 3], null)
else
@array = false
- super(name, self.class.extract_value_from_default(default), sql_type, null)
+ super(name, default_value, sql_type, null)
end
+
+ @default_function = default if has_default_function?(default_value, default)
end
# :stopdoc:
@@ -74,12 +81,14 @@ module ActiveRecord
return default unless default
case default
+ when /\A'(.*)'::(num|date|tstz|ts|int4|int8)range\z/m
+ $1
# Numeric types
- when /\A\(?(-?\d+(\.\d*)?\)?)\z/
+ when /\A\(?(-?\d+(\.\d*)?\)?(::bigint)?)\z/
$1
# Character types
when /\A\(?'(.*)'::.*\b(?:character varying|bpchar|text)\z/m
- $1
+ $1.gsub(/''/, "'")
# Binary data types
when /\A'(.*)'::bytea\z/m
$1
@@ -124,6 +133,14 @@ module ActiveRecord
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?
@@ -131,8 +148,16 @@ module ActiveRecord
@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
@@ -170,6 +195,8 @@ module ActiveRecord
:decimal
when 'hstore'
:hstore
+ when 'ltree'
+ :ltree
# Network address types
when 'inet'
:inet
@@ -209,12 +236,14 @@ module ActiveRecord
# UUID type
when 'uuid'
:uuid
- # JSON type
- when 'json'
- :json
+ # 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
@@ -238,6 +267,8 @@ module ActiveRecord
# <encoding></tt> call on the connection.
# * <tt>:min_messages</tt> - An optional client min messages that is used in a
# <tt>SET client_min_messages TO <min_messages></tt> call on the connection.
+ # * <tt>:variables</tt> - An optional hash of additional parameters that
+ # will be used in <tt>SET SESSION key = val</tt> calls on the connection.
# * <tt>:insert_returning</tt> - An optional boolean to control the use or <tt>RETURNING</tt> for <tt>INSERT</tt> statements
# defaults to true.
#
@@ -252,7 +283,7 @@ module ActiveRecord
attr_accessor :array
end
- class TableDefinition < ActiveRecord::ConnectionAdapters::TableDefinition
+ module ColumnMethods
def xml(*args)
options = args.extract_options!
column(args[0], 'xml', options)
@@ -263,10 +294,38 @@ module ActiveRecord
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
@@ -286,6 +345,45 @@ module ActiveRecord
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
@@ -297,12 +395,13 @@ module ActiveRecord
private
- def new_column_definition(base, name, type)
- definition = ColumnDefinition.new base, name, type
- @columns << definition
- @columns_hash[name] = definition
- definition
- end
+ def create_column_definition(name, type)
+ ColumnDefinition.new name, type
+ end
+ end
+
+ class Table < ActiveRecord::ConnectionAdapters::Table
+ include ColumnMethods
end
ADAPTER_NAME = 'PostgreSQL'
@@ -318,6 +417,12 @@ module ActiveRecord
timestamp: { name: "timestamp" },
time: { name: "time" },
date: { name: "date" },
+ daterange: { name: "daterange" },
+ numrange: { name: "numrange" },
+ tsrange: { name: "tsrange" },
+ tstzrange: { name: "tstzrange" },
+ int4range: { name: "int4range" },
+ int8range: { name: "int8range" },
binary: { name: "bytea" },
boolean: { name: "boolean" },
xml: { name: "xml" },
@@ -327,13 +432,15 @@ module ActiveRecord
cidr: { name: "cidr" },
macaddr: { name: "macaddr" },
uuid: { name: "uuid" },
- json: { name: "json" }
+ json: { name: "json" },
+ ltree: { name: "ltree" }
}
include Quoting
include ReferentialIntegrity
include SchemaStatements
include DatabaseStatements
+ include Savepoints
# Returns 'PostgreSQL' as adapter name for identification purposes.
def adapter_name
@@ -345,6 +452,7 @@ module ActiveRecord
def prepare_column_options(column, types)
spec = super
spec[:array] = 'true' if column.respond_to?(:array) && column.array
+ spec[:default] = "\"#{column.default_function}\"" if column.default_function
spec
end
@@ -371,6 +479,10 @@ module ActiveRecord
true
end
+ def index_algorithms
+ { concurrently: 'CONCURRENTLY' }
+ end
+
class StatementPool < ConnectionAdapters::StatementPool
def initialize(connection, max)
super
@@ -432,14 +544,13 @@ module ActiveRecord
def initialize(connection, logger, connection_parameters, config)
super(connection, logger)
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::PostgreSQL.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
- connection_parameters.delete :prepared_statements
-
@connection_parameters, @config = connection_parameters, config
# @local_tz is initialized as nil to avoid warnings when connect tries to use it
@@ -448,7 +559,7 @@ module ActiveRecord
connect
@statements = StatementPool.new @connection,
- config.fetch(:statement_limit) { 1000 }
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 })
if postgresql_version < 80200
raise "Your version of PostgreSQL (#{postgresql_version}) is too old, please upgrade!"
@@ -456,7 +567,7 @@ module ActiveRecord
initialize_type_map
@local_tz = execute('SHOW TIME ZONE', 'SCHEMA').first["TimeZone"]
- @use_insert_returning = @config.key?(:insert_returning) ? @config[:insert_returning] : true
+ @use_insert_returning = @config.key?(:insert_returning) ? self.class.type_cast_config_to_boolean(@config[:insert_returning]) : true
end
# Clears the prepared statements cache.
@@ -466,8 +577,7 @@ module ActiveRecord
# Is this connection alive and ready for queries?
def active?
- @connection.query 'SELECT 1'
- true
+ @connection.connect_poll != PG::PGRES_POLLING_FAILED
rescue PGError
false
end
@@ -521,26 +631,53 @@ module ActiveRecord
true
end
- # Returns true, since this connection adapter supports savepoints.
- def supports_savepoints?
- true
- end
-
# Returns true.
def supports_explain?
true
end
- # Returns the configured supported identifier length supported by PostgreSQL
- def table_alias_length
- @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
+ # Returns true if pg > 9.1
+ def supports_extensions?
+ postgresql_version >= 90100
end
- def add_column_options!(sql, options)
- if options[:array] || options[:column].try(:array)
- sql << '[]'
+ # Range datatypes weren't introduced until PostgreSQL 9.2
+ def supports_ranges?
+ postgresql_version >= 90200
+ end
+
+ def enable_extension(name)
+ exec_query("CREATE EXTENSION IF NOT EXISTS \"#{name}\"").tap {
+ reload_type_map
+ }
+ end
+
+ def disable_extension(name)
+ exec_query("DROP EXTENSION IF EXISTS \"#{name}\" CASCADE").tap {
+ reload_type_map
+ }
+ end
+
+ def extension_enabled?(name)
+ 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
end
- super
+ 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 }
+ else
+ super
+ end
+ end
+
+ # Returns the configured supported identifier length supported by PostgreSQL
+ def table_alias_length
+ @table_alias_length ||= query('SHOW max_identifier_length', 'SCHEMA')[0][0].to_i
end
# Set the authorized user for this session
@@ -572,6 +709,10 @@ module ActiveRecord
@use_insert_returning
end
+ def valid_type?(type)
+ !native_database_types[type].nil?
+ end
+
protected
# Returns the version of the connected PostgreSQL server.
@@ -584,6 +725,8 @@ module ActiveRecord
UNIQUE_VIOLATION = "23505"
def translate_exception(exception, message)
+ return exception unless exception.respond_to?(:result)
+
case exception.result.try(:error_field, PGresult::PG_DIAG_SQLSTATE)
when UNIQUE_VIOLATION
RecordNotUnique.new(message, exception)
@@ -596,6 +739,11 @@ module ActiveRecord
private
+ def reload_type_map
+ OID::TYPE_MAP.clear
+ initialize_type_map
+ end
+
def initialize_type_map
result = execute('SELECT oid, typname, typelem, typdelim, typinput FROM pg_type', 'SCHEMA')
leaves, nodes = result.partition { |row| row['typelem'] == '0' }
@@ -609,7 +757,14 @@ module ActiveRecord
# populate composite types
nodes.find_all { |row| OID::TYPE_MAP.key? row['typelem'].to_i }.each do |row|
- vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
+ if OID.registered_type? row['typname']
+ # this composite type is explicitly registered
+ vector = OID::NAMES[row['typname']]
+ else
+ # use the default for composite types
+ vector = OID::Vector.new row['typdelim'], OID::TYPE_MAP[row['typelem'].to_i]
+ end
+
OID::TYPE_MAP[row['oid'].to_i] = vector
end
@@ -622,37 +777,37 @@ module ActiveRecord
FEATURE_NOT_SUPPORTED = "0A000" # :nodoc:
- def exec_no_cache(sql, binds)
- @connection.async_exec(sql)
+ def exec_no_cache(sql, name, binds)
+ log(sql, name, binds) { @connection.async_exec(sql) }
end
- def exec_cache(sql, binds)
- begin
- stmt_key = prepare_statement sql
+ def exec_cache(sql, name, binds)
+ stmt_key = prepare_statement(sql)
- # Clear the queue
- @connection.get_last_result
+ log(sql, name, binds, stmt_key) do
@connection.send_query_prepared(stmt_key, binds.map { |col, val|
type_cast(val, col)
})
@connection.block
@connection.get_last_result
- rescue PGError => e
- # Get the PG code for the failure. Annoyingly, the code for
- # prepared statements whose return value may have changed is
- # FEATURE_NOT_SUPPORTED. Check here for more details:
- # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
- begin
- code = e.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
- rescue
- raise e
- end
- if FEATURE_NOT_SUPPORTED == code
- @statements.delete sql_key(sql)
- retry
- else
- raise e
- end
+ end
+ rescue ActiveRecord::StatementInvalid => e
+ pgerror = e.original_exception
+
+ # Get the PG code for the failure. Annoyingly, the code for
+ # prepared statements whose return value may have changed is
+ # FEATURE_NOT_SUPPORTED. Check here for more details:
+ # http://git.postgresql.org/gitweb/?p=postgresql.git;a=blob;f=src/backend/utils/cache/plancache.c#l573
+ begin
+ code = pgerror.result.result_error_field(PGresult::PG_DIAG_SQLSTATE)
+ rescue
+ raise e
+ end
+ if FEATURE_NOT_SUPPORTED == code
+ @statements.delete sql_key(sql)
+ retry
+ else
+ raise e
end
end
@@ -669,6 +824,8 @@ module ActiveRecord
unless @statements.key? sql_key
nextkey = @statements.next_key
@connection.prepare nextkey, sql
+ # Clear the queue
+ @connection.get_last_result
@statements[sql_key] = nextkey
end
@statements[sql_key]
@@ -706,11 +863,24 @@ module ActiveRecord
# If using Active Record's time zone support configure the connection to return
# TIMESTAMP WITH ZONE types in UTC.
+ # (SET TIME ZONE does not use an equals sign like other SET variables)
if ActiveRecord::Base.default_timezone == :utc
execute("SET time zone 'UTC'", 'SCHEMA')
elsif @local_tz
execute("SET time zone '#{@local_tz}'", 'SCHEMA')
end
+
+ # SET statements from :variables config hash
+ # http://www.postgresql.org/docs/8.3/static/sql-set.html
+ variables = @config[:variables] || {}
+ variables.map do |k, v|
+ if v == ':default' || v == :default
+ # Sets the value to the global or compile default
+ execute("SET SESSION #{k.to_s} TO DEFAULT", 'SCHEMA')
+ elsif !v.nil?
+ execute("SET SESSION #{k.to_s} TO #{quote(v)}", 'SCHEMA')
+ end
+ end
end
# Returns the current ID of a table's sequence.
@@ -785,8 +955,12 @@ module ActiveRecord
$1.strip if $1
end
- def table_definition
- TableDefinition.new(self)
+ def create_table_definition(name, temporary, options)
+ TableDefinition.new native_database_types, name, temporary, options
+ end
+
+ def update_table_definition(table_name, base)
+ Table.new(table_name, base)
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 aad1f9a7ef..e5c9f6f54a 100644
--- a/activerecord/lib/active_record/connection_adapters/schema_cache.rb
+++ b/activerecord/lib/active_record/connection_adapters/schema_cache.rb
@@ -1,7 +1,8 @@
+
module ActiveRecord
module ConnectionAdapters
class SchemaCache
- attr_reader :columns, :columns_hash, :primary_keys, :tables, :version
+ attr_reader :version
attr_accessor :connection
def initialize(conn)
@@ -14,6 +15,10 @@ module ActiveRecord
prepare_default_proc
end
+ def primary_keys(table_name)
+ @primary_keys[table_name]
+ end
+
# A cached lookup for table existence.
def table_exists?(name)
return @tables[name] if @tables.key? name
@@ -30,6 +35,21 @@ module ActiveRecord
end
end
+ def tables(name)
+ @tables[name]
+ end
+
+ # Get the columns for a table
+ def columns(table)
+ @columns[table]
+ end
+
+ # Get the columns for a table as a hash, key is the column name
+ # value is the column object.
+ def columns_hash(table)
+ @columns_hash[table]
+ end
+
# Clears out internal caches
def clear!
@columns.clear
@@ -39,6 +59,12 @@ module ActiveRecord
@version = nil
end
+ def size
+ [@columns, @columns_hash, @primary_keys, @tables].map { |x|
+ x.size
+ }.inject :+
+ end
+
# Clear out internal caches for table with +table_name+.
def clear_table_cache!(table_name)
@columns.delete table_name
@@ -50,9 +76,9 @@ 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 do |val|
- self.instance_variable_get(val).inject({}) { |h, v| h[v[0]] = v[1]; h }
- end
+ [@version] + [@columns, @columns_hash, @primary_keys, @tables].map { |val|
+ Hash[val]
+ }
end
def marshal_load(array)
@@ -68,7 +94,7 @@ module ActiveRecord
end
@columns_hash.default_proc = Proc.new do |h, table_name|
- h[table_name] = Hash[columns[table_name].map { |col|
+ h[table_name] = Hash[columns(table_name).map { |col|
[col.name, col]
}]
end
diff --git a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
index 4d5cb72c67..e5ad08b6b0 100644
--- a/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
+++ b/activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb
@@ -6,9 +6,9 @@ gem 'sqlite3', '~> 1.3.6'
require 'sqlite3'
module ActiveRecord
- module ConnectionHandling
+ module ConnectionHandling # :nodoc:
# sqlite3 adapter reuses sqlite_connection.
- def sqlite3_connection(config) # :nodoc:
+ def sqlite3_connection(config)
# Require database.
unless config[:database]
raise ArgumentError, "No database file specified. Missing argument: database"
@@ -17,16 +17,18 @@ module ActiveRecord
# Allow database path relative to Rails.root, but only if
# the database path is not the special path that tells
# Sqlite to build a database only in memory.
- if defined?(Rails.root) && ':memory:' != config[:database]
- config[:database] = File.expand_path(config[:database], Rails.root)
+ if ':memory:' != config[:database]
+ config[:database] = File.expand_path(config[:database], Rails.root) if defined?(Rails.root)
+ dirname = File.dirname(config[:database])
+ Dir.mkdir(dirname) unless File.directory?(dirname)
end
db = SQLite3::Database.new(
- config[:database],
+ config[:database].to_s,
:results_as_hash => true
)
- db.busy_timeout(config[:timeout]) if config[:timeout]
+ db.busy_timeout(ConnectionAdapters::SQLite3Adapter.type_cast_config_to_integer(config[:timeout])) if config[:timeout]
ConnectionAdapters::SQLite3Adapter.new(db, logger, config)
end
@@ -51,6 +53,23 @@ module ActiveRecord
#
# * <tt>:database</tt> - Path to the database file.
class SQLite3Adapter < AbstractAdapter
+ include Savepoints
+
+ NATIVE_DATABASE_TYPES = {
+ primary_key: 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL',
+ string: { name: "varchar", limit: 255 },
+ text: { name: "text" },
+ integer: { name: "integer" },
+ float: { name: "float" },
+ decimal: { name: "decimal" },
+ datetime: { name: "datetime" },
+ timestamp: { name: "datetime" },
+ time: { name: "time" },
+ date: { name: "date" },
+ binary: { name: "blob" },
+ boolean: { name: "boolean" }
+ }
+
class Version
include Comparable
@@ -107,13 +126,14 @@ module ActiveRecord
@active = nil
@statements = StatementPool.new(@connection,
- config.fetch(:statement_limit) { 1000 })
+ self.class.type_cast_config_to_integer(config.fetch(:statement_limit) { 1000 }))
@config = config
- if config.fetch(:prepared_statements) { true }
+ if self.class.type_cast_config_to_boolean(config.fetch(:prepared_statements) { true })
+ @prepared_statements = true
@visitor = Arel::Visitors::SQLite.new self
else
- @visitor = BindSubstitution.new self
+ @visitor = unprepared_visitor
end
end
@@ -178,30 +198,19 @@ module ActiveRecord
true
end
- # Returns true
- def supports_autoincrement? #:nodoc:
+ def supports_index_sort_order?
true
end
- def supports_index_sort_order?
- true
+ # Returns 62. SQLite supports index names up to 64
+ # characters. The rest is used by rails internally to perform
+ # temporary rename operations
+ def allowed_index_name_length
+ index_name_length - 2
end
def native_database_types #:nodoc:
- {
- :primary_key => default_primary_key_type,
- :string => { :name => "varchar", :limit => 255 },
- :text => { :name => "text" },
- :integer => { :name => "integer" },
- :float => { :name => "float" },
- :decimal => { :name => "decimal" },
- :datetime => { :name => "datetime" },
- :timestamp => { :name => "datetime" },
- :time => { :name => "time" },
- :date => { :name => "date" },
- :binary => { :name => "blob" },
- :boolean => { :name => "boolean" }
- }
+ NATIVE_DATABASE_TYPES
end
# Returns the current database encoding format as a string, eg: 'UTF-8'
@@ -217,8 +226,8 @@ module ActiveRecord
# QUOTING ==================================================
def quote(value, column = nil)
- if value.kind_of?(String) && column && column.type == :binary && column.class.respond_to?(:string_to_binary)
- s = column.class.string_to_binary(value).unpack("H*")[0]
+ if value.kind_of?(String) && column && column.type == :binary
+ s = value.unpack("H*")[0]
"x'#{s}'"
else
super
@@ -229,6 +238,10 @@ module ActiveRecord
@connection.class.quote(s)
end
+ def quote_table_name_for_assignment(table, attr)
+ quote_column_name(attr)
+ end
+
def quote_column_name(name) #:nodoc:
%Q("#{name.to_s.gsub('"', '""')}")
end
@@ -251,7 +264,7 @@ module ActiveRecord
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.encode! 'utf-8'
+ value = value.encode Encoding::UTF_8
end
value
end
@@ -280,8 +293,8 @@ module ActiveRecord
def exec_query(sql, name = nil, binds = [])
log(sql, name, binds) do
- # Don't cache statements without bind values
- if binds.empty?
+ # 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
@@ -337,18 +350,6 @@ module ActiveRecord
exec_query(sql, name).rows
end
- def create_savepoint
- execute("SAVEPOINT #{current_savepoint_name}")
- end
-
- def rollback_to_savepoint
- execute("ROLLBACK TO SAVEPOINT #{current_savepoint_name}")
- end
-
- def release_savepoint
- execute("RELEASE SAVEPOINT #{current_savepoint_name}")
- end
-
def begin_db_transaction #:nodoc:
log('begin transaction',nil) { @connection.transaction }
end
@@ -424,8 +425,9 @@ module ActiveRecord
#
# Example:
# rename_table('octopuses', 'octopi')
- def rename_table(name, new_name)
- exec_query "ALTER TABLE #{quote_table_name(name)} RENAME TO #{quote_table_name(new_name)}"
+ def rename_table(table_name, new_name)
+ exec_query "ALTER TABLE #{quote_table_name(table_name)} RENAME TO #{quote_table_name(new_name)}"
+ rename_table_indexes(table_name, new_name)
end
# See: http://www.sqlite.org/lang_altertable.html
@@ -444,15 +446,11 @@ module ActiveRecord
end
end
- def remove_column(table_name, *column_names) #:nodoc:
- raise ArgumentError.new("You must specify at least one column name. Example: remove_column(:people, :first_name)") if column_names.empty?
- column_names.each do |column_name|
- alter_table(table_name) do |definition|
- definition.columns.delete(definition[column_name])
- end
+ def remove_column(table_name, column_name, type = nil, options = {}) #:nodoc:
+ alter_table(table_name) do |definition|
+ definition.remove_column column_name
end
end
- alias :remove_columns :remove_column
def change_column_default(table_name, column_name, default) #:nodoc:
alter_table(table_name) do |definition|
@@ -488,6 +486,7 @@ module ActiveRecord
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)
end
protected
@@ -502,7 +501,7 @@ module ActiveRecord
end
def alter_table(table_name, options = {}) #:nodoc:
- altered_table_name = "altered_#{table_name}"
+ altered_table_name = "a#{table_name}"
caller = lambda {|definition| yield definition if block_given?}
transaction do
@@ -519,27 +518,24 @@ module ActiveRecord
def copy_table(from, to, options = {}) #:nodoc:
from_primary_key = primary_key(from)
- options[:primary_key] = from_primary_key if from_primary_key != 'id'
- unless options[:primary_key]
- options[:id] = columns(from).detect{|c| c.name == 'id'}.present? && from_primary_key == 'id'
- end
+ options[:id] = false
create_table(to, options) do |definition|
@definition = definition
+ @definition.primary_key(from_primary_key) if from_primary_key.present?
columns(from).each do |column|
column_name = options[:rename] ?
(options[:rename][column.name] ||
options[:rename][column.name.to_sym] ||
column.name) : column.name
+ next if column_name == from_primary_key
@definition.column(column_name, column.type,
:limit => column.limit, :default => column.default,
:precision => column.precision, :scale => column.scale,
:null => column.null)
end
- @definition.primary_key(from_primary_key) if from_primary_key
yield @definition if block_given?
end
-
copy_table_indexes(from, to, options[:rename] || {})
copy_table_contents(from, to,
@definition.columns.map {|column| column.name},
@@ -549,10 +545,10 @@ module ActiveRecord
def copy_table_indexes(from, to, rename = {}) #:nodoc:
indexes(from).each do |index|
name = index.name
- if to == "altered_#{from}"
- name = "temp_#{name}"
- elsif from == "altered_#{to}"
- name = name[5..-1]
+ if to == "a#{from}"
+ name = "t#{name}"
+ elsif from == "a#{to}"
+ name = name[1..-1]
end
to_column_names = columns(to).map { |c| c.name }
@@ -562,7 +558,7 @@ module ActiveRecord
unless columns.empty?
# index name can't be the same
- opts = { :name => name.gsub(/_(#{from})_/, "_#{to}_") }
+ opts = { name: name.gsub(/(^|_)(#{from})_/, "\\1#{to}_"), internal: true }
opts[:unique] = true if index.unique
add_index(to, columns, opts)
end
@@ -577,9 +573,17 @@ module ActiveRecord
quoted_columns = columns.map { |col| quote_column_name(col) } * ','
quoted_to = quote_table_name(to)
+
+ raw_column_mappings = Hash[columns(from).map { |c| [c.name, c] }]
+
exec_query("SELECT * FROM #{quote_table_name(from)}").each do |row|
sql = "INSERT INTO #{quoted_to} (#{quoted_columns}) VALUES ("
- sql << columns.map {|col| quote row[column_mappings[col]]} * ', '
+
+ column_values = columns.map do |col|
+ quote(row[column_mappings[col]], raw_column_mappings[col])
+ end
+
+ sql << column_values * ', '
sql << ')'
exec_query sql
end
@@ -589,14 +593,6 @@ module ActiveRecord
@sqlite_version ||= SQLite3Adapter::Version.new(select_value('select sqlite_version(*)'))
end
- def default_primary_key_type
- if supports_autoincrement?
- 'INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL'
- else
- 'INTEGER PRIMARY KEY NOT NULL'
- end
- end
-
def translate_exception(exception, message)
case exception.message
when /column(s)? .* (is|are) not unique/
@@ -605,7 +601,6 @@ module ActiveRecord
super
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/connection_handling.rb b/activerecord/lib/active_record/connection_handling.rb
index 3531be05bf..a1943dfcb0 100644
--- a/activerecord/lib/active_record/connection_handling.rb
+++ b/activerecord/lib/active_record/connection_handling.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module ConnectionHandling
# Establishes the connection to the database. Accepts a hash as input where
@@ -6,25 +5,25 @@ module ActiveRecord
# example for regular databases (MySQL, Postgresql, etc):
#
# ActiveRecord::Base.establish_connection(
- # :adapter => "mysql",
- # :host => "localhost",
- # :username => "myuser",
- # :password => "mypass",
- # :database => "somedatabase"
+ # adapter: "mysql",
+ # host: "localhost",
+ # username: "myuser",
+ # password: "mypass",
+ # database: "somedatabase"
# )
#
# Example for SQLite database:
#
# ActiveRecord::Base.establish_connection(
- # :adapter => "sqlite",
- # :database => "path/to/dbfile"
+ # adapter: "sqlite",
+ # database: "path/to/dbfile"
# )
#
# Also accepts keys as strings (for parsing from YAML for example):
#
# ActiveRecord::Base.establish_connection(
- # "adapter" => "sqlite",
- # "database" => "path/to/dbfile"
+ # "adapter" => "sqlite",
+ # "database" => "path/to/dbfile"
# )
#
# Or a URL:
@@ -55,17 +54,17 @@ module ActiveRecord
end
def connection_id
- Thread.current['ActiveRecord::Base.connection_id']
+ ActiveRecord::RuntimeRegistry.connection_id
end
def connection_id=(connection_id)
- Thread.current['ActiveRecord::Base.connection_id'] = connection_id
+ ActiveRecord::RuntimeRegistry.connection_id = connection_id
end
# Returns the configuration of the associated connection as a hash:
#
# ActiveRecord::Base.connection_config
- # # => {:pool=>5, :timeout=>5000, :database=>"db/development.sqlite3", :adapter=>"sqlite3"}
+ # # => {pool: 5, timeout: 5000, database: "db/development.sqlite3", adapter: "sqlite3"}
#
# Please use only for reading.
def connection_config
@@ -80,7 +79,7 @@ module ActiveRecord
connection_handler.retrieve_connection(self)
end
- # Returns true if Active Record is connected.
+ # Returns +true+ if Active Record is connected.
def connected?
connection_handler.connected?(self)
end
diff --git a/activerecord/lib/active_record/core.rb b/activerecord/lib/active_record/core.rb
index f97c363871..366ebde418 100644
--- a/activerecord/lib/active_record/core.rb
+++ b/activerecord/lib/active_record/core.rb
@@ -3,101 +3,94 @@ require 'active_support/core_ext/object/duplicable'
require 'thread'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- ##
- # :singleton-method:
- #
- # Accepts a logger conforming to the interface of Log4r which is then
- # passed on to any new database connections made and which can be
- # retrieved on both a class and instance level by calling +logger+.
- mattr_accessor :logger, instance_accessor: false
+ module Core
+ extend ActiveSupport::Concern
- ##
- # :singleton-method:
- # Contains the database configuration - as is typically stored in config/database.yml -
- # as a Hash.
- #
- # For example, the following database.yml...
- #
- # development:
- # adapter: sqlite3
- # database: db/development.sqlite3
- #
- # production:
- # adapter: sqlite3
- # database: db/production.sqlite3
- #
- # ...would result in ActiveRecord::Base.configurations to look like this:
- #
- # {
- # 'development' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/development.sqlite3'
- # },
- # 'production' => {
- # 'adapter' => 'sqlite3',
- # 'database' => 'db/production.sqlite3'
- # }
- # }
- mattr_accessor :configurations, instance_accessor: false
- self.configurations = {}
+ included do
+ ##
+ # :singleton-method:
+ #
+ # Accepts a logger conforming to the interface of Log4r which is then
+ # passed on to any new database connections made and which can be
+ # retrieved on both a class and instance level by calling +logger+.
+ mattr_accessor :logger, instance_writer: false
- ##
- # :singleton-method:
- # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
- # dates and times from the database. This is set to :utc by default.
- mattr_accessor :default_timezone, instance_accessor: false
- self.default_timezone = :utc
+ ##
+ # :singleton-method:
+ # Contains the database configuration - as is typically stored in config/database.yml -
+ # as a Hash.
+ #
+ # For example, the following database.yml...
+ #
+ # development:
+ # adapter: sqlite3
+ # database: db/development.sqlite3
+ #
+ # production:
+ # adapter: sqlite3
+ # database: db/production.sqlite3
+ #
+ # ...would result in ActiveRecord::Base.configurations to look like this:
+ #
+ # {
+ # 'development' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/development.sqlite3'
+ # },
+ # 'production' => {
+ # 'adapter' => 'sqlite3',
+ # 'database' => 'db/production.sqlite3'
+ # }
+ # }
+ mattr_accessor :configurations, instance_writer: false
+ self.configurations = {}
- ##
- # :singleton-method:
- # Specifies the format to use when dumping the database schema with Rails'
- # Rakefile. If :sql, the schema is dumped as (potentially database-
- # specific) SQL statements. If :ruby, the schema is dumped as an
- # ActiveRecord::Schema file which can be loaded into any database that
- # supports migrations. Use :ruby if you want to have different database
- # adapters for, e.g., your development and test environments.
- mattr_accessor :schema_format, instance_accessor: false
- self.schema_format = :ruby
+ ##
+ # :singleton-method:
+ # Determines whether to use Time.utc (using :utc) or Time.local (using :local) when pulling
+ # dates and times from the database. This is set to :utc by default.
+ mattr_accessor :default_timezone, instance_writer: false
+ self.default_timezone = :utc
- ##
- # :singleton-method:
- # Specify whether or not to use timestamps for migration versions
- mattr_accessor :timestamped_migrations, instance_accessor: false
- self.timestamped_migrations = true
+ ##
+ # :singleton-method:
+ # Specifies the format to use when dumping the database schema with Rails'
+ # Rakefile. If :sql, the schema is dumped as (potentially database-
+ # specific) SQL statements. If :ruby, the schema is dumped as an
+ # ActiveRecord::Schema file which can be loaded into any database that
+ # supports migrations. Use :ruby if you want to have different database
+ # adapters for, e.g., your development and test environments.
+ mattr_accessor :schema_format, instance_writer: false
+ self.schema_format = :ruby
- mattr_accessor :connection_handler, instance_accessor: false
- self.connection_handler = ConnectionAdapters::ConnectionHandler.new
+ ##
+ # :singleton-method:
+ # Specify whether or not to use timestamps for migration versions
+ mattr_accessor :timestamped_migrations, instance_writer: false
+ self.timestamped_migrations = true
- mattr_accessor :dependent_restrict_raises, instance_accessor: false
- self.dependent_restrict_raises = true
- end
+ def self.disable_implicit_join_references=(value)
+ ActiveSupport::Deprecation.warn("Implicit join references were removed with Rails 4.1." \
+ "Make sure to remove this configuration because it does nothing.")
+ end
- module Core
- extend ActiveSupport::Concern
+ class_attribute :default_connection_handler, instance_writer: false
- included do
- ##
- # :singleton-method:
- # The connection handler
- config_attribute :connection_handler
+ def self.connection_handler
+ ActiveRecord::RuntimeRegistry.connection_handler || default_connection_handler
+ end
- %w(logger configurations default_timezone schema_format timestamped_migrations).each do |name|
- config_attribute name, global: true
+ def self.connection_handler=(handler)
+ ActiveRecord::RuntimeRegistry.connection_handler = handler
end
+
+ self.default_connection_handler = ConnectionAdapters::ConnectionHandler.new
end
module ClassMethods
- def inherited(child_class) #:nodoc:
- child_class.initialize_generated_modules
- super
- end
-
def initialize_generated_modules
- @attribute_methods_mutex = Mutex.new
+ super
- # force attribute methods to be higher in inheritance hierarchy than other generated methods
- generated_attribute_methods
generated_feature_methods
end
@@ -115,6 +108,8 @@ module ActiveRecord
super
elsif abstract_class?
"#{super}(abstract)"
+ elsif !connected?
+ "#{super}(no database connection)"
elsif table_exists?
attr_list = columns.map { |c| "#{c.name}: #{c.type}" } * ', '
"#{super}(#{attr_list})"
@@ -139,13 +134,18 @@ module ActiveRecord
# Returns the Arel engine.
def arel_engine
- @arel_engine ||= connection_handler.retrieve_connection_pool(self) ? self : active_record_super.arel_engine
+ @arel_engine ||=
+ if Base == self || connection_handler.retrieve_connection_pool(self)
+ self
+ else
+ superclass.arel_engine
+ end
end
private
def relation #:nodoc:
- relation = Relation.new(self, arel_table)
+ relation = Relation.create(self, arel_table)
if finder_needs_type_condition?
relation.where(type_condition).create_with(inheritance_column.to_sym => sti_name)
@@ -162,19 +162,23 @@ module ActiveRecord
#
# ==== Example:
# # Instantiates a single new object
- # User.new(:first_name => 'Jamie')
- def initialize(attributes = nil)
+ # 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)
- @columns_hash = self.class.column_types.dup
+ @column_types_override = nil
+ @column_types = self.class.column_types
init_internals
+ init_changed_attributes
ensure_proper_type
populate_with_current_scope_attributes
- assign_attributes(attributes) if attributes
+ # +options+ argument is only needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ init_attributes(attributes, options) if attributes
yield self if block_given?
run_callbacks :initialize unless _initialize_callbacks.empty?
@@ -192,7 +196,8 @@ module ActiveRecord
# post.title # => 'hello world'
def init_with(coder)
@attributes = self.class.initialize_attributes(coder['attributes'])
- @columns_hash = self.class.column_types.merge(coder['column_types'] || {})
+ @column_types_override = coder['column_types']
+ @column_types = self.class.column_types
init_internals
@@ -241,9 +246,7 @@ module ActiveRecord
run_callbacks(:initialize) unless _initialize_callbacks.empty?
@changed_attributes = {}
- self.class.column_defaults.each do |attr, orig_value|
- @changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
- end
+ init_changed_attributes
@aggregation_cache = {}
@association_cache = {}
@@ -252,7 +255,6 @@ module ActiveRecord
@new_record = true
ensure_proper_type
- populate_with_current_scope_attributes
super
end
@@ -284,7 +286,7 @@ module ActiveRecord
def ==(comparison_object)
super ||
comparison_object.instance_of?(self.class) &&
- id.present? &&
+ id &&
comparison_object.id == id
end
alias :eql? :==
@@ -295,9 +297,11 @@ module ActiveRecord
id.hash
end
- # Freeze the attributes hash such that associations are still accessible, even on destroyed records.
+ # Clone and freeze the attributes hash such that associations are still
+ # accessible, even on destroyed records, but cloned models will not be
+ # frozen.
def freeze
- @attributes.freeze
+ @attributes = @attributes.clone.freeze
self
end
@@ -306,13 +310,6 @@ module ActiveRecord
@attributes.frozen?
end
- # Allows sort on objects
- def <=>(other_object)
- if other_object.is_a?(self.class)
- self.to_key <=> other_object.to_key
- end
- end
-
# Returns +true+ if the record is read only. Records loaded through joins with piggy-back
# attributes will be marked as read only since they cannot be saved.
def readonly?
@@ -324,16 +321,15 @@ module ActiveRecord
@readonly = true
end
- # Returns the connection currently associated with the class. This can
- # also be used to "borrow" the connection to do database work that isn't
- # easily done without going straight to SQL.
- def connection
- self.class.connection
+ def connection_handler
+ self.class.connection_handler
end
# Returns the contents of the record as a nicely formatted string.
def inspect
- inspection = if @attributes
+ # We check defined?(@attributes) not to issue warnings if the object is
+ # allocated but not initialized.
+ inspection = if defined?(@attributes) && @attributes
self.class.column_names.collect { |name|
if has_attribute?(name)
"#{name}: #{attribute_for_inspect(name)}"
@@ -347,11 +343,57 @@ module ActiveRecord
# Returns a hash of the given methods with their names as keys and returned values as values.
def slice(*methods)
- Hash[methods.map { |method| [method, public_send(method)] }].with_indifferent_access
+ Hash[methods.map! { |method| [method, public_send(method)] }].with_indifferent_access
+ end
+
+ def set_transaction_state(state) # :nodoc:
+ @transaction_state = state
+ end
+
+ def has_transactional_callbacks? # :nodoc:
+ !_rollback_callbacks.empty? || !_commit_callbacks.empty? || !_create_callbacks.empty?
end
private
+ # Updates the attributes on this particular ActiveRecord object so that
+ # if it is associated with a transaction, then the state of the AR object
+ # will be updated to reflect the current state of the transaction
+ #
+ # The @transaction_state variable stores the states of the associated
+ # transaction. This relies on the fact that a transaction can only be in
+ # one rollback or commit (otherwise a list of states would be required)
+ # Each AR object inside of a transaction carries that transaction's
+ # TransactionState.
+ #
+ # This method checks to see if the ActiveRecord object's state reflects
+ # the TransactionState, and rolls back or commits the ActiveRecord object
+ # as appropriate.
+ #
+ # Since ActiveRecord objects can be inside multiple transactions, this
+ # method recursively goes through the parent of the TransactionState and
+ # checks if the ActiveRecord object reflects the state of the object.
+ def sync_with_transaction_state
+ update_attributes_from_transaction_state(@transaction_state, 0)
+ end
+
+ def update_attributes_from_transaction_state(transaction_state, depth)
+ if transaction_state && !has_transactional_callbacks?
+ unless @reflects_state[depth]
+ if transaction_state.committed?
+ committed!
+ elsif transaction_state.rolledback?
+ rolledback!
+ end
+ @reflects_state[depth] = true
+ end
+
+ if transaction_state.parent && !@reflects_state[depth+1]
+ update_attributes_from_transaction_state(transaction_state.parent, depth+1)
+ end
+ end
+ end
+
# Under Ruby 1.9, Array#flatten will call #to_ary (recursively) on each of the elements
# of the array, and then rescues from the possible NoMethodError. If those elements are
# ActiveRecord::Base's, then this triggers the various method_missing's that we have,
@@ -368,17 +410,33 @@ module ActiveRecord
pk = self.class.primary_key
@attributes[pk] = nil unless @attributes.key?(pk)
- @aggregation_cache = {}
- @association_cache = {}
- @attributes_cache = {}
- @previously_changed = {}
- @changed_attributes = {}
- @readonly = false
- @destroyed = false
- @marked_for_destruction = false
- @new_record = true
- @txn = nil
+ @aggregation_cache = {}
+ @association_cache = {}
+ @attributes_cache = {}
+ @readonly = false
+ @destroyed = false
+ @marked_for_destruction = false
+ @destroyed_by_association = nil
+ @new_record = true
+ @txn = nil
@_start_transaction_state = {}
+ @transaction_state = nil
+ @reflects_state = [false]
+ end
+
+ def init_changed_attributes
+ # Intentionally avoid using #column_defaults since overridden defaults (as is done in
+ # optimistic locking) won't get written unless they get marked as changed
+ self.class.columns.each do |c|
+ attr, orig_value = c.name, c.default
+ changed_attributes[attr] = orig_value if _field_changed?(attr, orig_value, @attributes[attr])
+ end
+ end
+
+ # This method is needed to make protected_attributes gem easier to hook.
+ # Remove it when we drop support to this gem.
+ def init_attributes(attributes, options)
+ assign_attributes(attributes)
end
end
end
diff --git a/activerecord/lib/active_record/counter_cache.rb b/activerecord/lib/active_record/counter_cache.rb
index 57838ff984..e1faadf1ab 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 counter names to reset
+ # * +counters+ - One or more association counters to reset
#
# ==== Examples
#
@@ -21,6 +21,7 @@ module ActiveRecord
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
if has_many_association.is_a? ActiveRecord::Reflection::ThroughReflection
has_many_association = has_many_association.through_reflection
@@ -49,14 +50,14 @@ module ActiveRecord
# ==== Parameters
#
# * +id+ - The id of the object you wish to update a counter on or an Array of ids.
- # * +counters+ - An Array of Hashes containing the names of the fields
+ # * +counters+ - A Hash containing the names of the fields
# to update as keys and the amount to update the field by as values.
#
# ==== Examples
#
# # For the Post with id of 5, decrement the comment_count by 1, and
# # increment the action_count by 1
- # Post.update_counters 5, :comment_count => -1, :action_count => 1
+ # Post.update_counters 5, comment_count: -1, action_count: 1
# # Executes the following SQL:
# # UPDATE posts
# # SET comment_count = COALESCE(comment_count, 0) - 1,
@@ -64,7 +65,7 @@ module ActiveRecord
# # WHERE id = 5
#
# # For the Posts with id of 10 and 15, increment the comment_count by 1
- # Post.update_counters [10, 15], :comment_count => 1
+ # Post.update_counters [10, 15], comment_count: 1
# # Executes the following SQL:
# # UPDATE posts
# # SET comment_count = COALESCE(comment_count, 0) + 1
@@ -79,16 +80,17 @@ module ActiveRecord
where(primary_key => id).update_all updates.join(', ')
end
- # Increment a number field by one, usually representing a count.
+ # Increment a numeric field by one, via a direct SQL update.
#
- # This is used for caching aggregate values, so that they don't need to be computed every time.
- # For example, a DiscussionBoard may cache post_count and comment_count otherwise every time the board is
- # shown it would have to run an SQL query to find how many posts and comments there are.
+ # This method is used primarily for maintaining counter_cache columns used to
+ # store aggregate values. For example, a DiscussionBoard may cache posts_count
+ # and comments_count to avoid running an SQL query to calculate the number of
+ # posts and comments there are each time it is displayed.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be incremented.
- # * +id+ - The id of the object that should be incremented.
+ # * +id+ - The id of the object that should be incremented or an Array of ids.
#
# ==== Examples
#
@@ -98,14 +100,15 @@ module ActiveRecord
update_counters(id, counter_name => 1)
end
- # Decrement a number field by one, usually representing a count.
+ # Decrement a numeric field by one, via a direct SQL update.
#
- # This works the same as increment_counter but reduces the column value by 1 instead of increasing it.
+ # This works the same as increment_counter but reduces the column value by
+ # 1 instead of increasing it.
#
# ==== Parameters
#
# * +counter_name+ - The name of the field that should be decremented.
- # * +id+ - The id of the object that should be decremented.
+ # * +id+ - The id of the object that should be decremented or an Array of ids.
#
# ==== Examples
#
diff --git a/activerecord/lib/active_record/dynamic_matchers.rb b/activerecord/lib/active_record/dynamic_matchers.rb
index 3bac31c6aa..e650ebcf64 100644
--- a/activerecord/lib/active_record/dynamic_matchers.rb
+++ b/activerecord/lib/active_record/dynamic_matchers.rb
@@ -35,7 +35,7 @@ module ActiveRecord
end
def pattern
- /^#{prefix}_([_a-zA-Z]\w*)#{suffix}$/
+ @pattern ||= /\A#{prefix}_([_a-zA-Z]\w*)#{suffix}\Z/
end
def prefix
diff --git a/activerecord/lib/active_record/errors.rb b/activerecord/lib/active_record/errors.rb
index 0637dd58b6..7e38719811 100644
--- a/activerecord/lib/active_record/errors.rb
+++ b/activerecord/lib/active_record/errors.rb
@@ -22,7 +22,7 @@ module ActiveRecord
# end
#
# # Comments are not patches, this assignment raises AssociationTypeMismatch.
- # @ticket.patches << Comment.new(:content => "Please attach tests to your patch.")
+ # @ticket.patches << Comment.new(content: "Please attach tests to your patch.")
class AssociationTypeMismatch < ActiveRecordError
end
@@ -57,26 +57,23 @@ module ActiveRecord
class RecordNotDestroyed < ActiveRecordError
end
- # Raised when SQL statement cannot be executed by the database (for example, it's often the case for
- # MySQL when Ruby driver used is too old).
+ # Superclass for all database execution errors.
+ #
+ # Wraps the underlying database error as +original_exception+.
class StatementInvalid < ActiveRecordError
- end
-
- # Raised when SQL statement is invalid and the application gets a blank result.
- class ThrowResult < ActiveRecordError
- end
-
- # Parent class for all specific exceptions which wrap database driver exceptions
- # provides access to the original exception also.
- class WrappedDatabaseException < StatementInvalid
attr_reader :original_exception
- def initialize(message, original_exception)
+ def initialize(message, original_exception = nil)
super(message)
@original_exception = original_exception
end
end
+ # Defunct wrapper class kept for compatibility.
+ # +StatementInvalid+ wraps the original exception now.
+ class WrappedDatabaseException < StatementInvalid
+ end
+
# Raised when a record cannot be inserted because it would violate a uniqueness constraint.
class RecordNotUnique < WrappedDatabaseException
end
@@ -158,6 +155,15 @@ module ActiveRecord
# Raised when unknown attributes are supplied via mass assignment.
class UnknownAttributeError < NoMethodError
+
+ attr_reader :record, :attribute
+
+ def initialize(record, attribute)
+ @record = record
+ @attribute = attribute.to_s
+ super("unknown attribute: #{attribute}")
+ end
+
end
# Raised when an error occurred while doing a mass assignment to an attribute through the
@@ -193,6 +199,17 @@ module ActiveRecord
end
+ # Raised when a relation cannot be mutated because it's already loaded.
+ #
+ # class Task < ActiveRecord::Base
+ # end
+ #
+ # relation = Task.all
+ # relation.loaded? # => true
+ #
+ # # Methods which try to mutate a loaded relation fail.
+ # relation.where!(title: 'TODO') # => ActiveRecord::ImmutableRelation
+ # relation.limit!(5) # => ActiveRecord::ImmutableRelation
class ImmutableRelation < ActiveRecordError
end
diff --git a/activerecord/lib/active_record/explain.rb b/activerecord/lib/active_record/explain.rb
index 9e0390bed1..e65dab07ba 100644
--- a/activerecord/lib/active_record/explain.rb
+++ b/activerecord/lib/active_record/explain.rb
@@ -1,58 +1,22 @@
require 'active_support/lazy_load_hooks'
+require 'active_record/explain_registry'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :auto_explain_threshold_in_seconds, instance_accessor: false
- end
-
module Explain
- delegate :auto_explain_threshold_in_seconds, :auto_explain_threshold_in_seconds=, to: 'ActiveRecord::Model'
-
- # If auto explain is enabled, this method triggers EXPLAIN logging for the
- # queries triggered by the block if it takes more than the threshold as a
- # whole. That is, the threshold is not checked against each individual
- # query, but against the duration of the entire block. This approach is
- # convenient for relations.
- #
- # The available_queries_for_explain thread variable collects the queries
- # to be explained. If the value is nil, it means queries are not being
- # currently collected. A false value indicates collecting is turned
- # off. Otherwise it is an array of queries.
- def logging_query_plan # :nodoc:
- return yield unless logger
-
- threshold = auto_explain_threshold_in_seconds
- current = Thread.current
- if threshold && current[:available_queries_for_explain].nil?
- begin
- queries = current[:available_queries_for_explain] = []
- start = Time.now
- result = yield
- logger.warn(exec_explain(queries)) if Time.now - start > threshold
- result
- ensure
- current[:available_queries_for_explain] = nil
- end
- else
- yield
- end
- end
-
- # Relation#explain needs to be able to collect the queries regardless of
- # whether auto explain is enabled. This method serves that purpose.
+ # Executes the block with the collect flag enabled. Queries are collected
+ # asynchronously by the subscriber and returned.
def collecting_queries_for_explain # :nodoc:
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], []
- return yield, current[:available_queries_for_explain]
+ ExplainRegistry.collect = true
+ yield
+ ExplainRegistry.queries
ensure
- # Note that the return value above does not depend on this assigment.
- current[:available_queries_for_explain] = original
+ ExplainRegistry.reset
end
# Makes the adapter execute EXPLAIN for the tuples of queries and bindings.
# Returns a formatted string ready to be logged.
def exec_explain(queries) # :nodoc:
- str = queries && queries.map do |sql, bind|
+ str = queries.map do |sql, bind|
[].tap do |msg|
msg << "EXPLAIN for: #{sql}"
unless bind.empty?
@@ -67,22 +31,8 @@ module ActiveRecord
def str.inspect
self
end
- str
- end
- # Silences automatic EXPLAIN logging for the duration of the block.
- #
- # This has high priority, no EXPLAINs will be run even if downwards
- # the threshold is set to 0.
- #
- # As the name of the method suggests this only applies to automatic
- # EXPLAINs, manual calls to <tt>ActiveRecord::Relation#explain</tt> run.
- def silence_auto_explain
- current = Thread.current
- original, current[:available_queries_for_explain] = current[:available_queries_for_explain], false
- yield
- ensure
- current[:available_queries_for_explain] = original
+ str
end
end
end
diff --git a/activerecord/lib/active_record/explain_registry.rb b/activerecord/lib/active_record/explain_registry.rb
new file mode 100644
index 0000000000..f5cd57e075
--- /dev/null
+++ b/activerecord/lib/active_record/explain_registry.rb
@@ -0,0 +1,30 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for EXPLAIN. For example
+ #
+ # ActiveRecord::ExplainRegistry.queries
+ #
+ # returns the collected queries local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class ExplainRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :queries, :collect
+
+ def initialize
+ reset
+ end
+
+ def collect?
+ @collect
+ end
+
+ def reset
+ @collect = false
+ @queries = []
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/explain_subscriber.rb b/activerecord/lib/active_record/explain_subscriber.rb
index 0f927496fb..6a49936644 100644
--- a/activerecord/lib/active_record/explain_subscriber.rb
+++ b/activerecord/lib/active_record/explain_subscriber.rb
@@ -1,4 +1,5 @@
require 'active_support/notifications'
+require 'active_record/explain_registry'
module ActiveRecord
class ExplainSubscriber # :nodoc:
@@ -7,8 +8,8 @@ module ActiveRecord
end
def finish(name, id, payload)
- if queries = Thread.current[:available_queries_for_explain]
- queries << payload.values_at(:sql, :binds) unless ignore_payload?(payload)
+ if ExplainRegistry.collect? && !ignore_payload?(payload)
+ ExplainRegistry.queries << payload.values_at(:sql, :binds)
end
end
@@ -18,7 +19,7 @@ module ActiveRecord
# On the other hand, we want to monitor the performance of our real database
# queries, not the performance of the access to the query cache.
IGNORED_PAYLOADS = %w(SCHEMA EXPLAIN CACHE)
- EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)/i
+ EXPLAINED_SQLS = /\A\s*(select|update|delete|insert)\b/i
def ignore_payload?(payload)
payload[:exception] || IGNORED_PAYLOADS.include?(payload[:name]) || payload[:sql] !~ EXPLAINED_SQLS
end
diff --git a/activerecord/lib/active_record/fixture_set/file.rb b/activerecord/lib/active_record/fixture_set/file.rb
index 11b53275e1..fbd7a4d891 100644
--- a/activerecord/lib/active_record/fixture_set/file.rb
+++ b/activerecord/lib/active_record/fixture_set/file.rb
@@ -24,7 +24,6 @@ module ActiveRecord
rows.each(&block)
end
- RESCUE_ERRORS = [ ArgumentError, Psych::SyntaxError ] # :nodoc:
private
def rows
@@ -32,7 +31,7 @@ module ActiveRecord
begin
data = YAML.load(render(IO.read(@file)))
- rescue *RESCUE_ERRORS => error
+ rescue ArgumentError, Psych::SyntaxError => error
raise Fixture::FormatError, "a YAML error occurred parsing #{@file}. Please note that YAML must be consistently indented using spaces. Tabs are not allowed. Please have a look at http://www.yaml.org/faq.html\nThe exact error was:\n #{error.class}: #{error}", error.backtrace
end
@rows = data ? validate(data).to_a : []
diff --git a/activerecord/lib/active_record/fixtures.rb b/activerecord/lib/active_record/fixtures.rb
index 413bd147de..3bb3131bd1 100644
--- a/activerecord/lib/active_record/fixtures.rb
+++ b/activerecord/lib/active_record/fixtures.rb
@@ -5,8 +5,6 @@ require 'active_support/dependencies'
require 'active_record/fixture_set/file'
require 'active_record/errors'
-require 'active_support/deprecation' # temporary
-
module ActiveRecord
class FixtureClassNotFound < ActiveRecord::ActiveRecordError #:nodoc:
end
@@ -250,7 +248,7 @@ module ActiveRecord
#
# ### in fruit.rb
#
- # belongs_to :eater, :polymorphic => true
+ # belongs_to :eater, polymorphic: true
#
# ### in fruits.yml
#
@@ -365,11 +363,11 @@ module ActiveRecord
#
# first:
# name: Smurf
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# second:
# name: Fraggle
- # *DEFAULTS
+ # <<: *DEFAULTS
#
# Any fixture labeled "DEFAULTS" is safely ignored.
class FixtureSet
@@ -381,16 +379,16 @@ module ActiveRecord
@@all_cached_fixtures = Hash.new { |h,k| h[k] = {} }
- def self.default_fixture_model_name(fixture_set_name) # :nodoc:
- ActiveRecord::Base.pluralize_table_names ?
+ def self.default_fixture_model_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ config.pluralize_table_names ?
fixture_set_name.singularize.camelize :
fixture_set_name.camelize
end
- def self.default_fixture_table_name(fixture_set_name) # :nodoc:
- "#{ ActiveRecord::Base.table_name_prefix }"\
+ def self.default_fixture_table_name(fixture_set_name, config = ActiveRecord::Base) # :nodoc:
+ "#{ config.table_name_prefix }"\
"#{ fixture_set_name.tr('/', '_') }"\
- "#{ ActiveRecord::Base.table_name_suffix }".to_sym
+ "#{ config.table_name_suffix }".to_sym
end
def self.reset_cache
@@ -438,9 +436,47 @@ module ActiveRecord
cattr_accessor :all_loaded_fixtures
self.all_loaded_fixtures = {}
- def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {})
+ class ClassCache
+ def initialize(class_names, config)
+ @class_names = class_names.stringify_keys
+ @config = config
+
+ # Remove string values that aren't constants or subclasses of AR
+ @class_names.delete_if { |k,klass|
+ unless klass.is_a? Class
+ klass = klass.safe_constantize
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
+ !insert_class(@class_names, k, klass)
+ }
+ end
+
+ def [](fs_name)
+ @class_names.fetch(fs_name) {
+ klass = default_fixture_model(fs_name, @config).safe_constantize
+ insert_class(@class_names, fs_name, klass)
+ }
+ end
+
+ private
+
+ def insert_class(class_names, name, klass)
+ # We only want to deal with AR objects.
+ if klass && klass < ActiveRecord::Base
+ class_names[name] = klass
+ else
+ class_names[name] = nil
+ end
+ end
+
+ def default_fixture_model(fs_name, config)
+ ActiveRecord::FixtureSet.default_fixture_model_name(fs_name, config)
+ end
+ end
+
+ def self.create_fixtures(fixtures_directory, fixture_set_names, class_names = {}, config = ActiveRecord::Base)
fixture_set_names = Array(fixture_set_names).map(&:to_s)
- class_names = class_names.stringify_keys
+ class_names = ClassCache.new class_names, config
# FIXME: Apparently JK uses this.
connection = block_given? ? yield : ActiveRecord::Base.connection
@@ -454,10 +490,12 @@ module ActiveRecord
fixtures_map = {}
fixture_sets = files_to_read.map do |fs_name|
+ klass = class_names[fs_name]
+ conn = klass ? klass.connection : connection
fixtures_map[fs_name] = new( # ActiveRecord::FixtureSet.new
- connection,
+ conn,
fs_name,
- class_names[fs_name] || default_fixture_model_name(fs_name),
+ klass,
::File.join(fixtures_directory, fs_name))
end
@@ -499,27 +537,31 @@ module ActiveRecord
Zlib.crc32(label.to_s) % MAX_ID
end
- attr_reader :table_name, :name, :fixtures, :model_class
+ attr_reader :table_name, :name, :fixtures, :model_class, :config
- def initialize(connection, name, class_name, path)
- @fixtures = {} # Ordered hash
+ def initialize(connection, name, class_name, path, config = ActiveRecord::Base)
@name = name
@path = path
+ @config = config
+ @model_class = nil
+
+ if class_name.is_a?(String)
+ ActiveSupport::Deprecation.warn("The ability to pass in strings as a class name will be removed in Rails 4.2, consider using the class itself instead.")
+ end
if class_name.is_a?(Class) # TODO: Should be an AR::Base type class, or any?
@model_class = class_name
else
- @model_class = class_name.constantize rescue nil
+ @model_class = class_name.safe_constantize if class_name
end
- @connection = ( model_class.respond_to?(:connection) ?
- model_class.connection : connection )
+ @connection = connection
@table_name = ( model_class.respond_to?(:table_name) ?
model_class.table_name :
- self.class.default_fixture_table_name(name) )
+ self.class.default_fixture_table_name(name, config) )
- read_fixture_files
+ @fixtures = read_fixture_files path, @model_class
end
def [](x)
@@ -541,7 +583,7 @@ module ActiveRecord
# Return a hash of rows to be inserted. The key is the table, the value is
# a list of rows to insert to that table.
def table_rows
- now = ActiveRecord::Base.default_timezone == :utc ? Time.now.utc : Time.now
+ now = config.default_timezone == :utc ? Time.now.utc : Time.now
now = now.to_s(:db)
# allow a standard key to be used for doing defaults in YAML
@@ -553,7 +595,7 @@ module ActiveRecord
rows[table_name] = fixtures.map do |label, fixture|
row = fixture.to_hash
- if model_class && model_class < ActiveRecord::Model
+ if model_class
# fill in timestamp columns if they aren't specified and the model is set to record_timestamps
if model_class.record_timestamps
timestamp_column_names.each do |c_name|
@@ -563,7 +605,7 @@ module ActiveRecord
# interpolate the fixture label
row.each do |key, value|
- row[key] = label if value == "$LABEL"
+ row[key] = label if "$LABEL" == value
end
# generate a primary key if necessary
@@ -593,14 +635,9 @@ module ActiveRecord
row[fk_name] = ActiveRecord::FixtureSet.identify(value)
end
- when :has_and_belongs_to_many
- if (targets = row.delete(association.name.to_s))
- targets = targets.is_a?(Array) ? targets : targets.split(/\s*,\s*/)
- table_name = association.join_table
- rows[table_name].concat targets.map { |target|
- { association.foreign_key => row[primary_key_name],
- association.association_foreign_key => ActiveRecord::FixtureSet.identify(target) }
- }
+ when :has_many
+ if association.options[:through]
+ add_join_records(rows, row, HasManyThroughProxy.new(association))
end
end
end
@@ -611,11 +648,50 @@ module ActiveRecord
rows
end
+ class ReflectionProxy # :nodoc:
+ def initialize(association)
+ @association = association
+ end
+
+ def join_table
+ @association.join_table
+ end
+
+ def name
+ @association.name
+ end
+ end
+
+ class HasManyThroughProxy < ReflectionProxy # :nodoc:
+ def rhs_key
+ @association.foreign_key
+ end
+
+ def lhs_key
+ @association.through_reflection.foreign_key
+ end
+ end
+
private
def primary_key_name
@primary_key_name ||= model_class && model_class.primary_key
end
+ def 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
+
+ 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) }
+ }
+ end
+ end
+
def has_primary_key_column?
@has_primary_key_column ||= primary_key_name &&
model_class.columns.any? { |c| c.name == primary_key_name }
@@ -634,12 +710,12 @@ module ActiveRecord
@column_names ||= @connection.columns(@table_name).collect { |c| c.name }
end
- def read_fixture_files
- yaml_files = Dir["#{@path}/**/*.yml"].select { |f|
+ def read_fixture_files(path, model_class)
+ yaml_files = Dir["#{path}/{**,*}/*.yml"].select { |f|
::File.file?(f)
- } + [yaml_file_path]
+ } + [yaml_file_path(path)]
- yaml_files.each do |file|
+ yaml_files.each_with_object({}) do |file, fixtures|
FixtureSet::File.open(file) do |fh|
fh.each do |fixture_name, row|
fixtures[fixture_name] = ActiveRecord::Fixture.new(row, model_class)
@@ -648,8 +724,8 @@ module ActiveRecord
end
end
- def yaml_file_path
- "#{@path}.yml"
+ def yaml_file_path(path)
+ "#{path}.yml"
end
end
@@ -657,6 +733,7 @@ module ActiveRecord
#--
# Deprecate 'Fixtures' in favor of 'FixtureSet'.
#++
+ # :nodoc:
Fixtures = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('ActiveRecord::Fixtures', 'ActiveRecord::FixtureSet')
class Fixture #:nodoc:
@@ -703,24 +780,33 @@ module ActiveRecord
module TestFixtures
extend ActiveSupport::Concern
- included do
- setup :setup_fixtures
- teardown :teardown_fixtures
+ def before_setup
+ setup_fixtures
+ super
+ end
+
+ def after_teardown
+ super
+ teardown_fixtures
+ end
- class_attribute :fixture_path
+ included do
+ class_attribute :fixture_path, :instance_writer => false
class_attribute :fixture_table_names
class_attribute :fixture_class_names
class_attribute :use_transactional_fixtures
class_attribute :use_instantiated_fixtures # true, false, or :no_instances
class_attribute :pre_loaded_fixtures
+ class_attribute :config
self.fixture_table_names = []
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
self.pre_loaded_fixtures = false
+ self.config = ActiveRecord::Base
self.fixture_class_names = Hash.new do |h, fixture_set_name|
- h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name)
+ h[fixture_set_name] = ActiveRecord::FixtureSet.default_fixture_model_name(fixture_set_name, self.config)
end
end
@@ -729,40 +815,31 @@ module ActiveRecord
#
# Examples:
#
- # set_fixture_class :some_fixture => SomeModel,
+ # set_fixture_class some_fixture: SomeModel,
# 'namespaced/fixture' => Another::Model
#
# The keys must be the fixture names, that coincide with the short paths to the fixture files.
- #--
- # It is also possible to pass the class name instead of the class:
- # set_fixture_class 'some_fixture' => 'SomeModel'
- # I think this option is redundant, i propose to deprecate it.
- # Isn't it easier to always pass the class itself?
- # (2011-12-20 alexeymuranov)
- #++
def set_fixture_class(class_names = {})
self.fixture_class_names = self.fixture_class_names.merge(class_names.stringify_keys)
end
def fixtures(*fixture_set_names)
if fixture_set_names.first == :all
- fixture_set_names = Dir["#{fixture_path}/**/*.yml"].map { |f|
- File.basename f, '.yml'
- }
+ fixture_set_names = Dir["#{fixture_path}/{**,*}/*.{yml}"]
+ fixture_set_names.map! { |f| f[(fixture_path.to_s.size + 1)..-5] }
else
fixture_set_names = fixture_set_names.flatten.map { |n| n.to_s }
end
self.fixture_table_names |= fixture_set_names
- require_fixture_classes(fixture_set_names)
+ require_fixture_classes(fixture_set_names, self.config)
setup_fixture_accessors(fixture_set_names)
end
def try_to_load_dependency(file_name)
require_dependency file_name
rescue LoadError => e
- # Let's hope the developer has included it himself
-
+ # Let's hope the developer has included it
# Let's warn in case this is a subdependency, otherwise
# subdependency error messages are totally cryptic
if ActiveRecord::Base.logger
@@ -770,7 +847,7 @@ module ActiveRecord
end
end
- def require_fixture_classes(fixture_set_names = nil)
+ def require_fixture_classes(fixture_set_names = nil, config = ActiveRecord::Base)
if fixture_set_names
fixture_set_names = fixture_set_names.map { |n| n.to_s }
else
@@ -778,7 +855,7 @@ module ActiveRecord
end
fixture_set_names.each do |file_name|
- file_name = file_name.singularize if ActiveRecord::Base.pluralize_table_names
+ file_name = file_name.singularize if config.pluralize_table_names
try_to_load_dependency(file_name)
end
end
@@ -830,9 +907,7 @@ module ActiveRecord
!self.class.uses_transaction?(method_name)
end
- def setup_fixtures
- return if ActiveRecord::Base.configurations.blank?
-
+ def setup_fixtures(config = ActiveRecord::Base)
if pre_loaded_fixtures && !use_transactional_fixtures
raise RuntimeError, 'pre_loaded_fixtures requires use_transactional_fixtures'
end
@@ -846,7 +921,7 @@ module ActiveRecord
if @@already_loaded_fixtures[self.class]
@loaded_fixtures = @@already_loaded_fixtures[self.class]
else
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
@@already_loaded_fixtures[self.class] = @loaded_fixtures
end
@fixture_connections = enlist_fixture_connections
@@ -857,48 +932,45 @@ module ActiveRecord
else
ActiveRecord::FixtureSet.reset_cache
@@already_loaded_fixtures[self.class] = nil
- @loaded_fixtures = load_fixtures
+ @loaded_fixtures = load_fixtures(config)
end
# Instantiate fixtures for every test if requested.
- instantiate_fixtures if use_instantiated_fixtures
+ instantiate_fixtures(config) if use_instantiated_fixtures
end
def teardown_fixtures
- return unless defined?(ActiveRecord) && !ActiveRecord::Base.configurations.blank?
-
- unless run_in_transaction?
- ActiveRecord::FixtureSet.reset_cache
- end
-
# Rollback changes if a transaction is active.
if run_in_transaction?
@fixture_connections.each do |connection|
connection.rollback_transaction if connection.transaction_open?
end
@fixture_connections.clear
+ else
+ ActiveRecord::FixtureSet.reset_cache
end
+
ActiveRecord::Base.clear_active_connections!
end
def enlist_fixture_connections
- ActiveRecord::Base.connection_handler.connection_pools.map(&:connection)
+ ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
end
private
- def load_fixtures
- fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names)
+ def load_fixtures(config)
+ fixtures = ActiveRecord::FixtureSet.create_fixtures(fixture_path, fixture_table_names, fixture_class_names, config)
Hash[fixtures.map { |f| [f.name, f] }]
end
# for pre_loaded_fixtures, only require the classes once. huge speed improvement
@@required_fixture_classes = false
- def instantiate_fixtures
+ def instantiate_fixtures(config)
if pre_loaded_fixtures
raise RuntimeError, 'Load fixtures before instantiating them.' if ActiveRecord::FixtureSet.all_loaded_fixtures.empty?
unless @@required_fixture_classes
- self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys
+ self.class.require_fixture_classes ActiveRecord::FixtureSet.all_loaded_fixtures.keys, config
@@required_fixture_classes = true
end
ActiveRecord::FixtureSet.instantiate_all_loaded_fixtures(self, load_instances?)
diff --git a/activerecord/lib/active_record/inheritance.rb b/activerecord/lib/active_record/inheritance.rb
index 35273b0d81..7e1e120288 100644
--- a/activerecord/lib/active_record/inheritance.rb
+++ b/activerecord/lib/active_record/inheritance.rb
@@ -1,29 +1,41 @@
+require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- # Determine whether to store the full constant name including namespace when using STI
- mattr_accessor :store_full_sti_class, instance_accessor: false
- self.store_full_sti_class = true
- end
-
module Inheritance
extend ActiveSupport::Concern
included do
- config_attribute :store_full_sti_class
+ # Determines whether to store the full constant name including namespace when using STI.
+ class_attribute :store_full_sti_class, instance_writer: false
+ self.store_full_sti_class = true
end
module ClassMethods
- # True if this isn't a concrete subclass needing a STI type condition.
- def descends_from_active_record?
- sup = active_record_super
+ # Determines if one of the attributes passed in is the inheritance column,
+ # and if the inheritance column is attr accessible, it initializes an
+ # instance of the given subclass instead of the base class.
+ def new(*args, &block)
+ if abstract_class? || self == Base
+ raise NotImplementedError, "#{self} is an abstract class and can not be instantiated."
+ end
+ if (attrs = args.first).is_a?(Hash)
+ if subclass = subclass_from_attrs(attrs)
+ return subclass.new(*args, &block)
+ end
+ end
+ # Delegate to the original .new
+ super
+ end
- if sup.abstract_class?
- sup.descends_from_active_record?
- elsif self == Base
+ # Returns +true+ if this does not need STI type condition. Returns
+ # +false+ if STI type condition needs to be applied.
+ def descends_from_active_record?
+ if self == Base
false
+ elsif superclass.abstract_class?
+ superclass.descends_from_active_record?
else
- [Base, Model].include?(sup) || !columns_hash.include?(inheritance_column)
+ superclass == Base || !columns_hash.include?(inheritance_column)
end
end
@@ -40,9 +52,8 @@ module ActiveRecord
@symbolized_sti_name ||= sti_name.present? ? sti_name.to_sym : symbolized_base_class
end
- # Returns the class descending directly from ActiveRecord::Base (or
- # that includes ActiveRecord::Model), or an abstract class, if any, in
- # the inheritance hierarchy.
+ # Returns the class descending directly from ActiveRecord::Base, or
+ # an abstract class, if any, in the inheritance hierarchy.
#
# If A extends AR::Base, A.base_class will return A. If B descends from A
# through some arbitrarily deep hierarchy, B.base_class will return A.
@@ -50,15 +61,14 @@ module ActiveRecord
# If B < A and C < B and if A is an abstract_class then both B.base_class
# and C.base_class would return B as the answer since A is an abstract_class.
def base_class
- unless self < ActiveRecord::Tag
+ unless self < Base
raise ActiveRecordError, "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
end
- sup = active_record_super
- if sup == Base || sup == Model || sup.abstract_class?
+ if superclass == Base || superclass.abstract_class?
self
else
- sup.base_class
+ superclass.base_class
end
end
@@ -88,23 +98,6 @@ module ActiveRecord
store_full_sti_class ? name : name.demodulize
end
- # Finder methods must instantiate through this method to work with the
- # single-table inheritance model that makes it possible to create
- # objects of different types from the same table.
- def instantiate(record, column_types = {})
- sti_class = find_sti_class(record[inheritance_column])
- column_types = sti_class.decorate_columns(column_types)
- sti_class.allocate.init_with('attributes' => record, 'column_types' => column_types)
- end
-
- # For internal use.
- #
- # If this class includes ActiveRecord::Model then it won't have a
- # superclass. So this provides a way to get to the 'root' (ActiveRecord::Model).
- def active_record_super #:nodoc:
- superclass < Model ? superclass : Model
- end
-
protected
# Returns the class type of the record using the current module as a prefix. So descendants of
@@ -124,9 +117,10 @@ module ActiveRecord
begin
constant = ActiveSupport::Dependencies.constantize(candidate)
return constant if candidate == constant.to_s
- rescue NameError => e
- # We don't want to swallow NoMethodError < NameError errors
- raise e unless e.instance_of?(NameError)
+ # We don't want to swallow NoMethodError < NameError errors
+ rescue NoMethodError
+ raise
+ rescue NameError
end
end
@@ -136,32 +130,59 @@ module ActiveRecord
private
+ # Called by +instantiate+ to decide which class to use for a new
+ # record instance. For single-table inheritance, we check the record
+ # for a +type+ column and return the corresponding class.
+ def discriminate_class_for_record(record)
+ if using_single_table_inheritance?(record)
+ find_sti_class(record[inheritance_column])
+ else
+ super
+ end
+ end
+
+ def using_single_table_inheritance?(record)
+ record[inheritance_column].present? && columns_hash.include?(inheritance_column)
+ end
+
def find_sti_class(type_name)
- if type_name.blank? || !columns_hash.include?(inheritance_column)
- self
+ if store_full_sti_class
+ ActiveSupport::Dependencies.constantize(type_name)
else
- begin
- if store_full_sti_class
- ActiveSupport::Dependencies.constantize(type_name)
- else
- compute_type(type_name)
- end
- rescue NameError
- raise SubclassNotFound,
- "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
- "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
- "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
- "or overwrite #{name}.inheritance_column to use another column for that information."
- end
+ compute_type(type_name)
end
+ rescue NameError
+ raise SubclassNotFound,
+ "The single-table inheritance mechanism failed to locate the subclass: '#{type_name}'. " +
+ "This error is raised because the column '#{inheritance_column}' is reserved for storing the class in case of inheritance. " +
+ "Please rename this column if you didn't intend it to be used for storing the inheritance class " +
+ "or overwrite #{name}.inheritance_column to use another column for that information."
end
def type_condition(table = arel_table)
- sti_column = table[inheritance_column.to_sym]
+ sti_column = table[inheritance_column]
sti_names = ([self] + descendants).map { |model| model.sti_name }
sti_column.in(sti_names)
end
+
+ # Detect the subclass from the inheritance column of attrs. If the inheritance column value
+ # is not self or a valid subclass, raises ActiveRecord::SubclassNotFound
+ # If this is a StrongParameters hash, and access to inheritance_column is not permitted,
+ # this will ignore the inheritance column and return nil
+ def subclass_from_attrs(attrs)
+ subclass_name = attrs.with_indifferent_access[inheritance_column]
+
+ if subclass_name.present? && subclass_name != self.name
+ subclass = subclass_name.safe_constantize
+
+ unless descendants.include?(subclass)
+ raise ActiveRecord::SubclassNotFound.new("Invalid single-table inheritance type: #{subclass_name} is not a subclass of #{name}")
+ end
+
+ subclass
+ end
+ end
end
private
diff --git a/activerecord/lib/active_record/integration.rb b/activerecord/lib/active_record/integration.rb
index 23c272ef12..2589b2f3da 100644
--- a/activerecord/lib/active_record/integration.rb
+++ b/activerecord/lib/active_record/integration.rb
@@ -1,5 +1,18 @@
module ActiveRecord
module Integration
+ extend ActiveSupport::Concern
+
+ included do
+ ##
+ # :singleton-method:
+ # Indicates the format used to generate the timestamp in the cache key.
+ # Accepts any of the symbols in <tt>Time::DATE_FORMATS</tt>.
+ #
+ # This is +:nsec+, by default.
+ class_attribute :cache_timestamp_format, :instance_writer => false
+ self.cache_timestamp_format = :nsec
+ end
+
# Returns a String, which Action Pack uses for constructing an URL to this
# object. The default implementation returns this record's id as a String,
# or nil if this record's unsaved.
@@ -8,7 +21,7 @@ module ActiveRecord
# <tt>resources :users</tt> route. Normally, +user_path+ will
# construct a path with the user object's 'id' in it:
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/1"
#
# You can override +to_param+ in your model to make +user_path+ construct
@@ -20,7 +33,7 @@ module ActiveRecord
# end
# end
#
- # user = User.find_by_name('Phusion')
+ # user = User.find_by(name: 'Phusion')
# user_path(user) # => "/users/Phusion"
def to_param
# We can't use alias_method here, because method 'id' optimizes itself on the fly.
@@ -29,8 +42,6 @@ module ActiveRecord
# Returns a cache key that can be used to identify this record.
#
- # ==== Examples
- #
# Product.new.cache_key # => "products/new"
# Product.find(5).cache_key # => "products/5" (updated_at not available)
# Person.find(5).cache_key # => "people/5-20071224150000" (updated_at available)
@@ -38,8 +49,8 @@ module ActiveRecord
case
when new_record?
"#{self.class.model_name.cache_key}/new"
- when timestamp = self[:updated_at]
- timestamp = timestamp.utc.to_s(:nsec)
+ when timestamp = max_updated_column_timestamp
+ timestamp = timestamp.utc.to_s(cache_timestamp_format)
"#{self.class.model_name.cache_key}/#{id}-#{timestamp}"
else
"#{self.class.model_name.cache_key}/#{id}"
diff --git a/activerecord/lib/active_record/locale/en.yml b/activerecord/lib/active_record/locale/en.yml
index 896132d566..b1fbd38622 100644
--- a/activerecord/lib/active_record/locale/en.yml
+++ b/activerecord/lib/active_record/locale/en.yml
@@ -4,11 +4,15 @@ en:
#created_at: "Created at"
#updated_at: "Updated at"
+ # Default error messages
+ errors:
+ messages:
+ taken: "has already been taken"
+
# Active Record models configuration
activerecord:
errors:
messages:
- taken: "has already been taken"
record_invalid: "Validation failed: %{errors}"
restrict_dependent_destroy:
one: "Cannot delete record because a dependent %{record} exists"
diff --git a/activerecord/lib/active_record/locking/optimistic.rb b/activerecord/lib/active_record/locking/optimistic.rb
index e96ed00f9c..55776a91c0 100644
--- a/activerecord/lib/active_record/locking/optimistic.rb
+++ b/activerecord/lib/active_record/locking/optimistic.rb
@@ -1,9 +1,4 @@
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :lock_optimistically, instance_accessor: false
- self.lock_optimistically = true
- end
-
module Locking
# == What is Optimistic Locking
#
@@ -56,7 +51,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :lock_optimistically
+ class_attribute :lock_optimistically, instance_writer: false
+ self.lock_optimistically = true
end
def locking_enabled? #:nodoc:
@@ -70,7 +66,7 @@ module ActiveRecord
send(lock_col + '=', previous_lock_value + 1)
end
- def update(attribute_names = @attributes.keys) #:nodoc:
+ def update_record(attribute_names = @attributes.keys) #:nodoc:
return super unless locking_enabled?
return 0 if attribute_names.empty?
@@ -86,11 +82,11 @@ module ActiveRecord
stmt = relation.where(
relation.table[self.class.primary_key].eq(id).and(
- relation.table[lock_col].eq(self.class.quote_value(previous_lock_value))
+ relation.table[lock_col].eq(self.class.quote_value(previous_lock_value, column_for_attribute(lock_col)))
)
).arel.compile_update(arel_attributes_with_values_for_update(attribute_names))
- affected_rows = connection.update stmt
+ affected_rows = self.class.connection.update stmt
unless affected_rows == 1
raise ActiveRecord::StaleObjectError.new(self, "update")
@@ -121,7 +117,7 @@ module ActiveRecord
if locking_enabled?
column_name = self.class.locking_column
column = self.class.columns_hash[column_name]
- substitute = connection.substitute_at(column, relation.bind_values.length)
+ substitute = self.class.connection.substitute_at(column, relation.bind_values.length)
relation = relation.where(self.class.arel_table[column_name].eq(substitute))
relation.bind_values << [column, self[column_name].to_i]
@@ -142,6 +138,7 @@ module ActiveRecord
# Set the column to use for optimistic locking. Defaults to +lock_version+.
def locking_column=(value)
+ @column_defaults = nil
@locking_column = value.to_s
end
@@ -153,6 +150,7 @@ module ActiveRecord
# Quote the column name used for optimistic locking.
def quoted_locking_column
+ ActiveSupport::Deprecation.warn "ActiveRecord::Base.quoted_locking_column is deprecated and will be removed in Rails 4.2 or later."
connection.quote_column_name(locking_column)
end
diff --git a/activerecord/lib/active_record/locking/pessimistic.rb b/activerecord/lib/active_record/locking/pessimistic.rb
index 58af92f0b1..ff7102d35b 100644
--- a/activerecord/lib/active_record/locking/pessimistic.rb
+++ b/activerecord/lib/active_record/locking/pessimistic.rb
@@ -3,12 +3,12 @@ module ActiveRecord
# Locking::Pessimistic provides support for row-level locking using
# SELECT ... FOR UPDATE and other lock types.
#
- # Pass <tt>:lock => true</tt> to <tt>ActiveRecord::Base.find</tt> to obtain an exclusive
+ # Chain <tt>ActiveRecord::Base#find</tt> to <tt>ActiveRecord::QueryMethods#lock</tt> to obtain an exclusive
# lock on the selected rows:
# # select * from accounts where id=1 for update
- # Account.find(1, :lock => true)
+ # Account.lock.find(1)
#
- # Pass <tt>:lock => 'some locking clause'</tt> to give a database-specific locking clause
+ # Call <tt>lock('some locking clause')</tt> to use a database-specific locking clause
# of your own such as 'LOCK IN SHARE MODE' or 'FOR UPDATE NOWAIT'. Example:
#
# Account.transaction do
@@ -26,7 +26,7 @@ module ActiveRecord
#
# Account.transaction do
# # select * from accounts where ...
- # accounts = Account.where(...).all
+ # accounts = Account.where(...)
# account1 = accounts.detect { |account| ... }
# account2 = accounts.detect { |account| ... }
# # select * from accounts where id=? for update
@@ -64,7 +64,7 @@ module ActiveRecord
end
# Wraps the passed block in a transaction, locking the object
- # before yielding. You pass can the SQL locking clause
+ # before yielding. You can pass the SQL locking clause
# as argument (see <tt>lock!</tt>).
def with_lock(lock = true)
transaction do
diff --git a/activerecord/lib/active_record/log_subscriber.rb b/activerecord/lib/active_record/log_subscriber.rb
index ca79950049..927fbab8f0 100644
--- a/activerecord/lib/active_record/log_subscriber.rb
+++ b/activerecord/lib/active_record/log_subscriber.rb
@@ -1,13 +1,13 @@
module ActiveRecord
class LogSubscriber < ActiveSupport::LogSubscriber
IGNORE_PAYLOAD_NAMES = ["SCHEMA", "EXPLAIN"]
-
+
def self.runtime=(value)
- Thread.current[:active_record_sql_runtime] = value
+ ActiveRecord::RuntimeRegistry.sql_runtime = value
end
def self.runtime
- Thread.current[:active_record_sql_runtime] ||= 0
+ ActiveRecord::RuntimeRegistry.sql_runtime ||= 0
end
def self.reset_runtime
@@ -17,7 +17,19 @@ module ActiveRecord
def initialize
super
- @odd_or_even = false
+ @odd = false
+ end
+
+ def render_bind(column, value)
+ if column
+ if column.binary?
+ value = "<#{value.bytesize} bytes of binary data>"
+ end
+
+ [column.name, value]
+ else
+ [nil, value]
+ end
end
def sql(event)
@@ -29,12 +41,12 @@ module ActiveRecord
return if IGNORE_PAYLOAD_NAMES.include?(payload[:name])
name = "#{payload[:name]} (#{event.duration.round(1)}ms)"
- sql = payload[:sql].squeeze(' ')
+ sql = payload[:sql]
binds = nil
unless (payload[:binds] || []).empty?
binds = " " + payload[:binds].map { |col,v|
- [col.name, v]
+ render_bind(col, v)
}.inspect
end
@@ -48,17 +60,8 @@ module ActiveRecord
debug " #{name} #{sql}#{binds}"
end
- def identity(event)
- return unless logger.debug?
-
- name = color(event.payload[:name], odd? ? CYAN : MAGENTA, true)
- line = odd? ? color(event.payload[:line], nil, true) : event.payload[:line]
-
- debug " #{name} #{line}"
- end
-
def odd?
- @odd_or_even = !@odd_or_even
+ @odd = !@odd
end
def logger
diff --git a/activerecord/lib/active_record/migration.rb b/activerecord/lib/active_record/migration.rb
index d5ee98382d..5224a6b67c 100644
--- a/activerecord/lib/active_record/migration.rb
+++ b/activerecord/lib/active_record/migration.rb
@@ -32,7 +32,7 @@ module ActiveRecord
class PendingMigrationError < ActiveRecordError#:nodoc:
def initialize
- super("Migrations are pending run 'rake db:migrate RAILS_ENV=#{ENV['RAILS_ENV']}' to resolve the issue")
+ super("Migrations are pending; run 'bin/rake db:migrate RAILS_ENV=#{::Rails.env}' to resolve this issue.")
end
end
@@ -102,7 +102,7 @@ module ActiveRecord
# table definition.
# * <tt>drop_table(name)</tt>: Drops the table called +name+.
# * <tt>change_table(name, options)</tt>: Allows to make column alterations to
- # the table called +name+. It makes the table object availabe to a block that
+ # the table called +name+. It makes the table object available to a block that
# can then add/remove columns, indexes or foreign keys to it.
# * <tt>rename_table(old_name, new_name)</tt>: Renames the table called +old_name+
# to +new_name+.
@@ -120,8 +120,8 @@ module ActiveRecord
# a column but keeps the type and content.
# * <tt>change_column(table_name, column_name, type, options)</tt>: Changes
# the column to a different type using the same parameters as add_column.
- # * <tt>remove_column(table_name, column_names)</tt>: Removes the column listed in
- # +column_names+ from the table called +table_name+.
+ # * <tt>remove_column(table_name, column_name, type, options)</tt>: Removes the column
+ # named +column_name+ from the table called +table_name+.
# * <tt>add_index(table_name, column_names, options)</tt>: Adds a new index
# with the name of the column. Other options include
# <tt>:name</tt>, <tt>:unique</tt> (e.g.
@@ -330,6 +330,24 @@ module ActiveRecord
#
# For a list of commands that are reversible, please see
# <tt>ActiveRecord::Migration::CommandRecorder</tt>.
+ #
+ # == Transactional Migrations
+ #
+ # If the database adapter supports DDL transactions, all migrations will
+ # automatically be wrapped in a transaction. There are queries that you
+ # can't execute inside a transaction though, and for these situations
+ # you can turn the automatic transactions off.
+ #
+ # class ChangeEnum < ActiveRecord::Migration
+ # disable_ddl_transaction!
+ #
+ # def up
+ # execute "ALTER TYPE model_size ADD VALUE 'new_value'"
+ # end
+ # end
+ #
+ # Remember that you can still open your own transactions, even if you
+ # are in a Migration with <tt>self.disable_ddl_transaction!</tt>.
class Migration
autoload :CommandRecorder, 'active_record/migration/command_recorder'
@@ -339,54 +357,175 @@ module ActiveRecord
class CheckPending
def initialize(app)
@app = app
+ @last_check = 0
end
def call(env)
- ActiveRecord::Migration.check_pending!
+ mtime = ActiveRecord::Migrator.last_migration.mtime.to_i
+ if @last_check < mtime
+ ActiveRecord::Migration.check_pending!
+ @last_check = mtime
+ end
@app.call(env)
end
end
class << self
attr_accessor :delegate # :nodoc:
- end
+ attr_accessor :disable_ddl_transaction # :nodoc:
- def self.check_pending!
- raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
- end
+ def check_pending!
+ raise ActiveRecord::PendingMigrationError if ActiveRecord::Migrator.needs_migration?
+ end
- def self.method_missing(name, *args, &block) # :nodoc:
- (delegate || superclass.delegate).send(name, *args, &block)
+ def method_missing(name, *args, &block) # :nodoc:
+ (delegate || superclass.delegate).send(name, *args, &block)
+ end
+
+ def migrate(direction)
+ new.migrate direction
+ end
+
+ # Disable DDL transactions for this migration.
+ def disable_ddl_transaction!
+ @disable_ddl_transaction = true
+ end
end
- def self.migrate(direction)
- new.migrate direction
+ def disable_ddl_transaction # :nodoc:
+ self.class.disable_ddl_transaction
end
cattr_accessor :verbose
-
attr_accessor :name, :version
def initialize(name = self.class.name, version = nil)
@name = name
@version = version
@connection = nil
- @reverting = false
end
+ self.verbose = true
# instantiate the delegate object after initialize is defined
- self.verbose = true
self.delegate = new
- def revert
- @reverting = true
- yield
- ensure
- @reverting = false
+ # Reverses the migration commands for the given block and
+ # the given migrations.
+ #
+ # The following migration will remove the table 'horses'
+ # and create the table 'apples' on the way up, and the reverse
+ # on the way down.
+ #
+ # class FixTLMigration < ActiveRecord::Migration
+ # def change
+ # revert do
+ # create_table(:horses) do |t|
+ # t.text :content
+ # t.datetime :remind_at
+ # end
+ # end
+ # create_table(:apples) do |t|
+ # t.string :variety
+ # end
+ # end
+ # end
+ #
+ # Or equivalently, if +TenderloveMigration+ is defined as in the
+ # documentation for Migration:
+ #
+ # require_relative '2012121212_tenderlove_migration'
+ #
+ # class FixupTLMigration < ActiveRecord::Migration
+ # def change
+ # revert TenderloveMigration
+ #
+ # create_table(:apples) do |t|
+ # t.string :variety
+ # end
+ # end
+ # end
+ #
+ # This command can be nested.
+ def revert(*migration_classes)
+ run(*migration_classes.reverse, revert: true) unless migration_classes.empty?
+ if block_given?
+ if @connection.respond_to? :revert
+ @connection.revert { yield }
+ else
+ recorder = CommandRecorder.new(@connection)
+ @connection = recorder
+ suppress_messages do
+ @connection.revert { yield }
+ end
+ @connection = recorder.delegate
+ recorder.commands.each do |cmd, args, block|
+ send(cmd, *args, &block)
+ end
+ end
+ end
end
def reverting?
- @reverting
+ @connection.respond_to?(:reverting) && @connection.reverting
+ end
+
+ class ReversibleBlockHelper < Struct.new(:reverting) # :nodoc:
+ def up
+ yield unless reverting
+ end
+
+ def down
+ yield if reverting
+ end
+ end
+
+ # Used to specify an operation that can be run in one direction or another.
+ # Call the methods +up+ and +down+ of the yielded object to run a block
+ # only in one given direction.
+ # The whole block will be called in the right order within the migration.
+ #
+ # In the following example, the looping on users will always be done
+ # when the three columns 'first_name', 'last_name' and 'full_name' exist,
+ # even when migrating down:
+ #
+ # class SplitNameMigration < ActiveRecord::Migration
+ # def change
+ # add_column :users, :first_name, :string
+ # add_column :users, :last_name, :string
+ #
+ # reversible do |dir|
+ # User.reset_column_information
+ # User.all.each do |u|
+ # dir.up { u.first_name, u.last_name = u.full_name.split(' ') }
+ # dir.down { u.full_name = "#{u.first_name} #{u.last_name}" }
+ # u.save
+ # end
+ # end
+ #
+ # revert { add_column :users, :full_name, :string }
+ # end
+ # end
+ def reversible
+ helper = ReversibleBlockHelper.new(reverting?)
+ execute_block{ yield helper }
+ end
+
+ # Runs the given migration classes.
+ # Last argument can specify options:
+ # - :direction (default is :up)
+ # - :revert (default is false)
+ def run(*migration_classes)
+ opts = migration_classes.extract_options!
+ dir = opts[:direction] || :up
+ dir = (dir == :down ? :up : :down) if opts[:revert]
+ if reverting?
+ # If in revert and going :up, say, we want to execute :down without reverting, so
+ revert { run(*migration_classes, direction: dir, revert: true) }
+ else
+ migration_classes.each do |migration_class|
+ migration_class.new.exec_migration(@connection, dir)
+ end
+ end
end
def up
@@ -412,29 +551,9 @@ module ActiveRecord
time = nil
ActiveRecord::Base.connection_pool.with_connection do |conn|
- @connection = conn
- if respond_to?(:change)
- if direction == :down
- recorder = CommandRecorder.new(@connection)
- suppress_messages do
- @connection = recorder
- change
- end
- @connection = conn
- time = Benchmark.measure {
- self.revert {
- recorder.inverse.each do |cmd, args|
- send(cmd, *args)
- end
- }
- }
- else
- time = Benchmark.measure { change }
- end
- else
- time = Benchmark.measure { send(direction) }
+ time = Benchmark.measure do
+ exec_migration(conn, direction)
end
- @connection = nil
end
case direction
@@ -443,6 +562,21 @@ module ActiveRecord
end
end
+ def exec_migration(conn, direction)
+ @connection = conn
+ if respond_to?(:change)
+ if direction == :down
+ revert { change }
+ else
+ change
+ end
+ else
+ send(direction)
+ end
+ ensure
+ @connection = nil
+ end
+
def write(text="")
puts(text) if verbose
end
@@ -481,10 +615,10 @@ module ActiveRecord
arg_list = arguments.map{ |a| a.inspect } * ', '
say_with_time "#{method}(#{arg_list})" do
- unless reverting?
+ unless @connection.respond_to? :revert
unless arguments.empty? || method == :execute
- arguments[0] = Migrator.proper_table_name(arguments.first)
- arguments[1] = Migrator.proper_table_name(arguments.second) if method == :rename_table
+ arguments[0] = proper_table_name(arguments.first, table_name_options)
+ arguments[1] = proper_table_name(arguments.second, table_name_options) if method == :rename_table
end
end
return super unless connection.respond_to?(method)
@@ -495,7 +629,7 @@ module ActiveRecord
def copy(destination, sources, options = {})
copied = []
- FileUtils.mkdir_p(destination) unless File.exists?(destination)
+ FileUtils.mkdir_p(destination) unless File.exist?(destination)
destination_migrations = ActiveRecord::Migrator.migrations(destination)
last = destination_migrations.last
@@ -503,8 +637,17 @@ module ActiveRecord
source_migrations = ActiveRecord::Migrator.migrations(path)
source_migrations.each do |migration|
- source = File.read(migration.filename)
- source = "# This migration comes from #{scope} (originally #{migration.version})\n#{source}"
+ source = File.binread(migration.filename)
+ inserted_comment = "# This migration comes from #{scope} (originally #{migration.version})\n"
+ if /\A#.*\b(?:en)?coding:\s*\S+/ =~ source
+ # If we have a magic comment in the original migration,
+ # insert our comment after the first newline(end of the magic comment line)
+ # so the magic keep working.
+ # Note that magic comments must be at the first line(except sh-bang).
+ source[/\n/] = "\n#{inserted_comment}"
+ else
+ source = "#{inserted_comment}#{source}"
+ end
if duplicate = destination_migrations.detect { |m| m.name == migration.name }
if options[:on_skip] && duplicate.scope != scope.to_s
@@ -518,7 +661,7 @@ module ActiveRecord
old_path, migration.filename = migration.filename, new_path
last = migration
- File.open(migration.filename, "w") { |f| f.write source }
+ File.binwrite(migration.filename, source)
copied << migration
options[:on_copy].call(scope, migration, old_path) if options[:on_copy]
destination_migrations << migration
@@ -528,6 +671,18 @@ module ActiveRecord
copied
end
+ # Finds the correct table name given an Active Record object.
+ # Uses the Active Record object's own table_name, or pre/suffix from the
+ # options passed in.
+ def proper_table_name(name, options = {})
+ if name.respond_to? :table_name
+ name.table_name
+ else
+ "#{options[:table_name_prefix]}#{name}#{options[:table_name_suffix]}"
+ end
+ end
+
+ # Determines the version number of the next migration.
def next_migration_number(number)
if ActiveRecord::Base.timestamped_migrations
[Time.now.utc.strftime("%Y%m%d%H%M%S"), "%.14d" % number].max
@@ -535,6 +690,22 @@ module ActiveRecord
"%.3d" % number
end
end
+
+ def table_name_options(config = ActiveRecord::Base)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
+
+ private
+ def execute_block
+ if connection.respond_to? :execute_block
+ super # use normal delegation to record the block
+ else
+ yield
+ end
+ end
end
# MigrationProxy is used to defer loading of the actual migration classes
@@ -550,7 +721,11 @@ module ActiveRecord
File.basename(filename)
end
- delegate :migrate, :announce, :write, :to => :migration
+ def mtime
+ File.mtime filename
+ end
+
+ delegate :migrate, :announce, :write, :disable_ddl_transaction, to: :migration
private
@@ -565,6 +740,16 @@ module ActiveRecord
end
+ class NullMigration < MigrationProxy #:nodoc:
+ def initialize
+ super(nil, 0, nil, nil)
+ end
+
+ def mtime
+ 0
+ end
+ end
+
class Migrator#:nodoc:
class << self
attr_writer :migrations_paths
@@ -635,12 +820,24 @@ module ActiveRecord
end
def last_version
- migrations(migrations_paths).last.try(:version)||0
+ last_migration.version
end
- def proper_table_name(name)
- # Use the Active Record objects own table_name, or pre/suffix from ActiveRecord::Base if name is a symbol/string
- name.table_name rescue "#{ActiveRecord::Base.table_name_prefix}#{name}#{ActiveRecord::Base.table_name_suffix}"
+ def last_migration #:nodoc:
+ 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
@@ -659,7 +856,7 @@ module ActiveRecord
files = Dir[*paths.map { |p| "#{p}/**/[0-9]*_*.rb" }]
migrations = files.map do |file|
- version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?.rb/).first
+ version, name, scope = file.scan(/([0-9]+)_([_a-z0-9]*)\.?([_a-z0-9]*)?\.rb\z/).first
raise IllegalMigrationNameError.new(file) unless version
version = version.to_i
@@ -691,13 +888,7 @@ module ActiveRecord
@direction = direction
@target_version = target_version
@migrated_versions = nil
-
- if Array(migrations).grep(String).empty?
- @migrations = migrations
- else
- ActiveSupport::Deprecation.warn "instantiate this class with a list of migrations"
- @migrations = self.class.migrations(migrations)
- end
+ @migrations = migrations
validate(@migrations)
@@ -705,7 +896,7 @@ module ActiveRecord
end
def current_version
- migrated.sort.last || 0
+ migrated.max || 0
end
def current_migration
@@ -714,11 +905,15 @@ module ActiveRecord
alias :current :current_migration
def run
- target = migrations.detect { |m| m.version == @target_version }
- raise UnknownMigrationVersionError.new(@target_version) if target.nil?
- unless (up? && migrated.include?(target.version.to_i)) || (down? && !migrated.include?(target.version.to_i))
- target.migrate(@direction)
- record_version_state_after_migrating(target.version)
+ migration = migrations.detect { |m| m.version == @target_version }
+ raise UnknownMigrationVersionError.new(@target_version) if migration.nil?
+ unless (up? && migrated.include?(migration.version.to_i)) || (down? && !migrated.include?(migration.version.to_i))
+ begin
+ execute_migration_in_transaction(migration, @direction)
+ rescue => e
+ canceled_msg = use_transaction?(migration) ? ", this migration was canceled" : ""
+ raise StandardError, "An error has occurred#{canceled_msg}:\n\n#{e}", e.backtrace
+ end
end
end
@@ -727,25 +922,13 @@ module ActiveRecord
raise UnknownMigrationVersionError.new(@target_version)
end
- running = runnable
-
- if block_given?
- ActiveSupport::Deprecation.warn(<<-eomsg)
-block argument to migrate is deprecated, please filter migrations before constructing the migrator
- eomsg
- running.select! { |m| yield m }
- end
-
- running.each do |migration|
+ runnable.each do |migration|
Base.logger.info "Migrating to #{migration.name} (#{migration.version})" if Base.logger
begin
- ddl_transaction do
- migration.migrate(@direction)
- record_version_state_after_migrating(migration.version)
- end
+ execute_migration_in_transaction(migration, @direction)
rescue => e
- canceled_msg = Base.connection.supports_ddl_transactions? ? "this and " : ""
+ canceled_msg = use_transaction?(migration) ? "this and " : ""
raise StandardError, "An error has occurred, #{canceled_msg}all later migrations canceled:\n\n#{e}", e.backtrace
end
end
@@ -780,6 +963,13 @@ block argument to migrate is deprecated, please filter migrations before constru
migrated.include?(migration.version.to_i)
end
+ def execute_migration_in_transaction(migration, direction)
+ ddl_transaction(migration) do
+ migration.migrate(direction)
+ record_version_state_after_migrating(migration.version)
+ end
+ end
+
def target
migrations.detect { |m| m.version == @target_version }
end
@@ -819,12 +1009,16 @@ block argument to migrate is deprecated, please filter migrations before constru
end
# Wrap the migration in a transaction only if supported by the adapter.
- def ddl_transaction
- if Base.connection.supports_ddl_transactions?
+ def ddl_transaction(migration)
+ if use_transaction?(migration)
Base.transaction { yield }
else
yield
end
end
+
+ def use_transaction?(migration)
+ !migration.disable_ddl_transaction && Base.connection.supports_ddl_transactions?
+ end
end
end
diff --git a/activerecord/lib/active_record/migration/command_recorder.rb b/activerecord/lib/active_record/migration/command_recorder.rb
index 95f4360578..01c73be849 100644
--- a/activerecord/lib/active_record/migration/command_recorder.rb
+++ b/activerecord/lib/active_record/migration/command_recorder.rb
@@ -16,69 +16,118 @@ module ActiveRecord
class CommandRecorder
include JoinTable
- attr_accessor :commands, :delegate
+ attr_accessor :commands, :delegate, :reverting
def initialize(delegate = nil)
@commands = []
@delegate = delegate
+ @reverting = false
+ end
+
+ # While executing the given block, the recorded will be in reverting mode.
+ # All commands recorded will end up being recorded reverted
+ # and in reverse order.
+ # For example:
+ #
+ # recorder.revert{ recorder.record(:rename_table, [:old, :new]) }
+ # # same effect as recorder.record(:rename_table, [:new, :old])
+ def revert
+ @reverting = !@reverting
+ previous = @commands
+ @commands = []
+ yield
+ ensure
+ @commands = previous.concat(@commands.reverse)
+ @reverting = !@reverting
end
# record +command+. +command+ should be a method name and arguments.
# For example:
#
# recorder.record(:method_name, [:arg1, :arg2])
- def record(*command)
- @commands << command
+ def record(*command, &block)
+ if @reverting
+ @commands << inverse_of(*command, &block)
+ else
+ @commands << (command << block)
+ end
end
- # Returns a list that represents commands that are the inverse of the
- # commands stored in +commands+. For example:
+ # Returns the inverse of the given command. For example:
#
- # recorder.record(:rename_table, [:old, :new])
- # recorder.inverse # => [:rename_table, [:new, :old]]
+ # recorder.inverse_of(:rename_table, [:old, :new])
+ # # => [:rename_table, [:new, :old]]
#
# This method will raise an +IrreversibleMigration+ exception if it cannot
- # invert the +commands+.
- def inverse
- @commands.reverse.map { |name, args|
- method = :"invert_#{name}"
- raise IrreversibleMigration unless respond_to?(method, true)
- send(method, args)
- }
+ # invert the +command+.
+ def inverse_of(command, args, &block)
+ method = :"invert_#{command}"
+ raise IrreversibleMigration unless respond_to?(method, true)
+ send(method, args, &block)
end
def respond_to?(*args) # :nodoc:
super || delegate.respond_to?(*args)
end
- [:create_table, :create_join_table, :change_table, :rename_table, :add_column, :remove_column, :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps, :change_column, :change_column_default, :add_reference, :remove_reference].each do |method|
+ [:create_table, :create_join_table, :rename_table, :add_column, :remove_column,
+ :rename_index, :rename_column, :add_index, :remove_index, :add_timestamps, :remove_timestamps,
+ :change_column_default, :add_reference, :remove_reference, :transaction,
+ :drop_join_table, :drop_table, :execute_block, :enable_extension,
+ :change_column, :execute, :remove_columns, # irreversible methods need to be here too
+ ].each do |method|
class_eval <<-EOV, __FILE__, __LINE__ + 1
- def #{method}(*args) # def create_table(*args)
- record(:"#{method}", args) # record(:create_table, args)
- end # end
+ def #{method}(*args, &block) # def create_table(*args, &block)
+ record(:"#{method}", args, &block) # record(:create_table, args, &block)
+ end # end
EOV
end
alias :add_belongs_to :add_reference
alias :remove_belongs_to :remove_reference
- private
-
- def invert_create_table(args)
- [:drop_table, [args.first]]
+ def change_table(table_name, options = {})
+ yield ConnectionAdapters::Table.new(table_name, self)
end
- def invert_create_join_table(args)
- table_name = find_join_table_name(*args)
+ private
- [:drop_table, [table_name]]
+ module StraightReversions
+ private
+ { transaction: :transaction,
+ execute_block: :execute_block,
+ create_table: :drop_table,
+ create_join_table: :drop_join_table,
+ add_column: :remove_column,
+ add_timestamps: :remove_timestamps,
+ add_reference: :remove_reference,
+ enable_extension: :disable_extension
+ }.each do |cmd, inv|
+ [[inv, cmd], [cmd, inv]].uniq.each do |method, inverse|
+ class_eval <<-EOV, __FILE__, __LINE__ + 1
+ def invert_#{method}(args, &block) # def invert_create_table(args, &block)
+ [:#{inverse}, args, block] # [:drop_table, args, block]
+ end # end
+ EOV
+ end
+ end
+ end
+
+ include StraightReversions
+
+ def invert_drop_table(args, &block)
+ if args.size == 1 && block == nil
+ raise ActiveRecord::IrreversibleMigration, "To avoid mistakes, drop_table is only reversible if given options or a block (can be empty)."
+ end
+ super
end
def invert_rename_table(args)
[:rename_table, args.reverse]
end
- def invert_add_column(args)
- [:remove_column, args.first(2)]
+ def invert_remove_column(args)
+ raise ActiveRecord::IrreversibleMigration, "remove_column is only reversible if given a type." if args.size <= 2
+ super
end
def invert_rename_index(args)
@@ -91,27 +140,21 @@ module ActiveRecord
def invert_add_index(args)
table, columns, options = *args
- index_name = options.try(:[], :name)
- options_hash = index_name ? {:name => index_name} : {:column => columns}
- [:remove_index, [table, options_hash]]
+ [:remove_index, [table, (options || {}).merge(column: columns)]]
end
- def invert_remove_timestamps(args)
- [:add_timestamps, args]
- end
+ def invert_remove_index(args)
+ table, options = *args
- def invert_add_timestamps(args)
- [:remove_timestamps, args]
- end
+ unless options && options.is_a?(Hash) && options[:column]
+ raise ActiveRecord::IrreversibleMigration, "remove_index is only reversible if given a :column option."
+ end
- def invert_add_reference(args)
- [:remove_reference, args]
+ options = options.dup
+ [:add_index, [table, options.delete(:column), options]]
end
- alias :invert_add_belongs_to :invert_add_reference
- def invert_remove_reference(args)
- [:add_reference, args]
- end
+ alias :invert_add_belongs_to :invert_add_reference
alias :invert_remove_belongs_to :invert_remove_reference
# Forwards any missing method call to the \target.
diff --git a/activerecord/lib/active_record/migration/join_table.rb b/activerecord/lib/active_record/migration/join_table.rb
index e880ae97bb..ebf64cbcdc 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, table_2].sort.join("_").to_sym
+ [table_1.to_s, table_2.to_s].sort.join("_").to_sym
end
end
end
diff --git a/activerecord/lib/active_record/model.rb b/activerecord/lib/active_record/model.rb
deleted file mode 100644
index f059840f4d..0000000000
--- a/activerecord/lib/active_record/model.rb
+++ /dev/null
@@ -1,167 +0,0 @@
-require 'active_support/core_ext/module/attribute_accessors'
-
-module ActiveRecord
- module Configuration # :nodoc:
- # This just abstracts out how we define configuration options in AR. Essentially we
- # have mattr_accessors on the ActiveRecord:Model constant that define global defaults.
- # Classes that then use AR get class_attributes defined, which means that when they
- # are assigned the default will be overridden for that class and subclasses. (Except
- # when options[:global] == true, in which case there is one global value always.)
- def config_attribute(name, options = {})
- if options[:global]
- class_eval <<-CODE, __FILE__, __LINE__ + 1
- def self.#{name}; ActiveRecord::Model.#{name}; end
- def #{name}; ActiveRecord::Model.#{name}; end
- def self.#{name}=(val); ActiveRecord::Model.#{name} = val; end
- CODE
- else
- options[:instance_writer] ||= false
- class_attribute name, options
-
- singleton_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
- remove_method :#{name}
- def #{name}; ActiveRecord::Model.#{name}; end
- CODE
- end
- end
- end
-
- # This allows us to detect an ActiveRecord::Model while it's in the process of
- # being included.
- module Tag; end
-
- # <tt>ActiveRecord::Model</tt> can be included into a class to add Active Record
- # persistence. This is an alternative to inheriting from <tt>ActiveRecord::Base</tt>.
- #
- # class Post
- # include ActiveRecord::Model
- # end
- module Model
- extend ActiveSupport::Concern
- extend ConnectionHandling
- extend ActiveModel::Observing::ClassMethods
-
- def self.append_features(base)
- base.class_eval do
- include Tag
- extend Configuration
- end
-
- super
- end
-
- included do
- extend ActiveModel::Naming
- extend ActiveSupport::Benchmarkable
- extend ActiveSupport::DescendantsTracker
-
- extend QueryCache::ClassMethods
- extend Querying
- extend Translation
- extend DynamicMatchers
- extend Explain
- extend ConnectionHandling
-
- initialize_generated_modules unless self == Base
- end
-
- include Persistence
- include ReadonlyAttributes
- include ModelSchema
- include Inheritance
- include Scoping
- include Sanitization
- include AttributeAssignment
- include ActiveModel::Conversion
- include Integration
- include Validations
- include CounterCache
- include Locking::Optimistic
- include Locking::Pessimistic
- include AttributeMethods
- include Callbacks
- include ActiveModel::Observing
- include Timestamp
- include Associations
- include ActiveModel::SecurePassword
- include AutosaveAssociation
- include NestedAttributes
- include Aggregations
- include Transactions
- include Reflection
- include Serialization
- include Store
- include Core
-
- class << self
- def arel_engine
- self
- end
-
- def abstract_class?
- false
- end
-
- # Defines the name of the table column which will store the class name on single-table
- # inheritance situations.
- #
- # The default inheritance column name is +type+, which means it's a
- # reserved word inside Active Record. To be able to use single-table
- # inheritance with another column name, or to use the column +type+ in
- # your own model for something else, you can set +inheritance_column+:
- #
- # self.inheritance_column = 'zoink'
- def inheritance_column
- 'type'
- end
- end
-
- class DeprecationProxy < BasicObject #:nodoc:
- def initialize(model = Model, base = Base)
- @model = model
- @base = base
- end
-
- def method_missing(name, *args, &block)
- if @model.respond_to?(name, true)
- @model.send(name, *args, &block)
- else
- ::ActiveSupport::Deprecation.warn(
- "The object passed to the active_record load hook was previously ActiveRecord::Base " \
- "(a Class). Now it is ActiveRecord::Model (a Module). You have called `#{name}' which " \
- "is only defined on ActiveRecord::Base. Please change your code so that it works with " \
- "a module rather than a class. (Model is included in Base, so anything added to Model " \
- "will be available on Base as well.)"
- )
- @base.send(name, *args, &block)
- end
- end
-
- alias send method_missing
-
- def extend(*mods)
- ::ActiveSupport::Deprecation.warn(
- "The object passed to the active_record load hook was previously ActiveRecord::Base " \
- "(a Class). Now it is ActiveRecord::Model (a Module). You have called `extend' which " \
- "would add singleton methods to Model. This is presumably not what you want, since the " \
- "methods would not be inherited down to Base. Rather than using extend, please use " \
- "ActiveSupport::Concern + include, which will ensure that your class methods are " \
- "inherited."
- )
- @base.extend(*mods)
- end
- end
- end
-
- # This hook is where config accessors on Model get defined.
- #
- # We don't want to just open the Model module and add stuff to it in other files, because
- # that would cause Model to load, which causes all sorts of loading order issues.
- #
- # We need this hook rather than just using the :active_record one, because users of the
- # :active_record hook may need to use config options.
- ActiveSupport.run_load_hooks(:active_record_config, Model)
-
- # Load Base at this point, because the active_record load hook is run in that file.
- Base
-end
diff --git a/activerecord/lib/active_record/model_schema.rb b/activerecord/lib/active_record/model_schema.rb
index 99de16cd33..dc5ff02882 100644
--- a/activerecord/lib/active_record/model_schema.rb
+++ b/activerecord/lib/active_record/model_schema.rb
@@ -1,18 +1,4 @@
-
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :primary_key_prefix_type, instance_accessor: false
-
- mattr_accessor :table_name_prefix, instance_accessor: false
- self.table_name_prefix = ""
-
- mattr_accessor :table_name_suffix, instance_accessor: false
- self.table_name_suffix = ""
-
- mattr_accessor :pluralize_table_names, instance_accessor: false
- self.pluralize_table_names = true
- end
-
module ModelSchema
extend ActiveSupport::Concern
@@ -24,7 +10,7 @@ module ActiveRecord
# the Product class will look for "productid" instead of "id" as the primary column. If the
# latter is specified, the Product class will look for "product_id" instead of "id". Remember
# that this is a global setting for all Active Records.
- config_attribute :primary_key_prefix_type, global: true
+ mattr_accessor :primary_key_prefix_type, instance_writer: false
##
# :singleton-method:
@@ -36,20 +22,31 @@ module ActiveRecord
# If you are organising your models within modules you can add a prefix to the models within
# a namespace by defining a singleton method in the parent module called table_name_prefix which
# returns your chosen prefix.
- config_attribute :table_name_prefix
+ class_attribute :table_name_prefix, instance_writer: false
+ self.table_name_prefix = ""
##
# :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.
- config_attribute :table_name_suffix
+ class_attribute :table_name_suffix, instance_writer: false
+ self.table_name_suffix = ""
+
+ ##
+ # :singleton-method:
+ # Accessor for the name of the schema migrations table. By default, the value is "schema_migrations"
+ class_attribute :schema_migrations_table_name, instance_accessor: false
+ self.schema_migrations_table_name = "schema_migrations"
##
# :singleton-method:
# Indicates whether table names should be the pluralized versions of the corresponding class names.
# If true, the default table name for a Product class will be +products+. If false, it would just be +product+.
# See table_name for the full rules on table/class naming. This is true, by default.
- config_attribute :pluralize_table_names
+ class_attribute :pluralize_table_names, instance_writer: false
+ self.pluralize_table_names = true
+
+ self.inheritance_column = 'type'
end
module ClassMethods
@@ -133,7 +130,7 @@ module ActiveRecord
@quoted_table_name = nil
@arel_table = nil
@sequence_name = nil unless defined?(@explicit_sequence_name) && @explicit_sequence_name
- @relation = Relation.new(self, arel_table)
+ @relation = Relation.create(self, arel_table)
end
# Returns a quoted version of the table name, used to construct SQL statements.
@@ -144,9 +141,9 @@ module ActiveRecord
# Computes the table name, (re)sets it internally, and returns it.
def reset_table_name #:nodoc:
self.table_name = if abstract_class?
- active_record_super == Base ? nil : active_record_super.table_name
- elsif active_record_super.abstract_class?
- active_record_super.table_name || compute_table_name
+ superclass == Base ? nil : superclass.table_name
+ elsif superclass.abstract_class?
+ superclass.table_name || compute_table_name
else
compute_table_name
end
@@ -156,9 +153,17 @@ module ActiveRecord
(parents.detect{ |p| p.respond_to?(:table_name_prefix) } || self).table_name_prefix
end
- # The name of the column containing the object's class when Single Table Inheritance is used
+ # Defines the name of the table column which will store the class name on single-table
+ # inheritance situations.
+ #
+ # The default inheritance column name is +type+, which means it's a
+ # reserved word inside Active Record. To be able to use single-table
+ # inheritance with another column name, or to use the column +type+ in
+ # your own model for something else, you can set +inheritance_column+:
+ #
+ # self.inheritance_column = 'zoink'
def inheritance_column
- (@inheritance_column ||= nil) || active_record_super.inheritance_column
+ (@inheritance_column ||= nil) || superclass.inheritance_column
end
# Sets the value of inheritance_column
@@ -206,7 +211,7 @@ module ActiveRecord
# 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|
+ @columns ||= connection.schema_cache.columns(table_name).map do |col|
col = col.dup
col.primary = (col.name == primary_key)
col
@@ -225,14 +230,20 @@ module ActiveRecord
def decorate_columns(columns_hash) # :nodoc:
return if columns_hash.empty?
- serialized_attributes.each_key do |key|
- columns_hash[key] = AttributeMethods::Serialization::Type.new(columns_hash[key])
+ @serialized_column_names ||= self.columns_hash.keys.find_all do |name|
+ serialized_attributes.key?(name)
end
- columns_hash.each do |name, col|
- if create_time_zone_conversion_attribute?(name, col)
- columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(col)
- end
+ @serialized_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::Serialization::Type.new(columns_hash[name])
+ end
+
+ @time_zone_column_names ||= self.columns_hash.find_all do |name, col|
+ create_time_zone_conversion_attribute?(name, col)
+ end.map!(&:first)
+
+ @time_zone_column_names.each do |name|
+ columns_hash[name] = AttributeMethods::TimeZoneConversion::Type.new(columns_hash[name])
end
columns_hash
@@ -255,19 +266,6 @@ module ActiveRecord
@content_columns ||= columns.reject { |c| c.primary || c.name =~ /(_id|_count)$/ || c.name == inheritance_column }
end
- # Returns a hash of all the methods added to query each of the columns in the table with the name of the method as the key
- # and true as the value. This makes it possible to do O(1) lookups in respond_to? to check if a given method for attribute
- # is available.
- def column_methods_hash #:nodoc:
- @dynamic_methods_hash ||= column_names.each_with_object(Hash.new(false)) do |attr, methods|
- attr_name = attr.to_s
- methods[attr.to_sym] = attr_name
- methods["#{attr}=".to_sym] = attr_name
- methods["#{attr}?".to_sym] = attr_name
- methods["#{attr}_before_type_cast".to_sym] = attr_name
- end
- end
-
# Resets all the cached information about columns, which will cause them
# to be reloaded on the next request.
#
@@ -286,7 +284,7 @@ module ActiveRecord
#
# JobLevel.reset_column_information
# %w{assistant executive manager director}.each do |type|
- # JobLevel.create(:name => type)
+ # JobLevel.create(name: type)
# end
# end
#
@@ -299,16 +297,19 @@ module ActiveRecord
undefine_attribute_methods
connection.schema_cache.clear_table_cache!(table_name) if table_exists?
- @arel_engine = nil
- @column_defaults = nil
- @column_names = nil
- @columns = nil
- @columns_hash = nil
- @column_types = nil
- @content_columns = nil
- @dynamic_methods_hash = nil
- @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
- @relation = nil
+ @arel_engine = nil
+ @column_defaults = nil
+ @column_names = nil
+ @columns = nil
+ @columns_hash = nil
+ @column_types = nil
+ @content_columns = nil
+ @dynamic_methods_hash = nil
+ @inheritance_column = nil unless defined?(@explicit_inheritance_column) && @explicit_inheritance_column
+ @relation = nil
+ @serialized_column_names = nil
+ @time_zone_column_names = nil
+ @cached_time_zone = nil
end
# This is a hook for use by modules that need to do extra stuff to
@@ -331,7 +332,7 @@ module ActiveRecord
base = base_class
if self == base
# Nested classes are prefixed with singular parent table name.
- if parent < ActiveRecord::Model && !parent.abstract_class?
+ if parent < Base && !parent.abstract_class?
contained = parent.table_name
contained = contained.singularize if parent.pluralize_table_names
contained += '_'
diff --git a/activerecord/lib/active_record/nested_attributes.rb b/activerecord/lib/active_record/nested_attributes.rb
index 2e7fb3bbb3..df28451bb7 100644
--- a/activerecord/lib/active_record/nested_attributes.rb
+++ b/activerecord/lib/active_record/nested_attributes.rb
@@ -3,11 +3,6 @@ require 'active_support/core_ext/object/try'
require 'active_support/core_ext/hash/indifferent_access'
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :nested_attributes_options, instance_accessor: false
- self.nested_attributes_options = {}
- end
-
module NestedAttributes #:nodoc:
class TooManyRecords < ActiveRecordError
end
@@ -15,7 +10,8 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :nested_attributes_options
+ class_attribute :nested_attributes_options, instance_writer: false
+ self.nested_attributes_options = {}
end
# = Active Record Nested Attributes
@@ -54,15 +50,15 @@ module ActiveRecord
# Enabling nested attributes on a one-to-one association allows you to
# create the member and avatar in one go:
#
- # params = { :member => { :name => 'Jack', :avatar_attributes => { :icon => 'smiling' } } }
+ # params = { member: { name: 'Jack', avatar_attributes: { icon: 'smiling' } } }
# member = Member.create(params[:member])
# member.avatar.id # => 2
# member.avatar.icon # => 'smiling'
#
# It also allows you to update the avatar through the member:
#
- # params = { :member => { :avatar_attributes => { :id => '2', :icon => 'sad' } } }
- # member.update_attributes params[:member]
+ # params = { member: { avatar_attributes: { id: '2', icon: 'sad' } } }
+ # member.update params[:member]
# member.avatar.icon # => 'sad'
#
# By default you will only be able to set and update attributes on the
@@ -72,13 +68,13 @@ module ActiveRecord
#
# class Member < ActiveRecord::Base
# has_one :avatar
- # accepts_nested_attributes_for :avatar, :allow_destroy => true
+ # accepts_nested_attributes_for :avatar, allow_destroy: true
# end
#
# Now, when you add the <tt>_destroy</tt> key to the attributes hash, with a
# value that evaluates to +true+, you will destroy the associated model:
#
- # member.avatar_attributes = { :id => '2', :_destroy => '1' }
+ # member.avatar_attributes = { id: '2', _destroy: '1' }
# member.avatar.marked_for_destruction? # => true
# member.save
# member.reload.avatar # => nil
@@ -94,22 +90,23 @@ module ActiveRecord
# accepts_nested_attributes_for :posts
# end
#
- # You can now set or update attributes on an associated post model through
- # the attribute hash.
+ # You can now set or update attributes on the associated posts through
+ # an attribute hash for a member: include the key +:posts_attributes+
+ # with an array of hashes of post attributes as a value.
#
# For each hash that does _not_ have an <tt>id</tt> key a new record will
# be instantiated, unless the hash also contains a <tt>_destroy</tt> key
# that evaluates to +true+.
#
- # params = { :member => {
- # :name => 'joe', :posts_attributes => [
- # { :title => 'Kari, the awesome Ruby documentation browser!' },
- # { :title => 'The egalitarian assumption of the modern citizen' },
- # { :title => '', :_destroy => '1' } # this will be ignored
+ # params = { member: {
+ # name: 'joe', posts_attributes: [
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
+ # { title: 'The egalitarian assumption of the modern citizen' },
+ # { title: '', _destroy: '1' } # this will be ignored
# ]
# }}
#
- # member = Member.create(params['member'])
+ # member = Member.create(params[:member])
# member.posts.length # => 2
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
@@ -118,48 +115,48 @@ module ActiveRecord
# hashes if they fail to pass your criteria. For example, the previous
# example could be rewritten as:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => proc { |attributes| attributes['title'].blank? }
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: proc { |attributes| attributes['title'].blank? }
+ # end
#
- # params = { :member => {
- # :name => 'joe', :posts_attributes => [
- # { :title => 'Kari, the awesome Ruby documentation browser!' },
- # { :title => 'The egalitarian assumption of the modern citizen' },
- # { :title => '' } # this will be ignored because of the :reject_if proc
+ # params = { member: {
+ # name: 'joe', posts_attributes: [
+ # { title: 'Kari, the awesome Ruby documentation browser!' },
+ # { title: 'The egalitarian assumption of the modern citizen' },
+ # { title: '' } # this will be ignored because of the :reject_if proc
# ]
# }}
#
- # member = Member.create(params['member'])
+ # member = Member.create(params[:member])
# member.posts.length # => 2
# member.posts.first.title # => 'Kari, the awesome Ruby documentation browser!'
# member.posts.second.title # => 'The egalitarian assumption of the modern citizen'
#
# Alternatively, :reject_if also accepts a symbol for using methods:
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => :new_record?
- # end
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :new_record?
+ # end
#
- # class Member < ActiveRecord::Base
- # has_many :posts
- # accepts_nested_attributes_for :posts, :reject_if => :reject_posts
+ # class Member < ActiveRecord::Base
+ # has_many :posts
+ # accepts_nested_attributes_for :posts, reject_if: :reject_posts
#
- # def reject_posts(attributed)
- # attributed['title'].blank?
- # end
- # end
+ # def reject_posts(attributed)
+ # attributed['title'].blank?
+ # end
+ # end
#
# If the hash contains an <tt>id</tt> key that matches an already
# associated record, the matching record will be modified:
#
# member.attributes = {
- # :name => 'Joe',
- # :posts_attributes => [
- # { :id => 1, :title => '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
- # { :id => 2, :title => '[UPDATED] other post' }
+ # name: 'Joe',
+ # posts_attributes: [
+ # { id: 1, title: '[UPDATED] An, as of yet, undisclosed awesome Ruby documentation browser!' },
+ # { id: 2, title: '[UPDATED] other post' }
# ]
# }
#
@@ -174,19 +171,42 @@ module ActiveRecord
#
# class Member < ActiveRecord::Base
# has_many :posts
- # accepts_nested_attributes_for :posts, :allow_destroy => true
+ # accepts_nested_attributes_for :posts, allow_destroy: true
# end
#
- # params = { :member => {
- # :posts_attributes => [{ :id => '2', :_destroy => '1' }]
+ # params = { member: {
+ # posts_attributes: [{ id: '2', _destroy: '1' }]
# }}
#
- # member.attributes = params['member']
+ # member.attributes = params[:member]
# member.posts.detect { |p| p.id == 2 }.marked_for_destruction? # => true
# member.posts.length # => 2
# member.save
# member.reload.posts.length # => 1
#
+ # Nested attributes for an associated collection can also be passed in
+ # the form of a hash of hashes instead of an array of hashes:
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: { first: { title: 'Foo' },
+ # second: { title: 'Bar' } })
+ #
+ # has the same effect as
+ #
+ # Member.create(name: 'joe',
+ # posts_attributes: [ { title: 'Foo' },
+ # { title: 'Bar' } ])
+ #
+ # The keys of the hash which is the value for +:posts_attributes+ are
+ # ignored in this case.
+ # However, it is not allowed to use +'id'+ or +:id+ for one of
+ # such keys, otherwise the hash will be wrapped in an array and
+ # interpreted as an attribute hash for a single post.
+ #
+ # Passing attributes for an associated collection in the form of a hash
+ # of hashes can be used with hashes generated from HTTP/HTML parameters,
+ # where there maybe no natural way to submit an array of hashes.
+ #
# === Saving
#
# All changes to models, including the destruction of those marked for
@@ -201,14 +221,35 @@ module ActiveRecord
# <tt>inverse_of</tt> as this example illustrates:
#
# class Member < ActiveRecord::Base
- # has_many :posts, :inverse_of => :member
+ # has_many :posts, inverse_of: :member
# accepts_nested_attributes_for :posts
# end
#
# class Post < ActiveRecord::Base
- # belongs_to :member, :inverse_of => :posts
+ # belongs_to :member, inverse_of: :posts
# validates_presence_of :member
# end
+ #
+ # Note that if you do not specify the <tt>inverse_of</tt> option, then
+ # Active Record will try to automatically guess the inverse association
+ # based on heuristics.
+ #
+ # For one-to-one nested associations, if you build the new (in-memory)
+ # child object yourself before assignment, then this module will not
+ # overwrite it, e.g.:
+ #
+ # class Member < ActiveRecord::Base
+ # has_one :avatar
+ # accepts_nested_attributes_for :avatar
+ #
+ # def avatar
+ # super || build_avatar(width: 200)
+ # end
+ # end
+ #
+ # member = Member.new
+ # member.avatar_attributes = {icon: 'sad'}
+ # member.avatar.width # => 200
module ClassMethods
REJECT_ALL_BLANK_PROC = proc { |attributes| attributes.all? { |key, value| key == '_destroy' || value.blank? } }
@@ -252,11 +293,11 @@ module ActiveRecord
#
# Examples:
# # creates avatar_attributes=
- # accepts_nested_attributes_for :avatar, :reject_if => proc { |attributes| attributes['name'].blank? }
+ # accepts_nested_attributes_for :avatar, reject_if: proc { |attributes| attributes['name'].blank? }
# # creates avatar_attributes=
- # accepts_nested_attributes_for :avatar, :reject_if => :all_blank
+ # accepts_nested_attributes_for :avatar, reject_if: :all_blank
# # creates avatar_attributes= and posts_attributes=
- # accepts_nested_attributes_for :avatar, :posts, :allow_destroy => true
+ # accepts_nested_attributes_for :avatar, :posts, allow_destroy: true
def accepts_nested_attributes_for(*attr_names)
options = { :allow_destroy => false, :update_only => false }
options.update(attr_names.extract_options!)
@@ -265,7 +306,7 @@ module ActiveRecord
attr_names.each do |association_name|
if reflection = reflect_on_association(association_name)
- reflection.options[:autosave] = true
+ reflection.autosave = true
add_autosave_association_callbacks(reflection)
nested_attributes_options = self.nested_attributes_options.dup
@@ -273,23 +314,36 @@ module ActiveRecord
self.nested_attributes_options = nested_attributes_options
type = (reflection.collection? ? :collection : :one_to_one)
-
- # def pirate_attributes=(attributes)
- # assign_nested_attributes_for_one_to_one_association(:pirate, attributes, mass_assignment_options)
- # end
- generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
- if method_defined?(:#{association_name}_attributes=)
- remove_method(:#{association_name}_attributes=)
- end
- def #{association_name}_attributes=(attributes)
- assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
- end
- eoruby
+ generate_association_writer(association_name, type)
else
raise ArgumentError, "No association found for name `#{association_name}'. Has it been defined yet?"
end
end
end
+
+ private
+
+ # Generates a writer method for this association. Serves as a point for
+ # accessing the objects in the association. For example, this method
+ # could generate the following:
+ #
+ # def pirate_attributes=(attributes)
+ # assign_nested_attributes_for_one_to_one_association(:pirate, attributes)
+ # end
+ #
+ # This redirects the attempts to write objects in an association through
+ # the helper methods defined below. Makes it seem like the nested
+ # associations are just regular associations.
+ def generate_association_writer(association_name, type)
+ generated_feature_methods.module_eval <<-eoruby, __FILE__, __LINE__ + 1
+ if method_defined?(:#{association_name}_attributes=)
+ remove_method(:#{association_name}_attributes=)
+ end
+ def #{association_name}_attributes=(attributes)
+ assign_nested_attributes_for_#{type}_association(:#{association_name}, attributes)
+ end
+ eoruby
+ end
end
# Returns ActiveRecord::AutosaveAssociation::marked_for_destruction? It's
@@ -323,20 +377,28 @@ module ActiveRecord
def assign_nested_attributes_for_one_to_one_association(association_name, attributes)
options = self.nested_attributes_options[association_name]
attributes = attributes.with_indifferent_access
+ existing_record = send(association_name)
- if (options[:update_only] || !attributes['id'].blank?) && (record = send(association_name)) &&
- (options[:update_only] || record.id.to_s == attributes['id'].to_s)
- assign_to_or_mark_for_destruction(record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
+ if (options[:update_only] || !attributes['id'].blank?) && existing_record &&
+ (options[:update_only] || existing_record.id.to_s == attributes['id'].to_s)
+ assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy]) unless call_reject_if(association_name, attributes)
elsif attributes['id'].present?
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
elsif !reject_new_record?(association_name, attributes)
- method = "build_#{association_name}"
- if respond_to?(method)
- send(method, attributes.except(*UNASSIGNABLE_KEYS))
+ assignable_attributes = attributes.except(*UNASSIGNABLE_KEYS)
+
+ if existing_record && existing_record.new_record?
+ existing_record.assign_attributes(assignable_attributes)
+ association(association_name).initialize_attributes(existing_record)
else
- raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ method = "build_#{association_name}"
+ if respond_to?(method)
+ send(method, assignable_attributes)
+ else
+ raise ArgumentError, "Cannot build association `#{association_name}'. Are you trying to build a polymorphic one-to-one association?"
+ end
end
end
end
@@ -352,9 +414,9 @@ module ActiveRecord
# For example:
#
# assign_nested_attributes_for_collection_association(:people, {
- # '1' => { :id => '1', :name => 'Peter' },
- # '2' => { :name => 'John' },
- # '3' => { :id => '2', :_destroy => true }
+ # '1' => { id: '1', name: 'Peter' },
+ # '2' => { name: 'John' },
+ # '3' => { id: '2', _destroy: true }
# })
#
# Will update the name of the Person with ID 1, build a new associated
@@ -364,9 +426,9 @@ module ActiveRecord
# Also accepts an Array of attribute hashes:
#
# assign_nested_attributes_for_collection_association(:people, [
- # { :id => '1', :name => 'Peter' },
- # { :name => 'John' },
- # { :id => '2', :_destroy => true }
+ # { id: '1', name: 'Peter' },
+ # { name: 'John' },
+ # { id: '2', _destroy: true }
# ])
def assign_nested_attributes_for_collection_association(association_name, attributes_collection)
options = self.nested_attributes_options[association_name]
@@ -375,20 +437,7 @@ module ActiveRecord
raise ArgumentError, "Hash or Array expected, got #{attributes_collection.class.name} (#{attributes_collection.inspect})"
end
- if limit = options[:limit]
- limit = case limit
- when Symbol
- send(limit)
- when Proc
- limit.call
- else
- limit
- end
-
- if limit && attributes_collection.size > limit
- raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
- end
- end
+ check_record_limit!(options[:limit], attributes_collection)
if attributes_collection.is_a? Hash
keys = attributes_collection.keys
@@ -416,23 +465,44 @@ module ActiveRecord
association.build(attributes.except(*UNASSIGNABLE_KEYS))
end
elsif existing_record = existing_records.detect { |record| record.id.to_s == attributes['id'].to_s }
- unless association.loaded? || call_reject_if(association_name, attributes)
+ unless call_reject_if(association_name, attributes)
# Make sure we are operating on the actual object which is in the association's
# proxy_target array (either by finding it, or adding it if not found)
- target_record = association.target.detect { |record| record == existing_record }
-
+ # Take into account that the proxy_target may have changed due to callbacks
+ target_record = association.target.detect { |record| record.id.to_s == attributes['id'].to_s }
if target_record
existing_record = target_record
else
- association.add_to_target(existing_record)
+ association.add_to_target(existing_record, :skip_callbacks)
end
- end
- if !call_reject_if(association_name, attributes)
assign_to_or_mark_for_destruction(existing_record, attributes, options[:allow_destroy])
end
else
- raise_nested_attributes_record_not_found(association_name, attributes['id'])
+ raise_nested_attributes_record_not_found!(association_name, attributes['id'])
+ end
+ end
+ 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).
+ #
+ # Will raise an TooManyRecords error if the attributes_collection is
+ # larger than the limit.
+ def check_record_limit!(limit, attributes_collection)
+ if limit
+ limit = case limit
+ when Symbol
+ send(limit)
+ when Proc
+ limit.call
+ else
+ limit
+ end
+
+ if limit && attributes_collection.size > limit
+ raise TooManyRecords, "Maximum #{limit} records are allowed. Got #{attributes_collection.size} records instead."
end
end
end
@@ -456,6 +526,11 @@ module ActiveRecord
has_destroy_flag?(attributes) || call_reject_if(association_name, attributes)
end
+ # Determines if a record with the particular +attributes+ should be
+ # rejected by calling the reject_if Symbol or Proc (if defined).
+ # The reject_if option is defined by +accepts_nested_attributes_for+.
+ #
+ # Returns false if there is a +destroy_flag+ on the attributes.
def call_reject_if(association_name, attributes)
return false if has_destroy_flag?(attributes)
case callback = self.nested_attributes_options[association_name][:reject_if]
@@ -466,7 +541,7 @@ module ActiveRecord
end
end
- def raise_nested_attributes_record_not_found(association_name, record_id)
+ 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}"
end
end
diff --git a/activerecord/lib/active_record/null_relation.rb b/activerecord/lib/active_record/null_relation.rb
index 4c1c91e3df..080b20134d 100644
--- a/activerecord/lib/active_record/null_relation.rb
+++ b/activerecord/lib/active_record/null_relation.rb
@@ -6,7 +6,7 @@ module ActiveRecord
@records = []
end
- def pluck(_column_name)
+ def pluck(*column_names)
[]
end
@@ -39,19 +39,23 @@ module ActiveRecord
end
def to_sql
- @to_sql ||= ""
+ ""
end
- def where_values_hash
- {}
+ def count(*)
+ 0
end
- def count
+ def sum(*)
0
end
- def calculate(_operation, _column_name, _options = {})
- nil
+ def calculate(_operation, _column_name)
+ if _operation == :count
+ 0
+ else
+ nil
+ end
end
def exists?(_id = false)
diff --git a/activerecord/lib/active_record/observer.rb b/activerecord/lib/active_record/observer.rb
deleted file mode 100644
index 6b2f6f98a5..0000000000
--- a/activerecord/lib/active_record/observer.rb
+++ /dev/null
@@ -1,126 +0,0 @@
-
-module ActiveRecord
- # = Active Record Observer
- #
- # Observer classes respond to life cycle callbacks to implement trigger-like
- # behavior outside the original class. This is a great way to reduce the
- # clutter that normally comes when the model class is burdened with
- # functionality that doesn't pertain to the core responsibility of the
- # class. Example:
- #
- # class CommentObserver < ActiveRecord::Observer
- # def after_save(comment)
- # Notifications.comment("admin@do.com", "New comment was posted", comment).deliver
- # end
- # end
- #
- # This Observer sends an email when a Comment#save is finished.
- #
- # class ContactObserver < ActiveRecord::Observer
- # def after_create(contact)
- # contact.logger.info('New contact added!')
- # end
- #
- # def after_destroy(contact)
- # contact.logger.warn("Contact with an id of #{contact.id} was destroyed!")
- # end
- # end
- #
- # This Observer uses logger to log when specific callbacks are triggered.
- #
- # == Observing a class that can't be inferred
- #
- # Observers will by default be mapped to the class with which they share a name. So CommentObserver will
- # be tied to observing Comment, ProductManagerObserver to ProductManager, and so on. If you want to name your observer
- # differently than the class you're interested in observing, you can use the Observer.observe class method which takes
- # either the concrete class (Product) or a symbol for that class (:product):
- #
- # class AuditObserver < ActiveRecord::Observer
- # observe :account
- #
- # def after_update(account)
- # AuditTrail.new(account, "UPDATED")
- # end
- # end
- #
- # If the audit observer needs to watch more than one kind of object, this can be specified with multiple arguments:
- #
- # class AuditObserver < ActiveRecord::Observer
- # observe :account, :balance
- #
- # def after_update(record)
- # AuditTrail.new(record, "UPDATED")
- # end
- # end
- #
- # The AuditObserver will now act on both updates to Account and Balance by treating them both as records.
- #
- # == Available callback methods
- #
- # The observer can implement callback methods for each of the methods described in the Callbacks module.
- #
- # == Storing Observers in Rails
- #
- # If you're using Active Record within Rails, observer classes are usually stored in app/models with the
- # naming convention of app/models/audit_observer.rb.
- #
- # == Configuration
- #
- # In order to activate an observer, list it in the <tt>config.active_record.observers</tt> configuration
- # setting in your <tt>config/application.rb</tt> file.
- #
- # config.active_record.observers = :comment_observer, :signup_observer
- #
- # Observers will not be invoked unless you define these in your application configuration.
- #
- # If you are using Active Record outside Rails, activate the observers explicitly in a configuration or
- # environment file:
- #
- # ActiveRecord::Base.add_observer CommentObserver.instance
- # ActiveRecord::Base.add_observer SignupObserver.instance
- #
- # == Loading
- #
- # Observers register themselves in the model class they observe, since it is the class that
- # notifies them of events when they occur. As a side-effect, when an observer is loaded its
- # corresponding model class is loaded.
- #
- # Up to (and including) Rails 2.0.2 observers were instantiated between plugins and
- # application initializers. Now observers are loaded after application initializers,
- # so observed models can make use of extensions.
- #
- # If by any chance you are using observed models in the initialization you can still
- # load their observers by calling <tt>ModelObserver.instance</tt> before. Observers are
- # singletons and that call instantiates and registers them.
- #
- class Observer < ActiveModel::Observer
-
- protected
-
- def observed_classes
- klasses = super
- klasses + klasses.map { |klass| klass.descendants }.flatten
- end
-
- def add_observer!(klass)
- super
- define_callbacks klass
- end
-
- def define_callbacks(klass)
- observer = self
- observer_name = observer.class.name.underscore.gsub('/', '__')
-
- ActiveRecord::Callbacks::CALLBACKS.each do |callback|
- next unless respond_to?(callback)
- callback_meth = :"_notify_#{observer_name}_for_#{callback}"
- unless klass.respond_to?(callback_meth)
- klass.send(:define_method, callback_meth) do |&block|
- observer.update(callback, self, &block)
- end
- klass.send(callback, callback_meth)
- end
- end
- end
- end
-end
diff --git a/activerecord/lib/active_record/persistence.rb b/activerecord/lib/active_record/persistence.rb
index 611d3d97c3..a73a140ef1 100644
--- a/activerecord/lib/active_record/persistence.rb
+++ b/activerecord/lib/active_record/persistence.rb
@@ -15,18 +15,18 @@ module ActiveRecord
#
# ==== Examples
# # Create a single new object
- # User.create(:first_name => 'Jamie')
+ # User.create(first_name: 'Jamie')
#
# # Create an Array of new objects
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }])
+ # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }])
#
# # Create a single object and pass it into a block to set other attributes.
- # User.create(:first_name => 'Jamie') do |u|
+ # User.create(first_name: 'Jamie') do |u|
# u.is_admin = false
# end
#
# # Creating an Array of new objects using a block, where the block is executed for each object:
- # User.create([{ :first_name => 'Jamie' }, { :first_name => 'Jeremy' }]) do |u|
+ # User.create([{ first_name: 'Jamie' }, { first_name: 'Jeremy' }]) do |u|
# u.is_admin = false
# end
def create(attributes = nil, &block)
@@ -38,16 +38,44 @@ module ActiveRecord
object
end
end
+
+ # Given an attributes hash, +instantiate+ returns a new instance of
+ # the appropriate class.
+ #
+ # For example, +Post.all+ may return Comments, Messages, and Emails
+ # by storing the record's subclass in a +type+ attribute. By calling
+ # +instantiate+ instead of +new+, finder methods ensure they get new
+ # instances of the appropriate class for each record.
+ #
+ # 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)
+ end
+
+ private
+ # Called by +instantiate+ to decide which class to use for a new
+ # record instance.
+ #
+ # See +ActiveRecord::Inheritance#discriminate_class_for_record+ for
+ # the single-table inheritance discriminator.
+ def discriminate_class_for_record(record)
+ self
+ end
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.
def new_record?
+ sync_with_transaction_state
@new_record
end
# Returns true if this object has been destroyed, otherwise returns false.
def destroyed?
+ sync_with_transaction_state
@destroyed
end
@@ -64,19 +92,20 @@ module ActiveRecord
#
# By default, save always run validations. If any of them fail the action
# is cancelled and +save+ returns +false+. However, if you supply
- # :validate => false, validations are bypassed altogether. See
+ # validate: false, validations are bypassed altogether. See
# ActiveRecord::Validations for more information.
#
# There's a series of callbacks associated with +save+. If any of the
# <tt>before_*</tt> callbacks return +false+ the action is cancelled and
# +save+ returns +false+. See ActiveRecord::Callbacks for further
# details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save(*)
- begin
- create_or_update
- rescue ActiveRecord::RecordInvalid
- false
- end
+ create_or_update
+ rescue ActiveRecord::RecordInvalid
+ false
end
# Saves the model.
@@ -92,6 +121,9 @@ module ActiveRecord
# the <tt>before_*</tt> callbacks return +false+ the action is cancelled
# and <tt>save!</tt> raises ActiveRecord::RecordNotSaved. See
# ActiveRecord::Callbacks for further details.
+ #
+ # Attributes marked as readonly are silently ignored if the record is
+ # being updated.
def save!(*)
create_or_update || raise(RecordNotSaved)
end
@@ -104,7 +136,7 @@ module ActiveRecord
# record's primary key, and no callbacks are executed.
#
# To enforce the object's +before_destroy+ and +after_destroy+
- # callbacks, Observer methods, or any <tt>:dependent</tt> association
+ # callbacks or any <tt>:dependent</tt> association
# options, use <tt>#destroy</tt>.
def delete
self.class.delete(id) if persisted?
@@ -143,7 +175,7 @@ module ActiveRecord
# inheritance structures where you want a subclass to appear as the
# superclass. This can be used along with record identification in
# Action Pack to allow, say, <tt>Client < Company</tt> to do something
- # like render <tt>:partial => @client.becomes(Company)</tt> to render that
+ # like render <tt>partial: @client.becomes(Company)</tt> to render that
# instance using the companies/company partial instead of clients/client.
#
# Note: The new instance will share a link to the same attributes as the original class.
@@ -155,7 +187,18 @@ module ActiveRecord
became.instance_variable_set("@new_record", new_record?)
became.instance_variable_set("@destroyed", destroyed?)
became.instance_variable_set("@errors", errors)
- became.public_send("#{klass.inheritance_column}=", klass.name) unless self.class.descends_from_active_record?
+ became
+ end
+
+ # Wrapper around +becomes+ that also changes the instance's sti column value.
+ # This is especially useful if you want to persist the changed class in your
+ # database.
+ #
+ # Note: The old instance's sti column value will be changed too, as both objects
+ # share the same set of attributes.
+ def becomes!(klass)
+ became = becomes(klass)
+ became.public_send("#{klass.inheritance_column}=", klass.sti_name) unless self.class.descends_from_active_record?
became
end
@@ -167,17 +210,19 @@ module ActiveRecord
# * updated_at/updated_on column is updated if that column is available.
# * Updates all the attributes that are dirty in this object.
#
+ # This method raises an +ActiveRecord::ActiveRecordError+ if the
+ # attribute is marked as readonly.
def update_attribute(name, value)
name = name.to_s
verify_readonly_attribute(name)
send("#{name}=", value)
- save(:validate => false)
+ save(validate: false)
end
# Updates the attributes of the model from the passed-in hash and saves the
# record, all wrapped in a transaction. If the object is invalid, the saving
# will fail and false will be returned.
- def update_attributes(attributes)
+ def update(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -186,9 +231,11 @@ module ActiveRecord
end
end
- # Updates its receiver just like +update_attributes+ but calls <tt>save!</tt> instead
+ alias update_attributes update
+
+ # Updates its receiver just like +update+ but calls <tt>save!</tt> instead
# of +save+, so an exception is raised if the record is invalid.
- def update_attributes!(attributes)
+ def update!(attributes)
# The following transaction covers any possible database side-effects of the
# attributes assignment. For example, setting the IDs of a child collection.
with_transaction_returning_status do
@@ -197,26 +244,28 @@ module ActiveRecord
end
end
- # Updates a single attribute of an object, without having to explicitly call save on that object.
- #
- # * Validation is skipped.
- # * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
- #
- # Raises an +ActiveRecordError+ when called on new objects, or when the +name+
- # attribute is marked as readonly.
+ alias update_attributes! update!
+
+ # Equivalent to <code>update_columns(name => value)</code>.
def update_column(name, value)
update_columns(name => value)
end
- # Updates the attributes from the passed-in hash, without having to explicitly call save on that object.
+ # Updates the attributes directly in the database issuing an UPDATE SQL
+ # statement and sets them in the receiver:
#
- # * Validation is skipped.
+ # user.update_columns(last_request_at: Time.current)
+ #
+ # This is the fastest way to update attributes because it goes straight to
+ # the database, but take into account that in consequence the regular update
+ # procedures are totally bypassed. In particular:
+ #
+ # * Validations are skipped.
# * Callbacks are skipped.
- # * updated_at/updated_on column is not updated if that column is available.
+ # * +updated_at+/+updated_on+ are not updated.
#
- # Raises an +ActiveRecordError+ when called on new objects, or when at least
- # one of the attributes is marked as readonly.
+ # This method raises an +ActiveRecord::ActiveRecordError+ when called on new
+ # objects, or when at least one of the attributes is marked as readonly.
def update_columns(attributes)
raise ActiveRecordError, "can not update on a new record object" unless persisted?
@@ -224,11 +273,13 @@ module ActiveRecord
verify_readonly_attribute(key.to_s)
end
- attributes.each do |k,v|
- raw_write_attribute(k,v)
+ updated_count = self.class.unscoped.where(self.class.primary_key => id).update_all(attributes)
+
+ attributes.each do |k, v|
+ raw_write_attribute(k, v)
end
- self.class.where(self.class.primary_key => id).update_all(attributes) == 1
+ updated_count == 1
end
# Initializes +attribute+ to zero if +nil+ and adds the value passed as +by+ (default is 1).
@@ -282,10 +333,54 @@ module ActiveRecord
toggle(attribute).update_attribute(attribute, self[attribute])
end
- # Reloads the attributes of this object from the database.
- # The optional options argument is passed to find when reloading so you
- # may do e.g. record.reload(:lock => true) to reload the same record with
- # an exclusive row lock.
+ # Reloads the record from the database.
+ #
+ # This method finds record by its primary key (which could be assigned manually) and
+ # modifies the receiver in-place:
+ #
+ # account = Account.new
+ # # => #<Account id: nil, email: nil>
+ # account.id = 1
+ # account.reload
+ # # Account Load (1.2ms) SELECT "accounts".* FROM "accounts" WHERE "accounts"."id" = $1 LIMIT 1 [["id", 1]]
+ # # => #<Account id: 1, email: 'account@example.com'>
+ #
+ # Attributes are reloaded from the database, and caches busted, in
+ # particular the associations cache.
+ #
+ # If the record no longer exists in the database <tt>ActiveRecord::RecordNotFound</tt>
+ # is raised. Otherwise, in addition to the in-place modification the method
+ # returns +self+ for convenience.
+ #
+ # The optional <tt>:lock</tt> flag option allows you to lock the reloaded record:
+ #
+ # reload(lock: true) # reload with pessimistic locking
+ #
+ # Reloading is commonly used in test suites to test something is actually
+ # written to the database, or when some action modifies the corresponding
+ # row in the database but not the object in memory:
+ #
+ # assert account.deposit!(25)
+ # assert_equal 25, account.credit # check it is updated in memory
+ # assert_equal 25, account.reload.credit # check it is also persisted
+ #
+ # Another commom use case is optimistic locking handling:
+ #
+ # def with_optimistic_retry
+ # begin
+ # yield
+ # rescue ActiveRecord::StaleObjectError
+ # begin
+ # # Reload lock_version in particular.
+ # reload
+ # rescue ActiveRecord::RecordNotFound
+ # # If the record is gone there is nothing to do.
+ # else
+ # retry
+ # end
+ # end
+ # end
+ #
def reload(options = nil)
clear_aggregation_cache
clear_association_cache
@@ -298,14 +393,16 @@ module ActiveRecord
end
@attributes.update(fresh_object.instance_variable_get('@attributes'))
- @columns_hash = fresh_object.instance_variable_get('@columns_hash')
- @attributes_cache = {}
+ @column_types = self.class.column_types
+ @column_types_override = fresh_object.instance_variable_get('@column_types_override')
+ @attributes_cache = {}
self
end
# Saves the record with the updated_at/on attributes set to the current time.
- # Please note that no validation is performed and no callbacks are executed.
+ # 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.
#
@@ -315,16 +412,25 @@ module ActiveRecord
# If used along with +belongs_to+ then +touch+ will invoke +touch+ method on associated object.
#
# class Brake < ActiveRecord::Base
- # belongs_to :car, :touch => true
+ # belongs_to :car, touch: true
# end
#
# class Car < ActiveRecord::Base
- # belongs_to :corporation, :touch => true
+ # belongs_to :corporation, touch: true
# end
#
# # triggers @brake.car.touch and @brake.car.corporation.touch
# @brake.touch
+ #
+ # Note that +touch+ must be used on a persisted object, or else an
+ # ActiveRecordError will be thrown. For example:
+ #
+ # ball = Ball.new
+ # ball.touch(:updated_at) # => raises ActiveRecordError
+ #
def touch(name = nil)
+ raise ActiveRecordError, "can not touch on a new record object" unless persisted?
+
attributes = timestamp_attributes_for_update_in_model
attributes << name if name
@@ -339,7 +445,7 @@ module ActiveRecord
changes[self.class.locking_column] = increment_lock if locking_enabled?
- @changed_attributes.except!(*changes.keys)
+ changed_attributes.except!(*changes.keys)
primary_key = self.class.primary_key
self.class.unscoped.where(primary_key => self[primary_key]).update_all(changes) == 1
end
@@ -358,7 +464,7 @@ module ActiveRecord
def relation_for_destroy
pk = self.class.primary_key
column = self.class.columns_hash[pk]
- substitute = connection.substitute_at(column, 0)
+ substitute = self.class.connection.substitute_at(column, 0)
relation = self.class.unscoped.where(
self.class.arel_table[pk].eq(substitute))
@@ -369,23 +475,24 @@ module ActiveRecord
def create_or_update
raise ReadOnlyRecord if readonly?
- result = new_record? ? create : update
+ 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(attribute_names = @attributes.keys)
- attributes_with_values = arel_attributes_with_values_for_update(attribute_names)
- return 0 if attributes_with_values.empty?
- klass = self.class
- stmt = klass.unscoped.where(klass.arel_table[klass.primary_key].eq(id)).arel.compile_update(attributes_with_values)
- klass.connection.update stmt
+ def update_record(attribute_names = @attributes.keys)
+ attributes_values = arel_attributes_with_values_for_update(attribute_names)
+ if attributes_values.empty?
+ 0
+ else
+ self.class.unscoped.update_record attributes_values, id, id_was
+ end
end
# Creates a record with values matching those of the instance attributes
# and returns its id.
- def create(attribute_names = @attributes.keys)
+ def create_record(attribute_names = @attributes.keys)
attributes_values = arel_attributes_with_values_for_create(attribute_names)
new_id = self.class.unscoped.insert attributes_values
diff --git a/activerecord/lib/active_record/query_cache.rb b/activerecord/lib/active_record/query_cache.rb
index 2bd8ecda20..df8654e5c1 100644
--- a/activerecord/lib/active_record/query_cache.rb
+++ b/activerecord/lib/active_record/query_cache.rb
@@ -4,20 +4,22 @@ module ActiveRecord
class QueryCache
module ClassMethods
# Enable the query cache within the block if Active Record is configured.
+ # If it's not, it will execute the given block.
def cache(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
+ if ActiveRecord::Base.connected?
connection.cache(&block)
+ else
+ yield
end
end
# Disable the query cache within the block if Active Record is configured.
+ # If it's not, it will execute the given block.
def uncached(&block)
- if ActiveRecord::Base.configurations.blank?
- yield
- else
+ if ActiveRecord::Base.connected?
connection.uncached(&block)
+ else
+ yield
end
end
end
diff --git a/activerecord/lib/active_record/querying.rb b/activerecord/lib/active_record/querying.rb
index 13e09eda53..6bee4f38e7 100644
--- a/activerecord/lib/active_record/querying.rb
+++ b/activerecord/lib/active_record/querying.rb
@@ -1,20 +1,21 @@
-
module ActiveRecord
module Querying
- delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, :to => :all
- delegate :first_or_create, :first_or_create!, :first_or_initialize, :to => :all
- delegate :find_by, :find_by!, :to => :all
- delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, :to => :all
- delegate :find_each, :find_in_batches, :to => :all
+ delegate :find, :take, :take!, :first, :first!, :last, :last!, :exists?, :any?, :many?, to: :all
+ delegate :first_or_create, :first_or_create!, :first_or_initialize, to: :all
+ delegate :find_or_create_by, :find_or_create_by!, :find_or_initialize_by, to: :all
+ delegate :find_by, :find_by!, to: :all
+ delegate :destroy, :destroy_all, :delete, :delete_all, :update, :update_all, to: :all
+ delegate :find_each, :find_in_batches, to: :all
delegate :select, :group, :order, :except, :reorder, :limit, :offset, :joins,
:where, :preload, :eager_load, :includes, :from, :lock, :readonly,
- :having, :create_with, :uniq, :references, :none, :to => :all
- delegate :count, :average, :minimum, :maximum, :sum, :calculate, :pluck, :ids, :to => :all
+ :having, :create_with, :uniq, :distinct, :references, :none, :unscope, to: :all
+ delegate :count, :average, :minimum, :maximum, :sum, :calculate, to: :all
+ delegate :pluck, :ids, to: :all
# Executes a custom SQL query against your database and returns all the results. The results will
# be returned as an array with columns requested encapsulated as attributes of the model you call
# this method from. If you call <tt>Product.find_by_sql</tt> then the results will be returned in
- # a Product object with the attributes you specified in the SQL query.
+ # a +Product+ object with the attributes you specified in the SQL query.
#
# If you call a complicated SQL query which spans multiple tables the columns specified by the
# SELECT will be attributes of the model, whether or not they are columns of the corresponding
@@ -25,27 +26,25 @@ module ActiveRecord
# MySQL specific terms will lock you to using that particular database engine or require you to
# change your call if you switch engines.
#
- # ==== Examples
# # A simple SQL query spanning multiple tables
# Post.find_by_sql "SELECT p.title, c.author FROM posts p, comments c WHERE p.id = c.post_id"
- # > [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ # # => [#<Post:0x36bff9c @attributes={"title"=>"Ruby Meetup", "first_name"=>"Quentin"}>, ...]
+ #
+ # You can use the same string replacement techniques as you can with <tt>ActiveRecord::QueryMethods#where</tt>:
#
- # # You can use the same string replacement techniques as you can with ActiveRecord#find
# Post.find_by_sql ["SELECT title FROM posts WHERE author = ? AND created > ?", author_id, start_date]
- # > [#<Post:0x36bff9c @attributes={"title"=>"The Cheap Man Buys Twice"}>, ...]
+ # 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 = [])
- logging_query_plan do
- 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
+ result_set = connection.select_all(sanitize_sql(sql), "#{name} Load", binds)
+ column_types = {}
- result_set.map { |record| instantiate(record, 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
+
+ result_set.map { |record| instantiate(record, column_types) }
end
# Returns the result of an SQL statement that should only include a COUNT(*) in the SELECT part.
@@ -56,14 +55,10 @@ module ActiveRecord
#
# * +sql+ - An SQL statement which should return a count query from the database, see the example below.
#
- # ==== Examples
- #
# Product.count_by_sql "SELECT COUNT(*) FROM sales s, customers c WHERE s.customer_id = c.id"
def count_by_sql(sql)
- logging_query_plan do
- sql = sanitize_conditions(sql)
- connection.select_value(sql, "#{name} Count").to_i
- end
+ sql = sanitize_conditions(sql)
+ connection.select_value(sql, "#{name} Count").to_i
end
end
end
diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb
index d7e35fb771..eef08aea88 100644
--- a/activerecord/lib/active_record/railtie.rb
+++ b/activerecord/lib/active_record/railtie.rb
@@ -36,6 +36,26 @@ module ActiveRecord
rake_tasks do
require "active_record/base"
+
+ ActiveRecord::Tasks::DatabaseTasks.seed_loader = Rails.application
+ ActiveRecord::Tasks::DatabaseTasks.env = Rails.env
+
+ namespace :db do
+ task :load_config do
+ ActiveRecord::Tasks::DatabaseTasks.db_dir = Rails.application.config.paths["db"].first
+ ActiveRecord::Tasks::DatabaseTasks.database_configuration = Rails.application.config.database_configuration
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths = Rails.application.paths['db/migrate'].to_a
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path = File.join Rails.root, 'test', 'fixtures'
+ ActiveRecord::Tasks::DatabaseTasks.root = Rails.root
+
+ if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
+ if engine.paths['db/migrate'].existent
+ ActiveRecord::Tasks::DatabaseTasks.migrations_paths += engine.paths['db/migrate'].to_a
+ end
+ end
+ end
+ end
+
load "active_record/railties/databases.rake"
end
@@ -49,7 +69,7 @@ module ActiveRecord
Rails.logger.extend ActiveSupport::Logger.broadcast console
end
- runner do |app|
+ runner do
require "active_record/base"
end
@@ -64,7 +84,7 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) { self.logger ||= ::Rails.logger }
end
- initializer "active_record.migration_error" do |app|
+ initializer "active_record.migration_error" do
if config.active_record.delete(:migration_error) == :page_load
config.app_middleware.insert_after "::ActionDispatch::Callbacks",
"ActiveRecord::Migration::CheckPending"
@@ -80,7 +100,7 @@ module ActiveRecord
if File.file?(filename)
cache = Marshal.load File.binread filename
if cache.version == ActiveRecord::Migrator.current_version
- ActiveRecord::Model.connection.schema_cache = cache
+ self.connection.schema_cache = cache
else
warn "Ignoring db/schema_cache.dump because it has expired. The current schema version is #{ActiveRecord::Migrator.current_version}, but the one in the cache is #{cache.version}."
end
@@ -102,15 +122,13 @@ module ActiveRecord
# and then establishes the connection.
initializer "active_record.initialize_database" do |app|
ActiveSupport.on_load(:active_record) do
- unless ENV['DATABASE_URL']
- self.configurations = app.config.database_configuration
- end
+ self.configurations = app.config.database_configuration || {}
establish_connection
end
end
# Expose database runtime to controller for logging.
- initializer "active_record.log_runtime" do |app|
+ initializer "active_record.log_runtime" do
require "active_record/railties/controller_runtime"
ActiveSupport.on_load(:action_controller) do
include ActiveRecord::Railties::ControllerRuntime
@@ -122,8 +140,10 @@ module ActiveRecord
ActiveSupport.on_load(:active_record) do
ActionDispatch::Reloader.send(hook) do
- ActiveRecord::Model.clear_reloadable_connections!
- ActiveRecord::Model.clear_cache!
+ if ActiveRecord::Base.connected?
+ ActiveRecord::Base.clear_reloadable_connections!
+ ActiveRecord::Base.clear_cache!
+ end
end
end
end
@@ -132,16 +152,5 @@ module ActiveRecord
path = app.paths["db"].first
config.watchable_files.concat ["#{path}/schema.rb", "#{path}/structure.sql"]
end
-
- config.after_initialize do |app|
- ActiveSupport.on_load(:active_record) do
- ActiveRecord::Model.instantiate_observers
-
- ActionDispatch::Reloader.to_prepare do
- ActiveRecord::Model.instantiate_observers
- end
- end
-
- end
end
end
diff --git a/activerecord/lib/active_record/railties/console_sandbox.rb b/activerecord/lib/active_record/railties/console_sandbox.rb
index 90b462fad6..604a220303 100644
--- a/activerecord/lib/active_record/railties/console_sandbox.rb
+++ b/activerecord/lib/active_record/railties/console_sandbox.rb
@@ -1,4 +1,5 @@
-ActiveRecord::Base.connection.begin_db_transaction
+ActiveRecord::Base.connection.begin_transaction(joinable: false)
+
at_exit do
- ActiveRecord::Base.connection.rollback_db_transaction
+ ActiveRecord::Base.connection.rollback_transaction
end
diff --git a/activerecord/lib/active_record/railties/controller_runtime.rb b/activerecord/lib/active_record/railties/controller_runtime.rb
index 7695eacbff..af4840476c 100644
--- a/activerecord/lib/active_record/railties/controller_runtime.rb
+++ b/activerecord/lib/active_record/railties/controller_runtime.rb
@@ -21,9 +21,10 @@ module ActiveRecord
def cleanup_view_runtime
if ActiveRecord::Base.connected?
db_rt_before_render = ActiveRecord::LogSubscriber.reset_runtime
+ self.db_runtime = (db_runtime || 0) + db_rt_before_render
runtime = super
db_rt_after_render = ActiveRecord::LogSubscriber.reset_runtime
- self.db_runtime = db_rt_before_render + db_rt_after_render
+ self.db_runtime += db_rt_after_render
runtime - db_rt_after_render
else
super
diff --git a/activerecord/lib/active_record/railties/databases.rake b/activerecord/lib/active_record/railties/databases.rake
index 69a9526fcc..52b3d3e5e6 100644
--- a/activerecord/lib/active_record/railties/databases.rake
+++ b/activerecord/lib/active_record/railties/databases.rake
@@ -2,14 +2,8 @@ require 'active_record'
db_namespace = namespace :db do
task :load_config do
- ActiveRecord::Base.configurations = Rails.application.config.database_configuration
- ActiveRecord::Migrator.migrations_paths = Rails.application.paths['db/migrate'].to_a
-
- if defined?(ENGINE_PATH) && engine = Rails::Engine.find(ENGINE_PATH)
- if engine.paths['db/migrate'].existent
- ActiveRecord::Migrator.migrations_paths += engine.paths['db/migrate'].to_a
- end
- end
+ ActiveRecord::Base.configurations = ActiveRecord::Tasks::DatabaseTasks.database_configuration || {}
+ ActiveRecord::Migrator.migrations_paths = ActiveRecord::Tasks::DatabaseTasks.migrations_paths
end
namespace :create do
@@ -18,7 +12,7 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Create the database from DATABASE_URL or config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)'
task :create => [:load_config] do
if ENV['DATABASE_URL']
ActiveRecord::Tasks::DatabaseTasks.create_database_url
@@ -156,7 +150,7 @@ db_namespace = namespace :db do
begin
puts ActiveRecord::Tasks::DatabaseTasks.collation_current
rescue NoMethodError
- $stderr.puts 'Sorry, your database adapter is not supported yet, feel free to submit a patch'
+ $stderr.puts 'Sorry, your database adapter is not supported yet. Feel free to submit a patch.'
end
end
@@ -166,11 +160,11 @@ db_namespace = namespace :db do
end
# desc "Raises an error if there are pending migrations"
- task :abort_if_pending_migrations => [:environment, :load_config] do
- pending_migrations = ActiveRecord::Migrator.new(:up, ActiveRecord::Migrator.migrations_paths).pending_migrations
+ task :abort_if_pending_migrations => :environment do
+ pending_migrations = ActiveRecord::Migrator.open(ActiveRecord::Migrator.migrations_paths).pending_migrations
if pending_migrations.any?
- puts "You have #{pending_migrations.size} pending migrations:"
+ puts "You have #{pending_migrations.size} pending #{pending_migrations.size > 1 ? 'migrations:' : 'migration:'}"
pending_migrations.each do |pending_migration|
puts ' %4d %s' % [pending_migration.version, pending_migration.name]
end
@@ -178,13 +172,13 @@ db_namespace = namespace :db do
end
end
- desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the db first)'
+ desc 'Create the database, load the schema, and initialize with the seed data (use db:reset to also drop the database first)'
task :setup => ['db:schema:load_if_ruby', 'db:structure:load_if_sql', :seed]
desc 'Load the seed data from db/seeds.rb'
task :seed do
db_namespace['abort_if_pending_migrations'].invoke
- Rails.application.load_seed
+ ActiveRecord::Tasks::DatabaseTasks.load_seed
end
namespace :fixtures do
@@ -192,7 +186,15 @@ db_namespace = namespace :db do
task :load => [:environment, :load_config] do
require 'active_record/fixtures'
- base_dir = File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ base_dir = if ENV['FIXTURES_PATH']
+ STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
+ "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
+ "instead."
+ File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ else
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ end
+
fixtures_dir = File.join [base_dir, ENV['FIXTURES_DIR']].compact
(ENV['FIXTURES'] ? ENV['FIXTURES'].split(/,/) : Dir["#{fixtures_dir}/**/*.yml"].map {|f| f[(fixtures_dir.size + 1)..-5] }).each do |fixture_file|
@@ -209,7 +211,16 @@ db_namespace = namespace :db do
puts %Q(The fixture ID for "#{label}" is #{ActiveRecord::FixtureSet.identify(label)}.) if label
- base_dir = ENV['FIXTURES_PATH'] ? File.join(Rails.root, ENV['FIXTURES_PATH']) : File.join(Rails.root, 'test', 'fixtures')
+ base_dir = if ENV['FIXTURES_PATH']
+ STDERR.puts "Using FIXTURES_PATH env variable is deprecated, please use " +
+ "ActiveRecord::Tasks::DatabaseTasks.fixtures_path = '/path/to/fixtures' " +
+ "instead."
+ File.join [Rails.root, ENV['FIXTURES_PATH'] || %w{test fixtures}].flatten
+ else
+ ActiveRecord::Tasks::DatabaseTasks.fixtures_path
+ end
+
+
Dir["#{base_dir}/**/*.yml"].each do |file|
if data = YAML::load(ERB.new(IO.read(file)).result)
data.keys.each do |key|
@@ -225,10 +236,10 @@ db_namespace = namespace :db do
end
namespace :schema do
- desc 'Create a db/schema.rb file that can be portably used against any DB supported by AR'
+ desc 'Create a db/schema.rb file that is portable against any DB supported by AR'
task :dump => [:environment, :load_config] do
require 'active_record/schema_dumper'
- filename = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
+ filename = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
File.open(filename, "w:utf-8") do |file|
ActiveRecord::SchemaDumper.dump(ActiveRecord::Base.connection, file)
end
@@ -237,12 +248,9 @@ db_namespace = namespace :db do
desc 'Load a schema.rb file into the database'
task :load => [:environment, :load_config] do
- file = ENV['SCHEMA'] || "#{Rails.root}/db/schema.rb"
- if File.exists?(file)
- load(file)
- else
- abort %{#{file} doesn't exist yet. Run `rake db:migrate` to create it then try again. If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded}
- end
+ file = ENV['SCHEMA'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, 'schema.rb')
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(file)
+ load(file)
end
task :load_if_ruby => ['db:create', :environment] do
@@ -253,7 +261,7 @@ db_namespace = namespace :db do
desc 'Create a db/schema_cache.dump file.'
task :dump => [:environment, :load_config] do
con = ActiveRecord::Base.connection
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
con.schema_cache.clear!
con.tables.each { |table| con.schema_cache.add(table) }
@@ -262,70 +270,35 @@ db_namespace = namespace :db do
desc 'Clear a db/schema_cache.dump file.'
task :clear => [:environment, :load_config] do
- filename = File.join(Rails.application.config.paths["db"].first, "schema_cache.dump")
- FileUtils.rm(filename) if File.exists?(filename)
+ filename = File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "schema_cache.dump")
+ FileUtils.rm(filename) if File.exist?(filename)
end
end
end
namespace :structure do
- def set_firebird_env(config)
- ENV['ISC_USER'] = config['username'].to_s if config['username']
- ENV['ISC_PASSWORD'] = config['password'].to_s if config['password']
- end
-
- def firebird_db_string(config)
- FireRuby::Database.db_string_for(config.symbolize_keys)
- end
-
desc 'Dump the database structure to db/structure.sql. Specify another file with DB_STRUCTURE=db/my_structure.sql'
task :dump => [:environment, :load_config] do
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
+ filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- File.open(filename, "w:utf-8") { |f| f << ActiveRecord::Base.connection.structure_dump }
- when 'sqlserver'
- `smoscript -s #{current_config['host']} -d #{current_config['database']} -u #{current_config['username']} -p #{current_config['password']} -f #{filename} -A -U`
- when "firebird"
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -a #{db_string} > #{filename}"
- else
- raise "Task not supported by '#{current_config["adapter"]}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(current_config, filename)
if ActiveRecord::Base.connection.supports_migrations?
- File.open(filename, "a") { |f| f << ActiveRecord::Base.connection.dump_schema_information }
+ File.open(filename, "a") do |f|
+ f.puts ActiveRecord::Base.connection.dump_schema_information
+ f.print "\n"
+ end
end
db_namespace['structure:dump'].reenable
end
# desc "Recreate the databases from the structure.sql file"
task :load => [:environment, :load_config] do
- current_config = ActiveRecord::Tasks::DatabaseTasks.current_config(:env => (ENV['RAILS_ENV'] || 'test'))
- filename = ENV['DB_STRUCTURE'] || File.join(Rails.root, "db", "structure.sql")
- case current_config['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
- when 'sqlserver'
- `sqlcmd -S #{current_config['host']} -d #{current_config['database']} -U #{current_config['username']} -P #{current_config['password']} -i #{filename}`
- when 'oci', 'oracle'
- ActiveRecord::Base.establish_connection(current_config)
- IO.read(filename).split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- set_firebird_env(current_config)
- db_string = firebird_db_string(current_config)
- sh "isql -i #{filename} #{db_string}"
- else
- raise "Task not supported by '#{current_config['adapter']}'"
- end
+ filename = ENV['DB_STRUCTURE'] || File.join(ActiveRecord::Tasks::DatabaseTasks.db_dir, "structure.sql")
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file(filename)
+ current_config = ActiveRecord::Tasks::DatabaseTasks.current_config
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(current_config, filename)
end
task :load_if_sql => ['db:create', :environment] do
@@ -347,9 +320,16 @@ db_namespace = namespace :db do
# desc "Recreate the test database from an existent schema.rb file"
task :load_schema => 'db:test:purge' do
- ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
- ActiveRecord::Schema.verbose = false
- db_namespace["schema:load"].invoke
+ begin
+ should_reconnect = ActiveRecord::Base.connection_pool.active_connection?
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['test'])
+ ActiveRecord::Schema.verbose = false
+ db_namespace["schema:load"].invoke
+ ensure
+ if should_reconnect
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations[ActiveRecord::Tasks::DatabaseTasks.env])
+ end
+ end
end
# desc "Recreate the test database from an existent structure.sql file"
@@ -380,31 +360,11 @@ db_namespace = namespace :db do
# desc "Empty the test database"
task :purge => [:environment, :load_config] do
- abcs = ActiveRecord::Base.configurations
- case abcs['test']['adapter']
- when /mysql/, /postgresql/, /sqlite/
- ActiveRecord::Tasks::DatabaseTasks.purge abcs['test']
- when 'sqlserver'
- test = abcs.deep_dup['test']
- test_database = test['database']
- test['database'] = 'master'
- ActiveRecord::Base.establish_connection(test)
- ActiveRecord::Base.connection.recreate_database!(test_database)
- when "oci", "oracle"
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.structure_drop.split(";\n\n").each do |ddl|
- ActiveRecord::Base.connection.execute(ddl)
- end
- when 'firebird'
- ActiveRecord::Base.establish_connection(:test)
- ActiveRecord::Base.connection.recreate_database!
- else
- raise "Task not supported by '#{abcs['test']['adapter']}'"
- end
+ ActiveRecord::Tasks::DatabaseTasks.purge ActiveRecord::Base.configurations['test']
end
# desc 'Check for pending migrations and load the test schema'
- task :prepare => 'db:abort_if_pending_migrations' do
+ task :prepare => :load_config do
unless ActiveRecord::Base.configurations.blank?
db_namespace['test:load'].invoke
end
@@ -430,7 +390,7 @@ namespace :railties do
puts "NOTE: Migration #{migration.basename} from #{name} has been skipped. Migration with the same name already exists."
end
- on_copy = Proc.new do |name, migration, old_path|
+ on_copy = Proc.new do |name, migration|
puts "Copied migration #{migration.basename} from #{name}"
end
@@ -440,5 +400,5 @@ namespace :railties do
end
end
-task 'test:prepare' => 'db:test:prepare'
+task 'test:prepare' => ['db:test:prepare', 'db:test:load', 'db:abort_if_pending_migrations']
diff --git a/activerecord/lib/active_record/readonly_attributes.rb b/activerecord/lib/active_record/readonly_attributes.rb
index b3c20c4aff..b3ddfd63d4 100644
--- a/activerecord/lib/active_record/readonly_attributes.rb
+++ b/activerecord/lib/active_record/readonly_attributes.rb
@@ -20,10 +20,5 @@ module ActiveRecord
self._attr_readonly
end
end
-
- def _attr_readonly
- ActiveSupport::Deprecation.warn("Instance level _attr_readonly method is deprecated, please use class level method.")
- defined?(@_attr_readonly) ? @_attr_readonly : self.class._attr_readonly
- end
end
end
diff --git a/activerecord/lib/active_record/reflection.rb b/activerecord/lib/active_record/reflection.rb
index f322b96f79..bce7766501 100644
--- a/activerecord/lib/active_record/reflection.rb
+++ b/activerecord/lib/active_record/reflection.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
# = Active Record Reflection
module Reflection # :nodoc:
@@ -6,10 +5,31 @@ module ActiveRecord
included do
class_attribute :reflections
+ class_attribute :aggregate_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)
+ end
+
+ def self.add_reflection(ar, name, reflection)
+ ar.reflections = ar.reflections.merge(name => reflection)
+ end
+
+ def self.add_aggregate_reflection(ar, name, reflection)
+ ar.aggregate_reflections = ar.aggregate_reflections.merge(name => reflection)
end
- # Reflection enables to interrogate Active Record classes and objects
+ # \Reflection enables to interrogate Active Record classes and objects
# about their associations and aggregations. This information can,
# for example, be used in a form builder that takes an Active Record object
# and creates input fields for all of the attributes depending on their type
@@ -18,22 +38,9 @@ module ActiveRecord
# MacroReflection class has info for AggregateReflection and AssociationReflection
# classes.
module ClassMethods
- def create_reflection(macro, name, scope, options, active_record)
- case macro
- when :has_many, :belongs_to, :has_one, :has_and_belongs_to_many
- klass = options[:through] ? ThroughReflection : AssociationReflection
- reflection = klass.new(macro, name, scope, options, active_record)
- when :composed_of
- reflection = AggregateReflection.new(macro, name, scope, options, active_record)
- end
-
- self.reflections = self.reflections.merge(name => reflection)
- reflection
- end
-
# Returns an array of AggregateReflection objects for all the aggregations in the class.
def reflect_on_all_aggregations
- reflections.values.grep(AggregateReflection)
+ aggregate_reflections.values
end
# Returns the AggregateReflection object for the named +aggregation+ (use the symbol).
@@ -41,8 +48,7 @@ module ActiveRecord
# Account.reflect_on_aggregation(:balance) # => the balance AggregateReflection
#
def reflect_on_aggregation(aggregation)
- reflection = reflections[aggregation]
- reflection if reflection.is_a?(AggregateReflection)
+ aggregate_reflections[aggregation]
end
# Returns an array of AssociationReflection objects for all the
@@ -56,7 +62,7 @@ module ActiveRecord
# Account.reflect_on_all_associations(:has_many) # returns an array of all has_many associations
#
def reflect_on_all_associations(macro = nil)
- association_reflections = reflections.values.grep(AssociationReflection)
+ association_reflections = reflections.values
macro ? association_reflections.select { |reflection| reflection.macro == macro } : association_reflections
end
@@ -66,8 +72,7 @@ module ActiveRecord
# Invoice.reflect_on_association(:line_items).macro # returns :has_many
#
def reflect_on_association(association)
- reflection = reflections[association]
- reflection if reflection.is_a?(AssociationReflection)
+ reflections[association]
end
# Returns an array of AssociationReflection objects for all associations which have <tt>:autosave</tt> enabled.
@@ -76,18 +81,23 @@ module ActiveRecord
end
end
- # Abstract base class for AggregateReflection and AssociationReflection. Objects of
+ # Base class for AggregateReflection and AssociationReflection. Objects of
# AggregateReflection and AssociationReflection are returned by the Reflection::ClassMethods.
+ #
+ # MacroReflection
+ # AggregateReflection
+ # AssociationReflection
+ # ThroughReflection
class MacroReflection
# Returns the name of the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>:balance</tt>
+ # <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>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
@@ -95,8 +105,8 @@ module ActiveRecord
# Returns the hash of options used for the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>{ :class_name => "Money" }</tt>
- # <tt>has_many :clients</tt> returns +{}+
+ # <tt>composed_of :balance, class_name: 'Money'</tt> returns <tt>{ class_name: "Money" }</tt>
+ # <tt>has_many :clients</tt> returns <tt>{}</tt>
attr_reader :options
attr_reader :active_record
@@ -109,13 +119,19 @@ module ActiveRecord
@scope = scope
@options = options
@active_record = active_record
+ @klass = options[:class]
@plural_name = active_record.pluralize_table_names ?
name.to_s.pluralize : name.to_s
end
+ def autosave=(autosave)
+ @automatic_inverse_of = false
+ @options[:autosave] = autosave
+ end
+
# Returns the class for the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns the Money class
+ # <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
@@ -123,7 +139,7 @@ module ActiveRecord
# Returns the class name for the macro.
#
- # <tt>composed_of :balance, :class_name => 'Money'</tt> returns <tt>'Money'</tt>
+ # <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
@@ -174,23 +190,33 @@ module ActiveRecord
@klass ||= active_record.send(:compute_type, class_name)
end
- def initialize(*args)
+ attr_reader :type, :foreign_type
+
+ def initialize(macro, name, scope, options, active_record)
super
- @collection = [:has_many, :has_and_belongs_to_many].include?(macro)
+ @collection = :has_many == macro
+ @automatic_inverse_of = nil
+ @type = options[:as] && "#{options[:as]}_type"
+ @foreign_type = options[:foreign_type] || "#{name}_type"
+ @constructable = calculate_constructable(macro, options)
end
- # Returns a new, unsaved instance of the associated class. +options+ will
+ # 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 constructable? # :nodoc:
+ @constructable
+ end
+
def table_name
- @table_name ||= klass.table_name
+ klass.table_name
end
def quoted_table_name
- @quoted_table_name ||= klass.quoted_table_name
+ klass.quoted_table_name
end
def join_table
@@ -201,16 +227,8 @@ module ActiveRecord
@foreign_key ||= options[:foreign_key] || derive_foreign_key
end
- def foreign_type
- @foreign_type ||= options[:foreign_type] || "#{name}_type"
- end
-
- def type
- @type ||= options[:as] && "#{options[:as]}_type"
- end
-
def primary_key_column
- @primary_key_column ||= klass.columns.find { |c| c.name == klass.primary_key }
+ klass.columns_hash[klass.primary_key]
end
def association_foreign_key
@@ -234,20 +252,8 @@ module ActiveRecord
end
end
- def columns(tbl_name)
- @columns ||= klass.connection.columns(tbl_name)
- end
-
- def reset_column_information
- @columns = nil
- end
-
def check_validity!
check_validity_of_inverse!
-
- if has_and_belongs_to_many? && association_foreign_key == foreign_key
- raise HasAndBelongsToManyAssociationForeignKeyNeeded.new(self)
- end
end
def check_validity_of_inverse!
@@ -263,7 +269,7 @@ module ActiveRecord
end
def source_reflection
- nil
+ self
end
# A chain of reflections from this one back to the owner. For more see the explanation in
@@ -285,13 +291,13 @@ module ActiveRecord
alias :source_macro :macro
def has_inverse?
- @options[:inverse_of]
+ inverse_name
end
def inverse_of
- if has_inverse?
- @inverse_of ||= klass.reflect_on_association(options[:inverse_of])
- end
+ return unless inverse_name
+
+ @inverse_of ||= klass.reflect_on_association inverse_name
end
def polymorphic_inverse_of(associated_class)
@@ -315,10 +321,10 @@ module ActiveRecord
# the parent's validation.
#
# Unless you explicitly disable validation with
- # <tt>:validate => false</tt>, validation will take place when:
+ # <tt>validate: false</tt>, validation will take place when:
#
- # * you explicitly enable validation; <tt>:validate => true</tt>
- # * you use autosave; <tt>:autosave => true</tt>
+ # * you explicitly enable validation; <tt>validate: true</tt>
+ # * 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)
@@ -329,10 +335,6 @@ module ActiveRecord
macro == :belongs_to
end
- def has_and_belongs_to_many?
- macro == :has_and_belongs_to_many
- end
-
def association_class
case macro
when :belongs_to
@@ -341,8 +343,6 @@ module ActiveRecord
else
Associations::BelongsToAssociation
end
- when :has_and_belongs_to_many
- Associations::HasAndBelongsToManyAssociation
when :has_many
if options[:through]
Associations::HasManyThroughAssociation
@@ -362,7 +362,92 @@ module ActiveRecord
options.key? :polymorphic
end
+ VALID_AUTOMATIC_INVERSE_MACROS = [:has_many, :has_one, :belongs_to]
+ INVALID_AUTOMATIC_INVERSE_OPTIONS = [:conditions, :through, :polymorphic, :foreign_key]
+
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ self
+ end
+
private
+
+ def calculate_constructable(macro, options)
+ case macro
+ when :belongs_to
+ !options[:polymorphic]
+ when :has_one
+ !options[:through]
+ else
+ true
+ end
+ end
+
+ # Attempts to find the inverse association name automatically.
+ # If it cannot find a suitable inverse association name, it returns
+ # nil.
+ def inverse_name
+ options.fetch(:inverse_of) do
+ if @automatic_inverse_of == false
+ nil
+ else
+ @automatic_inverse_of ||= automatic_inverse_of
+ end
+ end
+ end
+
+ # 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
+
+ begin
+ 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.
+ reflection = false
+ end
+
+ if valid_inverse_reflection?(reflection)
+ return inverse_name
+ end
+ end
+
+ false
+ end
+
+ # Checks if the inverse reflection that is returned from the
+ # +automatic_inverse_of+ method is a valid reflection. We must
+ # make sure that the reflection's active_record name matches up
+ # with the current reflection's klass name.
+ #
+ # Note: klass will always be valid because when there's a NameError
+ # from calling +klass+, +reflection+ will already be set to false.
+ def valid_inverse_reflection?(reflection)
+ reflection &&
+ klass.name == reflection.active_record.name &&
+ can_find_inverse_of_automatically?(reflection)
+ end
+
+ # Checks to see if the reflection doesn't have any options that prevent
+ # us from being able to guess the inverse automatically. First, the
+ # <tt>inverse_of</tt> option cannot be set to false. Second, we must
+ # have <tt>has_many</tt>, <tt>has_one</tt>, <tt>belongs_to</tt> associations.
+ # Third, we must not have options such as <tt>:polymorphic</tt> or
+ # <tt>:foreign_key</tt> which prevent us from correctly guessing the
+ # inverse association.
+ #
+ # Anything with a scope can additionally ruin our attempt at finding an
+ # inverse, so we exclude reflections with scopes.
+ def can_find_inverse_of_automatically?(reflection)
+ reflection.options[:inverse_of] != false &&
+ VALID_AUTOMATIC_INVERSE_MACROS.include?(reflection.macro) &&
+ !INVALID_AUTOMATIC_INVERSE_OPTIONS.any? { |opt| reflection.options[opt] } &&
+ !reflection.scope
+ end
+
def derive_class_name
class_name = name.to_s.camelize
class_name = class_name.singularize if collection?
@@ -394,16 +479,30 @@ module ActiveRecord
delegate :foreign_key, :foreign_type, :association_foreign_key,
:active_record_primary_key, :type, :to => :source_reflection
- # Gets the source of the through reflection. It checks both a singularized
+ def initialize(macro, name, scope, options, active_record)
+ super
+ @source_reflection_name = options[:source]
+ end
+
+ # Returns the source of the through reflection. It checks both a singularized
# and pluralized form for <tt>:belongs_to</tt> or <tt>:has_many</tt>.
#
# class Post < ActiveRecord::Base
# has_many :taggings
- # has_many :tags, :through => :taggings
+ # has_many :tags, through: :taggings
# end
#
+ # class Tagging < ActiveRecord::Base
+ # belongs_to :post
+ # belongs_to :tag
+ # end
+ #
+ # 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">
+ #
def source_reflection
- @source_reflection ||= source_reflection_names.collect { |name| through_reflection.klass.reflect_on_association(name) }.compact.first
+ through_reflection.klass.reflect_on_association(source_reflection_name)
end
# Returns the AssociationReflection object specified in the <tt>:through</tt> option
@@ -411,14 +510,15 @@ module ActiveRecord
#
# class Post < ActiveRecord::Base
# has_many :taggings
- # has_many :tags, :through => :taggings
+ # has_many :tags, through: :taggings
# end
#
# tags_reflection = Post.reflect_on_association(:tags)
- # taggings_reflection = tags_reflection.through_reflection
+ # tags_reflection.through_reflection
+ # # => <ActiveRecord::Reflection::AssociationReflection: @macro=:has_many, @name=:taggings, @active_record=Post, @plural_name="taggings">
#
def through_reflection
- @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
@@ -427,9 +527,22 @@ module ActiveRecord
# The chain is built by recursively calling #chain on the source reflection and the through
# reflection. The base case for the recursion is a normal association, which just returns
# [self] as its #chain.
+ #
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # 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>]
+ #
def chain
@chain ||= begin
- chain = source_reflection.chain + through_reflection.chain
+ a = source_reflection.chain
+ b = through_reflection.chain
+ chain = a + b
chain[0] = self # Use self so we don't lose the information from :source_type
chain
end
@@ -439,12 +552,12 @@ module ActiveRecord
#
# class Person
# has_many :articles
- # has_many :comment_tags, :through => :articles
+ # has_many :comment_tags, through: :articles
# end
#
# class Article
# has_many :comments
- # has_many :comment_tags, :through => :comments, :source => :tags
+ # has_many :comment_tags, through: :comments, source: :tags
# end
#
# class Comment
@@ -461,7 +574,7 @@ module ActiveRecord
# Add to it the scope from this reflection (if any)
scope_chain.first << scope if scope
- through_scope_chain = through_reflection.scope_chain
+ through_scope_chain = through_reflection.scope_chain.map(&:dup)
if options[:source_type]
through_scope_chain.first <<
@@ -480,7 +593,7 @@ module ActiveRecord
# A through association is nested if there would be more than one join table
def nested?
- chain.length > 2 || through_reflection.macro == :has_and_belongs_to_many
+ chain.length > 2
end
# We want to use the klass from this reflection, rather than just delegate straight to
@@ -489,20 +602,47 @@ module ActiveRecord
def association_primary_key(klass = nil)
# Get the "actual" source reflection if the immediate source reflection has a
# source reflection itself
- source_reflection = self.source_reflection
- while source_reflection.source_reflection
- source_reflection = source_reflection.source_reflection
- end
-
- source_reflection.options[:primary_key] || primary_key(klass || self.klass)
+ actual_source_reflection.options[:primary_key] || primary_key(klass || self.klass)
end
- # Gets an array of possible <tt>:through</tt> source reflection names:
+ # Gets an array of possible <tt>:through</tt> source reflection names in both singular and plural form.
#
- # [:singularized, :pluralized]
+ # class Post < ActiveRecord::Base
+ # has_many :taggings
+ # has_many :tags, through: :taggings
+ # end
+ #
+ # tags_reflection = Post.reflect_on_association(:tags)
+ # tags_reflection.source_reflection_names
+ # # => [:tag, :tags]
#
def source_reflection_names
- @source_reflection_names ||= (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }
+ (options[:source] ? [options[:source]] : [name.to_s.singularize, name]).collect { |n| n.to_sym }.uniq
+ end
+
+ def source_reflection_name # :nodoc:
+ return @source_reflection_name if @source_reflection_name
+
+ names = [name.to_s.singularize, name].collect { |n| n.to_sym }.uniq
+ names = names.find_all { |n|
+ through_reflection.klass.reflect_on_association(n)
+ }
+
+ if names.length > 1
+ example_options = options.dup
+ example_options[:source] = source_reflection_names.first
+ ActiveSupport::Deprecation.warn <<-eowarn
+Ambiguous source reflection for through association. Please specify a :source
+directive on your declaration like:
+
+ class #{active_record.name} < ActiveRecord::Base
+ #{macro} :#{name}, #{example_options}
+ end
+
+ eowarn
+ end
+
+ @source_reflection_name = names.first
end
def source_options
@@ -541,6 +681,12 @@ module ActiveRecord
check_validity_of_inverse!
end
+ protected
+
+ def actual_source_reflection # FIXME: this is a horrible name
+ source_reflection.actual_source_reflection
+ end
+
private
def derive_class_name
# get the class_name of the belongs_to association of the through reflection
diff --git a/activerecord/lib/active_record/relation.rb b/activerecord/lib/active_record/relation.rb
index ed80422336..60f2726a6e 100644
--- a/activerecord/lib/active_record/relation.rb
+++ b/activerecord/lib/active_record/relation.rb
@@ -10,28 +10,32 @@ module ActiveRecord
:extending]
SINGLE_VALUE_METHODS = [:limit, :offset, :lock, :readonly, :from, :reordering,
- :reverse_order, :uniq, :create_with]
+ :reverse_order, :distinct, :create_with, :uniq]
VALUE_METHODS = MULTI_VALUE_METHODS + SINGLE_VALUE_METHODS
include FinderMethods, Calculations, SpawnMethods, QueryMethods, Batches, Explain, Delegation
attr_reader :table, :klass, :loaded
- attr_accessor :default_scoped
alias :model :klass
alias :loaded? :loaded
- alias :default_scoped? :default_scoped
def initialize(klass, table, values = {})
- @klass = klass
- @table = table
- @values = values
- @implicit_readonly = nil
- @loaded = false
- @default_scoped = false
+ @klass = klass
+ @table = table
+ @values = values
+ @loaded = false
end
- def insert(values)
+ def initialize_copy(other)
+ # This method is a hot spot, so for now, use Hash[] to dup the hash.
+ # https://bugs.ruby-lang.org/issues/7166
+ @values = Hash[@values]
+ @values[:bind] = @values[:bind].dup if @values.key? :bind
+ reset
+ end
+
+ def insert(values) # :nodoc:
primary_key_value = nil
if primary_key && Hash === values
@@ -48,16 +52,7 @@ module ActiveRecord
im = arel.create_insert
im.into @table
- conn = @klass.connection
-
- substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
- binds = substitutes.map do |arel_attr, value|
- [@klass.columns_hash[arel_attr.name], value]
- end
-
- substitutes.each_with_index do |tuple, i|
- tuple[1] = conn.substitute_at(binds[i][0], i)
- end
+ substitutes, binds = substitute_values values
if values.empty? # empty insert
im.values = Arel.sql(connection.empty_insert_statement_value)
@@ -65,7 +60,7 @@ module ActiveRecord
im.insert substitutes
end
- conn.insert(
+ @klass.connection.insert(
im,
'SQL',
primary_key,
@@ -74,6 +69,29 @@ module ActiveRecord
binds)
end
+ 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.connection.update(
+ um,
+ 'SQL',
+ binds)
+ end
+
+ def substitute_values(values) # :nodoc:
+ substitutes = values.sort_by { |arel_attr,_| arel_attr.name }
+ binds = substitutes.map do |arel_attr, value|
+ [@klass.columns_hash[arel_attr.name], value]
+ end
+
+ substitutes.each_with_index do |tuple, i|
+ tuple[1] = @klass.connection.substitute_at(binds[i][0], i)
+ end
+
+ [substitutes, binds]
+ end
+
# Initializes new record from relation while maintaining the current
# scope.
#
@@ -90,12 +108,6 @@ module ActiveRecord
scoping { @klass.new(*args, &block) }
end
- def initialize_copy(other)
- @values = @values.dup
- @values[:bind] = @values[:bind].dup if @values[:bind]
- reset
- end
-
alias build new
# Tries to create a new record with the same scoped attributes
@@ -127,46 +139,77 @@ module ActiveRecord
scoping { @klass.create!(*args, &block) }
end
- # Tries to load the first record; if it fails, then <tt>create</tt> is called with the same arguments as this method.
- #
- # Expects arguments in the same format as +Base.create+.
+ def first_or_create(attributes = nil, &block) # :nodoc:
+ first || create(attributes, &block)
+ end
+
+ def first_or_create!(attributes = nil, &block) # :nodoc:
+ first || create!(attributes, &block)
+ end
+
+ def first_or_initialize(attributes = nil, &block) # :nodoc:
+ first || new(attributes, &block)
+ end
+
+ # Finds the first record with the given attributes, or creates a record
+ # with the attributes if one is not found:
#
- # ==== Examples
- # # Find the first user named Penélope or create a new one.
- # User.where(:first_name => 'Penélope').first_or_create
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # # Find the first user named "Penélope" or create a new one.
+ # User.find_or_create_by(first_name: 'Penélope')
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Penélope or create a new one.
+ # # Find the first user named "Penélope" or create a new one.
# # We already have one so the existing record will be returned.
- # User.where(:first_name => 'Penélope').first_or_create
- # # => <User id: 1, first_name: 'Penélope', last_name: nil>
+ # User.find_or_create_by(first_name: 'Penélope')
+ # # => #<User id: 1, first_name: "Penélope", last_name: nil>
#
- # # Find the first user named Scarlett or create a new one with a particular last name.
- # User.where(:first_name => 'Scarlett').first_or_create(:last_name => 'Johansson')
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
+ # # Find the first user named "Scarlett" or create a new one with
+ # # a particular last name.
+ # User.create_with(last_name: 'Johansson').find_or_create_by(first_name: 'Scarlett')
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
#
- # # Find the first user named Scarlett or create a new one with a different last name.
- # # We already have one so the existing record will be returned.
- # User.where(:first_name => 'Scarlett').first_or_create do |user|
- # user.last_name = "O'Hara"
+ # This method accepts a block, which is passed down to +create+. The last example
+ # above can be alternatively written this way:
+ #
+ # # Find the first user named "Scarlett" or create a new one with a
+ # # different last name.
+ # User.find_or_create_by(first_name: 'Scarlett') do |user|
+ # user.last_name = 'Johansson'
# end
- # # => <User id: 2, first_name: 'Scarlett', last_name: 'Johansson'>
- def first_or_create(attributes = nil, &block)
- first || create(attributes, &block)
+ # # => #<User id: 2, first_name: "Scarlett", last_name: "Johansson">
+ #
+ # This method always returns a record, but if creation was attempted and
+ # failed due to validation errors it won't be persisted, you get what
+ # +create+ returns in such situation.
+ #
+ # Please note *this method is not atomic*, it runs first a SELECT, and if
+ # there are no results an INSERT is attempted. If there are other threads
+ # or processes there is a race condition between both calls and it could
+ # be the case that you end up with two similar records.
+ #
+ # Whether that is a problem or not depends on the logic of the
+ # application, but in the particular case in which rows have a UNIQUE
+ # constraint an exception may be raised, just retry:
+ #
+ # begin
+ # CreditAccount.find_or_create_by(user_id: user.id)
+ # rescue ActiveRecord::RecordNotUnique
+ # retry
+ # end
+ #
+ def find_or_create_by(attributes, &block)
+ find_by(attributes) || create(attributes, &block)
end
- # Like <tt>first_or_create</tt> but calls <tt>create!</tt> so an exception is raised if the created record is invalid.
- #
- # Expects arguments in the same format as <tt>Base.create!</tt>.
- def first_or_create!(attributes = nil, &block)
- first || create!(attributes, &block)
+ # Like <tt>find_or_create_by</tt>, but calls <tt>create!</tt> so an exception
+ # is raised if the created record is invalid.
+ def find_or_create_by!(attributes, &block)
+ find_by(attributes) || create!(attributes, &block)
end
- # Like <tt>first_or_create</tt> but calls <tt>new</tt> instead of <tt>create</tt>.
- #
- # Expects arguments in the same format as <tt>Base.new</tt>.
- def first_or_initialize(attributes = nil, &block)
- first || new(attributes, &block)
+ # Like <tt>find_or_create_by</tt>, but calls <tt>new</tt> instead of <tt>create</tt>.
+ def find_or_initialize_by(attributes, &block)
+ find_by(attributes) || new(attributes, &block)
end
# Runs EXPLAIN on the query or queries triggered by this relation and
@@ -179,8 +222,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
- _, queries = collecting_queries_for_explain { exec_queries }
- exec_explain(queries)
+ exec_explain(collecting_queries_for_explain { exec_queries })
end
# Converts relation objects to Array.
@@ -202,8 +244,7 @@ module ActiveRecord
def empty?
return @records.empty? if loaded?
- c = count
- c.respond_to?(:zero?) ? c.zero? : c.empty?
+ limit_value == 0 ? true : !exists?
end
# Returns true if there are any records.
@@ -226,9 +267,10 @@ module ActiveRecord
# Scope all queries to the current scope.
#
- # Comment.where(:post_id => 1).scoping do
- # Comment.first # SELECT * FROM comments WHERE post_id = 1
+ # Comment.where(post_id: 1).scoping do
+ # Comment.first
# end
+ # # => SELECT "comments".* FROM "comments" WHERE "comments"."post_id" = 1 ORDER BY "comments"."id" ASC LIMIT 1
#
# Please check unscoped if you want to remove all previous scopes (including
# the default_scope) during the execution of a block.
@@ -257,7 +299,7 @@ module ActiveRecord
# Book.where('title LIKE ?', '%Rails%').update_all(author: 'David')
#
# # Update all books that match conditions, but limit it to 5 ordered by date
- # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(:author => 'David')
+ # Book.where('title LIKE ?', '%Rails%').order(:created_at).limit(5).update_all(author: 'David')
def update_all(updates)
raise ArgumentError, "Empty list of attributes to change" if updates.blank?
@@ -299,18 +341,16 @@ module ActiveRecord
id.map.with_index { |one_id, idx| update(one_id, attributes[idx]) }
else
object = find(id)
- object.update_attributes(attributes)
+ object.update(attributes)
object
end
end
# Destroys the records matching +conditions+ by instantiating each
# record and calling its +destroy+ method. Each object's callbacks are
- # executed (including <tt>:dependent</tt> association options and
- # +before_destroy+/+after_destroy+ Observer methods). Returns the
+ # executed (including <tt>:dependent</tt> association options). Returns the
# collection of objects that were destroyed; each will be frozen, to
- # reflect that no changes should be made (since they can't be
- # persisted).
+ # reflect that no changes should be made (since they can't be persisted).
#
# Note: Instantiation, callback execution, and deletion of each
# record can be time consuming when you're removing many records at
@@ -330,7 +370,7 @@ module ActiveRecord
#
# Person.destroy_all("last_login < '2004-04-04'")
# Person.destroy_all(status: "inactive")
- # Person.where(:age => 0..18).destroy_all
+ # Person.where(age: 0..18).destroy_all
def destroy_all(conditions = nil)
if conditions
where(conditions).destroy_all
@@ -375,7 +415,7 @@ module ActiveRecord
#
# Post.delete_all("person_id = 5 AND (category = 'Something' OR category = 'Else')")
# Post.delete_all(["person_id = ? AND (category = ? OR category = ?)", 5, 'Something', 'Else'])
- # Post.where(:person_id => 5).where(:category => ['Something', 'Else']).delete_all
+ # Post.where(person_id: 5).where(category: ['Something', 'Else']).delete_all
#
# Both calls delete the affected posts all at once with a single DELETE statement.
# If you need to destroy dependent associations or call your <tt>before_*</tt> or
@@ -410,8 +450,7 @@ module ActiveRecord
# Deletes the row with a primary key matching the +id+ argument, using a
# SQL +DELETE+ statement, and returns the number of rows deleted. Active
# Record objects are not instantiated, so the object's callbacks are not
- # executed, including any <tt>:dependent</tt> association options or
- # Observer methods.
+ # executed, including any <tt>:dependent</tt> association options.
#
# You can delete multiple rows at once by passing an Array of <tt>id</tt>s.
#
@@ -438,17 +477,7 @@ module ActiveRecord
#
# Post.where(published: true).load # => #<ActiveRecord::Relation>
def load
- unless loaded?
- # We monitor here the entire execution rather than individual SELECTs
- # because from the point of view of the user fetching the records of a
- # relation is a single unit of work. You want to know if this call takes
- # too long, not if the individual queries take too long.
- #
- # It could be the case that none of the queries involved surpass the
- # threshold, and at the same time the sum of them all does. The user
- # should get a query plan logged in that case.
- logging_query_plan { exec_queries }
- end
+ exec_queries unless loaded?
self
end
@@ -468,22 +497,37 @@ module ActiveRecord
# Returns sql statement for the relation.
#
- # Users.where(name: 'Oscar').to_sql
+ # User.where(name: 'Oscar').to_sql
# # => SELECT "users".* FROM "users" WHERE "users"."name" = 'Oscar'
def to_sql
- @to_sql ||= klass.connection.to_sql(arel, bind_values.dup)
+ @to_sql ||= begin
+ relation = self
+ connection = klass.connection
+ visitor = connection.visitor
+
+ if eager_loading?
+ find_with_associations { |rel| relation = rel }
+ end
+
+ ast = relation.arel.ast
+ binds = relation.bind_values.dup
+ visitor.accept(ast) do
+ connection.quote(*binds.shift.reverse)
+ end
+ end
end
- # Returns a hash of where conditions
+ # Returns a hash of where conditions.
#
- # Users.where(name: 'Oscar').where_values_hash
- # # => {:name=>"oscar"}
+ # User.where(name: 'Oscar').where_values_hash
+ # # => {name: "Oscar"}
def where_values_hash
- equalities = with_default_scope.where_values.grep(Arel::Nodes::Equality).find_all { |node|
+ equalities = where_values.grep(Arel::Nodes::Equality).find_all { |node|
node.left.relation.name == table_name
}
binds = Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }]
+ binds.merge!(Hash[bind_values.find_all(&:first).map { |column, v| [column.name, v] }])
Hash[equalities.map { |where|
name = where.left.name
@@ -505,11 +549,17 @@ module ActiveRecord
# Joins that are also marked for preloading. In which case we should just eager load them.
# Note that this is a naive implementation because we could have strings and symbols which
# represent the same association, but that aren't matched by this. Also, we could have
- # nested hashes which partially match, e.g. { :a => :b } & { :a => [:b, :c] }
+ # nested hashes which partially match, e.g. { a: :b } & { a: [:b, :c] }
def joined_includes_values
includes_values & joins_values
end
+ # +uniq+ and +uniq!+ are silently deprecated. +uniq_value+ delegates to +distinct_value+
+ # to maintain backwards compatibility. Use +distinct_value+ instead.
+ def uniq_value
+ distinct_value
+ end
+
# Compares two relations for equality.
def ==(other)
case other
@@ -524,23 +574,13 @@ module ActiveRecord
q.pp(self.to_a)
end
- def with_default_scope #:nodoc:
- if default_scoped? && default_scope = klass.send(:build_default_scope)
- default_scope = default_scope.merge(self)
- default_scope.default_scoped = false
- default_scope
- else
- self
- end
- end
-
# Returns true if relation is blank.
def blank?
to_a.blank?
end
def values
- @values.dup
+ Hash[@values]
end
def inspect
@@ -553,34 +593,24 @@ module ActiveRecord
private
def exec_queries
- default_scoped = with_default_scope
-
- if default_scoped.equal?(self)
- @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
+ @records = eager_loading? ? find_with_associations : @klass.find_by_sql(arel, bind_values)
- preload = preload_values
- preload += includes_values unless eager_loading?
- preload.each do |associations|
- ActiveRecord::Associations::Preloader.new(@records, associations).run
- end
-
- # @readonly_value is true only if set explicitly. @implicit_readonly is true if there
- # are JOINS and no explicit SELECT.
- readonly = readonly_value.nil? ? @implicit_readonly : readonly_value
- @records.each { |record| record.readonly! } if readonly
- else
- @records = default_scoped.to_a
+ preload = preload_values
+ preload += includes_values unless eager_loading?
+ preloader = ActiveRecord::Associations::Preloader.new
+ preload.each do |associations|
+ preloader.preload @records, associations
end
+ @records.each { |record| record.readonly! } if readonly_value
+
@loaded = true
@records
end
def references_eager_loaded_tables?
joined_tables = arel.join_sources.map do |join|
- if join.is_a?(Arel::Nodes::StringJoin)
- tables_in_string(join.left)
- else
+ unless join.is_a?(Arel::Nodes::StringJoin)
[join.left.table_name, join.left.table_alias]
end
end
@@ -589,37 +619,8 @@ module ActiveRecord
# always convert table names to downcase as in Oracle quoted table names are in uppercase
joined_tables = joined_tables.flatten.compact.map { |t| t.downcase }.uniq
- string_tables = tables_in_string(to_sql)
-
- if (references_values - joined_tables).any?
- true
- elsif (string_tables - joined_tables).any?
- ActiveSupport::Deprecation.warn(
- "It looks like you are eager loading table(s) (one of: #{string_tables.join(', ')}) " \
- "that are referenced in a string SQL snippet. For example: \n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\")\n" \
- "\n" \
- "Currently, Active Record recognises the table in the string, and knows to JOIN the " \
- "comments table to the query, rather than loading comments in a separate query. " \
- "However, doing this without writing a full-blown SQL parser is inherently flawed. " \
- "Since we don't want to write an SQL parser, we are removing this functionality. " \
- "From now on, you must explicitly tell Active Record when you are referencing a table " \
- "from a string:\n" \
- "\n" \
- " Post.includes(:comments).where(\"comments.title = 'foo'\").references(:comments)\n\n"
- )
- true
- else
- false
- end
- 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_']
+ (references_values - joined_tables).any?
end
end
end
diff --git a/activerecord/lib/active_record/relation/batches.rb b/activerecord/lib/active_record/relation/batches.rb
index 8af0c6a8ef..49b01909c6 100644
--- a/activerecord/lib/active_record/relation/batches.rb
+++ b/activerecord/lib/active_record/relation/batches.rb
@@ -11,7 +11,7 @@ module ActiveRecord
# The #find_each method uses #find_in_batches with a batch size of 1000 (or as
# specified by the +:batch_size+ option).
#
- # Person.all.find_each do |person|
+ # Person.find_each do |person|
# person.do_awesome_stuff
# end
#
@@ -19,54 +19,85 @@ module ActiveRecord
# person.party_all_night!
# end
#
- # You can also pass the +:start+ option to specify
- # an offset to control the starting point.
- def find_each(options = {})
- find_in_batches(options) do |records|
- records.each { |record| yield record }
- end
- end
-
- # Yields each batch of records that was found by the find +options+ as
- # an array. The size of each batch is set by the +:batch_size+
- # option; the default is 1000.
+ # If you do not provide a block to #find_each, it will return an Enumerator
+ # for chaining with other methods:
+ #
+ # Person.find_each.with_index do |person, index|
+ # person.award_trophy(index + 1)
+ # end
+ #
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
#
- # You can control the starting point for the batch processing by
- # supplying the +:start+ option. This is especially useful if you
- # want multiple workers dealing with the same processing queue. You can
- # make worker 1 handle all the records between id 0 and 10,000 and
- # worker 2 handle from 10,000 and beyond (by setting the +:start+
- # option on that worker).
+ # # Let's process for a batch of 2000 records, skipping the first 2000 rows
+ # Person.find_each(start: 2000, batch_size: 2000) do |person|
+ # person.party_all_night!
+ # end
#
- # It's not possible to set the order. That is automatically set to
+ # NOTE: It's not possible to set the order. That is automatically set to
# ascending on the primary key ("id ASC") to make the batch ordering
# work. This also means that this method only works with integer-based
- # primary keys. You can't set the limit either, that's used to control
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
# the batch sizes.
+ def find_each(options = {})
+ if block_given?
+ find_in_batches(options) do |records|
+ records.each { |record| yield record }
+ end
+ else
+ enum_for :find_each, options
+ end
+ end
+
+ # Yields each batch of records that was found by the find +options+ as
+ # an array.
#
# Person.where("age > 21").find_in_batches do |group|
# sleep(50) # Make sure it doesn't get too crowded in there!
# group.each { |person| person.party_all_night! }
# end
#
+ # ==== Options
+ # * <tt>:batch_size</tt> - Specifies the size of the batch. Default to 1000.
+ # * <tt>:start</tt> - Specifies the starting point for the batch processing.
+ # This is especially useful if you want multiple workers dealing with
+ # the same processing queue. You can make worker 1 handle all the records
+ # between id 0 and 10,000 and worker 2 handle from 10,000 and beyond
+ # (by setting the +:start+ option on that worker).
+ #
# # Let's process the next 2000 records
- # Person.all.find_in_batches(start: 2000, batch_size: 2000) do |group|
+ # Person.find_in_batches(start: 2000, batch_size: 2000) do |group|
# group.each { |person| person.party_all_night! }
# end
+ #
+ # NOTE: It's not possible to set the order. That is automatically set to
+ # ascending on the primary key ("id ASC") to make the batch ordering
+ # work. This also means that this method only works with integer-based
+ # primary keys.
+ #
+ # NOTE: You can't set the limit either, that's used to control
+ # the batch sizes.
def find_in_batches(options = {})
options.assert_valid_keys(:start, :batch_size)
relation = self
- unless arel.orders.blank? && arel.taken.blank?
- ActiveRecord::Base.logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
+ if logger && (arel.orders.present? || arel.taken.present?)
+ logger.warn("Scoped order and limit are ignored, it's forced to be batch order and batch size")
end
- start = options.delete(:start) || 0
+ start = options.delete(:start)
batch_size = options.delete(:batch_size) || 1000
relation = relation.reorder(batch_order).limit(batch_size)
- records = relation.where(table[primary_key].gteq(start)).to_a
+ records = start ? relation.where(table[primary_key].gteq(start)).to_a : relation.to_a
while records.any?
records_size = records.size
diff --git a/activerecord/lib/active_record/relation/calculations.rb b/activerecord/lib/active_record/relation/calculations.rb
index 7c43d844d0..2d267183ce 100644
--- a/activerecord/lib/active_record/relation/calculations.rb
+++ b/activerecord/lib/active_record/relation/calculations.rb
@@ -1,5 +1,3 @@
-require 'active_support/core_ext/object/try'
-
module ActiveRecord
module Calculations
# Count the records.
@@ -13,61 +11,51 @@ module ActiveRecord
# Person.count(:all)
# # => performs a COUNT(*) (:all is an alias for '*')
#
- # Person.count(:age, distinct: true)
+ # Person.distinct.count(:age)
# # => counts the number of different age values
#
- # Person.where("age > 26").count { |person| person.gender == 'female' }
- # # => queries people where "age > 26" then count the loaded results filtering by gender
- def count(column_name = nil, options = {})
- if block_given?
- self.to_a.count { |item| yield item }
- else
- column_name, options = nil, column_name if column_name.is_a?(Hash)
- calculate(:count, column_name, options)
- end
+ # If +count+ is used with +group+, it returns a Hash whose keys represent the aggregated column,
+ # and the values are the respective amounts:
+ #
+ # Person.group(:city).count
+ # # => { 'Rome' => 5, 'Paris' => 3 }
+ def count(column_name = nil)
+ calculate(:count, column_name)
end
# Calculates the average value on a given column. Returns +nil+ if there's
# no row. See +calculate+ for examples with options.
#
- # Person.average('age') # => 35.8
- def average(column_name, options = {})
- calculate(:average, column_name, options)
+ # Person.average(:age) # => 35.8
+ def average(column_name)
+ calculate(:average, column_name)
end
# Calculates the minimum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.minimum('age') # => 7
- def minimum(column_name, options = {})
- calculate(:minimum, column_name, options)
+ # Person.minimum(:age) # => 7
+ def minimum(column_name)
+ calculate(:minimum, column_name)
end
# Calculates the maximum value on a given column. The value is returned
# with the same data type of the column, or +nil+ if there's no row. See
# +calculate+ for examples with options.
#
- # Person.maximum('age') # => 93
- def maximum(column_name, options = {})
- calculate(:maximum, column_name, options)
+ # Person.maximum(:age) # => 93
+ def maximum(column_name)
+ calculate(:maximum, column_name)
end
# Calculates the sum of values on a given column. The value is returned
# with the same data type of the column, 0 if there's no row. See
# +calculate+ for examples with options.
#
- # Person.sum('age') # => 4562
- # # => returns the total sum of all people's age
- #
- # Person.where('age > 100').sum { |person| person.age - 100 }
- # # queries people where "age > 100" then perform a sum calculation with the block returns
+ # Person.sum(:age) # => 4562
def sum(*args)
- if block_given?
- self.to_a.sum(*args) { |item| yield item }
- else
- calculate(:sum, *args)
- end
+ calculate(:sum, *args)
end
# This calculates aggregate values in the given column. Methods for count, sum, average,
@@ -83,18 +71,17 @@ module ActiveRecord
#
# values = Person.group('last_name').maximum(:age)
# puts values["Drake"]
- # => 43
+ # # => 43
#
- # drake = Family.find_by_last_name('Drake')
+ # drake = Family.find_by(last_name: 'Drake')
# values = Person.group(:family).maximum(:age) # Person belongs_to :family
# puts values[drake]
- # => 43
+ # # => 43
#
# values.each do |family, max_age|
# ...
# end
#
- # Examples:
# Person.calculate(:count, :all) # The same as Person.count
# Person.average(:age) # SELECT AVG(age) FROM people...
#
@@ -102,24 +89,20 @@ module ActiveRecord
# Person.group(:last_name).having("min(age) > 17").minimum(:age)
#
# Person.sum("2 * age")
- def calculate(operation, column_name, options = {})
- relation = with_default_scope
+ def calculate(operation, column_name)
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ column_name = attribute_alias(column_name)
+ end
- if relation.equal?(self)
- if has_include?(column_name)
- construct_relation_for_association_calculations.calculate(operation, column_name, options)
- else
- perform_calculation(operation, column_name, options)
- end
+ if has_include?(column_name)
+ construct_relation_for_association_calculations.calculate(operation, column_name)
else
- relation.calculate(operation, column_name, options)
+ perform_calculation(operation, column_name)
end
- rescue ThrowResult
- 0
end
- # Use <tt>pluck</tt> as a shortcut to select a single attribute without
- # loading a bunch of records just to grab one attribute you want.
+ # Use <tt>pluck</tt> as a shortcut to select one or more attributes without
+ # loading a bunch of records just to grab the attributes you want.
#
# Person.pluck(:name)
#
@@ -128,11 +111,9 @@ module ActiveRecord
# Person.all.map(&:name)
#
# Pluck returns an <tt>Array</tt> of attribute values type-casted to match
- # the plucked column name, if it can be deduced. Plucking an SQL fragment
+ # the plucked column names, if they can be deduced. Plucking an SQL fragment
# returns String values by default.
#
- # Examples:
- #
# Person.pluck(:id)
# # SELECT people.id FROM people
# # => [1, 2, 3]
@@ -141,11 +122,11 @@ module ActiveRecord
# # SELECT people.id, people.name FROM people
# # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
#
- # Person.uniq.pluck(:role)
+ # Person.pluck('DISTINCT role')
# # SELECT DISTINCT role FROM people
# # => ['admin', 'member', 'guest']
#
- # Person.where(:age => 21).limit(5).pluck(:id)
+ # Person.where(age: 21).limit(5).pluck(:id)
# # SELECT people.id FROM people WHERE people.age = 21 LIMIT 5
# # => [2, 3]
#
@@ -155,31 +136,31 @@ module ActiveRecord
#
def pluck(*column_names)
column_names.map! do |column_name|
- if column_name.is_a?(Symbol) && self.column_names.include?(column_name.to_s)
- "#{connection.quote_table_name(table_name)}.#{connection.quote_column_name(column_name)}"
+ if column_name.is_a?(Symbol) && attribute_alias?(column_name)
+ attribute_alias(column_name)
else
- column_name
+ column_name.to_s
end
end
if has_include?(column_names.first)
construct_relation_for_association_calculations.pluck(*column_names)
else
- result = klass.connection.select_all(select(column_names).arel, nil, bind_values)
+ relation = spawn
+ relation.select_values = column_names.map { |cn|
+ columns_hash.key?(cn) ? arel_table[cn] : cn
+ }
+ result = klass.connection.select_all(relation.arel, nil, bind_values)
columns = result.columns.map do |key|
klass.column_types.fetch(key) {
- result.column_types.fetch(key) {
- Class.new { def type_cast(v); v; end }.new
- }
+ result.column_types.fetch(key) { result.identity_type }
}
end
result = result.map do |attributes|
values = klass.initialize_attributes(attributes).values
- columns.zip(values).map do |column, value|
- column.type_cast(value)
- end
+ columns.zip(values).map { |column, value| column.type_cast value }
end
columns.one? ? result.map!(&:first) : result
end
@@ -187,8 +168,6 @@ module ActiveRecord
# Pluck all the ID's for the relation using the table's primary key
#
- # Examples:
- #
# Person.ids # SELECT people.id FROM people
# Person.joins(:companies).ids # SELECT people.id FROM people INNER JOIN companies ON companies.person_id = people.id
def ids
@@ -201,21 +180,21 @@ module ActiveRecord
eager_loading? || (includes_values.present? && (column_name || references_eager_loaded_tables?))
end
- def perform_calculation(operation, column_name, options = {})
+ def perform_calculation(operation, column_name)
operation = operation.to_s.downcase
- distinct = options[:distinct]
+ # If #count is used with #distinct / #uniq it is considered distinct. (eg. relation.distinct.count)
+ distinct = self.distinct_value
if operation == "count"
- column_name ||= (select_for_count || :all)
+ column_name ||= select_for_count
unless arel.ast.grep(Arel::Nodes::OuterJoin).empty?
distinct = true
end
column_name = primary_key if column_name == :all && distinct
-
- distinct = nil if column_name =~ /\s*DISTINCT\s+/i
+ distinct = nil if column_name =~ /\s*DISTINCT[\s(]+/i
end
if group_values.any?
@@ -241,6 +220,8 @@ module ActiveRecord
# Postgresql doesn't like ORDER BY when there are no GROUP BY
relation = reorder(nil)
+ column_alias = column_name
+
if operation == "count" && (relation.limit_value || relation.offset_value)
# Shortcut when limit is zero.
return 0 if relation.limit_value == 0
@@ -251,13 +232,20 @@ module ActiveRecord
select_value = operation_over_aggregate_column(column, operation, distinct)
+ column_alias = select_value.alias
relation.select_values = [select_value]
query_builder = relation.arel
end
- result = @klass.connection.select_value(query_builder, nil, relation.bind_values)
- type_cast_calculated_value(result, column_for(column_name), operation)
+ result = @klass.connection.select_all(query_builder, nil, relation.bind_values)
+ row = result.first
+ value = row && row.values.first
+ column = result.column_types.fetch(column_alias) do
+ column_for(column_name)
+ end
+
+ type_cast_calculated_value(value, column, operation)
end
def execute_grouped_calculation(operation, column_name, distinct) #:nodoc:
@@ -271,17 +259,19 @@ module ActiveRecord
group_fields = group_attrs
end
- group_aliases = group_fields.map { |field| column_alias_for(field) }
+ group_aliases = group_fields.map { |field|
+ column_alias_for(field)
+ }
group_columns = group_aliases.zip(group_fields).map { |aliaz,field|
- [aliaz, column_for(field)]
+ [aliaz, field]
}
- group = @klass.connection.adapter_name == 'FrontBase' ? group_aliases : group_fields
+ group = group_fields
if operation == 'count' && column_name == :all
aggregate_alias = 'count_all'
else
- aggregate_alias = column_alias_for(operation, column_name)
+ aggregate_alias = column_alias_for([operation, column_name].join(' '))
end
select_values = [
@@ -300,7 +290,8 @@ module ActiveRecord
end
}
- relation = except(:group).group(group)
+ relation = except(:group)
+ relation.group_values = group
relation.select_values = select_values
calculated_data = @klass.connection.select_all(relation, nil, bind_values)
@@ -312,7 +303,10 @@ module ActiveRecord
end
Hash[calculated_data.map do |row|
- key = group_columns.map { |aliaz, column|
+ key = group_columns.map { |aliaz, col_name|
+ column = calculated_data.column_types.fetch(aliaz) do
+ column_for(col_name)
+ end
type_cast_calculated_value(row[aliaz], column)
}
key = key.first if key.size == 1
@@ -329,10 +323,12 @@ module ActiveRecord
# column_alias_for("count(distinct users.id)") # => "count_distinct_users_id"
# column_alias_for("count(*)") # => "count_all"
# column_alias_for("count", "id") # => "count_id"
- def column_alias_for(*keys)
- keys.map! {|k| k.respond_to?(:to_sql) ? k.to_sql : k}
- table_name = keys.join(' ')
- table_name.downcase!
+ def column_alias_for(keys)
+ if keys.respond_to? :name
+ keys = "#{keys.relation.name}.#{keys.name}"
+ end
+
+ table_name = keys.to_s.downcase
table_name.gsub!(/\*/, 'all')
table_name.gsub!(/\W+/, ' ')
table_name.strip!
@@ -343,13 +339,13 @@ module ActiveRecord
def column_for(field)
field_name = field.respond_to?(:name) ? field.name.to_s : field.to_s.split('.').last
- @klass.columns.detect { |c| c.name.to_s == field_name }
+ @klass.columns_hash[field_name]
end
def type_cast_calculated_value(value, column, operation = nil)
case operation
when 'count' then value.to_i
- when 'sum' then type_cast_using_column(value || '0', column)
+ when 'sum' then type_cast_using_column(value || 0, column)
when 'average' then value.respond_to?(:to_d) ? value.to_d : value
else type_cast_using_column(value, column)
end
@@ -359,10 +355,12 @@ module ActiveRecord
column ? column.type_cast(value) : value
end
+ # TODO: refactor to allow non-string `select_values` (eg. Arel nodes).
def select_for_count
if select_values.present?
- select = select_values.join(", ")
- select if select !~ /[,*]/
+ select_values.join(", ")
+ else
+ :all
end
end
diff --git a/activerecord/lib/active_record/relation/delegation.rb b/activerecord/lib/active_record/relation/delegation.rb
index ab8b36c8ab..1e15bddcdf 100644
--- a/activerecord/lib/active_record/relation/delegation.rb
+++ b/activerecord/lib/active_record/relation/delegation.rb
@@ -1,44 +1,137 @@
+require 'active_support/concern'
+require 'active_support/deprecation'
module ActiveRecord
module Delegation # :nodoc:
- # Set up common delegations for performance (avoids method_missing)
+ module DelegateCache
+ def relation_delegate_class(klass) # :nodoc:
+ @relation_delegate_cache[klass]
+ end
+
+ def initialize_relation_delegate_cache # :nodoc:
+ @relation_delegate_cache = cache = {}
+ [
+ ActiveRecord::Relation,
+ ActiveRecord::Associations::CollectionProxy,
+ ActiveRecord::AssociationRelation
+ ].each do |klass|
+ delegate = Class.new(klass) {
+ include ClassSpecificRelation
+ }
+ const_set klass.name.gsub('::', '_'), delegate
+ cache[klass] = delegate
+ end
+ end
+
+ def inherited(child_class)
+ child_class.initialize_relation_delegate_cache
+ super
+ end
+ end
+
+ extend ActiveSupport::Concern
+
+ # This module creates compiled delegation methods dynamically at runtime, which makes
+ # subsequent calls to that method faster by avoiding method_missing. The delegations
+ # may vary depending on the klass of a relation, so we create a subclass of Relation
+ # for each different klass, and the delegations are compiled into that subclass only.
+
delegate :to_xml, :to_yaml, :length, :collect, :map, :each, :all?, :include?, :to_ary, :to => :to_a
delegate :table_name, :quoted_table_name, :primary_key, :quoted_primary_key,
- :connection, :columns_hash, :auto_explain_threshold_in_seconds, :to => :klass
+ :connection, :columns_hash, :to => :klass
+
+ module ClassSpecificRelation # :nodoc:
+ extend ActiveSupport::Concern
+
+ included do
+ @delegation_mutex = Mutex.new
+ end
+
+ module ClassMethods # :nodoc:
+ def name
+ superclass.name
+ end
+
+ def delegate_to_scoped_klass(method)
+ @delegation_mutex.synchronize do
+ return if method_defined?(method)
- def self.delegate_to_scoped_klass(method)
- if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.#{method}(*args, &block) }
+ if method.to_s =~ /\A[a-zA-Z_]\w*[!?]?\z/
+ module_eval <<-RUBY, __FILE__, __LINE__ + 1
+ def #{method}(*args, &block)
+ scoping { @klass.#{method}(*args, &block) }
+ end
+ RUBY
+ else
+ define_method method do |*args, &block|
+ scoping { @klass.send(method, *args, &block) }
+ end
+ end
end
- RUBY
- else
- module_eval <<-RUBY, __FILE__, __LINE__ + 1
- def #{method}(*args, &block)
- scoping { @klass.send(#{method.inspect}, *args, &block) }
+ end
+
+ def delegate(method, opts = {})
+ @delegation_mutex.synchronize do
+ return if method_defined?(method)
+ super
end
- RUBY
+ end
+ end
+
+ protected
+
+ def method_missing(method, *args, &block)
+ if @klass.respond_to?(method)
+ self.class.delegate_to_scoped_klass(method)
+ scoping { @klass.send(method, *args, &block) }
+ elsif array_delegable?(method)
+ self.class.delegate method, :to => :to_a
+ to_a.send(method, *args, &block)
+ elsif arel.respond_to?(method)
+ self.class.delegate method, :to => :arel
+ arel.send(method, *args, &block)
+ else
+ super
+ end
+ end
+ end
+
+ module ClassMethods # :nodoc:
+ def create(klass, *args)
+ relation_class_for(klass).new(klass, *args)
+ end
+
+ private
+
+ def relation_class_for(klass)
+ klass.relation_delegate_class(self)
end
end
def respond_to?(method, include_private = false)
- super || Array.method_defined?(method) ||
+ super || array_delegable?(method) ||
@klass.respond_to?(method, include_private) ||
arel.respond_to?(method, include_private)
end
protected
+ def array_delegable?(method)
+ defined = Array.method_defined?(method)
+ if defined && method.to_s.ends_with?('!')
+ ActiveSupport::Deprecation.warn(
+ "Association will no longer delegate #{method} to #to_a as of Rails 4.2. You instead must first call #to_a on the association to expose the array to be acted on."
+ )
+ end
+ defined
+ end
+
def method_missing(method, *args, &block)
if @klass.respond_to?(method)
- ::ActiveRecord::Delegation.delegate_to_scoped_klass(method)
scoping { @klass.send(method, *args, &block) }
- elsif Array.method_defined?(method)
- ::ActiveRecord::Delegation.delegate method, :to => :to_a
+ elsif array_delegable?(method)
to_a.send(method, *args, &block)
elsif arel.respond_to?(method)
- ::ActiveRecord::Delegation.delegate method, :to => :arel
arel.send(method, *args, &block)
else
super
diff --git a/activerecord/lib/active_record/relation/finder_methods.rb b/activerecord/lib/active_record/relation/finder_methods.rb
index 84aaa39fed..3a02bf90e9 100644
--- a/activerecord/lib/active_record/relation/finder_methods.rb
+++ b/activerecord/lib/active_record/relation/finder_methods.rb
@@ -1,7 +1,7 @@
-require 'active_support/core_ext/hash/indifferent_access'
-
module ActiveRecord
module FinderMethods
+ ONE_AS_ONE = '1 AS one'
+
# Find by id - This can either be a specific id (1), a list of ids (1, 5, 6), or an array of ids ([5, 6, 10]).
# If no record can be found for all of the listed ids, then RecordNotFound will be raised. If the primary key
# is an integer, find by id coerces its arguments using +to_i+.
@@ -13,9 +13,11 @@ module ActiveRecord
# Person.find([1]) # returns an array for the object with ID = 1
# Person.where("administrator = 1").order("created_on DESC").find(1)
#
- # Note that returned records may not be in the same order as the ids you
- # provide since database rows are unordered. Give an explicit <tt>order</tt>
- # to ensure the results are sorted.
+ # <tt>ActiveRecord::RecordNotFound</tt> will be raised if one or more ids are not found.
+ #
+ # NOTE: The returned records may not be in the same order as the ids you
+ # provide since database rows are unordered. You'd need to provide an explicit <tt>order</tt>
+ # option if you want the results are sorted.
#
# ==== Find with lock
#
@@ -30,6 +32,34 @@ module ActiveRecord
# person.visits += 1
# person.save!
# end
+ #
+ # ==== Variations of +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4)
+ # # returns a chainable list (which can be empty).
+ #
+ # Person.find_by(name: 'Spartacus', rating: 4)
+ # # returns the first item or nil.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_initialize
+ # # returns the first item or returns a new instance (requires you call .save to persist against the database).
+ #
+ # Person.where(name: 'Spartacus', rating: 4).first_or_create
+ # # returns the first item or creates it and returns it, available since Rails 3.2.1.
+ #
+ # ==== Alternatives for +find+
+ #
+ # Person.where(name: 'Spartacus', rating: 4).exists?(conditions = :none)
+ # # returns a boolean indicating if any record with the given conditions exist.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).select("field1, field2, field3")
+ # # returns a chainable list of instances with only the mentioned fields.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).ids
+ # # returns an Array of ids, available since Rails 3.2.1.
+ #
+ # Person.where(name: 'Spartacus', rating: 4).pluck(:field1, :field2)
+ # # 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) }
@@ -39,7 +69,7 @@ module ActiveRecord
end
# Finds the first record matching the specified conditions. There
- # is no implied ording so if order matters, you should specify it
+ # is no implied ordering so if order matters, you should specify it
# yourself.
#
# If no record is found, returns <tt>nil</tt>.
@@ -60,7 +90,7 @@ module ActiveRecord
# order. The order will depend on the database implementation.
# If an order is supplied it will be respected.
#
- # Person.take # returns an object fetched by SELECT * FROM people
+ # Person.take # returns an object fetched by SELECT * FROM people LIMIT 1
# Person.take(5) # returns 5 objects fetched by SELECT * FROM people LIMIT 5
# Person.where(["name LIKE '%?'", name]).take
def take(limit = nil)
@@ -78,16 +108,25 @@ module ActiveRecord
#
# Person.first # returns the first object fetched by SELECT * FROM people
# Person.where(["user_name = ?", user_name]).first
- # Person.where(["user_name = :u", { :u => user_name }]).first
+ # Person.where(["user_name = :u", { u: user_name }]).first
# Person.order("created_on DESC").offset(5).first
# Person.first(3) # returns the first three objects fetched by SELECT * FROM people LIMIT 3
+ #
+ # ==== Rails 3
+ #
+ # Person.first # SELECT "people".* FROM "people" LIMIT 1
+ #
+ # NOTE: Rails 3 may not order this query by the primary key and the order
+ # will depend on the database implementation. In order to ensure that behavior,
+ # use <tt>User.order(:id).first</tt> instead.
+ #
+ # ==== Rails 4
+ #
+ # Person.first # SELECT "people".* FROM "people" ORDER BY "people"."id" ASC LIMIT 1
+ #
def first(limit = nil)
if limit
- if order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(limit).to_a
- else
- limit(limit).to_a
- end
+ find_first_with_limit(limit)
else
find_first
end
@@ -139,14 +178,14 @@ module ActiveRecord
# * String - Finds the record with a primary key corresponding to this
# string (such as <tt>'5'</tt>).
# * Array - Finds the record that matches these +find+-style conditions
- # (such as <tt>['color = ?', 'red']</tt>).
+ # (such as <tt>['name LIKE ?', "%#{query}%"]</tt>).
# * Hash - Finds the record that matches these +find+-style conditions
- # (such as <tt>{color: 'red'}</tt>).
+ # (such as <tt>{name: 'David'}</tt>).
# * +false+ - Returns always +false+.
# * No args - Returns +false+ if the table is empty, +true+ otherwise.
#
- # For more information about specifying conditions as a Hash or Array,
- # see the Conditions section in the introduction to ActiveRecord::Base.
+ # For more information about specifying conditions as a hash or array,
+ # see the Conditions section in the introduction to <tt>ActiveRecord::Base</tt>.
#
# Note: You can't pass in a condition as a string (like <tt>name =
# 'Jamie'</tt>), since it would be sanitized and then queried against
@@ -159,12 +198,13 @@ module ActiveRecord
# Person.exists?(false)
# Person.exists?
def exists?(conditions = :none)
- conditions = conditions.id if ActiveRecord::Model === conditions
+ conditions = conditions.id if Base === conditions
return false if !conditions
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- relation = relation.except(:select, :order).select("1 AS one").limit(1)
+ relation = apply_join_dependency(self, construct_join_dependency)
+ return false if ActiveRecord::NullRelation === relation
+
+ relation = relation.except(:select, :order).select(ONE_AS_ONE).limit(1)
case conditions
when Array, Hash
@@ -173,68 +213,94 @@ module ActiveRecord
relation = relation.where(table[primary_key].eq(conditions)) if conditions != :none
end
- connection.select_value(relation, "#{name} Exists", relation.bind_values)
- rescue ThrowResult
- false
+ connection.select_value(relation, "#{name} Exists", relation.bind_values) ? true : false
end
- protected
+ # This method is called whenever no records are found with either a single
+ # id or multiple ids and raises a +ActiveRecord::RecordNotFound+ exception.
+ #
+ # The error message is different depending on whether a single id or
+ # multiple ids are provided. If multiple ids are provided, then the number
+ # of results obtained should be provided in the +result_size+ argument and
+ # the expected number of results should be provided in the +expected_size+
+ # argument.
+ def raise_record_not_found_exception!(ids, result_size, expected_size) #:nodoc:
+ conditions = arel.where_sql
+ conditions = " [#{conditions}]" if conditions
+
+ if Array(ids).size == 1
+ error = "Couldn't find #{@klass.name} with #{primary_key}=#{ids}#{conditions}"
+ else
+ error = "Couldn't find all #{@klass.name.pluralize} with IDs "
+ error << "(#{ids.join(", ")})#{conditions} (found #{result_size} results, but was looking for #{expected_size})"
+ end
- def find_with_associations
- join_dependency = construct_join_dependency_for_association_find
- relation = construct_relation_for_association_find(join_dependency)
- rows = connection.select_all(relation, 'SQL', relation.bind_values.dup)
- join_dependency.instantiate(rows)
- rescue ThrowResult
- []
+ raise RecordNotFound, error
end
- def construct_join_dependency_for_association_find
- including = (eager_load_values + includes_values).uniq
- ActiveRecord::Associations::JoinDependency.new(@klass, including, [])
+ private
+
+ def find_with_associations
+ join_dependency = construct_join_dependency
+
+ aliases = join_dependency.aliases
+ relation = select aliases.columns
+ relation = apply_join_dependency(relation, join_dependency)
+
+ if block_given?
+ yield relation
+ else
+ if ActiveRecord::NullRelation === relation
+ []
+ else
+ rows = connection.select_all(relation.arel, 'SQL', relation.bind_values.dup)
+ join_dependency.instantiate(rows, aliases)
+ end
+ end
end
- def construct_relation_for_association_calculations
- including = (eager_load_values + includes_values).uniq
- join_dependency = ActiveRecord::Associations::JoinDependency.new(@klass, including, arel.froms.first)
- relation = except(:includes, :eager_load, :preload)
- apply_join_dependency(relation, join_dependency)
+ def construct_join_dependency(joins = [])
+ including = eager_load_values + includes_values
+ ActiveRecord::Associations::JoinDependency.new(@klass, including, joins)
end
- def construct_relation_for_association_find(join_dependency)
- relation = except(:includes, :eager_load, :preload, :select).select(join_dependency.columns)
- apply_join_dependency(relation, join_dependency)
+ def construct_relation_for_association_calculations
+ apply_join_dependency(self, construct_join_dependency(arel.froms.first))
end
def apply_join_dependency(relation, join_dependency)
- join_dependency.join_associations.each do |association|
- relation = association.join_relation(relation)
- end
+ relation = relation.except(:includes, :eager_load, :preload)
+ relation = relation.joins join_dependency
- limitable_reflections = using_limitable_reflections?(join_dependency.reflections)
-
- if !limitable_reflections && relation.limit_value
- limited_id_condition = construct_limited_ids_condition(relation.except(:select))
- relation = relation.where(limited_id_condition)
+ if using_limitable_reflections?(join_dependency.reflections)
+ relation
+ else
+ if relation.limit_value
+ limited_ids = limited_ids_for(relation)
+ limited_ids.empty? ? relation.none! : relation.where!(table[primary_key].in(limited_ids))
+ end
+ relation.except(:limit, :offset)
end
-
- relation = relation.except(:limit, :offset) unless limitable_reflections
-
- relation
end
- def construct_limited_ids_condition(relation)
- orders = relation.order_values.map { |val| val.presence }.compact
- values = @klass.connection.distinct("#{@klass.connection.quote_table_name table_name}.#{primary_key}", orders)
+ def limited_ids_for(relation)
+ values = @klass.connection.columns_for_distinct(
+ "#{quoted_table_name}.#{quoted_primary_key}", relation.order_values)
+
+ relation = relation.except(:select).select(values).distinct!
- relation = relation.dup
+ id_rows = @klass.connection.select_all(relation.arel, 'SQL', relation.bind_values)
+ id_rows.map {|row| row[primary_key]}
+ end
- ids_array = relation.select(values).collect {|row| row[primary_key]}
- ids_array.empty? ? raise(ThrowResult) : table[primary_key].in(ids_array)
+ def using_limitable_reflections?(reflections)
+ reflections.none? { |r| r.collection? }
end
+ protected
+
def find_with_ids(*ids)
- return to_a.find { |*block_args| yield(*block_args) } if block_given?
+ raise UnknownPrimaryKey.new(@klass) if primary_key.nil?
expects_array = ids.first.kind_of?(Array)
return ids.first if expects_array && ids.first.empty?
@@ -261,11 +327,7 @@ module ActiveRecord
relation.bind_values += [[column, id]]
record = relation.take
- unless record
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
- raise RecordNotFound, "Couldn't find #{@klass.name} with #{primary_key}=#{id}#{conditions}"
- end
+ raise_record_not_found_exception!(id, 0, 1) unless record
record
end
@@ -288,12 +350,7 @@ module ActiveRecord
if result.size == expected_size
result
else
- conditions = arel.where_sql
- conditions = " [#{conditions}]" if conditions
-
- error = "Couldn't find all #{@klass.name.pluralize} with IDs "
- error << "(#{ids.join(", ")})#{conditions} (found #{result.size} results, but was looking for #{expected_size})"
- raise RecordNotFound, error
+ raise_record_not_found_exception!(ids, result.size, expected_size)
end
end
@@ -309,12 +366,15 @@ module ActiveRecord
if loaded?
@records.first
else
- @first ||=
- if with_default_scope.order_values.empty? && primary_key
- order(arel_table[primary_key].asc).limit(1).to_a.first
- else
- limit(1).to_a.first
- end
+ @first ||= find_first_with_limit(1).first
+ end
+ end
+
+ def find_first_with_limit(limit)
+ if order_values.empty? && primary_key
+ order(arel_table[primary_key].asc).limit(limit).to_a
+ else
+ limit(limit).to_a
end
end
@@ -330,9 +390,5 @@ module ActiveRecord
end
end
end
-
- def using_limitable_reflections?(reflections)
- reflections.none? { |r| r.collection? }
- end
end
end
diff --git a/activerecord/lib/active_record/relation/merger.rb b/activerecord/lib/active_record/relation/merger.rb
index 59226d316e..182b9ed89c 100644
--- a/activerecord/lib/active_record/relation/merger.rb
+++ b/activerecord/lib/active_record/relation/merger.rb
@@ -1,4 +1,5 @@
require 'active_support/core_ext/hash/keys'
+require "set"
module ActiveRecord
class Relation
@@ -21,7 +22,7 @@ module ActiveRecord
# build a relation to merge in rather than directly merging
# the values.
def other
- other = Relation.new(relation.klass, relation.table)
+ other = Relation.create(relation.klass, relation.table)
hash.each { |k, v|
if k == :joins
if Hash === v
@@ -38,20 +39,17 @@ module ActiveRecord
end
class Merger # :nodoc:
- attr_reader :relation, :values
+ attr_reader :relation, :values, :other
def initialize(relation, other)
- if other.default_scoped? && other.klass != relation.klass
- other = other.with_default_scope
- end
-
@relation = relation
@values = other.values
+ @other = other
end
NORMAL_VALUES = Relation::SINGLE_VALUE_METHODS +
Relation::MULTI_VALUE_METHODS -
- [:where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
+ [:joins, :where, :order, :bind, :reverse_order, :lock, :create_with, :reordering, :from] # :nodoc:
def normal_values
NORMAL_VALUES
@@ -60,26 +58,78 @@ module ActiveRecord
def merge
normal_values.each do |name|
value = values[name]
- relation.send("#{name}!", *value) unless value.blank?
+ # The unless clause is here mostly for performance reasons (since the `send` call might be moderately
+ # expensive), most of the time the value is going to be `nil` or `.blank?`, the only catch is that
+ # `false.blank?` returns `true`, so there needs to be an extra check so that explicit `false` values
+ # don't fall through the cracks.
+ relation.send("#{name}!", *value) unless value.nil? || (value.blank? && false != value)
end
merge_multi_values
merge_single_values
+ merge_joins
relation
end
private
+ def merge_joins
+ return if values[:joins].blank?
+
+ if other.klass == relation.klass
+ relation.joins!(*values[:joins])
+ else
+ joins_dependency, rest = values[:joins].partition do |join|
+ case join
+ when Hash, Symbol, Array
+ true
+ else
+ false
+ end
+ end
+
+ join_dependency = ActiveRecord::Associations::JoinDependency.new(other.klass,
+ joins_dependency,
+ [])
+ relation.joins! rest
+
+ @relation = relation.joins join_dependency
+ end
+ end
+
def merge_multi_values
- relation.where_values = merged_wheres
- relation.bind_values = merged_binds
+ lhs_wheres = relation.where_values
+ rhs_wheres = values[:where] || []
+
+ lhs_binds = relation.bind_values
+ rhs_binds = values[:bind] || []
+
+ removed, kept = partition_overwrites(lhs_wheres, rhs_wheres)
+
+ where_values = kept + rhs_wheres
+ bind_values = filter_binds(lhs_binds, removed) + rhs_binds
+
+ conn = relation.klass.connection
+ bv_index = 0
+ where_values.map! do |node|
+ if Arel::Nodes::Equality === node && Arel::Nodes::BindParam === node.right
+ substitute = conn.substitute_at(bind_values[bv_index].first, bv_index)
+ bv_index += 1
+ Arel::Nodes::Equality.new(node.left, substitute)
+ else
+ node
+ end
+ end
+
+ relation.where_values = where_values
+ relation.bind_values = bind_values
if values[:reordering]
# override any order specified in the original relation
relation.reorder! values[:order]
elsif values[:order]
- # merge in order_values from r
+ # merge in order_values from relation
relation.order! values[:order]
end
@@ -96,34 +146,28 @@ module ActiveRecord
end
end
- def merged_binds
- if values[:bind]
- (relation.bind_values + values[:bind]).uniq(&:first)
- else
- relation.bind_values
- end
+ def filter_binds(lhs_binds, removed_wheres)
+ return lhs_binds if removed_wheres.empty?
+
+ set = Set.new removed_wheres.map { |x| x.left.name }
+ lhs_binds.dup.delete_if { |col,_| set.include? col.name }
end
- def merged_wheres
- if values[:where]
- merged_wheres = relation.where_values + values[:where]
-
- unless relation.where_values.empty?
- # Remove equalities with duplicated left-hand. Last one wins.
- seen = {}
- merged_wheres = merged_wheres.reverse.reject { |w|
- nuke = false
- if w.respond_to?(:operator) && w.operator == :==
- nuke = seen[w.left]
- seen[w.left] = true
- end
- nuke
- }.reverse
- end
+ # Remove equalities from the existing relation with a LHS which is
+ # present in the relation being merged in.
+ # returns [things_to_remove, things_to_keep]
+ def partition_overwrites(lhs_wheres, rhs_wheres)
+ if lhs_wheres.empty? || rhs_wheres.empty?
+ return [[], lhs_wheres]
+ end
- merged_wheres
- else
- relation.where_values
+ nodes = rhs_wheres.find_all do |w|
+ w.respond_to?(:operator) && w.operator == :==
+ end
+ seen = Set.new(nodes) { |node| node.left }
+
+ lhs_wheres.partition do |w|
+ w.respond_to?(:operator) && w.operator == :== && seen.include?(w.left)
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder.rb b/activerecord/lib/active_record/relation/predicate_builder.rb
index 71030cb5d7..c60cd27a83 100644
--- a/activerecord/lib/active_record/relation/predicate_builder.rb
+++ b/activerecord/lib/active_record/relation/predicate_builder.rb
@@ -1,5 +1,20 @@
module ActiveRecord
class PredicateBuilder # :nodoc:
+ @handlers = []
+
+ autoload :RelationHandler, 'active_record/relation/predicate_builder/relation_handler'
+ autoload :ArrayHandler, 'active_record/relation/predicate_builder/array_handler'
+
+ def self.resolve_column_aliases(klass, hash)
+ hash = hash.dup
+ hash.keys.grep(Symbol) do |key|
+ if klass.attribute_alias? key
+ hash[klass.attribute_alias(key)] = hash.delete key
+ end
+ end
+ hash
+ end
+
def self.build_from_hash(klass, attributes, default_table)
queries = []
@@ -7,12 +22,12 @@ module ActiveRecord
table = default_table
if value.is_a?(Hash)
- table = Arel::Table.new(column, default_table.engine)
- association = klass.reflect_on_association(column.to_sym)
-
if value.empty?
- queries.concat ['1 = 2']
+ queries << '1=0'
else
+ table = Arel::Table.new(column, default_table.engine)
+ association = klass.reflect_on_association(column.to_sym)
+
value.each do |k, v|
queries.concat expand(association && association.klass, table, k, v)
end
@@ -36,11 +51,11 @@ module ActiveRecord
queries = []
# Find the foreign key when using queries such as:
- # Post.where(:author => author)
+ # Post.where(author: author)
#
# For polymorphic relationships, find the foreign key and type:
- # PriceEstimate.where(:estimate_of => treasure)
- if klass && value.class < ActiveRecord::Tag && reflection = klass.reflect_on_association(column.to_sym)
+ # PriceEstimate.where(estimate_of: treasure)
+ if klass && value.is_a?(Base) && reflection = klass.reflect_on_association(column.to_sym)
if reflection.polymorphic?
queries << build(table[reflection.foreign_type], value.class.base_class)
end
@@ -48,7 +63,7 @@ module ActiveRecord
column = reflection.foreign_key
end
- queries << build(table[column.to_sym], value)
+ queries << build(table[column], value)
queries
end
@@ -58,49 +73,41 @@ module ActiveRecord
key
else
key = key.to_s
- key.split('.').first.to_sym if key.include?('.')
+ key.split('.').first if key.include?('.')
end
end.compact
end
+ # Define how a class is converted to Arel nodes when passed to +where+.
+ # The handler can be any object that responds to +call+, and will be used
+ # for any value that +===+ the class given. For example:
+ #
+ # MyCustomDateRange = Struct.new(:start, :end)
+ # handler = proc do |column, range|
+ # Arel::Nodes::Between.new(column,
+ # Arel::Nodes::And.new([range.start, range.end])
+ # )
+ # end
+ # ActiveRecord::PredicateBuilder.register_handler(MyCustomDateRange, handler)
+ def self.register_handler(klass, handler)
+ @handlers.unshift([klass, handler])
+ end
+
+ register_handler(BasicObject, ->(attribute, value) { attribute.eq(value) })
+ # FIXME: I think we need to deprecate this behavior
+ register_handler(Class, ->(attribute, value) { attribute.eq(value.name) })
+ register_handler(Base, ->(attribute, value) { attribute.eq(value.id) })
+ register_handler(Range, ->(attribute, value) { attribute.in(value) })
+ register_handler(Relation, RelationHandler.new)
+ register_handler(Array, ArrayHandler.new)
+
private
def self.build(attribute, value)
- case value
- when Array, ActiveRecord::Associations::CollectionProxy
- values = value.to_a.map {|x| x.is_a?(ActiveRecord::Model) ? x.id : x}
- ranges, values = values.partition {|v| v.is_a?(Range)}
-
- values_predicate = if values.include?(nil)
- values = values.compact
-
- case values.length
- when 0
- attribute.eq(nil)
- when 1
- attribute.eq(values.first).or(attribute.eq(nil))
- else
- attribute.in(values).or(attribute.eq(nil))
- end
- else
- attribute.in(values)
- end
+ handler_for(value).call(attribute, value)
+ end
- array_predicates = ranges.map { |range| attribute.in(range) }
- array_predicates << values_predicate
- array_predicates.inject { |composite, predicate| composite.or(predicate) }
- when ActiveRecord::Relation
- value = value.select(value.klass.arel_table[value.klass.primary_key]) if value.select_values.empty?
- attribute.in(value.arel.ast)
- when Range
- attribute.in(value)
- when ActiveRecord::Model
- attribute.eq(value.id)
- when Class
- # FIXME: I think we need to deprecate this behavior
- attribute.eq(value.name)
- else
- attribute.eq(value)
- end
+ def self.handler_for(object)
+ @handlers.detect { |klass, _| klass === object }.last
end
end
end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
new file mode 100644
index 0000000000..2f6c34ac08
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/array_handler.rb
@@ -0,0 +1,29 @@
+module ActiveRecord
+ class PredicateBuilder
+ class ArrayHandler # :nodoc:
+ def call(attribute, value)
+ values = value.map { |x| x.is_a?(Base) ? x.id : x }
+ ranges, values = values.partition { |v| v.is_a?(Range) }
+
+ values_predicate = if values.include?(nil)
+ values = values.compact
+
+ case values.length
+ when 0
+ attribute.eq(nil)
+ when 1
+ attribute.eq(values.first).or(attribute.eq(nil))
+ else
+ attribute.in(values).or(attribute.eq(nil))
+ end
+ else
+ attribute.in(values)
+ end
+
+ array_predicates = ranges.map { |range| attribute.in(range) }
+ array_predicates << values_predicate
+ array_predicates.inject { |composite, predicate| composite.or(predicate) }
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
new file mode 100644
index 0000000000..618fa3cdd9
--- /dev/null
+++ b/activerecord/lib/active_record/relation/predicate_builder/relation_handler.rb
@@ -0,0 +1,13 @@
+module ActiveRecord
+ class PredicateBuilder
+ class RelationHandler # :nodoc:
+ def call(attribute, value)
+ if value.select_values.empty?
+ value = value.select(value.klass.arel_table[value.klass.primary_key])
+ end
+
+ attribute.in(value.arel.ast)
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/relation/query_methods.rb b/activerecord/lib/active_record/relation/query_methods.rb
index a9ace10093..bffd8b5d0f 100644
--- a/activerecord/lib/active_record/relation/query_methods.rb
+++ b/activerecord/lib/active_record/relation/query_methods.rb
@@ -4,6 +4,54 @@ module ActiveRecord
module QueryMethods
extend ActiveSupport::Concern
+ # WhereChain objects act as placeholder for queries in which #where does not have any parameter.
+ # In this case, #where must be chained with #not to return a new relation.
+ class WhereChain
+ def initialize(scope)
+ @scope = scope
+ end
+
+ # Returns a new relation expressing WHERE + NOT condition according to
+ # the conditions in the arguments.
+ #
+ # +not+ accepts conditions as a string, array, or hash. See #where for
+ # more details on each format.
+ #
+ # User.where.not("name = 'Jon'")
+ # # SELECT * FROM users WHERE NOT (name = 'Jon')
+ #
+ # User.where.not(["name = ?", "Jon"])
+ # # SELECT * FROM users WHERE NOT (name = 'Jon')
+ #
+ # User.where.not(name: "Jon")
+ # # SELECT * FROM users WHERE name != 'Jon'
+ #
+ # User.where.not(name: nil)
+ # # SELECT * FROM users WHERE name IS NOT NULL
+ #
+ # User.where.not(name: %w(Ko1 Nobu))
+ # # SELECT * FROM users WHERE name NOT IN ('Ko1', 'Nobu')
+ #
+ # User.where.not(name: "Jon", role: "admin")
+ # # SELECT * FROM users WHERE name != 'Jon' AND role != 'admin'
+ def not(opts, *rest)
+ where_value = @scope.send(:build_where, opts, rest).map do |rel|
+ case rel
+ when Arel::Nodes::In
+ Arel::Nodes::NotIn.new(rel.left, rel.right)
+ when Arel::Nodes::Equality
+ Arel::Nodes::NotEqual.new(rel.left, rel.right)
+ when String
+ Arel::Nodes::Not.new(Arel::Nodes::SqlLiteral.new(rel))
+ else
+ Arel::Nodes::Not.new(rel)
+ end
+ end
+ @scope.where_values += where_value
+ @scope
+ end
+ end
+
Relation::MULTI_VALUE_METHODS.each do |name|
class_eval <<-CODE, __FILE__, __LINE__ + 1
def #{name}_values # def select_values
@@ -52,6 +100,14 @@ module ActiveRecord
# firing an additional query. This will often result in a
# performance improvement over a simple +join+.
#
+ # You can also specify multiple relationships, like this:
+ #
+ # users = User.includes(:address, :friends)
+ #
+ # Loading nested relationships is possible using a Hash:
+ #
+ # users = User.includes(:address, friends: [:address, :followers])
+ #
# === conditions
#
# If you want to add conditions to your included models you'll have
@@ -63,14 +119,15 @@ module ActiveRecord
#
# User.includes(:posts).where('posts.name = ?', 'example').references(:posts)
def includes(*args)
- args.empty? ? self : spawn.includes!(*args)
+ check_if_method_has_arguments!(:includes, args)
+ spawn.includes!(*args)
end
- # Like #includes, but modifies the relation in place.
- def includes!(*args)
- args.reject! {|a| a.blank? }
+ def includes!(*args) # :nodoc:
+ args.reject!(&:blank?)
+ args.flatten!
- self.includes_values = (includes_values + args).flatten.uniq
+ self.includes_values |= args
self
end
@@ -81,11 +138,11 @@ module ActiveRecord
# FROM "users" LEFT OUTER JOIN "posts" ON "posts"."user_id" =
# "users"."id"
def eager_load(*args)
- args.blank? ? self : spawn.eager_load!(*args)
+ check_if_method_has_arguments!(:eager_load, args)
+ spawn.eager_load!(*args)
end
- # Like #eager_load, but modifies relation in place.
- def eager_load!(*args)
+ def eager_load!(*args) # :nodoc:
self.eager_load_values += args
self
end
@@ -95,11 +152,11 @@ module ActiveRecord
# User.preload(:posts)
# => SELECT "posts".* FROM "posts" WHERE "posts"."user_id" IN (1, 2, 3)
def preload(*args)
- args.blank? ? self : spawn.preload!(*args)
+ check_if_method_has_arguments!(:preload, args)
+ spawn.preload!(*args)
end
- # Like #preload, but modifies relation in place.
- def preload!(*args)
+ def preload!(*args) # :nodoc:
self.preload_values += args
self
end
@@ -113,14 +170,15 @@ module ActiveRecord
# User.includes(:posts).where("posts.name = 'foo'").references(:posts)
# # => Query now knows the string references posts, so adds a JOIN
def references(*args)
- args.blank? ? self : spawn.references!(*args)
+ check_if_method_has_arguments!(:references, args)
+ spawn.references!(*args)
end
- # Like #references, but modifies relation in place.
- def references!(*args)
+ def references!(*args) # :nodoc:
args.flatten!
+ args.map!(&:to_s)
- self.references_values = (references_values + args.map!(&:to_s)).uniq
+ self.references_values |= args
self
end
@@ -148,6 +206,16 @@ module ActiveRecord
# Model.select(:field, :other_field, :and_one_more)
# # => [#<Model 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">]
+ #
+ # If an alias was specified, it will be accessible from the resulting objects:
+ #
+ # Model.select('field AS field_one').first.field_one
+ # # => "value"
+ #
# Accessing attributes of an object that do not have fields retrieved by a select
# will throw <tt>ActiveModel::MissingAttributeError</tt>:
#
@@ -162,9 +230,10 @@ module ActiveRecord
end
end
- # Like #select, but modifies relation in place.
- def select!(*fields)
- self.select_values += fields.flatten
+ def select!(*fields) # :nodoc:
+ fields.flatten!
+
+ self.select_values += fields
self
end
@@ -180,12 +249,15 @@ module ActiveRecord
#
# User.group(:name)
# => [#<User id: 3, name: "Foo", ...>, #<User id: 2, name: "Oscar", ...>]
+ #
+ # 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, ...>]
def group(*args)
- args.blank? ? self : spawn.group!(*args)
+ check_if_method_has_arguments!(:group, args)
+ spawn.group!(*args)
end
- # Like #group, but modifies relation in place.
- def group!(*args)
+ def group!(*args) # :nodoc:
args.flatten!
self.group_values += args
@@ -212,20 +284,14 @@ module ActiveRecord
# User.order(:name, email: :desc)
# => SELECT "users".* FROM "users" ORDER BY "users"."name" ASC, "users"."email" DESC
def order(*args)
- args.blank? ? self : spawn.order!(*args)
+ check_if_method_has_arguments!(:order, args)
+ spawn.order!(*args)
end
- # Like #order, but modifies relation in place.
- def order!(*args)
- args.flatten!
-
- validate_order_args args
-
- references = args.reject { |arg| Arel::Node === arg }
- references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
- references!(references) if references.any?
+ def order!(*args) # :nodoc:
+ preprocess_order_args(args)
- self.order_values = args + self.order_values
+ self.order_values += args
self
end
@@ -237,32 +303,103 @@ module ActiveRecord
#
# User.order('email DESC').reorder('id ASC').order('name ASC')
#
- # generates a query with 'ORDER BY name ASC, id ASC'.
+ # generates a query with 'ORDER BY id ASC, name ASC'.
def reorder(*args)
- args.blank? ? self : spawn.reorder!(*args)
+ check_if_method_has_arguments!(:reorder, args)
+ spawn.reorder!(*args)
end
- # Like #reorder, but modifies relation in place.
- def reorder!(*args)
- args.flatten!
-
- validate_order_args args
+ def reorder!(*args) # :nodoc:
+ preprocess_order_args(args)
self.reordering_value = true
self.order_values = args
self
end
+ VALID_UNSCOPING_VALUES = Set.new([:where, :select, :group, :order, :lock,
+ :limit, :offset, :joins, :includes, :from,
+ :readonly, :having])
+
+ # Removes an unwanted relation that is already defined on a chain of relations.
+ # This is useful when passing around chains of relations and would like to
+ # modify the relations without reconstructing the entire chain.
+ #
+ # User.order('email DESC').unscope(:order) == User.all
+ #
+ # The method arguments are symbols which correspond to the names of the methods
+ # which should be unscoped. The valid arguments are given in VALID_UNSCOPING_VALUES.
+ # The method can also be called with multiple arguments. For example:
+ #
+ # User.order('email DESC').select('id').where(name: "John")
+ # .unscope(:order, :select, :where) == User.all
+ #
+ # One can additionally pass a hash as an argument to unscope specific :where values.
+ # This is done by passing a hash with a single key-value pair. The key should be
+ # :where and the value should be the where value to unscope. For example:
+ #
+ # User.where(name: "John", active: true).unscope(where: :name)
+ # == User.where(active: true)
+ #
+ # This method is applied before the default_scope is applied. So the conditions
+ # specified in default_scope will not be removed.
+ #
+ # Note that this method is more generalized than ActiveRecord::SpawnMethods#except
+ # because #except will only affect a particular relation's values. It won't wipe
+ # the order, grouping, etc. when that relation is merged. For example:
+ #
+ # Post.comments.except(:order)
+ #
+ # will still have an order if it comes from the default_scope on Comment.
+ def unscope(*args)
+ check_if_method_has_arguments!(:unscope, args)
+ spawn.unscope!(*args)
+ end
+
+ def unscope!(*args) # :nodoc:
+ args.flatten!
+
+ args.each do |scope|
+ case scope
+ when Symbol
+ symbol_unscoping(scope)
+ when Hash
+ scope.each do |key, target_value|
+ if key != :where
+ raise ArgumentError, "Hash arguments in .unscope(*args) must have :where as the key."
+ end
+
+ Array(target_value).each do |val|
+ where_unscoping(val)
+ end
+ end
+ else
+ raise ArgumentError, "Unrecognized scoping: #{args.inspect}. Use .unscope(where: :attribute_name) or .unscope(:order), for example."
+ end
+ end
+
+ self
+ end
+
# Performs a joins on +args+:
#
# User.joins(:posts)
# => SELECT "users".* FROM "users" INNER JOIN "posts" ON "posts"."user_id" = "users"."id"
+ #
+ # You can use strings in order to customize your joins:
+ #
+ # User.joins("LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id")
+ # => SELECT "users".* FROM "users" LEFT JOIN bookmarks ON bookmarks.bookmarkable_type = 'Post' AND bookmarks.user_id = users.id
def joins(*args)
- args.compact.blank? ? self : spawn.joins!(*args.flatten)
+ check_if_method_has_arguments!(:joins, args)
+
+ args.compact!
+ args.flatten!
+
+ spawn.joins!(*args)
end
- # Like #joins, but modifies relation in place.
- def joins!(*args)
+ def joins!(*args) # :nodoc:
self.joins_values += args
self
end
@@ -271,7 +408,7 @@ module ActiveRecord
spawn.bind!(value)
end
- def bind!(value)
+ def bind!(value) # :nodoc:
self.bind_values += [value]
self
end
@@ -357,17 +494,17 @@ module ActiveRecord
# author = Author.find(1)
#
# # The following queries will be equivalent:
- # Post.where(:author => author)
- # Post.where(:author_id => author)
+ # Post.where(author: author)
+ # Post.where(author_id: author)
#
# This also works with polymorphic belongs_to relationships:
#
- # treasure = Treasure.create(:name => 'gold coins')
- # treasure.price_estimates << PriceEstimate.create(:price => 125)
+ # treasure = Treasure.create(name: 'gold coins')
+ # treasure.price_estimates << PriceEstimate.create(price: 125)
#
# # The following queries will be equivalent:
- # PriceEstimate.where(:estimate_of => treasure)
- # PriceEstimate.where(:estimate_of_type => 'Treasure', :estimate_of_id => treasure)
+ # PriceEstimate.where(estimate_of: treasure)
+ # PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: treasure)
#
# === Joins
#
@@ -379,22 +516,41 @@ module ActiveRecord
# For hash conditions, you can either use the table name in the key, or use a sub-hash.
#
# User.joins(:posts).where({ "posts.published" => true })
- # User.joins(:posts).where({ :posts => { :published => true } })
+ # User.joins(:posts).where({ posts: { published: true } })
+ #
+ # === no argument
+ #
+ # If no argument is passed, #where returns a new instance of WhereChain, that
+ # can be chained with #not to return a new relation that negates the where clause.
#
- # === empty condition
+ # User.where.not(name: "Jon")
+ # # SELECT * FROM users WHERE name != 'Jon'
#
- # If the condition returns true for blank?, then where is a no-op and returns the current relation.
- def where(opts, *rest)
- opts.blank? ? self : spawn.where!(opts, *rest)
+ # See WhereChain for more details on #not.
+ #
+ # === blank condition
+ #
+ # If the condition is any blank-ish object, then #where is a no-op and returns
+ # the current relation.
+ def where(opts = :chain, *rest)
+ if opts == :chain
+ WhereChain.new(spawn)
+ elsif opts.blank?
+ self
+ else
+ spawn.where!(opts, *rest)
+ end
end
- # #where! is identical to #where, except that instead of returning a new relation, it adds
- # the condition to the existing relation.
- def where!(opts, *rest)
- references!(PredicateBuilder.references(opts)) if Hash === opts
+ def where!(opts = :chain, *rest) # :nodoc:
+ if opts == :chain
+ WhereChain.new(self)
+ else
+ references!(PredicateBuilder.references(opts)) if Hash === opts
- self.where_values += build_where(opts, rest)
- self
+ self.where_values += build_where(opts, rest)
+ self
+ end
end
# Allows to specify a HAVING clause. Note that you can't use HAVING
@@ -405,8 +561,7 @@ module ActiveRecord
opts.blank? ? self : spawn.having!(opts, *rest)
end
- # Like #having, but modifies relation in place.
- def having!(opts, *rest)
+ def having!(opts, *rest) # :nodoc:
references!(PredicateBuilder.references(opts)) if Hash === opts
self.having_values += build_where(opts, rest)
@@ -422,8 +577,7 @@ module ActiveRecord
spawn.limit!(value)
end
- # Like #limit, but modifies relation in place.
- def limit!(value)
+ def limit!(value) # :nodoc:
self.limit_value = value
self
end
@@ -439,8 +593,7 @@ module ActiveRecord
spawn.offset!(value)
end
- # Like #offset, but modifies relation in place.
- def offset!(value)
+ def offset!(value) # :nodoc:
self.offset_value = value
self
end
@@ -451,8 +604,7 @@ module ActiveRecord
spawn.lock!(locks)
end
- # Like #lock, but modifies relation in place.
- def lock!(locks = true)
+ def lock!(locks = true) # :nodoc:
case locks
when String, TrueClass, NilClass
self.lock_value = locks || true
@@ -468,7 +620,7 @@ module ActiveRecord
#
# The returned <tt>ActiveRecord::NullRelation</tt> inherits from Relation and implements the
# Null Object pattern. It is an object with defined null behavior and always returns an empty
- # array of records without quering the database.
+ # array of records without querying the database.
#
# Any subsequent condition chained to the returned relation will continue
# generating an empty relation and will not fire any query to the database.
@@ -478,13 +630,13 @@ module ActiveRecord
#
# For example:
#
- # @posts = current_user.visible_posts.where(:name => params[:name])
+ # @posts = current_user.visible_posts.where(name: params[:name])
# # => the visible_posts method is expected to return a chainable Relation
#
# def visible_posts
# case role
# when 'Country Manager'
- # Post.where(:country => country)
+ # Post.where(country: country)
# when 'Reviewer'
# Post.published
# when 'Bad User'
@@ -496,6 +648,10 @@ module ActiveRecord
extending(NullRelation)
end
+ def none! # :nodoc:
+ extending!(NullRelation)
+ end
+
# Sets readonly attributes for the returned relation. If value is
# true (default), attempting to update a record will result in an error.
#
@@ -506,8 +662,7 @@ module ActiveRecord
spawn.readonly!(value)
end
- # Like #readonly, but modifies relation in place.
- def readonly!(value = true)
+ def readonly!(value = true) # :nodoc:
self.readonly_value = value
self
end
@@ -529,12 +684,7 @@ module ActiveRecord
spawn.create_with!(value)
end
- # Like #create_with but modifies the relation in place. Raises
- # +ImmutableRelation+ if the relation has already been loaded.
- #
- # users = User.all.create_with!(name: 'Oscar')
- # users.new.name # => 'Oscar'
- def create_with!(value)
+ def create_with!(value) # :nodoc:
self.create_with_value = value ? create_with_value.merge(value) : {}
self
end
@@ -556,8 +706,7 @@ module ActiveRecord
spawn.from!(value, subquery_name)
end
- # Like #from, but modifies relation in place.
- def from!(value, subquery_name = nil)
+ def from!(value, subquery_name = nil) # :nodoc:
self.from_value = [value, subquery_name]
self
end
@@ -567,20 +716,22 @@ module ActiveRecord
# User.select(:name)
# # => Might return two records with the same name
#
- # User.select(:name).uniq
- # # => Returns 1 record per unique name
+ # User.select(:name).distinct
+ # # => Returns 1 record per distinct name
#
- # User.select(:name).uniq.uniq(false)
+ # User.select(:name).distinct.distinct(false)
# # => You can also remove the uniqueness
- def uniq(value = true)
- spawn.uniq!(value)
+ def distinct(value = true)
+ spawn.distinct!(value)
end
+ alias uniq distinct
- # Like #uniq, but modifies relation in place.
- def uniq!(value = true)
- self.uniq_value = value
+ # Like #distinct, but modifies relation in place.
+ def distinct!(value = true) # :nodoc:
+ self.distinct_value = value
self
end
+ alias uniq! distinct!
# Used to extend a scope with additional methods, either through
# a module or through a block provided.
@@ -626,11 +777,11 @@ module ActiveRecord
end
end
- # Like #extending, but modifies relation in place.
- def extending!(*modules, &block)
- modules << Module.new(&block) if block_given?
+ def extending!(*modules, &block) # :nodoc:
+ modules << Module.new(&block) if block
+ modules.flatten!
- self.extending_values += modules.flatten
+ self.extending_values += modules
extend(*extending_values) if extending_values.any?
self
@@ -643,37 +794,36 @@ module ActiveRecord
spawn.reverse_order!
end
- # Like #reverse_order, but modifies relation in place.
- def reverse_order!
+ def reverse_order! # :nodoc:
self.reverse_order_value = !reverse_order_value
self
end
# Returns the Arel object associated with the relation.
def arel
- @arel ||= with_default_scope.build_arel
+ @arel ||= build_arel
end
# Like #arel, but ignores the default scope of the model.
def build_arel
arel = Arel::SelectManager.new(table.engine, table)
- build_joins(arel, joins_values) unless joins_values.empty?
+ build_joins(arel, joins_values.flatten) unless joins_values.empty?
collapse_wheres(arel, (where_values - ['']).uniq)
- arel.having(*having_values.uniq.reject{|h| h.blank?}) unless having_values.empty?
+ arel.having(*having_values.uniq.reject(&:blank?)) unless having_values.empty?
arel.take(connection.sanitize_limit(limit_value)) if limit_value
arel.skip(offset_value.to_i) if offset_value
- arel.group(*group_values.uniq.reject{|g| g.blank?}) unless group_values.empty?
+ arel.group(*group_values.uniq.reject(&:blank?)) unless group_values.empty?
build_order(arel)
build_select(arel, select_values.uniq)
- arel.distinct(uniq_value)
+ arel.distinct(distinct_value)
arel.from(build_from) if from_value
arel.lock(lock_value) if lock_value
@@ -682,14 +832,45 @@ module ActiveRecord
private
+ def symbol_unscoping(scope)
+ if !VALID_UNSCOPING_VALUES.include?(scope)
+ raise ArgumentError, "Called unscope() with invalid unscoping argument ':#{scope}'. Valid arguments are :#{VALID_UNSCOPING_VALUES.to_a.join(", :")}."
+ end
+
+ single_val_method = Relation::SINGLE_VALUE_METHODS.include?(scope)
+ unscope_code = :"#{scope}_value#{'s' unless single_val_method}="
+
+ case scope
+ when :order
+ self.send(:reverse_order_value=, false)
+ result = []
+ else
+ result = [] unless single_val_method
+ end
+
+ self.send(unscope_code, result)
+ end
+
+ def where_unscoping(target_value)
+ target_value_sym = target_value.to_sym
+
+ where_values.reject! do |rel|
+ case rel
+ when Arel::Nodes::In, Arel::Nodes::NotIn, Arel::Nodes::Equality, Arel::Nodes::NotEqual
+ subrelation = (rel.left.kind_of?(Arel::Attributes::Attribute) ? rel.left : rel.right)
+ subrelation.name.to_sym == target_value_sym
+ else
+ raise "unscope(where: #{target_value.inspect}) failed: unscoping #{rel.class} is unimplemented."
+ end
+ end
+ end
+
def custom_join_ast(table, joins)
- joins = joins.reject { |join| join.blank? }
+ joins = joins.reject(&:blank?)
return [] if joins.empty?
- @implicit_readonly = true
-
- joins.map do |join|
+ joins.map! do |join|
case join
when Array
join = Arel.sql(join.join(' ')) if array_of_strings?(join)
@@ -701,22 +882,34 @@ module ActiveRecord
end
def collapse_wheres(arel, wheres)
- equalities = wheres.grep(Arel::Nodes::Equality)
-
- arel.where(Arel::Nodes::And.new(equalities)) unless equalities.empty?
-
- (wheres - equalities).each do |where|
+ predicates = wheres.map do |where|
+ next where if ::Arel::Nodes::Equality === where
where = Arel.sql(where) if String === where
- arel.where(Arel::Nodes::Grouping.new(where))
+ Arel::Nodes::Grouping.new(where)
end
+
+ arel.where(Arel::Nodes::And.new(predicates)) if predicates.present?
end
def build_where(opts, other = [])
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)
+
+ attributes.values.grep(ActiveRecord::Relation) do |rel|
+ self.bind_values += rel.bind_values
+ end
+
PredicateBuilder.build_from_hash(klass, attributes, table)
else
[opts]
@@ -728,6 +921,7 @@ module ActiveRecord
case opts
when Relation
name ||= 'subquery'
+ self.bind_values = opts.bind_values + self.bind_values
opts.arel.as(name.to_s)
else
opts
@@ -738,24 +932,22 @@ module ActiveRecord
buckets = joins.group_by do |join|
case join
when String
- 'string_join'
+ :string_join
when Hash, Symbol, Array
- 'association_join'
- when ActiveRecord::Associations::JoinDependency::JoinAssociation
- 'stashed_join'
+ :association_join
+ when ActiveRecord::Associations::JoinDependency
+ :stashed_join
when Arel::Nodes::Join
- 'join_node'
+ :join_node
else
raise 'unknown class: %s' % join.class.name
end
end
- association_joins = buckets['association_join'] || []
- stashed_association_joins = buckets['stashed_join'] || []
- join_nodes = (buckets['join_node'] || []).uniq
- string_joins = (buckets['string_join'] || []).map { |x|
- x.strip
- }.uniq
+ association_joins = buckets[:association_join] || []
+ stashed_association_joins = buckets[:stashed_join] || []
+ join_nodes = (buckets[:join_node] || []).uniq
+ string_joins = (buckets[:string_join] || []).map(&:strip).uniq
join_list = join_nodes + custom_join_ast(manager, string_joins)
@@ -765,23 +957,17 @@ module ActiveRecord
join_list
)
- join_dependency.graft(*stashed_association_joins)
+ joins = join_dependency.join_constraints stashed_association_joins
- @implicit_readonly = true unless association_joins.empty? && stashed_association_joins.empty?
+ joins.each { |join| manager.from(join) }
- # FIXME: refactor this to build an AST
- join_dependency.join_associations.each do |association|
- association.join_to(manager)
- end
-
- manager.join_sources.concat join_list
+ manager.join_sources.concat(join_list)
manager
end
def build_select(arel, selects)
unless selects.empty?
- @implicit_readonly = false
arel.project(*selects)
else
arel.project(@klass.arel_table[Arel.star])
@@ -791,12 +977,12 @@ module ActiveRecord
def reverse_sql_order(order_query)
order_query = ["#{quoted_table_name}.#{quoted_primary_key} ASC"] if order_query.empty?
- order_query.map do |o|
+ order_query.flat_map do |o|
case o
when Arel::Nodes::Ordering
o.reverse
when String
- o.to_s.split(',').collect do |s|
+ o.to_s.split(',').map! do |s|
s.strip!
s.gsub!(/\sasc\Z/i, ' DESC') || s.gsub!(/\sdesc\Z/i, ' ASC') || s.concat(' DESC')
end
@@ -809,18 +995,19 @@ module ActiveRecord
else
o
end
- end.flatten
+ end
end
def array_of_strings?(o)
- o.is_a?(Array) && o.all?{|obj| obj.is_a?(String)}
+ o.is_a?(Array) && o.all? { |obj| obj.is_a?(String) }
end
def build_order(arel)
- orders = order_values
+ orders = order_values.uniq
+ orders.reject!(&:blank?)
orders = reverse_sql_order(orders) if reverse_order_value
- orders = orders.uniq.reject(&:blank?).map do |order|
+ orders = orders.flat_map do |order|
case order
when Symbol
table[order].asc
@@ -829,18 +1016,53 @@ module ActiveRecord
else
order
end
- end.flatten
+ end
arel.order(*orders) unless orders.empty?
end
def validate_order_args(args)
- args.select { |a| Hash === a }.each do |h|
+ args.grep(Hash) do |h|
unless (h.values - [:asc, :desc]).empty?
raise ArgumentError, 'Direction should be :asc or :desc'
end
end
end
+ def preprocess_order_args(order_args)
+ order_args.flatten!
+ validate_order_args(order_args)
+
+ references = order_args.grep(String)
+ references.map! { |arg| arg =~ /^([a-zA-Z]\w*)\.(\w+)/ && $1 }.compact!
+ references!(references) if references.any?
+
+ # if a symbol is given we prepend the quoted table name
+ order_args.map! do |arg|
+ arg.is_a?(Symbol) ? Arel::Nodes::Ascending.new(klass.arel_table[arg]) : arg
+ end
+ end
+
+ # Checks to make sure that the arguments are not blank. Note that if some
+ # blank-like object were initially passed into the query method, then this
+ # method will not raise an error.
+ #
+ # Example:
+ #
+ # Post.references() # => raises an error
+ # Post.references([]) # => does not raise an error
+ #
+ # This particular method should be called with a method_name and the args
+ # passed into that method as an input. For example:
+ #
+ # def references(*args)
+ # check_if_method_has_arguments!("references", args)
+ # ...
+ # end
+ def check_if_method_has_arguments!(method_name, args)
+ if args.blank?
+ raise ArgumentError, "The method .#{method_name}() must contain arguments."
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/relation/spawn_methods.rb b/activerecord/lib/active_record/relation/spawn_methods.rb
index 5394c1b28b..2552cbd234 100644
--- a/activerecord/lib/active_record/relation/spawn_methods.rb
+++ b/activerecord/lib/active_record/relation/spawn_methods.rb
@@ -12,14 +12,11 @@ module ActiveRecord
# Merges in the conditions from <tt>other</tt>, if <tt>other</tt> is an <tt>ActiveRecord::Relation</tt>.
# Returns an array representing the intersection of the resulting records with <tt>other</tt>, if <tt>other</tt> is an array.
- #
- # ==== Examples
- #
- # Post.where(:published => true).joins(:comments).merge( Comment.where(:spam => false) )
+ # Post.where(published: true).joins(:comments).merge( Comment.where(spam: false) )
# # Performs a single join query with both where conditions.
#
# recent_posts = Post.order('created_at DESC').first(5)
- # Post.where(:published => true).merge(recent_posts)
+ # Post.where(published: true).merge(recent_posts)
# # Returns the intersection of all published posts with the 5 most recently created posts.
# # (This is just an example. You'd probably want to do this with a single query!)
#
@@ -29,7 +26,6 @@ module ActiveRecord
# # => Post.where(published: true).joins(:comments)
#
# This is mainly intended for sharing common conditions between multiple associations.
- #
def merge(other)
if other.is_a?(Array)
to_a & other
@@ -40,8 +36,7 @@ module ActiveRecord
end
end
- # Like #merge, but applies changes in place.
- def merge!(other)
+ def merge!(other) # :nodoc:
if !other.is_a?(Relation) && other.respond_to?(:to_proc)
instance_exec(&other)
else
@@ -52,31 +47,26 @@ module ActiveRecord
# Removes from the query the condition(s) specified in +skips+.
#
- # Example:
- #
# Post.order('id asc').except(:order) # discards the order condition
# Post.where('id > 10').order('id asc').except(:where) # discards the where condition but keeps the order
- #
def except(*skips)
- result = Relation.new(klass, table, values.except(*skips))
- result.default_scoped = default_scoped
- result.extend(*extending_values) if extending_values.any?
- result
+ relation_with values.except(*skips)
end
# Removes any condition from the query other than the one(s) specified in +onlies+.
#
- # Example:
- #
# Post.order('id asc').only(:where) # discards the order condition
# Post.order('id asc').only(:where, :order) # uses the specified order
- #
def only(*onlies)
- result = Relation.new(klass, table, values.slice(*onlies))
- result.default_scoped = default_scoped
- result.extend(*extending_values) if extending_values.any?
- result
+ relation_with values.slice(*onlies)
end
+ private
+
+ def relation_with(values) # :nodoc:
+ result = Relation.create(klass, table, values)
+ result.extend(*extending_values) if extending_values.any?
+ result
+ end
end
end
diff --git a/activerecord/lib/active_record/result.rb b/activerecord/lib/active_record/result.rb
index 425b9b41d8..1dc3bf3f12 100644
--- a/activerecord/lib/active_record/result.rb
+++ b/activerecord/lib/active_record/result.rb
@@ -3,11 +3,36 @@ module ActiveRecord
# This class encapsulates a Result returned from calling +exec_query+ on any
# database connection adapter. For example:
#
- # x = ActiveRecord::Base.connection.exec_query('SELECT * FROM foo')
- # x # => #<ActiveRecord::Result:0xdeadbeef>
+ # result = ActiveRecord::Base.connection.exec_query('SELECT id, title, body FROM posts')
+ # result # => #<ActiveRecord::Result:0xdeadbeef>
+ #
+ # # Get the column names of the result:
+ # result.columns
+ # # => ["id", "title", "body"]
+ #
+ # # Get the record values of the result:
+ # result.rows
+ # # => [[1, "title_1", "body_1"],
+ # [2, "title_2", "body_2"],
+ # ...
+ # ]
+ #
+ # # Get an array of hashes representing the result (column => value):
+ # result.to_hash
+ # # => [{"id" => 1, "title" => "title_1", "body" => "body_1"},
+ # {"id" => 2, "title" => "title_2", "body" => "body_2"},
+ # ...
+ # ]
+ #
+ # # ActiveRecord::Result also includes Enumerable.
+ # result.each do |row|
+ # puts row['title'] + " " + row['body']
+ # end
class Result
include Enumerable
+ IDENTITY_TYPE = Class.new { def type_cast(v); v; end }.new # :nodoc:
+
attr_reader :columns, :rows, :column_types
def initialize(columns, rows, column_types = {})
@@ -17,8 +42,20 @@ 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
- hash_rows.each { |row| yield row }
+ if block_given?
+ hash_rows.each { |row| yield row }
+ else
+ hash_rows.to_enum
+ end
end
def to_hash
@@ -52,14 +89,29 @@ module ActiveRecord
end
private
+
def hash_rows
@hash_rows ||=
begin
# We freeze the strings to prevent them getting duped when
- # used as keys in ActiveRecord::Model's @attributes hash
+ # used as keys in ActiveRecord::Base's @attributes hash
columns = @columns.map { |c| c.dup.freeze }
@rows.map { |row|
- Hash[columns.zip(row)]
+ # In the past we used Hash[columns.zip(row)]
+ # though elegant, the verbose way is much more efficient
+ # both time and memory wise cause it avoids a big array allocation
+ # this method is called a lot and needs to be micro optimised
+ hash = {}
+
+ index = 0
+ length = columns.length
+
+ while index < length
+ hash[columns[index]] = row[index]
+ index += 1
+ end
+
+ hash
}
end
end
diff --git a/activerecord/lib/active_record/runtime_registry.rb b/activerecord/lib/active_record/runtime_registry.rb
new file mode 100644
index 0000000000..63e6738622
--- /dev/null
+++ b/activerecord/lib/active_record/runtime_registry.rb
@@ -0,0 +1,17 @@
+require 'active_support/per_thread_registry'
+
+module ActiveRecord
+ # This is a thread locals registry for Active Record. For example:
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler
+ #
+ # returns the connection handler local to the current thread.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class RuntimeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ attr_accessor :connection_handler, :sql_runtime, :connection_id
+ end
+end
diff --git a/activerecord/lib/active_record/sanitization.rb b/activerecord/lib/active_record/sanitization.rb
index f3e47a958e..cab8fd745a 100644
--- a/activerecord/lib/active_record/sanitization.rb
+++ b/activerecord/lib/active_record/sanitization.rb
@@ -3,8 +3,8 @@ module ActiveRecord
extend ActiveSupport::Concern
module ClassMethods
- def quote_value(value, column = nil) #:nodoc:
- connection.quote(value,column)
+ def quote_value(value, column) #:nodoc:
+ connection.quote(value, column)
end
# Used to sanitize objects before they're used in an SQL SELECT statement. Delegates to <tt>connection.quote</tt>.
@@ -17,7 +17,7 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a WHERE clause.
# ["name='%s' and group_id='%s'", "foo'bar", 4] returns "name='foo''bar' and group_id='4'"
- # { :name => "foo'bar", :group_id => 4 } returns "name='foo''bar' and group_id='4'"
+ # { name: "foo'bar", group_id: 4 } returns "name='foo''bar' and group_id='4'"
# "name='foo''bar' and group_id='4'" returns "name='foo''bar' and group_id='4'"
def sanitize_sql_for_conditions(condition, table_name = self.table_name)
return nil if condition.blank?
@@ -32,11 +32,11 @@ module ActiveRecord
# Accepts an array, hash, or string of SQL conditions and sanitizes
# them into a valid SQL fragment for a SET clause.
- # { :name => nil, :group_id => 4 } returns "name = NULL , group_id='4'"
- def sanitize_sql_for_assignment(assignments)
+ # { name: nil, group_id: 4 } returns "name = NULL , group_id='4'"
+ def sanitize_sql_for_assignment(assignments, default_table_name = self.table_name)
case assignments
when Array; sanitize_sql_array(assignments)
- when Hash; sanitize_sql_hash_for_assignment(assignments)
+ when Hash; sanitize_sql_hash_for_assignment(assignments, default_table_name)
else assignments
end
end
@@ -46,12 +46,12 @@ module ActiveRecord
# aggregate attribute values.
# Given:
# class Person < ActiveRecord::Base
- # composed_of :address, :class_name => "Address",
- # :mapping => [%w(address_street street), %w(address_city city)]
+ # composed_of :address, class_name: "Address",
+ # mapping: [%w(address_street street), %w(address_city city)]
# end
# Then:
- # { :address => Address.new("813 abc st.", "chicago") }
- # # => { :address_street => "813 abc st.", :address_city => "chicago" }
+ # { address: Address.new("813 abc st.", "chicago") }
+ # # => { address_street: "813 abc st.", address_city: "chicago" }
def expand_hash_conditions_for_aggregates(attrs)
expanded_attrs = {}
attrs.each do |attr, value|
@@ -72,35 +72,36 @@ module ActiveRecord
end
# Sanitizes a hash of attribute/value pairs into SQL conditions for a WHERE clause.
- # { :name => "foo'bar", :group_id => 4 }
+ # { name: "foo'bar", group_id: 4 }
# # => "name='foo''bar' and group_id= 4"
- # { :status => nil, :group_id => [1,2,3] }
+ # { status: nil, group_id: [1,2,3] }
# # => "status IS NULL and group_id IN (1,2,3)"
- # { :age => 13..18 }
+ # { age: 13..18 }
# # => "age BETWEEN 13 AND 18"
# { 'other_records.id' => 7 }
# # => "`other_records`.`id` = 7"
- # { :other_records => { :id => 7 } }
+ # { other_records: { id: 7 } }
# # => "`other_records`.`id` = 7"
# And for value objects on a composed_of relationship:
- # { :address => Address.new("123 abc st.", "chicago") }
+ # { address: Address.new("123 abc st.", "chicago") }
# # => "address_street='123 abc st.' and address_city='chicago'"
def sanitize_sql_hash_for_conditions(attrs, default_table_name = self.table_name)
+ attrs = PredicateBuilder.resolve_column_aliases self, attrs
attrs = expand_hash_conditions_for_aggregates(attrs)
table = Arel::Table.new(table_name, arel_engine).alias(default_table_name)
- PredicateBuilder.build_from_hash(self.class, attrs, table).map { |b|
+ PredicateBuilder.build_from_hash(self, attrs, table).map { |b|
connection.visitor.accept b
}.join(' AND ')
end
alias_method :sanitize_sql_hash, :sanitize_sql_hash_for_conditions
# Sanitizes a hash of attribute/value pairs into SQL conditions for a SET clause.
- # { :status => nil, :group_id => 1 }
+ # { status: nil, group_id: 1 }
# # => "status = NULL , group_id = 1"
- def sanitize_sql_hash_for_assignment(attrs)
+ def sanitize_sql_hash_for_assignment(attrs, table)
attrs.map do |attr, value|
- "#{connection.quote_column_name(attr)} = #{quote_bound_value(value)}"
+ "#{connection.quote_table_name_for_assignment(table, attr)} = #{quote_bound_value(value)}"
end.join(', ')
end
@@ -126,7 +127,17 @@ module ActiveRecord
raise_if_bind_arity_mismatch(statement, statement.count('?'), values.size)
bound = values.dup
c = connection
- statement.gsub('?') { quote_bound_value(bound.shift, c) }
+ statement.gsub('?') do
+ replace_bind_variable(bound.shift, c)
+ end
+ end
+
+ def replace_bind_variable(value, c = connection) #:nodoc:
+ if ActiveRecord::Relation === value
+ value.to_sql
+ else
+ quote_bound_value(value, c)
+ end
end
def replace_named_bind_variables(statement, bind_vars) #:nodoc:
@@ -134,7 +145,7 @@ module ActiveRecord
if $1 == ':' # skip postgresql casts
$& # return the whole match
elsif bind_vars.include?(match = $2.to_sym)
- quote_bound_value(bind_vars[match])
+ replace_bind_variable(bind_vars[match])
else
raise PreparedStatementInvalid, "missing value for :#{match} in #{statement}"
end
diff --git a/activerecord/lib/active_record/schema.rb b/activerecord/lib/active_record/schema.rb
index eaa4aa7086..4bfd0167a4 100644
--- a/activerecord/lib/active_record/schema.rb
+++ b/activerecord/lib/active_record/schema.rb
@@ -29,16 +29,21 @@ module ActiveRecord
# ActiveRecord::Schema is only supported by database adapters that also
# support migrations, the two features being very similar.
class Schema < Migration
+
+ # Returns the migrations paths.
+ #
+ # ActiveRecord::Schema.new.migrations_paths
+ # # => ["db/migrate"] # Rails migration path by default.
def migrations_paths
ActiveRecord::Migrator.migrations_paths
end
- def define(info, &block)
+ def define(info, &block) # :nodoc:
instance_eval(&block)
unless info[:version].blank?
initialize_schema_migrations_table
- assume_migrated_upto_version(info[:version], migrations_paths)
+ connection.assume_migrated_upto_version(info[:version], migrations_paths)
end
end
diff --git a/activerecord/lib/active_record/schema_dumper.rb b/activerecord/lib/active_record/schema_dumper.rb
index 36bde44e7c..e055d571ab 100644
--- a/activerecord/lib/active_record/schema_dumper.rb
+++ b/activerecord/lib/active_record/schema_dumper.rb
@@ -17,13 +17,24 @@ module ActiveRecord
cattr_accessor :ignore_tables
@@ignore_tables = []
- def self.dump(connection=ActiveRecord::Base.connection, stream=STDOUT)
- new(connection).dump(stream)
- stream
+ class << self
+ def dump(connection=ActiveRecord::Base.connection, stream=STDOUT, config = ActiveRecord::Base)
+ new(connection, generate_options(config)).dump(stream)
+ stream
+ end
+
+ private
+ def generate_options(config)
+ {
+ table_name_prefix: config.table_name_prefix,
+ table_name_suffix: config.table_name_suffix
+ }
+ end
end
def dump(stream)
header(stream)
+ extensions(stream)
tables(stream)
trailer(stream)
stream
@@ -31,10 +42,11 @@ module ActiveRecord
private
- def initialize(connection)
+ def initialize(connection, options = {})
@connection = connection
@types = @connection.native_database_types
@version = Migrator::current_version rescue nil
+ @options = options
end
def header(stream)
@@ -66,6 +78,18 @@ HEADER
stream.puts "end"
end
+ def extensions(stream)
+ return unless @connection.supports_extensions?
+ extensions = @connection.extensions
+ if extensions.any?
+ stream.puts " # These are extensions that must be enabled in order to support this database"
+ extensions.each do |extension|
+ stream.puts " enable_extension #{extension.inspect}"
+ end
+ stream.puts
+ end
+ end
+
def tables(stream)
@connection.tables.sort.each do |tbl|
next if ['schema_migrations', ignore_tables].flatten.any? do |ignored|
@@ -93,9 +117,13 @@ HEADER
end
tbl.print " create_table #{remove_prefix_and_suffix(table).inspect}"
- if columns.detect { |c| c.name == pk }
+ pkcol = columns.detect { |c| c.name == pk }
+ if pkcol
if pk != 'id'
tbl.print %Q(, primary_key: "#{pk}")
+ elsif pkcol.sql_type == 'uuid'
+ tbl.print ", id: :uuid"
+ tbl.print %Q(, default: "#{pkcol.default_function}") if pkcol.default_function
end
else
tbl.print ", id: false"
@@ -105,7 +133,7 @@ HEADER
# then dump all non-primary key columns
column_specs = columns.map do |column|
- raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" if @types[column.type].nil?
+ raise StandardError, "Unknown type '#{column.sql_type}' for column '#{column.name}'" unless @connection.valid_type?(column.type)
next if column.name == pk
@connection.column_spec(column, @types)
end.compact
@@ -172,6 +200,10 @@ HEADER
statement_parts << ('where: ' + index.where.inspect) if index.where
+ statement_parts << ('using: ' + index.using.inspect) if index.using
+
+ statement_parts << ('type: ' + index.type.inspect) if index.type
+
' ' + statement_parts.join(', ')
end
@@ -181,7 +213,7 @@ HEADER
end
def remove_prefix_and_suffix(table)
- table.gsub(/^(#{ActiveRecord::Base.table_name_prefix})(.+)(#{ActiveRecord::Base.table_name_suffix})$/, "\\2")
+ table.gsub(/^(#{@options[:table_name_prefix]})(.+)(#{@options[:table_name_suffix]})$/, "\\2")
end
end
end
diff --git a/activerecord/lib/active_record/schema_migration.rb b/activerecord/lib/active_record/schema_migration.rb
index 9830abe7d8..a9d164e366 100644
--- a/activerecord/lib/active_record/schema_migration.rb
+++ b/activerecord/lib/active_record/schema_migration.rb
@@ -4,28 +4,37 @@ require 'active_record/base'
module ActiveRecord
class SchemaMigration < ActiveRecord::Base
+ class << self
- def self.table_name
- "#{Base.table_name_prefix}schema_migrations#{Base.table_name_suffix}"
- end
+ def table_name
+ "#{table_name_prefix}#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.index_name
- "#{Base.table_name_prefix}unique_schema_migrations#{Base.table_name_suffix}"
- end
+ def index_name
+ "#{table_name_prefix}unique_#{ActiveRecord::Base.schema_migrations_table_name}#{table_name_suffix}"
+ end
- def self.create_table
- unless connection.table_exists?(table_name)
- connection.create_table(table_name, :id => false) do |t|
- t.column :version, :string, :null => false
+ def table_exists?
+ connection.table_exists?(table_name)
+ end
+
+ def create_table(limit=nil)
+ unless table_exists?
+ version_options = {null: false}
+ version_options[:limit] = limit if limit
+
+ connection.create_table(table_name, id: false) do |t|
+ t.column :version, :string, version_options
+ end
+ connection.add_index table_name, :version, unique: true, name: index_name
end
- connection.add_index table_name, :version, :unique => true, :name => index_name
end
- end
- def self.drop_table
- if connection.table_exists?(table_name)
- connection.remove_index table_name, :name => index_name
- connection.drop_table(table_name)
+ def drop_table
+ if table_exists?
+ connection.remove_index table_name, name: index_name
+ connection.drop_table(table_name)
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping.rb b/activerecord/lib/active_record/scoping.rb
index 0c3fd1bd29..0cf3d59985 100644
--- a/activerecord/lib/active_record/scoping.rb
+++ b/activerecord/lib/active_record/scoping.rb
@@ -1,3 +1,4 @@
+require 'active_support/per_thread_registry'
module ActiveRecord
module Scoping
@@ -10,11 +11,11 @@ module ActiveRecord
module ClassMethods
def current_scope #:nodoc:
- Thread.current["#{self}_current_scope"]
+ ScopeRegistry.value_for(:current_scope, base_class.to_s)
end
def current_scope=(scope) #:nodoc:
- Thread.current["#{self}_current_scope"] = scope
+ ScopeRegistry.set_value_for(:current_scope, base_class.to_s, scope)
end
end
@@ -26,5 +27,56 @@ module ActiveRecord
end
end
+ # This class stores the +:current_scope+ and +:ignore_default_scope+ values
+ # for different classes. The registry is stored as a thread local, which is
+ # accessed through +ScopeRegistry.current+.
+ #
+ # This class allows you to store and get the scope values on different
+ # classes and different types of scopes. For example, if you are attempting
+ # to get the current_scope for the +Board+ model, then you would use the
+ # following code:
+ #
+ # registry = ActiveRecord::Scoping::ScopeRegistry
+ # registry.set_value_for(:current_scope, "Board", some_new_scope)
+ #
+ # Now when you run:
+ #
+ # registry.value_for(:current_scope, "Board")
+ #
+ # You will obtain whatever was defined in +some_new_scope+. The +value_for+
+ # and +set_value_for+ methods are delegated to the current +ScopeRegistry+
+ # object, so the above example code can also be called as:
+ #
+ # ActiveRecord::Scoping::ScopeRegistry.set_value_for(:current_scope,
+ # "Board", some_new_scope)
+ class ScopeRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ VALID_SCOPE_TYPES = [:current_scope, :ignore_default_scope]
+
+ def initialize
+ @registry = Hash.new { |hash, key| hash[key] = {} }
+ end
+
+ # Obtains the value for a given +scope_name+ and +variable_name+.
+ def value_for(scope_type, variable_name)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name]
+ end
+
+ # Sets the +value+ for a given +scope_type+ and +variable_name+.
+ def set_value_for(scope_type, variable_name, value)
+ raise_invalid_scope_type!(scope_type)
+ @registry[scope_type][variable_name] = value
+ end
+
+ private
+
+ def raise_invalid_scope_type!(scope_type)
+ if !VALID_SCOPE_TYPES.include?(scope_type)
+ raise ArgumentError, "Invalid scope type '#{scope_type}' sent to the registry. Scope types must be included in VALID_SCOPE_TYPES"
+ end
+ end
+ end
end
end
diff --git a/activerecord/lib/active_record/scoping/default.rb b/activerecord/lib/active_record/scoping/default.rb
index 6835d0e01b..01fec31544 100644
--- a/activerecord/lib/active_record/scoping/default.rb
+++ b/activerecord/lib/active_record/scoping/default.rb
@@ -1,4 +1,3 @@
-
module ActiveRecord
module Scoping
module Default
@@ -6,7 +5,8 @@ module ActiveRecord
included do
# Stores the default scope for the class.
- class_attribute :default_scopes, instance_writer: false
+ class_attribute :default_scopes, instance_writer: false, instance_predicate: false
+
self.default_scopes = []
end
@@ -28,14 +28,6 @@ module ActiveRecord
# Post.unscoped {
# Post.limit(10) # Fires "SELECT * FROM posts LIMIT 10"
# }
- #
- # It is recommended that you use the block form of unscoped because
- # chaining unscoped with +scope+ does not work. Assuming that
- # +published+ is a +scope+, the following two statements
- # are equal: the +default_scope+ is applied on both.
- #
- # Post.unscoped.published
- # Post.published
def unscoped
block_given? ? relation.scoping { yield } : relation
end
@@ -91,15 +83,14 @@ module ActiveRecord
scope = Proc.new if block_given?
if scope.is_a?(Relation) || !scope.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Calling #default_scope without a block is deprecated. For example instead " \
+ raise ArgumentError,
+ "Support for calling #default_scope without a block is removed. For example instead " \
"of `default_scope where(color: 'red')`, please use " \
"`default_scope { where(color: 'red') }`. (Alternatively you can just redefine " \
"self.default_scope.)"
- )
end
- self.default_scopes = default_scopes + [scope]
+ self.default_scopes += [scope]
end
def build_default_scope # :nodoc:
@@ -109,22 +100,18 @@ module ActiveRecord
elsif default_scopes.any?
evaluate_default_scope do
default_scopes.inject(relation) do |default_scope, scope|
- if !scope.is_a?(Relation) && scope.respond_to?(:call)
- default_scope.merge(unscoped { scope.call })
- else
- default_scope.merge(scope)
- end
+ default_scope.merge(unscoped { scope.call })
end
end
end
end
def ignore_default_scope? # :nodoc:
- Thread.current["#{self}_ignore_default_scope"]
+ ScopeRegistry.value_for(:ignore_default_scope, self)
end
def ignore_default_scope=(ignore) # :nodoc:
- Thread.current["#{self}_ignore_default_scope"] = ignore
+ ScopeRegistry.set_value_for(:ignore_default_scope, self, ignore)
end
# The ignore_default_scope flag is used to prevent an infinite recursion
@@ -140,7 +127,6 @@ module ActiveRecord
self.ignore_default_scope = false
end
end
-
end
end
end
diff --git a/activerecord/lib/active_record/scoping/named.rb b/activerecord/lib/active_record/scoping/named.rb
index fb5f5b5be0..2a5718f388 100644
--- a/activerecord/lib/active_record/scoping/named.rb
+++ b/activerecord/lib/active_record/scoping/named.rb
@@ -25,22 +25,18 @@ module ActiveRecord
if current_scope
current_scope.clone
else
- scope = relation
- scope.default_scoped = true
- scope
+ default_scoped
end
end
+ def default_scoped # :nodoc:
+ relation.merge(build_default_scope)
+ end
+
# Collects attributes from scopes that should be applied when creating
# an AR instance for the particular class this is called on.
def scope_attributes # :nodoc:
- if current_scope
- current_scope.scope_for_create
- else
- scope = relation
- scope.default_scoped = true
- scope.scope_for_create
- end
+ all.scope_for_create
end
# Are there default attributes associated with this scope?
@@ -116,7 +112,7 @@ module ActiveRecord
# Scopes can also be used while creating/building a record.
#
# class Article < ActiveRecord::Base
- # scope :published, -> { where(published: true) }
+ # scope :published, -> { where(published: true) }
# end
#
# Article.published.new.published # => true
@@ -126,7 +122,7 @@ module ActiveRecord
# on scopes. Assuming the following setup:
#
# class Article < ActiveRecord::Base
- # scope :published, -> { where(published: true) }
+ # scope :published, -> { where(published: true) }
# scope :featured, -> { where(featured: true) }
#
# def self.latest_article
@@ -134,37 +130,22 @@ module ActiveRecord
# end
#
# def self.titles
- # map(&:title)
+ # pluck(:title)
# end
- #
# end
#
# We are able to call the methods like this:
#
# Article.published.featured.latest_article
# Article.featured.titles
-
def scope(name, body, &block)
extension = Module.new(&block) if block
- # Check body.is_a?(Relation) to prevent the relation actually being
- # loaded by respond_to?
- if body.is_a?(Relation) || !body.respond_to?(:call)
- ActiveSupport::Deprecation.warn(
- "Using #scope without passing a callable object is deprecated. For " \
- "example `scope :red, where(color: 'red')` should be changed to " \
- "`scope :red, -> { where(color: 'red') }`. There are numerous gotchas " \
- "in the former usage and it makes the implementation more complicated " \
- "and buggy. (If you prefer, you can just define a class method named " \
- "`self.red`.)"
- )
- end
-
singleton_class.send(:define_method, name) do |*args|
- options = body.respond_to?(:call) ? unscoped { body.call(*args) } : body
- relation = all.merge(options)
+ scope = all.scoping { body.call(*args) }
+ scope = scope.extending(extension) if extension
- extension ? relation.extending(extension) : relation
+ scope || all
end
end
end
diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb
index e8dd312a47..bd9079b596 100644
--- a/activerecord/lib/active_record/serialization.rb
+++ b/activerecord/lib/active_record/serialization.rb
@@ -1,19 +1,11 @@
module ActiveRecord #:nodoc:
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :include_root_in_json, instance_accessor: false
- self.include_root_in_json = true
- end
-
# = Active Record Serialization
module Serialization
extend ActiveSupport::Concern
include ActiveModel::Serializers::JSON
included do
- singleton_class.class_eval do
- remove_method :include_root_in_json
- delegate :include_root_in_json, to: 'ActiveRecord::Model'
- end
+ self.include_root_in_json = false
end
def serializable_hash(options = nil)
diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb
index 834d01a1e8..1a766093d0 100644
--- a/activerecord/lib/active_record/serializers/xml_serializer.rb
+++ b/activerecord/lib/active_record/serializers/xml_serializer.rb
@@ -36,7 +36,7 @@ module ActiveRecord #:nodoc:
#
# For instance:
#
- # topic.to_xml(:skip_instruct => true, :except => [ :id, :bonus_time, :written_on, :replies_count ])
+ # topic.to_xml(skip_instruct: true, except: [ :id, :bonus_time, :written_on, :replies_count ])
#
# <topic>
# <title>The First Topic</title>
@@ -50,7 +50,7 @@ module ActiveRecord #:nodoc:
#
# To include first level associations use <tt>:include</tt>:
#
- # firm.to_xml :include => [ :account, :clients ]
+ # firm.to_xml include: [ :account, :clients ]
#
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
@@ -81,7 +81,7 @@ module ActiveRecord #:nodoc:
# associated with models.
#
# proc = Proc.new { |options, record| options[:builder].tag!('name-reverse', record.name.reverse) }
- # firm.to_xml :procs => [ proc ]
+ # firm.to_xml procs: [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -90,7 +90,7 @@ module ActiveRecord #:nodoc:
#
# To include deeper levels of associations pass a hash like this:
#
- # firm.to_xml :include => {:account => {}, :clients => {:include => :address}}
+ # firm.to_xml include: {account: {}, clients: {include: :address}}
# <?xml version="1.0" encoding="UTF-8"?>
# <firm>
# <id type="integer">1</id>
@@ -120,7 +120,7 @@ module ActiveRecord #:nodoc:
#
# To include any methods on the model being called use <tt>:methods</tt>:
#
- # firm.to_xml :methods => [ :calculated_earnings, :real_earnings ]
+ # firm.to_xml methods: [ :calculated_earnings, :real_earnings ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -132,7 +132,7 @@ module ActiveRecord #:nodoc:
# modified version of the options hash that was given to +to_xml+:
#
# proc = Proc.new { |options| options[:builder].tag!('abc', 'def') }
- # firm.to_xml :procs => [ proc ]
+ # firm.to_xml procs: [ proc ]
#
# <firm>
# # ... normal attributes as shown above ...
@@ -164,7 +164,7 @@ module ActiveRecord #:nodoc:
# def to_xml(options = {})
# require 'builder'
# options[:indent] ||= 2
- # xml = options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent])
+ # xml = options[:builder] ||= ::Builder::XmlMarkup.new(indent: options[:indent])
# xml.instruct! unless options[:skip_instruct]
# xml.level_one do
# xml.tag!(:second_level, 'content')
diff --git a/activerecord/lib/active_record/statement_cache.rb b/activerecord/lib/active_record/statement_cache.rb
new file mode 100644
index 0000000000..dd4ee0c4a0
--- /dev/null
+++ b/activerecord/lib/active_record/statement_cache.rb
@@ -0,0 +1,26 @@
+module ActiveRecord
+
+ # Statement cache is used to cache a single statement in order to avoid creating the AST again.
+ # Initializing the cache is done by passing the statement in the initialization block:
+ #
+ # cache = ActiveRecord::StatementCache.new do
+ # Book.where(name: "my book").limit(100)
+ # end
+ #
+ # The cached statement is executed by using the +execute+ method:
+ #
+ # cache.execute
+ #
+ # 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?
+ end
+
+ def execute
+ @relation.dup.to_a
+ end
+ end
+end
diff --git a/activerecord/lib/active_record/store.rb b/activerecord/lib/active_record/store.rb
index df7f58c81f..b841b977fc 100644
--- a/activerecord/lib/active_record/store.rb
+++ b/activerecord/lib/active_record/store.rb
@@ -42,21 +42,19 @@ module ActiveRecord
#
# All stored values are automatically available through accessors on the Active Record
# object, but sometimes you want to specialize this behavior. This can be done by overwriting
- # the default accessors (using the same name as the attribute) and calling
- # <tt>read_store_attribute(store_attribute_name, attr_name)</tt> and
- # <tt>write_store_attribute(store_attribute_name, attr_name, value)</tt> to actually
- # change things.
+ # the default accessors (using the same name as the attribute) and calling <tt>super</tt>
+ # to actually change things.
#
# class Song < ActiveRecord::Base
# # Uses a stored integer to hold the volume adjustment of the song
# store :settings, accessors: [:volume_adjustment]
#
# def volume_adjustment=(decibels)
- # write_store_attribute(:settings, :volume_adjustment, decibels.to_i)
+ # super(decibels.to_i)
# end
#
# def volume_adjustment
- # read_store_attribute(:settings, :volume_adjustment).to_i
+ # super.to_i
# end
# end
module Store
@@ -75,43 +73,89 @@ module ActiveRecord
def store_accessor(store_attribute, *keys)
keys = keys.flatten
- keys.each do |key|
- define_method("#{key}=") do |value|
- write_store_attribute(store_attribute, key, value)
- end
- define_method(key) do
- read_store_attribute(store_attribute, key)
+ _store_accessors_module.module_eval do
+ keys.each do |key|
+ define_method("#{key}=") do |value|
+ write_store_attribute(store_attribute, key, value)
+ end
+
+ define_method(key) do
+ read_store_attribute(store_attribute, key)
+ end
end
end
+ # assign new store attribute and create new hash to ensure that each class in the hierarchy
+ # has its own hash of stored attributes.
+ self.stored_attributes = {} if self.stored_attributes.blank?
self.stored_attributes[store_attribute] ||= []
self.stored_attributes[store_attribute] |= keys
end
+
+ def _store_accessors_module
+ @_store_accessors_module ||= begin
+ mod = Module.new
+ include mod
+ mod
+ end
+ end
end
protected
def read_store_attribute(store_attribute, key)
- attribute = initialize_store_attribute(store_attribute)
- attribute[key]
+ accessor = store_accessor_for(store_attribute)
+ accessor.read(self, store_attribute, key)
end
def write_store_attribute(store_attribute, key, value)
- attribute = initialize_store_attribute(store_attribute)
- if value != attribute[key]
- send :"#{store_attribute}_will_change!"
- attribute[key] = value
- end
+ accessor = store_accessor_for(store_attribute)
+ accessor.write(self, store_attribute, key, value)
end
private
- def initialize_store_attribute(store_attribute)
- attribute = send(store_attribute)
- unless attribute.is_a?(HashWithIndifferentAccess)
- attribute = IndifferentCoder.as_indifferent_hash(attribute)
- send :"#{store_attribute}=", attribute
+ def store_accessor_for(store_attribute)
+ @column_types[store_attribute.to_s].accessor
+ end
+
+ class HashAccessor
+ def self.read(object, attribute, key)
+ prepare(object, attribute)
+ object.public_send(attribute)[key]
+ end
+
+ def self.write(object, attribute, key, value)
+ prepare(object, attribute)
+ if value != read(object, attribute, key)
+ object.public_send :"#{attribute}_will_change!"
+ object.public_send(attribute)[key] = value
+ end
+ end
+
+ def self.prepare(object, attribute)
+ object.public_send :"#{attribute}=", {} unless object.send(attribute)
+ end
+ end
+
+ class StringKeyedHashAccessor < HashAccessor
+ def self.read(object, attribute, key)
+ super object, attribute, key.to_s
+ end
+
+ def self.write(object, attribute, key, value)
+ super object, attribute, key.to_s, value
+ end
+ end
+
+ class IndifferentHashAccessor < ActiveRecord::Store::HashAccessor
+ def self.prepare(object, store_attribute)
+ attribute = object.send(store_attribute)
+ unless attribute.is_a?(ActiveSupport::HashWithIndifferentAccess)
+ attribute = IndifferentCoder.as_indifferent_hash(attribute)
+ object.send :"#{store_attribute}=", attribute
+ end
+ attribute
end
- attribute
end
class IndifferentCoder # :nodoc:
@@ -129,17 +173,17 @@ module ActiveRecord
end
def load(yaml)
- self.class.as_indifferent_hash @coder.load(yaml)
+ self.class.as_indifferent_hash(@coder.load(yaml))
end
def self.as_indifferent_hash(obj)
case obj
- when HashWithIndifferentAccess
+ when ActiveSupport::HashWithIndifferentAccess
obj
when Hash
obj.with_indifferent_access
else
- HashWithIndifferentAccess.new
+ ActiveSupport::HashWithIndifferentAccess.new
end
end
end
diff --git a/activerecord/lib/active_record/tasks/database_tasks.rb b/activerecord/lib/active_record/tasks/database_tasks.rb
index fda51b3d76..be7d496d15 100644
--- a/activerecord/lib/active_record/tasks/database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/database_tasks.rb
@@ -1,9 +1,44 @@
module ActiveRecord
module Tasks # :nodoc:
- module DatabaseTasks # :nodoc:
+ class DatabaseAlreadyExists < StandardError; end # :nodoc:
+ class DatabaseNotSupported < StandardError; end # :nodoc:
+
+ # <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.
+ #
+ # 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
+ # want to change the defaults or when you want to use Active Record outside of Rails
+ # (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).
+ # * +database_configuration+: configuration of your databases (as in +config/database.yml+).
+ # * +db_dir+: your +db+ directory.
+ # * +fixtures_path+: a path to fixtures directory.
+ # * +migrations_paths+: a list of paths to directories with migrations.
+ # * +seed_loader+: an object which will load seeds, it needs to respond to the +load_seed+ method.
+ # * +root+: a path to the root of the application.
+ #
+ # Example usage of +DatabaseTasks+ outside Rails could look as such:
+ #
+ # include ActiveRecord::Tasks
+ # DatabaseTasks.database_configuration = YAML.load(File.read('my_database_config.yml'))
+ # DatabaseTasks.db_dir = 'db'
+ # # other settings...
+ #
+ # DatabaseTasks.create_current('production')
+ module DatabaseTasks
extend self
attr_writer :current_config
+ attr_accessor :database_configuration, :migrations_paths, :seed_loader, :db_dir,
+ :fixtures_path, :env, :root
LOCAL_HOSTS = ['127.0.0.1', 'localhost']
@@ -12,12 +47,12 @@ module ActiveRecord
@tasks[pattern] = task
end
- register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
- register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
- register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
+ register_task(/mysql/, ActiveRecord::Tasks::MySQLDatabaseTasks)
+ register_task(/postgresql/, ActiveRecord::Tasks::PostgreSQLDatabaseTasks)
+ register_task(/sqlite/, ActiveRecord::Tasks::SQLiteDatabaseTasks)
def current_config(options = {})
- options.reverse_merge! :env => Rails.env
+ options.reverse_merge! :env => env
if options.has_key?(:config)
@current_config = options[:config]
else
@@ -32,6 +67,8 @@ module ActiveRecord
def create(*arguments)
configuration = arguments.first
class_for_adapter(configuration['adapter']).new(*arguments).create
+ rescue DatabaseAlreadyExists
+ $stderr.puts "#{configuration['database']} already exists"
rescue Exception => error
$stderr.puts error, *(error.backtrace)
$stderr.puts "Couldn't create database for #{configuration.inspect}"
@@ -41,7 +78,7 @@ module ActiveRecord
each_local_configuration { |configuration| create configuration }
end
- def create_current(environment = Rails.env)
+ def create_current(environment = env)
each_current_configuration(environment) { |configuration|
create configuration
}
@@ -64,7 +101,7 @@ module ActiveRecord
each_local_configuration { |configuration| drop configuration }
end
- def drop_current(environment = Rails.env)
+ def drop_current(environment = env)
each_current_configuration(environment) { |configuration|
drop configuration
}
@@ -74,7 +111,7 @@ module ActiveRecord
drop database_url_config
end
- def charset_current(environment = Rails.env)
+ def charset_current(environment = env)
charset ActiveRecord::Base.configurations[environment]
end
@@ -83,7 +120,7 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).charset
end
- def collation_current(environment = Rails.env)
+ def collation_current(environment = env)
collation ActiveRecord::Base.configurations[environment]
end
@@ -108,6 +145,24 @@ module ActiveRecord
class_for_adapter(configuration['adapter']).new(*arguments).structure_load(filename)
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.}
+ message << %{ If you do not intend to use a database, you should instead alter #{Rails.root}/config/application.rb to limit the frameworks that will be loaded.} if defined?(::Rails)
+ Kernel.abort message
+ end
+ end
+
+ def load_seed
+ if seed_loader
+ seed_loader.load_seed
+ else
+ raise "You tried to load seed data, but no seed loader is specified. Please specify seed " +
+ "loader with ActiveRecord::Tasks::DatabaseTasks.seed_loader = your_seed_loader\n" +
+ "Seed loader should respond to load_seed method"
+ end
+ end
+
private
def database_url_config
@@ -117,12 +172,15 @@ module ActiveRecord
def class_for_adapter(adapter)
key = @tasks.keys.detect { |pattern| adapter[pattern] }
+ unless key
+ raise DatabaseNotSupported, "Rake tasks not supported by '#{adapter}' adapter"
+ end
@tasks[key]
end
def each_current_configuration(environment)
environments = [environment]
- environments << 'test' if environment.development?
+ environments << 'test' if environment == 'development'
configurations = ActiveRecord::Base.configurations.values_at(*environments)
configurations.compact.each do |configuration|
diff --git a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
index 3d27c97254..c755831e6d 100644
--- a/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/mysql_database_tasks.rb
@@ -1,7 +1,6 @@
module ActiveRecord
module Tasks # :nodoc:
class MySQLDatabaseTasks # :nodoc:
-
DEFAULT_CHARSET = ENV['CHARSET'] || 'utf8'
DEFAULT_COLLATION = ENV['COLLATION'] || 'utf8_unicode_ci'
ACCESS_DENIED_ERROR = 1045
@@ -16,18 +15,25 @@ module ActiveRecord
establish_connection configuration_without_database
connection.create_database configuration['database'], creation_options
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
rescue error_class => error
- raise error unless error.errno == ACCESS_DENIED_ERROR
-
- $stdout.print error.error
- establish_connection root_configuration_without_database
- connection.create_database configuration['database'], creation_options
- connection.execute grant_statement.gsub(/\s+/, ' ').strip
- establish_connection configuration
- rescue error_class => error
- $stderr.puts error.error
- $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
- $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ if error.respond_to?(:errno) && error.errno == ACCESS_DENIED_ERROR
+ $stdout.print error.error
+ establish_connection root_configuration_without_database
+ connection.create_database configuration['database'], creation_options
+ if configuration['username'] != 'root'
+ connection.execute grant_statement.gsub(/\s+/, ' ').strip
+ end
+ establish_connection configuration
+ else
+ $stderr.puts "Couldn't create database for #{configuration.inspect}, #{creation_options.inspect}"
+ $stderr.puts "(If you set the charset manually, make sure you have a matching collation)" if configuration['encoding']
+ end
end
def drop
@@ -53,7 +59,10 @@ module ActiveRecord
args.concat(["--result-file", "#{filename}"])
args.concat(["--no-data"])
args.concat(["#{configuration['database']}"])
- Kernel.system(*args)
+ unless Kernel.system(*args)
+ $stderr.puts "Could not dump the database structure. "\
+ "Make sure `mysqldump` is in your PATH and check the command output for warnings."
+ end
end
def structure_load(filename)
@@ -87,14 +96,15 @@ module ActiveRecord
end
def error_class
- case configuration['adapter']
- when /jdbc/
+ if configuration['adapter'] =~ /jdbc/
require 'active_record/railties/jdbcmysql_error'
ArJdbcMySQL::Error
- when /mysql2/
+ elsif defined?(Mysql2)
Mysql2::Error
- else
+ elsif defined?(Mysql)
Mysql::Error
+ else
+ StandardError
end
end
@@ -124,11 +134,11 @@ IDENTIFIED BY '#{configuration['password']}' WITH GRANT OPTION;
args << "--password=#{configuration['password']}" if configuration['password']
args.concat(['--default-character-set', configuration['encoding']]) if configuration['encoding']
configuration.slice('host', 'port', 'socket').each do |k, v|
- args.concat([ "--#{k}", v ]) if v
+ args.concat([ "--#{k}", v.to_s ]) if v
end
+
args
end
-
end
end
end
diff --git a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
index ea5cb888fb..3d02ee07d0 100644
--- a/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/postgresql_database_tasks.rb
@@ -3,7 +3,6 @@ require 'shellwords'
module ActiveRecord
module Tasks # :nodoc:
class PostgreSQLDatabaseTasks # :nodoc:
-
DEFAULT_ENCODING = ENV['CHARSET'] || 'utf8'
delegate :connection, :establish_connection, :clear_active_connections!,
@@ -18,6 +17,12 @@ module ActiveRecord
connection.create_database configuration['database'],
configuration.merge('encoding' => encoding)
establish_connection configuration
+ rescue ActiveRecord::StatementInvalid => error
+ if /database .* already exists/ === error.message
+ raise DatabaseAlreadyExists
+ else
+ raise
+ end
end
def drop
@@ -54,7 +59,7 @@ module ActiveRecord
def structure_load(filename)
set_psql_env
- Kernel.system("psql -f #{filename} #{configuration['database']}")
+ Kernel.system("psql -q -f #{Shellwords.escape(filename)} #{configuration['database']}")
end
private
diff --git a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
index da01058a82..5688931db2 100644
--- a/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
+++ b/activerecord/lib/active_record/tasks/sqlite_database_tasks.rb
@@ -1,18 +1,14 @@
module ActiveRecord
module Tasks # :nodoc:
class SQLiteDatabaseTasks # :nodoc:
-
delegate :connection, :establish_connection, to: ActiveRecord::Base
- def initialize(configuration, root = Rails.root)
+ def initialize(configuration, root = ActiveRecord::Tasks::DatabaseTasks.root)
@configuration, @root = configuration, root
end
def create
- if File.exist?(configuration['database'])
- $stderr.puts "#{configuration['database']} already exists"
- return
- end
+ raise DatabaseAlreadyExists if File.exist?(configuration['database'])
establish_connection configuration
connection
diff --git a/activerecord/lib/active_record/test_case.rb b/activerecord/lib/active_record/test_case.rb
deleted file mode 100644
index c035ad43a2..0000000000
--- a/activerecord/lib/active_record/test_case.rb
+++ /dev/null
@@ -1,95 +0,0 @@
-require 'active_support/test_case'
-
-ActiveSupport::Deprecation.warn('ActiveRecord::TestCase is deprecated, please use ActiveSupport::TestCase')
-module ActiveRecord
- # = Active Record Test Case
- #
- # Defines some test assertions to test against SQL queries.
- class TestCase < ActiveSupport::TestCase #:nodoc:
- def teardown
- SQLCounter.clear_log
- end
-
- def assert_date_from_db(expected, actual, message = nil)
- # SybaseAdapter doesn't have a separate column type just for dates,
- # so the time is in the string and incorrectly formatted
- if current_adapter?(:SybaseAdapter)
- assert_equal expected.to_s, actual.to_date.to_s, message
- else
- assert_equal expected.to_s, actual.to_s, message
- end
- end
-
- def assert_sql(*patterns_to_match)
- SQLCounter.clear_log
- yield
- SQLCounter.log_all
- ensure
- failed_patterns = []
- patterns_to_match.each do |pattern|
- failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
- end
- assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
- end
-
- def assert_queries(num = 1, options = {})
- ignore_none = options.fetch(:ignore_none) { num == :any }
- SQLCounter.clear_log
- yield
- ensure
- the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
- if num == :any
- assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
- else
- mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
- assert_equal num, the_log.size, mesg
- end
- end
-
- def assert_no_queries(&block)
- assert_queries(0, :ignore_none => true, &block)
- end
-
- end
-
- class SQLCounter
- class << self
- attr_accessor :ignored_sql, :log, :log_all
- def clear_log; self.log = []; self.log_all = []; end
- end
-
- self.clear_log
-
- self.ignored_sql = [/^PRAGMA (?!(table_info))/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
-
- # FIXME: this needs to be refactored so specific database can add their own
- # ignored SQL, or better yet, use a different notification for the queries
- # instead examining the SQL content.
- oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
- mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
- postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im]
-
- [oracle_ignored, mysql_ignored, postgresql_ignored].each do |db_ignored_sql|
- ignored_sql.concat db_ignored_sql
- end
-
- attr_reader :ignore
-
- def initialize(ignore = Regexp.union(self.class.ignored_sql))
- @ignore = ignore
- end
-
- def call(name, start, finish, message_id, values)
- sql = values[:sql]
-
- # FIXME: this seems bad. we should probably have a better way to indicate
- # the query was cached
- return if 'CACHE' == values[:name]
-
- self.class.log_all << sql
- self.class.log << sql unless ignore =~ sql
- end
- end
-
- ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
-end
diff --git a/activerecord/lib/active_record/timestamp.rb b/activerecord/lib/active_record/timestamp.rb
index bf95ccb298..9253150c4f 100644
--- a/activerecord/lib/active_record/timestamp.rb
+++ b/activerecord/lib/active_record/timestamp.rb
@@ -1,10 +1,5 @@
module ActiveRecord
- ActiveSupport.on_load(:active_record_config) do
- mattr_accessor :record_timestamps, instance_accessor: false
- self.record_timestamps = true
- end
-
# = Active Record Timestamp
#
# Active Record automatically timestamps create and update operations if the
@@ -15,9 +10,9 @@ module ActiveRecord
#
# config.active_record.record_timestamps = false
#
- # Timestamps are in the local timezone by default but you can use UTC by setting:
+ # Timestamps are in UTC by default but you can use the local timezone by setting:
#
- # config.active_record.default_timezone = :utc
+ # config.active_record.default_timezone = :local
#
# == Time Zone aware attributes
#
@@ -37,16 +32,18 @@ module ActiveRecord
extend ActiveSupport::Concern
included do
- config_attribute :record_timestamps, instance_writer: true
+ class_attribute :record_timestamps
+ self.record_timestamps = true
end
def initialize_dup(other) # :nodoc:
clear_timestamp_attributes
+ super
end
private
- def create
+ def create_record
if self.record_timestamps
current_time = current_time_from_proper_timezone
@@ -60,7 +57,7 @@ module ActiveRecord
super
end
- def update(*args)
+ def update_record(*args)
if should_record_timestamps?
current_time = current_time_from_proper_timezone
@@ -74,7 +71,7 @@ module ActiveRecord
end
def should_record_timestamps?
- self.record_timestamps && (!partial_updates? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
+ self.record_timestamps && (!partial_writes? || changed? || (attributes.keys & self.class.serialized_attributes.keys).present?)
end
def timestamp_attributes_for_create_in_model
@@ -101,6 +98,12 @@ module ActiveRecord
timestamp_attributes_for_create + timestamp_attributes_for_update
end
+ def max_updated_column_timestamp
+ if (timestamps = timestamp_attributes_for_update.map { |attr| self[attr] }.compact).present?
+ timestamps.map { |ts| ts.to_time }.max
+ end
+ end
+
def current_time_from_proper_timezone
self.class.default_timezone == :utc ? Time.now.utc : Time.now
end
diff --git a/activerecord/lib/active_record/transactions.rb b/activerecord/lib/active_record/transactions.rb
index 934393b4e7..45313b5e75 100644
--- a/activerecord/lib/active_record/transactions.rb
+++ b/activerecord/lib/active_record/transactions.rb
@@ -4,12 +4,15 @@ module ActiveRecord
# See ActiveRecord::Transactions::ClassMethods for documentation.
module Transactions
extend ActiveSupport::Concern
+ ACTIONS = [:create, :destroy, :update]
class TransactionError < ActiveRecordError # :nodoc:
end
included do
- define_callbacks :commit, :rollback, :terminator => "result == false", :scope => [:kind, :name]
+ define_callbacks :commit, :rollback,
+ terminator: ->(_, result) { result == false },
+ scope: [:kind, :name]
end
# = Active Record Transactions
@@ -108,10 +111,10 @@ module ActiveRecord
#
# # Suppose that we have a Number model with a unique column called 'i'.
# Number.transaction do
- # Number.create(:i => 0)
+ # Number.create(i: 0)
# begin
# # This will raise a unique constraint error...
- # Number.create(:i => 0)
+ # Number.create(i: 0)
# rescue ActiveRecord::StatementInvalid
# # ...which we ignore.
# end
@@ -119,7 +122,7 @@ module ActiveRecord
# # On PostgreSQL, the transaction is now unusable. The following
# # statement will cause a PostgreSQL error, even though the unique
# # constraint is no longer violated:
- # Number.create(:i => 1)
+ # Number.create(i: 1)
# # => "PGError: ERROR: current transaction is aborted, commands
# # ignored until end of transaction block"
# end
@@ -134,9 +137,9 @@ module ActiveRecord
# transaction. For example, the following behavior may be surprising:
#
# User.transaction do
- # User.create(:username => 'Kotori')
+ # User.create(username: 'Kotori')
# User.transaction do
- # User.create(:username => 'Nemu')
+ # User.create(username: 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
@@ -147,19 +150,19 @@ module ActiveRecord
# real transaction is committed.
#
# In order to get a ROLLBACK for the nested transaction you may ask for a real
- # sub-transaction by passing <tt>:requires_new => true</tt>. If anything goes wrong,
+ # sub-transaction by passing <tt>requires_new: true</tt>. If anything goes wrong,
# the database rolls back to the beginning of the sub-transaction without rolling
# back the parent transaction. If we add it to the previous example:
#
# User.transaction do
- # User.create(:username => 'Kotori')
- # User.transaction(:requires_new => true) do
- # User.create(:username => 'Nemu')
+ # User.create(username: 'Kotori')
+ # User.transaction(requires_new: true) do
+ # User.create(username: 'Nemu')
# raise ActiveRecord::Rollback
# end
# end
#
- # only "Kotori" is created. (This works on MySQL and PostgreSQL, but not on SQLite3.)
+ # only "Kotori" is created. This works on MySQL and PostgreSQL. SQLite3 version >= '3.6.8' also supports it.
#
# Most databases don't support true nested transactions. At the time of
# writing, the only database that we're aware of that supports true nested
@@ -194,7 +197,7 @@ module ActiveRecord
# automatically released. The following example demonstrates the problem:
#
# Model.connection.transaction do # BEGIN
- # Model.connection.transaction(:requires_new => true) do # CREATE SAVEPOINT active_record_1
+ # Model.connection.transaction(requires_new: true) do # CREATE SAVEPOINT active_record_1
# Model.connection.create_table(...) # active_record_1 now automatically released
# end # RELEASE savepoint active_record_1
# # ^^^^ BOOM! database error!
@@ -213,22 +216,17 @@ module ActiveRecord
# You can specify that the callback should only be fired by a certain action with
# the +:on+ option:
#
- # after_commit :do_foo, :on => :create
- # after_commit :do_bar, :on => :update
- # after_commit :do_baz, :on => :destroy
+ # after_commit :do_foo, on: :create
+ # after_commit :do_bar, on: :update
+ # after_commit :do_baz, on: :destroy
#
- # Also, to have the callback fired on create and update, but not on destroy:
- #
- # after_commit :do_zoo, :if => :persisted?
+ # after_commit :do_foo_bar, on: [:create, :update]
+ # after_commit :do_bar_baz, on: [:update, :destroy]
#
# Note that transactional fixtures do not play well with this feature. Please
# use the +test_after_commit+ gem to have these hooks fired in tests.
def after_commit(*args, &block)
- options = args.last
- if options.is_a?(Hash) && options[:on]
- options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
- end
+ set_options_for_callbacks!(args)
set_callback(:commit, :after, *args, &block)
end
@@ -236,12 +234,27 @@ module ActiveRecord
#
# Please check the documentation of +after_commit+ for options.
def after_rollback(*args, &block)
+ set_options_for_callbacks!(args)
+ set_callback(:rollback, :after, *args, &block)
+ end
+
+ private
+
+ def set_options_for_callbacks!(args)
options = args.last
if options.is_a?(Hash) && options[:on]
+ assert_valid_transaction_action(options[:on])
options[:if] = Array(options[:if])
- options[:if] << "transaction_include_action?(:#{options[:on]})"
+ fire_on = Array(options[:on])
+ options[:if] << "transaction_include_any_action?(#{fire_on})"
+ end
+ end
+
+ def assert_valid_transaction_action(actions)
+ actions = Array(actions)
+ if (actions - ACTIONS).any?
+ raise ArgumentError, ":on conditions for after_commit and after_rollback callbacks have to be one of #{ACTIONS.join(",")}"
end
- set_callback(:rollback, :after, *args, &block)
end
end
@@ -275,22 +288,26 @@ module ActiveRecord
clear_transaction_record_state
end
- # Call the after_commit callbacks
+ # Call the +after_commit+ callbacks.
+ #
+ # Ensure that it is not called if the object was never persisted (failed create),
+ # but call it after the commit of a destroyed object.
def committed! #:nodoc:
- run_callbacks :commit
+ run_callbacks :commit if destroyed? || persisted?
ensure
clear_transaction_record_state
end
- # Call the after rollback callbacks. The restore_state argument indicates if the record
+ # Call the +after_rollback+ callbacks. The +force_restore_state+ argument indicates if the record
# state should be rolled back to the beginning or just to the last savepoint.
def rolledback!(force_restore_state = false) #:nodoc:
run_callbacks :rollback
ensure
restore_transaction_record_state(force_restore_state)
+ clear_transaction_record_state
end
- # Add the record to the current transaction so that the :after_rollback and :after_commit callbacks
+ # Add the record to the current transaction so that the +after_rollback+ and +after_commit+ callbacks
# can be called.
def add_to_transaction
if self.class.connection.add_transaction_record(self)
@@ -325,9 +342,14 @@ 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[:new_record] = @new_record
- @_start_transaction_state[:destroyed] = @destroyed
+ unless @_start_transaction_state.include?(:new_record)
+ @_start_transaction_state[:new_record] = @new_record
+ end
+ unless @_start_transaction_state.include?(:destroyed)
+ @_start_transaction_state[:destroyed] = @destroyed
+ end
@_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) + 1
+ @_start_transaction_state[:frozen?] = @attributes.frozen?
end
# Clear the new record state and id of a record.
@@ -339,11 +361,11 @@ module ActiveRecord
# Restore the new record state and id of a record that was previously saved by a call to save_record_state.
def restore_transaction_record_state(force = false) #:nodoc:
unless @_start_transaction_state.empty?
- @_start_transaction_state[:level] = (@_start_transaction_state[:level] || 0) - 1
- if @_start_transaction_state[:level] < 1 || force
+ transaction_level = (@_start_transaction_state[:level] || 0) - 1
+ if transaction_level < 1 || force
restore_state = @_start_transaction_state
- was_frozen = @attributes.frozen?
- @attributes = @attributes.dup if was_frozen
+ was_frozen = restore_state[:frozen?]
+ @attributes = @attributes.dup if @attributes.frozen?
@new_record = restore_state[:new_record]
@destroyed = restore_state[:destroyed]
if restore_state.has_key?(:id)
@@ -353,7 +375,6 @@ module ActiveRecord
@attributes_cache.delete(self.class.primary_key)
end
@attributes.freeze if was_frozen
- @_start_transaction_state.clear
end
end
end
@@ -364,14 +385,16 @@ module ActiveRecord
end
# Determine if a transaction included an action for :create, :update, or :destroy. Used in filtering callbacks.
- def transaction_include_action?(action) #:nodoc:
- case action
- when :create
- transaction_record_state(:new_record)
- when :destroy
- destroyed?
- when :update
- !(transaction_record_state(:new_record) || destroyed?)
+ def transaction_include_any_action?(actions) #:nodoc:
+ actions.any? do |action|
+ case action
+ when :create
+ transaction_record_state(:new_record)
+ when :destroy
+ destroyed?
+ when :update
+ !(transaction_record_state(:new_record) || destroyed?)
+ end
end
end
end
diff --git a/activerecord/lib/active_record/validations.rb b/activerecord/lib/active_record/validations.rb
index 3706885881..26dca415ff 100644
--- a/activerecord/lib/active_record/validations.rb
+++ b/activerecord/lib/active_record/validations.rb
@@ -74,8 +74,7 @@ module ActiveRecord
protected
def perform_validations(options={}) # :nodoc:
- perform_validation = options[:validate] != false
- perform_validation ? valid?(options[:context]) : true
+ options[:validate] == false || valid?(options[:context])
end
end
end
diff --git a/activerecord/lib/active_record/validations/associated.rb b/activerecord/lib/active_record/validations/associated.rb
index 7f1972ccf9..b4785d3ba4 100644
--- a/activerecord/lib/active_record/validations/associated.rb
+++ b/activerecord/lib/active_record/validations/associated.rb
@@ -2,15 +2,15 @@ module ActiveRecord
module Validations
class AssociatedValidator < ActiveModel::EachValidator #:nodoc:
def validate_each(record, attribute, value)
- if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?(record.validation_context) }.any?
+ if Array.wrap(value).reject {|r| r.marked_for_destruction? || r.valid?}.any?
record.errors.add(attribute, :invalid, options.merge(:value => value))
end
end
end
module ClassMethods
- # Validates whether the associated object or objects are all valid
- # themselves. Works with any kind of association.
+ # Validates whether the associated object or objects are all valid.
+ # Works with any kind of association.
#
# class Book < ActiveRecord::Base
# has_many :pages
diff --git a/activerecord/lib/active_record/validations/presence.rb b/activerecord/lib/active_record/validations/presence.rb
index 81a3521d24..6b14c39686 100644
--- a/activerecord/lib/active_record/validations/presence.rb
+++ b/activerecord/lib/active_record/validations/presence.rb
@@ -5,8 +5,10 @@ module ActiveRecord
super
attributes.each do |attribute|
next unless record.class.reflect_on_association(attribute)
- value = record.send(attribute)
- if Array(value).all? { |r| r.marked_for_destruction? }
+ associated_records = Array(record.send(attribute))
+
+ # Superclass validates presence. Ensure present records aren't about to be destroyed.
+ if associated_records.present? && associated_records.all? { |r| r.marked_for_destruction? }
record.errors.add(attribute, :blank, options)
end
end
diff --git a/activerecord/lib/active_record/validations/uniqueness.rb b/activerecord/lib/active_record/validations/uniqueness.rb
index 5fa6a0b892..b55af692d6 100644
--- a/activerecord/lib/active_record/validations/uniqueness.rb
+++ b/activerecord/lib/active_record/validations/uniqueness.rb
@@ -1,54 +1,35 @@
-require 'active_support/core_ext/array/prepend_and_append'
-
module ActiveRecord
module Validations
class UniquenessValidator < ActiveModel::EachValidator # :nodoc:
def initialize(options)
- super(options.reverse_merge(:case_sensitive => true))
- end
-
- # Unfortunately, we have to tie Uniqueness validators to a class.
- def setup(klass)
- @klass = klass
+ if options[:conditions] && !options[:conditions].respond_to?(:call)
+ raise ArgumentError, "#{options[:conditions]} was passed as :conditions but is not callable. " \
+ "Pass a callable instead: `conditions: -> { where(approved: true) }`"
+ end
+ super({ case_sensitive: true }.merge!(options))
+ @klass = options[:class]
end
def validate_each(record, attribute, value)
finder_class = find_finder_class_for(record)
table = finder_class.arel_table
-
- coder = record.class.serialized_attributes[attribute.to_s]
-
- if value && coder
- value = coder.dump value
- end
+ value = deserialize_attribute(record, attribute, value)
relation = build_relation(finder_class, table, attribute, value)
- relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.send(:id))) if record.persisted?
-
- Array(options[:scope]).each do |scope_item|
- reflection = record.class.reflect_on_association(scope_item)
- if reflection
- scope_value = record.send(reflection.foreign_key)
- scope_item = reflection.foreign_key
- else
- scope_value = record.read_attribute(scope_item)
- end
- relation = relation.and(table[scope_item].eq(scope_value))
- end
-
+ relation = relation.and(table[finder_class.primary_key.to_sym].not_eq(record.id)) if record.persisted?
+ relation = scope_relation(record, table, relation)
relation = finder_class.unscoped.where(relation)
-
- if options[:conditions]
- relation = relation.merge(options[:conditions])
- end
+ relation = relation.merge(options[:conditions]) if options[:conditions]
if relation.exists?
- record.errors.add(attribute, :taken, options.except(:case_sensitive, :scope, :conditions).merge(:value => value))
+ error_options = options.except(:case_sensitive, :scope, :conditions)
+ error_options[:value] = value
+
+ record.errors.add(attribute, :taken, error_options)
end
end
protected
-
# The check for an existing value should be run from a class that
# isn't abstract. This means working down from the current class
# (self), to the first non-abstract class. Since classes don't know
@@ -58,7 +39,7 @@ module ActiveRecord
class_hierarchy = [record.class]
while class_hierarchy.first != @klass
- class_hierarchy.prepend(class_hierarchy.first.superclass)
+ class_hierarchy.unshift(class_hierarchy.first.superclass)
end
class_hierarchy.detect { |klass| !klass.abstract_class? }
@@ -71,18 +52,37 @@ module ActiveRecord
end
column = klass.columns_hash[attribute.to_s]
- value = column.limit ? value.to_s[0, column.limit] : value.to_s if !value.nil? && column.text?
+ value = klass.connection.type_cast(value, column)
+ value = value.to_s[0, column.limit] if value && column.limit && column.text?
if !options[:case_sensitive] && value && column.text?
# will use SQL LOWER function before comparison, unless it detects a case insensitive collation
- relation = klass.connection.case_insensitive_comparison(table, attribute, column, value)
+ klass.connection.case_insensitive_comparison(table, attribute, column, value)
else
- value = klass.connection.case_sensitive_modifier(value) unless value.nil?
- relation = table[attribute].eq(value)
+ value = klass.connection.case_sensitive_modifier(value) unless value.nil?
+ table[attribute].eq(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)
+ scope_value = record.send(reflection.foreign_key)
+ scope_item = reflection.foreign_key
+ else
+ scope_value = record.read_attribute(scope_item)
+ end
+ relation = relation.and(table[scope_item].eq(scope_value))
end
relation
end
+
+ def deserialize_attribute(record, attribute, value)
+ coder = record.class.serialized_attributes[attribute.to_s]
+ value = coder.dump value if value && coder
+ value
+ end
end
module ClassMethods
@@ -115,7 +115,7 @@ module ActiveRecord
# of the title attribute:
#
# class Article < ActiveRecord::Base
- # validates_uniqueness_of :title, conditions: where('status != ?', 'archived')
+ # validates_uniqueness_of :title, conditions: -> { where.not(status: 'archived') }
# end
#
# When the record is created, a check is performed to make sure that no
@@ -131,7 +131,7 @@ module ActiveRecord
# the uniqueness constraint.
# * <tt>:conditions</tt> - Specify the conditions to be included as a
# <tt>WHERE</tt> SQL fragment to limit the uniqueness constraint lookup
- # (e.g. <tt>conditions: where('status = ?', 'active')</tt>).
+ # (e.g. <tt>conditions: -> { where(status: 'active') }</tt>).
# * <tt>:case_sensitive</tt> - Looks for an exact match. Ignored by
# non-text columns (+true+ by default).
# * <tt>:allow_nil</tt> - If set to +true+, skips this validation if the
@@ -197,8 +197,8 @@ module ActiveRecord
# will result in the default Rails exception page being shown), or you
# can catch it and restart the transaction (e.g. by telling the user
# that the title already exists, and asking him to re-enter the title).
- # This technique is also known as optimistic concurrency control:
- # http://en.wikipedia.org/wiki/Optimistic_concurrency_control.
+ # This technique is also known as
+ # {optimistic concurrency control}[http://en.wikipedia.org/wiki/Optimistic_concurrency_control].
#
# The bundled ActiveRecord::ConnectionAdapters distinguish unique index
# constraint errors from other types of database errors by throwing an
diff --git a/activerecord/lib/active_record/version.rb b/activerecord/lib/active_record/version.rb
index 0c35adc11d..de5fd05468 100644
--- a/activerecord/lib/active_record/version.rb
+++ b/activerecord/lib/active_record/version.rb
@@ -1,10 +1,11 @@
module ActiveRecord
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActiveRecord as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActiveRecord.version.segments
+ STRING = ActiveRecord.version.to_s
end
end
diff --git a/activerecord/lib/rails/generators/active_record.rb b/activerecord/lib/rails/generators/active_record.rb
index 297cd094c2..dc29213235 100644
--- a/activerecord/lib/rails/generators/active_record.rb
+++ b/activerecord/lib/rails/generators/active_record.rb
@@ -1,23 +1,17 @@
require 'rails/generators/named_base'
-require 'rails/generators/migration'
require 'rails/generators/active_model'
+require 'rails/generators/active_record/migration'
require 'active_record'
module ActiveRecord
- module Generators
- class Base < Rails::Generators::NamedBase #:nodoc:
- include Rails::Generators::Migration
+ module Generators # :nodoc:
+ class Base < Rails::Generators::NamedBase # :nodoc:
+ include ActiveRecord::Generators::Migration
# Set the current directory as base for the inherited generators.
def self.base_root
File.dirname(__FILE__)
end
-
- # Implement the required interface for Rails::Generators::Migration.
- def self.next_migration_number(dirname) #:nodoc:
- next_migration_number = current_migration_number(dirname) + 1
- ActiveRecord::Migration.next_migration_number(next_migration_number)
- end
end
end
end
diff --git a/activerecord/lib/rails/generators/active_record/migration.rb b/activerecord/lib/rails/generators/active_record/migration.rb
new file mode 100644
index 0000000000..b7418cf42f
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration.rb
@@ -0,0 +1,18 @@
+require 'rails/generators/migration'
+
+module ActiveRecord
+ module Generators # :nodoc:
+ module Migration
+ extend ActiveSupport::Concern
+ include Rails::Generators::Migration
+
+ module ClassMethods
+ # Implement the required interface for Rails::Generators::Migration.
+ def next_migration_number(dirname)
+ next_migration_number = current_migration_number(dirname) + 1
+ ActiveRecord::Migration.next_migration_number(next_migration_number)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
index a3c274d9b9..3968acba64 100644
--- a/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/migration_generator.rb
@@ -1,20 +1,25 @@
require 'rails/generators/active_record'
module ActiveRecord
- module Generators
- class MigrationGenerator < Base
+ module Generators # :nodoc:
+ class MigrationGenerator < Base # :nodoc:
argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
def create_migration_file
set_local_assigns!
validate_file_name!
- migration_template "migration.rb", "db/migrate/#{file_name}.rb"
+ migration_template @migration_template, "db/migrate/#{file_name}.rb"
end
protected
attr_reader :migration_action, :join_tables
+ # sets the default migration template that is being used for the generation of the migration
+ # depending on the arguments which would be sent out in the command line, the migration template
+ # and the table name instance variables are setup.
+
def set_local_assigns!
+ @migration_template = "migration.rb"
case file_name
when /^(add|remove)_.*_(?:to|from)_(.*)/
@migration_action = $1
@@ -26,6 +31,9 @@ module ActiveRecord
set_index_names
end
+ when /^create_(.+)/
+ @table_name = $1.pluralize
+ @migration_template = "create_table_migration.rb"
end
end
@@ -42,9 +50,12 @@ module ActiveRecord
attribute.name.singularize.foreign_key
end.to_sym
end
-
- private
+ private
+ def attributes_with_index
+ attributes.select { |a| !a.reference? && a.has_index? }
+ end
+
def validate_file_name!
unless file_name =~ /^[_a-z0-9]+$/
raise IllegalMigrationNameError.new(file_name)
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
new file mode 100644
index 0000000000..fd94a2d038
--- /dev/null
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/create_table_migration.rb
@@ -0,0 +1,19 @@
+class <%= migration_class_name %> < ActiveRecord::Migration
+ def change
+ create_table :<%= table_name %> do |t|
+<% attributes.each do |attribute| -%>
+<% if attribute.password_digest? -%>
+ t.string :password_digest<%= attribute.inject_options %>
+<% else -%>
+ t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
+<% end -%>
+<% end -%>
+<% if options[:timestamps] %>
+ t.timestamps
+<% end -%>
+ end
+<% attributes_with_index.each do |attribute| -%>
+ add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+<% end -%>
+ end
+end
diff --git a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
index d5c07aecd3..ae9c74fd05 100644
--- a/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
+++ b/activerecord/lib/rails/generators/active_record/migration/templates/migration.rb
@@ -21,28 +21,16 @@ class <%= migration_class_name %> < ActiveRecord::Migration
end
end
<%- else -%>
- def up
+ def change
<% attributes.each do |attribute| -%>
<%- if migration_action -%>
<%- if attribute.reference? -%>
- remove_reference :<%= table_name %>, :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
+ remove_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
<%- else -%>
- remove_column :<%= table_name %>, :<%= attribute.name %>
- <%- end -%>
-<%- end -%>
-<%- end -%>
- end
-
- def down
-<% attributes.reverse.each do |attribute| -%>
-<%- if migration_action -%>
- <%- if attribute.reference? -%>
- add_reference :<%= table_name %>, :<%= attribute.name %><%= attribute.inject_options %>
- <%- else -%>
- add_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- if attribute.has_index? -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
+ remove_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
<%- end -%>
+ remove_column :<%= table_name %>, :<%= attribute.name %>, :<%= attribute.type %><%= attribute.inject_options %>
<%- end -%>
<%- end -%>
<%- end -%>
diff --git a/activerecord/lib/rails/generators/active_record/model/model_generator.rb b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
index 8e6ef20285..7e8d68ce69 100644
--- a/activerecord/lib/rails/generators/active_record/model/model_generator.rb
+++ b/activerecord/lib/rails/generators/active_record/model/model_generator.rb
@@ -1,8 +1,8 @@
require 'rails/generators/active_record'
module ActiveRecord
- module Generators
- class ModelGenerator < Base
+ module Generators # :nodoc:
+ class ModelGenerator < Base # :nodoc:
argument :attributes, :type => :array, :default => [], :banner => "field[:type][:index] field[:type][:index]"
check_class_collision
@@ -12,10 +12,13 @@ module ActiveRecord
class_option :parent, :type => :string, :desc => "The parent class for the generated model"
class_option :indexes, :type => :boolean, :default => true, :desc => "Add indexes for references and belongs_to columns"
+
+ # creates the migration file for the model.
+
def create_migration_file
return unless options[:migration] && options[:parent].nil?
attributes.each { |a| a.attr_options.delete(:index) if a.reference? && !a.has_index? } if options[:indexes] == false
- migration_template "migration.rb", "db/migrate/create_#{table_name}.rb"
+ migration_template "../../migration/templates/create_table_migration.rb", "db/migrate/create_#{table_name}.rb"
end
def create_model_file
@@ -39,6 +42,7 @@ module ActiveRecord
protected
+ # Used by the migration template to determine the parent name of the model
def parent_class_name
options[:parent] || "ActiveRecord::Base"
end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb b/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
deleted file mode 100644
index 3a3cf86d73..0000000000
--- a/activerecord/lib/rails/generators/active_record/model/templates/migration.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-class <%= migration_class_name %> < ActiveRecord::Migration
- def change
- create_table :<%= table_name %> do |t|
-<% attributes.each do |attribute| -%>
- t.<%= attribute.type %> :<%= attribute.name %><%= attribute.inject_options %>
-<% end -%>
-<% if options[:timestamps] %>
- t.timestamps
-<% end -%>
- end
-<% attributes_with_index.each do |attribute| -%>
- add_index :<%= table_name %>, :<%= attribute.index_name %><%= attribute.inject_index_options %>
-<% end -%>
- end
-end
diff --git a/activerecord/lib/rails/generators/active_record/model/templates/model.rb b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
index 056f55470c..808598699b 100644
--- a/activerecord/lib/rails/generators/active_record/model/templates/model.rb
+++ b/activerecord/lib/rails/generators/active_record/model/templates/model.rb
@@ -1,7 +1,10 @@
<% module_namespacing do -%>
class <%= class_name %> < <%= parent_class_name.classify %>
-<% attributes.select {|attr| attr.reference? }.each do |attribute| -%>
+<% attributes.select(&:reference?).each do |attribute| -%>
belongs_to :<%= attribute.name %><%= ', polymorphic: true' if attribute.polymorphic? %>
<% end -%>
+<% if attributes.any?(&:password_digest?) -%>
+ has_secure_password
+<% end -%>
end
<% end -%>
diff --git a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb b/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb
deleted file mode 100644
index c1c0e3f25b..0000000000
--- a/activerecord/lib/rails/generators/active_record/observer/observer_generator.rb
+++ /dev/null
@@ -1,15 +0,0 @@
-require 'rails/generators/active_record'
-
-module ActiveRecord
- module Generators
- class ObserverGenerator < Base
- check_class_collision :suffix => "Observer"
-
- def create_observer_file
- template 'observer.rb', File.join('app/models', class_path, "#{file_name}_observer.rb")
- end
-
- hook_for :test_framework
- end
- end
-end
diff --git a/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb b/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb
deleted file mode 100644
index eaa256a9bd..0000000000
--- a/activerecord/lib/rails/generators/active_record/observer/templates/observer.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-<% module_namespacing do -%>
-class <%= class_name %>Observer < ActiveRecord::Observer
-end
-<% end -%>
diff --git a/activerecord/test/cases/adapter_test.rb b/activerecord/test/cases/adapter_test.rb
index 93b01a3934..dd355e8d0c 100644
--- a/activerecord/test/cases/adapter_test.rb
+++ b/activerecord/test/cases/adapter_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "models/book"
module ActiveRecord
class AdapterTest < ActiveRecord::TestCase
@@ -6,6 +7,19 @@ module ActiveRecord
@connection = ActiveRecord::Base.connection
end
+ ##
+ # PostgreSQL does not support null bytes in strings
+ unless current_adapter?(:PostgreSQLAdapter)
+ def test_update_prepared_statement
+ b = Book.create(name: "my \x00 book")
+ b.reload
+ assert_equal "my \x00 book", b.name
+ b.update_attributes(name: "my other \x00 book")
+ b.reload
+ assert_equal "my other \x00 book", b.name
+ end
+ end
+
def test_tables
tables = @connection.tables
assert tables.include?("accounts")
@@ -69,16 +83,16 @@ module ActiveRecord
def test_not_specifying_database_name_for_cross_database_selects
begin
assert_nothing_raised do
- ActiveRecord::Model.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
+ ActiveRecord::Base.establish_connection(ActiveRecord::Base.configurations['arunit'].except(:database))
config = ARTest.connection_config
- ActiveRecord::Model.connection.execute(
+ ActiveRecord::Base.connection.execute(
"SELECT #{config['arunit']['database']}.pirates.*, #{config['arunit2']['database']}.courses.* " \
"FROM #{config['arunit']['database']}.pirates, #{config['arunit2']['database']}.courses"
)
end
ensure
- ActiveRecord::Model.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection 'arunit'
end
end
end
@@ -100,7 +114,7 @@ module ActiveRecord
end
end
- # test resetting sequences in odd tables in postgreSQL
+ # test resetting sequences in odd tables in PostgreSQL
if ActiveRecord::Base.connection.respond_to?(:reset_pk_sequence!)
require 'models/movie'
require 'models/subscriber'
@@ -153,25 +167,32 @@ module ActiveRecord
else
@connection.execute "INSERT INTO fk_test_has_fk (fk_id) VALUES (0)"
end
- # should deleted created record as otherwise disable_referential_integrity will try to enable contraints after executed block
+ # should delete created record as otherwise disable_referential_integrity will try to enable constraints after executed block
# and will fail (at least on Oracle)
@connection.execute "DELETE FROM fk_test_has_fk"
end
end
end
+
+ def test_select_all_always_return_activerecord_result
+ result = @connection.select_all "SELECT * FROM posts"
+ assert result.is_a?(ActiveRecord::Result)
+ end
end
class AdapterTestWithoutTransaction < ActiveRecord::TestCase
self.use_transactional_fixtures = false
+ class Klass < ActiveRecord::Base
+ end
+
def setup
- @klass = Class.new(ActiveRecord::Base)
- @klass.establish_connection 'arunit'
- @connection = @klass.connection
+ Klass.establish_connection 'arunit'
+ @connection = Klass.connection
end
def teardown
- @klass.remove_connection
+ Klass.remove_connection
end
test "transaction state is reset after a reconnect" do
diff --git a/activerecord/test/cases/adapters/mysql/active_schema_test.rb b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
index 94fc3564df..0878925a6c 100644
--- a/activerecord/test/cases/adapters/mysql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/active_schema_test.rb
@@ -2,47 +2,68 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ @connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
+
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- remove_method :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
- remove_method :execute
- alias_method :execute, :execute_without_stub
- end
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
end
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
assert_equal expected, add_index(:people, :last_name, :length => nil)
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
assert_equal expected, add_index(:people, :last_name, :length => 10)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
- ActiveRecord::ConnectionAdapters::MysqlAdapter.send(:remove_method, :index_name_exists?)
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :type => type)
+ end
+
+ %w(btree hash).each do |using|
+ expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :using => using)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
+ assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
+ assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :coyp)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
+ assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
def test_drop_table
assert_equal "DROP TABLE `people`", drop_table(:people)
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_create_mysql_database_with_encoding
assert_equal "CREATE DATABASE `matt` DEFAULT CHARACTER SET `utf8`", create_database(:matt)
assert_equal "CREATE DATABASE `aimonetti` DEFAULT CHARACTER SET `latin1`", create_database(:aimonetti, {:charset => 'latin1'})
@@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
def test_add_timestamps
with_real_execute do
begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- end
+ ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
@@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
- #we need to actually modify some data, so we make execute point to the original method
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
end
+
yield
ensure
- #before finishing, we restore the alias to the mock-up method
- ActiveRecord::ConnectionAdapters::AbstractMysqlAdapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
end
-
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
diff --git a/activerecord/test/cases/adapters/mysql/connection_test.rb b/activerecord/test/cases/adapters/mysql/connection_test.rb
index 4bccd2cc59..1844a2e0dc 100644
--- a/activerecord/test/cases/adapters/mysql/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql/connection_test.rb
@@ -1,32 +1,37 @@
require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
+ class Klass < ActiveRecord::Base
+ end
+
def setup
super
- @connection = ActiveRecord::Model.connection
+ @connection = ActiveRecord::Base.connection
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_true
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => true}))
- assert ActiveRecord::Model.connection.raw_connection.reconnect
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => true}))
+ assert ActiveRecord::Base.connection.raw_connection.reconnect
end
end
def test_connect_with_url
- run_without_connection do |orig|
+ run_without_connection do
ar_config = ARTest.connection_config['arunit']
+
+ skip "This test doesn't work with custom socket location" if ar_config['socket']
+
url = "mysql://#{ar_config["username"]}@localhost/#{ar_config["database"]}"
- klass = Class.new(ActiveRecord::Base)
- klass.establish_connection(url)
- assert_equal ar_config['database'], klass.connection.current_database
+ Klass.establish_connection(url)
+ assert_equal ar_config['database'], Klass.connection.current_database
end
end
def test_mysql_reconnect_attribute_after_connection_with_reconnect_false
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:reconnect => false}))
- assert !ActiveRecord::Model.connection.raw_connection.reconnect
+ ActiveRecord::Base.establish_connection(orig_connection.merge({:reconnect => false}))
+ assert !ActiveRecord::Base.connection.raw_connection.reconnect
end
end
@@ -117,7 +122,7 @@ class MysqlConnectionTest < ActiveRecord::TestCase
# Test that MySQL allows multiple results for stored procedures
if defined?(Mysql) && Mysql.const_defined?(:CLIENT_MULTI_RESULTS)
def test_multi_results
- rows = ActiveRecord::Model.connection.select_rows('CALL ten();')
+ rows = ActiveRecord::Base.connection.select_rows('CALL ten();')
assert_equal 10, rows[0][0].to_i, "ten() did not return 10 as expected: #{rows.inspect}"
assert @connection.active?, "Bad connection use by 'MysqlAdapter.select_rows'"
end
@@ -130,21 +135,38 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode"
+ 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
end
end
+ def test_mysql_set_session_variable
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal 3, session_mode.rows.first.first.to_i
+ 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}}))
+ global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal global_mode.rows, session_mode.rows
+ end
+ end
+
private
def run_without_connection
- original_connection = ActiveRecord::Model.remove_connection
+ original_connection = ActiveRecord::Base.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Model.establish_connection(original_connection)
+ ActiveRecord::Base.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/enum_test.rb b/activerecord/test/cases/adapters/mysql/enum_test.rb
index 40af317ad1..f4e7a3ef0a 100644
--- a/activerecord/test/cases/adapters/mysql/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql/enum_test.rb
@@ -5,6 +5,6 @@ class MysqlEnumTest < ActiveRecord::TestCase
end
def test_enum_limit
- assert_equal 5, EnumTest.columns.first.limit
+ assert_equal 6, EnumTest.columns.first.limit
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 ddfe42b375..9ad0744aee 100644
--- a/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/mysql/mysql_adapter_test.rb
@@ -16,6 +16,15 @@ module ActiveRecord
eosql
end
+ def test_valid_column
+ column = @conn.columns('ex').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ end
+
+ def test_invalid_column
+ assert_not @conn.valid_type?(:foobar)
+ end
+
def test_client_encoding
assert_equal Encoding::UTF_8, @conn.client_encoding
end
@@ -49,13 +58,11 @@ module ActiveRecord
end
def test_tables_quoting
- begin
- @conn.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
+ @conn.tables(nil, "foo-bar", nil)
+ flunk
+ rescue => e
+ # assertion for *quoted* database properly
+ assert_match(/database 'foo-bar'/, e.inspect)
end
def test_pk_and_sequence_for
@@ -88,14 +95,39 @@ module ActiveRecord
assert_equal @conn.default_sequence_name('ex_with_custom_index_type_pk', 'id'), seq
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')
+
+ result = @conn.exec_query('SELECT status FROM ex_with_non_boolean_tinyint_column')
+
+ assert_equal 2, result.column_types['status'].type_cast(result.last['status'])
+ end
+
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
private
- def insert(ctx, data)
+ def insert(ctx, data, table='ex')
binds = data.map { |name, value|
- [ctx.columns('ex').find { |x| x.name == name }, value]
+ [ctx.columns(table).find { |x| x.name == name }, value]
}
columns = binds.map(&:first).map(&:name)
- sql = "INSERT INTO ex (#{columns.join(", ")})
+ sql = "INSERT INTO #{table} (#{columns.join(", ")})
VALUES (#{(['?'] * columns.length).join(', ')})"
ctx.exec_insert(sql, 'SQL', binds)
diff --git a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
index 5164acf77f..8eb9565963 100644
--- a/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
end
- # dump structure of table with reserved word name
- def test_structure_dump
- assert_nothing_raised { @connection.structure_dump }
- end
-
# introspect table with reserved word name
def test_introspect
assert_nothing_raised { @connection.columns(:group) }
diff --git a/activerecord/test/cases/adapters/mysql/schema_test.rb b/activerecord/test/cases/adapters/mysql/schema_test.rb
index d94bb629a7..807a7a155e 100644
--- a/activerecord/test/cases/adapters/mysql/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql/schema_test.rb
@@ -35,6 +35,28 @@ module ActiveRecord
def test_table_exists_wrong_schema
assert(!@connection.table_exists?("#{@db_name}.zomg"), "table should not exist")
end
+
+ def test_dump_indexes
+ index_a_name = 'index_key_tests_on_snack'
+ index_b_name = 'index_key_tests_on_pizza'
+ index_c_name = 'index_key_tests_on_awesome'
+
+ table = 'key_tests'
+
+ indexes = @connection.indexes(table).sort_by {|i| i.name}
+ assert_equal 3,indexes.size
+
+ index_a = indexes.select{|i| i.name == index_a_name}[0]
+ index_b = indexes.select{|i| i.name == index_b_name}[0]
+ index_c = indexes.select{|i| i.name == index_c_name}[0]
+ assert_equal :btree, index_a.using
+ assert_nil index_a.type
+ assert_equal :btree, index_b.using
+ assert_nil index_b.type
+
+ assert_nil index_c.using
+ assert_equal :fulltext, index_c.type
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql/sql_types_test.rb b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
new file mode 100644
index 0000000000..1ddb1b91c9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql/sql_types_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
+ assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
+ assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary)
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
index a83399d0cd..4ccf568406 100644
--- a/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/active_schema_test.rb
@@ -2,40 +2,61 @@ require "cases/helper"
class ActiveSchemaTest < ActiveRecord::TestCase
def setup
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ @connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
+
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_without_stub, :execute
- remove_method :execute
def execute(sql, name = nil) return sql end
end
end
def teardown
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
- remove_method :execute
- alias_method :execute, :execute_without_stub
- end
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection(@connection)
end
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`)"
+ def (ActiveRecord::Base.connection).index_name_exists?(*); false; end
+
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`) "
assert_equal expected, add_index(:people, :last_name, :length => nil)
- expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name` ON `people` (`last_name`(10)) "
assert_equal expected, add_index(:people, :last_name, :length => 10)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(15)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15)
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`)"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15})
- expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10))"
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` ON `people` (`last_name`(15), `first_name`(10)) "
assert_equal expected, add_index(:people, [:last_name, :first_name], :length => {:last_name => 15, :first_name => 10})
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.send(:remove_method, :index_name_exists?)
+
+ %w(SPATIAL FULLTEXT UNIQUE).each do |type|
+ expected = "CREATE #{type} INDEX `index_people_on_last_name` ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :type => type)
+ end
+
+ %w(btree hash).each do |using|
+ expected = "CREATE INDEX `index_people_on_last_name` USING #{using} ON `people` (`last_name`) "
+ assert_equal expected, add_index(:people, :last_name, :using => using)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) "
+ assert_equal expected, add_index(:people, :last_name, :length => 10, :using => :btree)
+
+ expected = "CREATE INDEX `index_people_on_last_name` USING btree ON `people` (`last_name`(10)) ALGORITHM = COPY"
+ assert_equal expected, add_index(:people, :last_name, :length => 10, using: :btree, algorithm: :copy)
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :coyp)
+ end
+
+ expected = "CREATE INDEX `index_people_on_last_name_and_first_name` USING btree ON `people` (`last_name`(15), `first_name`(15)) "
+ assert_equal expected, add_index(:people, [:last_name, :first_name], :length => 15, :using => :btree)
end
def test_drop_table
@@ -70,8 +91,7 @@ class ActiveSchemaTest < ActiveRecord::TestCase
def test_add_timestamps
with_real_execute do
begin
- ActiveRecord::Base.connection.create_table :delete_me do |t|
- end
+ ActiveRecord::Base.connection.create_table :delete_me
ActiveRecord::Base.connection.add_timestamps :delete_me
assert column_present?('delete_me', 'updated_at', 'datetime')
assert column_present?('delete_me', 'created_at', 'datetime')
@@ -98,22 +118,20 @@ class ActiveSchemaTest < ActiveRecord::TestCase
private
def with_real_execute
- #we need to actually modify some data, so we make execute point to the original method
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
alias_method :execute_with_stub, :execute
remove_method :execute
alias_method :execute, :execute_without_stub
end
+
yield
ensure
- #before finishing, we restore the alias to the mock-up method
- ActiveRecord::ConnectionAdapters::Mysql2Adapter.class_eval do
+ ActiveRecord::Base.connection.singleton_class.class_eval do
remove_method :execute
alias_method :execute, :execute_with_stub
end
end
-
def method_missing(method_symbol, *arguments)
ActiveRecord::Base.connection.send(method_symbol, *arguments)
end
diff --git a/activerecord/test/cases/adapters/mysql2/connection_test.rb b/activerecord/test/cases/adapters/mysql2/connection_test.rb
index c63e4fe5b6..679c515e8c 100644
--- a/activerecord/test/cases/adapters/mysql2/connection_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/connection_test.rb
@@ -3,14 +3,14 @@ require "cases/helper"
class MysqlConnectionTest < ActiveRecord::TestCase
def setup
super
- @connection = ActiveRecord::Model.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ @connection = ActiveRecord::Base.connection
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_no_automatic_reconnection_after_timeout
@@ -46,29 +46,40 @@ class MysqlConnectionTest < ActiveRecord::TestCase
def test_mysql_strict_mode_disabled_dont_override_global_sql_mode
run_without_connection do |orig_connection|
- ActiveRecord::Model.establish_connection(orig_connection.merge({:strict => false}))
- global_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@GLOBAL.sql_mode"
- session_sql_mode = ActiveRecord::Model.connection.exec_query "SELECT @@SESSION.sql_mode"
+ 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
end
end
- def test_logs_name_structure_dump
- @connection.structure_dump
- assert_equal "SCHEMA", @connection.logged[0][1]
- assert_equal "SCHEMA", @connection.logged[2][1]
+ def test_mysql_set_session_variable
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:default_week_format => 3}}))
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal 3, session_mode.rows.first.first.to_i
+ 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}}))
+ global_mode = ActiveRecord::Base.connection.exec_query "SELECT @@GLOBAL.DEFAULT_WEEK_FORMAT"
+ session_mode = ActiveRecord::Base.connection.exec_query "SELECT @@SESSION.DEFAULT_WEEK_FORMAT"
+ assert_equal global_mode.rows, session_mode.rows
+ end
end
def test_logs_name_show_variable
@connection.show_variable 'foo'
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
end
def test_logs_name_rename_column_sql
@connection.execute "CREATE TABLE `bar_baz` (`foo` varchar(255))"
- @connection.logged = []
+ @subscriber.logged.clear
@connection.send(:rename_column_sql, 'bar_baz', 'foo', 'foo2')
- assert_equal "SCHEMA", @connection.logged[0][1]
+ assert_equal "SCHEMA", @subscriber.logged[0][1]
ensure
@connection.execute "DROP TABLE `bar_baz`"
end
@@ -76,11 +87,11 @@ class MysqlConnectionTest < ActiveRecord::TestCase
private
def run_without_connection
- original_connection = ActiveRecord::Model.remove_connection
+ original_connection = ActiveRecord::Base.remove_connection
begin
yield original_connection
ensure
- ActiveRecord::Model.establish_connection(original_connection)
+ ActiveRecord::Base.establish_connection(original_connection)
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/enum_test.rb b/activerecord/test/cases/adapters/mysql2/enum_test.rb
index f3a05e48ad..6dd9a5ec87 100644
--- a/activerecord/test/cases/adapters/mysql2/enum_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/enum_test.rb
@@ -5,6 +5,6 @@ class Mysql2EnumTest < ActiveRecord::TestCase
end
def test_enum_limit
- assert_equal 5, EnumTest.columns.first.limit
+ assert_equal 6, EnumTest.columns.first.limit
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 1017b0758d..1a82308176 100644
--- a/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/reserved_word_test.rb
@@ -2,7 +2,7 @@ require "cases/helper"
class Group < ActiveRecord::Base
Group.table_name = 'group'
- belongs_to :select, :class_name => 'Select'
+ belongs_to :select
has_one :values
end
@@ -63,11 +63,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { @connection.rename_column(:group, :order, :values) }
end
- # dump structure of table with reserved word name
- def test_structure_dump
- assert_nothing_raised { @connection.structure_dump }
- end
-
# introspect table with reserved word name
def test_introspect
assert_nothing_raised { @connection.columns(:group) }
@@ -89,7 +84,6 @@ class MysqlReservedWordTest < ActiveRecord::TestCase
assert_nothing_raised { x.save }
assert_nothing_raised { Group.find_by_order('y') }
assert_nothing_raised { Group.find(1) }
- x = Group.find(1)
end
# has_one association with reserved-word table name
diff --git a/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
new file mode 100644
index 0000000000..9ecd901eac
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/schema_migrations_test.rb
@@ -0,0 +1,26 @@
+require "cases/helper"
+
+module ActiveRecord
+ module ConnectionAdapters
+ class Mysql2Adapter
+ class SchemaMigrationsTest < ActiveRecord::TestCase
+ def test_initializes_schema_migrations_for_encoding_utf8mb4
+ conn = ActiveRecord::Base.connection
+
+ smtn = ActiveRecord::Migrator.schema_migrations_table_name
+ conn.drop_table(smtn) if conn.table_exists?(smtn)
+
+ config = conn.instance_variable_get(:@config)
+ original_encoding = config[:encoding]
+
+ config[:encoding] = 'utf8mb4'
+ conn.initialize_schema_migrations_table
+
+ assert conn.column_exists?(smtn, :version, :string, limit: Mysql2Adapter::MAX_INDEX_LENGTH_FOR_UTF8MB4)
+ ensure
+ config[:encoding] = original_encoding
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/adapters/mysql2/schema_test.rb b/activerecord/test/cases/adapters/mysql2/schema_test.rb
index 2c0ed73c92..5db60ff8a0 100644
--- a/activerecord/test/cases/adapters/mysql2/schema_test.rb
+++ b/activerecord/test/cases/adapters/mysql2/schema_test.rb
@@ -37,15 +37,34 @@ module ActiveRecord
end
def test_tables_quoting
- begin
- @connection.tables(nil, "foo-bar", nil)
- flunk
- rescue => e
- # assertion for *quoted* database properly
- assert_match(/database 'foo-bar'/, e.inspect)
- end
+ @connection.tables(nil, "foo-bar", nil)
+ flunk
+ rescue => e
+ # assertion for *quoted* database properly
+ assert_match(/database 'foo-bar'/, e.inspect)
end
+ def test_dump_indexes
+ index_a_name = 'index_key_tests_on_snack'
+ index_b_name = 'index_key_tests_on_pizza'
+ index_c_name = 'index_key_tests_on_awesome'
+
+ table = 'key_tests'
+
+ indexes = @connection.indexes(table).sort_by {|i| i.name}
+ assert_equal 3,indexes.size
+
+ index_a = indexes.select{|i| i.name == index_a_name}[0]
+ index_b = indexes.select{|i| i.name == index_b_name}[0]
+ index_c = indexes.select{|i| i.name == index_c_name}[0]
+ assert_equal :btree, index_a.using
+ assert_nil index_a.type
+ assert_equal :btree, index_b.using
+ assert_nil index_b.type
+
+ assert_nil index_c.using
+ assert_equal :fulltext, index_c.type
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/mysql2/sql_types_test.rb b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
new file mode 100644
index 0000000000..1ddb1b91c9
--- /dev/null
+++ b/activerecord/test/cases/adapters/mysql2/sql_types_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'varbinary(64)', type_to_sql(:binary, 64)
+ assert_equal 'varbinary(4095)', type_to_sql(:binary, 4095)
+ assert_equal 'blob(4096)', type_to_sql(:binary, 4096)
+ assert_equal 'blob', type_to_sql(:binary)
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
index 1b4f4a5fc9..22dd48e113 100644
--- a/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/active_schema_test.rb
@@ -16,6 +16,7 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
def test_create_database_with_encoding
assert_equal %(CREATE DATABASE "matt" ENCODING = 'utf8'), create_database(:matt)
assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, :encoding => :latin1)
+ assert_equal %(CREATE DATABASE "aimonetti" ENCODING = 'latin1'), create_database(:aimonetti, 'encoding' => :latin1)
end
def test_create_database_with_collation_and_ctype
@@ -24,14 +25,30 @@ class PostgresqlActiveSchemaTest < ActiveRecord::TestCase
def test_add_index
# add_index calls index_name_exists? which can't work since execute is stubbed
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:define_method, :index_name_exists?) do |*|
- false
- end
+ ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.stubs(:index_name_exists?).returns(false)
- expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" ("last_name") WHERE state = 'active')
assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'")
- ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.send(:remove_method, :index_name_exists?)
+ expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, algorithm: :concurrently)
+
+ %w(gin gist hash btree).each do |type|
+ expected = %(CREATE INDEX "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, using: type)
+
+ expected = %(CREATE INDEX CONCURRENTLY "index_people_on_last_name" ON "people" USING #{type} ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, using: type, algorithm: :concurrently)
+ end
+
+ assert_raise ArgumentError do
+ add_index(:people, :last_name, algorithm: :copy)
+ end
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name"))
+ assert_equal expected, add_index(:people, :last_name, :unique => true, :using => :gist)
+
+ expected = %(CREATE UNIQUE INDEX "index_people_on_last_name" ON "people" USING gist ("last_name") WHERE state = 'active')
+ assert_equal expected, add_index(:people, :last_name, :unique => true, :where => "state = 'active'", :using => :gist)
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/array_test.rb b/activerecord/test/cases/adapters/postgresql/array_test.rb
index 8774bf626f..9536cceb1d 100644
--- a/activerecord/test/cases/adapters/postgresql/array_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/array_test.rb
@@ -12,7 +12,8 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.transaction do
@connection.create_table('pg_arrays') do |t|
- t.string 'tags', :array => true
+ t.string 'tags', array: true
+ t.integer 'ratings', array: true
end
end
@column = PgArray.columns.find { |c| c.name == 'tags' }
@@ -27,6 +28,27 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert @column.array
end
+ def test_change_column_with_array
+ @connection.add_column :pg_arrays, :snippets, :string, array: true, default: []
+ @connection.change_column :pg_arrays, :snippets, :text, array: true, default: "{}"
+
+ PgArray.reset_column_information
+ column = PgArray.columns.find { |c| c.name == 'snippets' }
+
+ assert_equal :text, column.type
+ assert_equal [], column.default
+ assert column.array
+ end
+
+ def test_change_column_cant_make_non_array_column_to_array
+ @connection.add_column :pg_arrays, :a_string, :string
+ assert_raises ActiveRecord::StatementInvalid do
+ @connection.transaction do
+ @connection.change_column :pg_arrays, :a_string, :string, array: true
+ end
+ end
+ end
+
def test_type_cast_array
assert @column
@@ -57,42 +79,57 @@ class PostgresqlArrayTest < ActiveRecord::TestCase
assert_equal(['1','2','3'], x.tags)
end
- def test_multi_dimensional
- assert_cycle([['1','2'],['2','3']])
+ def test_multi_dimensional_with_strings
+ assert_cycle(:tags, [[['1'], ['2']], [['2'], ['3']]])
+ end
+
+ def test_multi_dimensional_with_integers
+ assert_cycle(:ratings, [[[1], [7]], [[8], [10]]])
end
def test_strings_with_quotes
- assert_cycle(['this has','some "s that need to be escaped"'])
+ assert_cycle(:tags, ['this has','some "s that need to be escaped"'])
end
def test_strings_with_commas
- assert_cycle(['this,has','many,values'])
+ assert_cycle(:tags, ['this,has','many,values'])
end
def test_strings_with_array_delimiters
- assert_cycle(['{','}'])
+ assert_cycle(:tags, ['{','}'])
end
def test_strings_with_null_strings
- assert_cycle(['NULL','NULL'])
+ assert_cycle(:tags, ['NULL','NULL'])
end
def test_contains_nils
- assert_cycle(['1',nil,nil])
+ assert_cycle(:tags, ['1',nil,nil])
+ end
+
+ def test_insert_fixture
+ tag_values = ["val1", "val2", "val3_with_'_multiple_quote_'_chars"]
+ @connection.insert_fixture({"tags" => tag_values}, "pg_arrays" )
+ assert_equal(PgArray.last.tags, tag_values)
+ end
+
+ def test_attribute_for_inspect_for_array_field
+ record = PgArray.new { |a| a.ratings = (1..11).to_a }
+ assert_equal("[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, ...]", record.attribute_for_inspect(:ratings))
end
private
- def assert_cycle array
+ def assert_cycle field, array
# test creation
- x = PgArray.create!(:tags => array)
+ x = PgArray.create!(field => array)
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
# test updating
- x = PgArray.create!(:tags => [])
- x.tags = array
+ x = PgArray.create!(field => [])
+ x.public_send("#{field}=", array)
x.save!
x.reload
- assert_equal(array, x.tags)
+ assert_equal(array, x.public_send(field))
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/bytea_test.rb b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
new file mode 100644
index 0000000000..b8dd35c4c5
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/bytea_test.rb
@@ -0,0 +1,104 @@
+# 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
+ self.table_name = 'bytea_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('bytea_data_type') do |t|
+ t.binary 'payload'
+ t.binary 'serialized'
+ end
+ end
+ end
+ @column = ByteaDataType.columns.find { |c| c.name == 'payload' }
+ assert(@column.is_a?(ActiveRecord::ConnectionAdapters::PostgreSQLColumn))
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists bytea_data_type'
+ end
+
+ def test_column
+ assert_equal :binary, @column.type
+ end
+
+ def test_type_cast_binary_converts_the_encoding
+ assert @column
+
+ data = "\u001F\x8B"
+ assert_equal('UTF-8', data.encoding.name)
+ assert_equal('ASCII-8BIT', @column.type_cast(data).encoding.name)
+ end
+
+ def test_type_cast_binary_value
+ data = "\u001F\x8B".force_encoding("BINARY")
+ assert_equal(data, @column.type_cast(data))
+ end
+
+ def test_type_case_nil
+ assert_equal(nil, @column.type_cast(nil))
+ end
+
+ def test_read_value
+ data = "\u001F"
+ @connection.execute "insert into bytea_data_type (payload) VALUES ('#{data}')"
+ record = ByteaDataType.first
+ assert_equal(data, record.payload)
+ record.delete
+ end
+
+ def test_read_nil_value
+ @connection.execute "insert into bytea_data_type (payload) VALUES (null)"
+ record = ByteaDataType.first
+ assert_equal(nil, record.payload)
+ record.delete
+ end
+
+ def test_write_value
+ data = "\u001F"
+ record = ByteaDataType.create(payload: data)
+ assert_not record.new_record?
+ assert_equal(data, record.payload)
+ end
+
+ def test_write_binary
+ data = File.read(File.join(File.dirname(__FILE__), '..', '..', '..', 'assets', 'example.log'))
+ assert(data.size > 1)
+ record = ByteaDataType.create(payload: data)
+ assert_not record.new_record?
+ assert_equal(data, record.payload)
+ assert_equal(data, ByteaDataType.where(id: record.id).first.payload)
+ end
+
+ def test_write_nil
+ record = ByteaDataType.create(payload: nil)
+ assert_not record.new_record?
+ assert_equal(nil, record.payload)
+ assert_equal(nil, ByteaDataType.where(id: record.id).first.payload)
+ end
+
+ class Serializer
+ def load(str); str; end
+ def dump(str); str; end
+ end
+
+ def test_serialize
+ klass = Class.new(ByteaDataType) {
+ serialize :serialized, Serializer.new
+ }
+ obj = klass.new
+ obj.serialized = "hello world"
+ obj.save!
+ obj.reload
+ assert_equal "hello world", obj.serialized
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/connection_test.rb b/activerecord/test/cases/adapters/postgresql/connection_test.rb
index 1ff307c735..81aa977c59 100644
--- a/activerecord/test/cases/adapters/postgresql/connection_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/connection_test.rb
@@ -7,14 +7,14 @@ module ActiveRecord
def setup
super
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
@connection = ActiveRecord::Base.connection
- @connection.extend(LogIntercepter)
- @connection.intercepted = true
end
def teardown
- @connection.intercepted = false
- @connection.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_encoding
@@ -47,74 +47,48 @@ module ActiveRecord
def test_tables_logs_name
@connection.tables('hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_indexes_logs_name
@connection.indexes('items', 'hello')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_exists_logs_name
@connection.table_exists?('items')
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_table_alias_length_logs_name
@connection.instance_variable_set("@table_alias_length", nil)
@connection.table_alias_length
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_current_database_logs_name
@connection.current_database
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_encoding_logs_name
@connection.encoding
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_schema_names_logs_name
@connection.schema_names
- assert_equal 'SCHEMA', @connection.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
- def test_reconnection_after_simulated_disconnection_with_verify
- assert @connection.active?
- original_connection_pid = @connection.query('select pg_backend_pid()')
-
- # Fail with bad connection on next query attempt.
- raw_connection = @connection.raw_connection
- raw_connection_class = class << raw_connection ; self ; end
- raw_connection_class.class_eval <<-CODE, __FILE__, __LINE__ + 1
- def query_fake(*args)
- if !( @called ||= false )
- self.stubs(:status).returns(PGconn::CONNECTION_BAD)
- @called = true
- raise PGError
- else
- self.unstub(:status)
- query_unfake(*args)
- end
- end
-
- alias query_unfake query
- alias query query_fake
- CODE
-
- begin
- @connection.verify!
- new_connection_pid = @connection.query('select pg_backend_pid()')
- ensure
- raw_connection_class.class_eval <<-CODE
- alias query query_unfake
- undef query_fake
- CODE
- end
-
- assert_not_equal original_connection_pid, new_connection_pid, "Should have a new underlying connection pid"
+ def test_statement_key_is_logged
+ bindval = 1
+ @connection.exec_query('SELECT $1::integer', 'SQL', [[nil, bindval]])
+ name = @subscriber.payloads.last[:statement_name]
+ assert name
+ res = @connection.exec_query("EXPLAIN (FORMAT JSON) EXECUTE #{name}(#{bindval})")
+ plan = res.column_types['QUERY PLAN'].type_cast res.rows.first.first
+ assert_operator plan.length, :>, 0
end
# Must have with_manual_interventions set to true for this
@@ -154,5 +128,46 @@ module ActiveRecord
end
end
+ def test_set_session_variable_true
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => true}}))
+ set_true = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
+ assert_equal set_true.rows, [["on"]]
+ end
+ end
+
+ def test_set_session_variable_false
+ run_without_connection do |orig_connection|
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => false}}))
+ set_false = ActiveRecord::Base.connection.exec_query "SHOW DEBUG_PRINT_PLAN"
+ assert_equal set_false.rows, [["off"]]
+ end
+ end
+
+ def test_set_session_variable_nil
+ run_without_connection do |orig_connection|
+ # This should be a no-op that does not raise an error
+ ActiveRecord::Base.establish_connection(orig_connection.deep_merge({:variables => {:debug_print_plan => nil}}))
+ end
+ end
+
+ def test_set_session_variable_default
+ run_without_connection do |orig_connection|
+ # This should execute a query that does not raise an error
+ 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 c7ce43d71e..c5ff8cb609 100644
--- a/activerecord/test/cases/adapters/postgresql/datatype_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/datatype_test.rb
@@ -3,6 +3,9 @@ require "cases/helper"
class PostgresqlArray < ActiveRecord::Base
end
+class PostgresqlRange < ActiveRecord::Base
+end
+
class PostgresqlTsvector < ActiveRecord::Base
end
@@ -30,6 +33,9 @@ end
class PostgresqlUUID < ActiveRecord::Base
end
+class PostgresqlLtree < ActiveRecord::Base
+end
+
class PostgresqlDataTypeTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
@@ -37,43 +43,157 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
@connection = ActiveRecord::Base.connection
@connection.execute("set lc_monetary = 'C'")
- @connection.execute("INSERT INTO postgresql_arrays (commission_by_quarter, nicknames) VALUES ( '{35000,21000,18000,17000}', '{foo,bar,baz}' )")
+ @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 (text_vector) VALUES (' ''text'' ''vector'' ')")
+ @connection.execute <<_SQL if @connection.supports_ranges?
+ INSERT INTO postgresql_ranges (
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ '[''2012-01-02'', ''2012-01-04'']',
+ '[0.1, 0.2]',
+ '[''2010-01-01 14:30'', ''2011-01-01 14:30'']',
+ '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'']',
+ '[1, 10]',
+ '[10, 100]'
+ )
+_SQL
+
+ @connection.execute <<_SQL if @connection.supports_ranges?
+ INSERT INTO postgresql_ranges (
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ '(''2012-01-02'', ''2012-01-04'')',
+ '[0.1, 0.2)',
+ '[''2010-01-01 14:30'', ''2011-01-01 14:30'')',
+ '[''2010-01-01 14:30:00+05'', ''2011-01-01 14:30:00-03'')',
+ '(1, 10)',
+ '(10, 100)'
+ )
+_SQL
+
+ @connection.execute <<_SQL if @connection.supports_ranges?
+ INSERT INTO postgresql_ranges (
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ '(''2012-01-02'',]',
+ '[0.1,]',
+ '[''2010-01-01 14:30'',]',
+ '[''2010-01-01 14:30:00+05'',]',
+ '(1,]',
+ '(10,]'
+ )
+_SQL
+
+ @connection.execute <<_SQL if @connection.supports_ranges?
+ INSERT INTO postgresql_ranges (
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ '[,]',
+ '[,]',
+ '[,]',
+ '[,]',
+ '[,]',
+ '[,]'
+ )
+_SQL
+
+ @connection.execute <<_SQL if @connection.supports_ranges?
+ INSERT INTO postgresql_ranges (
+ date_range,
+ num_range,
+ ts_range,
+ tstz_range,
+ int4_range,
+ int8_range
+ ) VALUES (
+ '(''2012-01-02'', ''2012-01-02'')',
+ '(0.1, 0.1)',
+ '(''2010-01-01 14:30'', ''2010-01-01 14:30'')',
+ '(''2010-01-01 14:30:00+05'', ''2010-01-01 06:30:00-03'')',
+ '(1, 1)',
+ '(10, 10)'
+ )
+_SQL
+
+ if @connection.supports_ranges?
+ @first_range = PostgresqlRange.find(1)
+ @second_range = PostgresqlRange.find(2)
+ @third_range = PostgresqlRange.find(3)
+ @fourth_range = PostgresqlRange.find(4)
+ @empty_range = PostgresqlRange.find(5)
+ end
+
+ @connection.execute("INSERT INTO postgresql_tsvectors (id, text_vector) VALUES (1, ' ''text'' ''vector'' ')")
+
@first_tsvector = PostgresqlTsvector.find(1)
- @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('567.89'::money)")
- @connection.execute("INSERT INTO postgresql_moneys (wealth) VALUES ('-567.89'::money)")
+ @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 (single, double) VALUES (123.456, 123456.789)")
+ @connection.execute("INSERT INTO postgresql_numbers (id, single, double) VALUES (1, 123.456, 123456.789)")
@first_number = PostgresqlNumber.find(1)
- @connection.execute("INSERT INTO postgresql_times (time_interval, scaled_time_interval) VALUES ('1 year 2 days ago', '3 weeks ago')")
+ @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 (cidr_address, inet_address, mac_address) VALUES('192.168.0/24', '172.16.1.254/32', '01:23:45:67:89:0a')")
+ @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 (bit_string, bit_string_varying) VALUES (B'00010101', X'15')")
+ @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 (obj_id) VALUES (1234)")
+ @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 (time) VALUES ('2010-01-01 10:00:00-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 (guid, compact_guid) VALUES('d96c3da0-96c1-012f-1316-64ce8f32c6d8', 'f06c715096c1012f131764ce8f32c6d8')")
+ @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_range_types
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal :daterange, @first_range.column_for_attribute(:date_range).type
+ assert_equal :numrange, @first_range.column_for_attribute(:num_range).type
+ assert_equal :tsrange, @first_range.column_for_attribute(:ts_range).type
+ assert_equal :tstzrange, @first_range.column_for_attribute(:tstz_range).type
+ assert_equal :int4range, @first_range.column_for_attribute(:int4_range).type
+ assert_equal :int8range, @first_range.column_for_attribute(:int8_range).type
+ end
+
def test_data_type_of_tsvector_types
assert_equal :tsvector, @first_tsvector.column_for_attribute(:text_vector).type
end
@@ -120,20 +240,216 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
assert_equal "'text' 'vector'", @first_tsvector.text_vector
end
+ def test_int4range_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal 1...11, @first_range.int4_range
+ assert_equal 2...10, @second_range.int4_range
+ assert_equal 2...Float::INFINITY, @third_range.int4_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int4_range)
+ assert_nil @empty_range.int4_range
+ end
+
+ def test_int8range_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal 10...101, @first_range.int8_range
+ assert_equal 11...100, @second_range.int8_range
+ assert_equal 11...Float::INFINITY, @third_range.int8_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.int8_range)
+ assert_nil @empty_range.int8_range
+ end
+
+ def test_daterange_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal Date.new(2012, 1, 2)...Date.new(2012, 1, 5), @first_range.date_range
+ assert_equal Date.new(2012, 1, 3)...Date.new(2012, 1, 4), @second_range.date_range
+ assert_equal Date.new(2012, 1, 3)...Float::INFINITY, @third_range.date_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.date_range)
+ assert_nil @empty_range.date_range
+ end
+
+ def test_numrange_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal BigDecimal.new('0.1')..BigDecimal.new('0.2'), @first_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('0.2'), @second_range.num_range
+ assert_equal BigDecimal.new('0.1')...BigDecimal.new('Infinity'), @third_range.num_range
+ assert_equal BigDecimal.new('-Infinity')...BigDecimal.new('Infinity'), @fourth_range.num_range
+ assert_nil @empty_range.num_range
+ end
+
+ def test_tsrange_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ tz = ::ActiveRecord::Base.default_timezone
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)..Time.send(tz, 2011, 1, 1, 14, 30, 0), @first_range.ts_range
+ assert_equal Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 1, 1, 14, 30, 0), @second_range.ts_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.ts_range)
+ assert_nil @empty_range.ts_range
+ end
+
+ def test_tstzrange_values
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')..Time.parse('2011-01-01 17:30:00 UTC'), @first_range.tstz_range
+ assert_equal Time.parse('2010-01-01 09:30:00 UTC')...Time.parse('2011-01-01 17:30:00 UTC'), @second_range.tstz_range
+ assert_equal(-Float::INFINITY...Float::INFINITY, @fourth_range.tstz_range)
+ assert_nil @empty_range.tstz_range
+ end
+
def test_money_values
assert_equal 567.89, @first_money.wealth
assert_equal(-567.89, @second_money.wealth)
end
+ def test_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_create_tstzrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ tstzrange = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2011-02-02 14:30:00 CDT')
+ range = PostgresqlRange.new(:tstz_range => tstzrange)
+ assert range.save
+ assert range.reload
+ assert_equal range.tstz_range, tstzrange
+ assert_equal range.tstz_range, Time.parse('2010-01-01 13:30:00 UTC')...Time.parse('2011-02-02 19:30:00 UTC')
+ end
+
+ def test_update_tstzrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ new_tstzrange = Time.parse('2010-01-01 14:30:00 CDT')...Time.parse('2011-02-02 14:30:00 CET')
+ @first_range.tstz_range = new_tstzrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_tstzrange, @first_range.tstz_range
+ @first_range.tstz_range = Time.parse('2010-01-01 14:30:00 +0100')...Time.parse('2010-01-01 13:30:00 +0000')
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.tstz_range
+ end
+
+ def test_create_tsrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ tz = ::ActiveRecord::Base.default_timezone
+ tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
+ range = PostgresqlRange.new(:ts_range => tsrange)
+ assert range.save
+ assert range.reload
+ assert_equal range.ts_range, tsrange
+ end
+
+ def test_update_tsrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ tz = ::ActiveRecord::Base.default_timezone
+ new_tsrange = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2011, 2, 2, 14, 30, 0)
+ @first_range.ts_range = new_tsrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_tsrange, @first_range.ts_range
+ @first_range.ts_range = Time.send(tz, 2010, 1, 1, 14, 30, 0)...Time.send(tz, 2010, 1, 1, 14, 30, 0)
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.ts_range
+ end
+
+ def test_create_numrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
+ range = PostgresqlRange.new(:num_range => numrange)
+ assert range.save
+ assert range.reload
+ assert_equal range.num_range, numrange
+ end
+
+ def test_update_numrange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ new_numrange = BigDecimal.new('0.5')...BigDecimal.new('1')
+ @first_range.num_range = new_numrange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_numrange, @first_range.num_range
+ @first_range.num_range = BigDecimal.new('0.5')...BigDecimal.new('0.5')
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.num_range
+ end
+
+ def test_create_daterange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ daterange = Range.new(Date.new(2012, 1, 1), Date.new(2013, 1, 1), true)
+ range = PostgresqlRange.new(:date_range => daterange)
+ assert range.save
+ assert range.reload
+ assert_equal range.date_range, daterange
+ end
+
+ def test_update_daterange
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ new_daterange = Date.new(2012, 2, 3)...Date.new(2012, 2, 10)
+ @first_range.date_range = new_daterange
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_daterange, @first_range.date_range
+ @first_range.date_range = Date.new(2012, 2, 3)...Date.new(2012, 2, 3)
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.date_range
+ end
+
+ def test_create_int4range
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ int4range = Range.new(3, 50, true)
+ range = PostgresqlRange.new(:int4_range => int4range)
+ assert range.save
+ assert range.reload
+ assert_equal range.int4_range, int4range
+ end
+
+ def test_update_int4range
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ new_int4range = 6...10
+ @first_range.int4_range = new_int4range
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_int4range, @first_range.int4_range
+ @first_range.int4_range = 3...3
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.int4_range
+ end
+
+ def test_create_int8range
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ int8range = Range.new(30, 50, true)
+ range = PostgresqlRange.new(:int8_range => int8range)
+ assert range.save
+ assert range.reload
+ assert_equal range.int8_range, int8range
+ end
+
+ def test_update_int8range
+ skip "PostgreSQL 9.2 required for range datatypes" unless @connection.supports_ranges?
+ new_int8range = 60000...10000000
+ @first_range.int8_range = new_int8range
+ assert @first_range.save
+ assert @first_range.reload
+ assert_equal new_int8range, @first_range.int8_range
+ @first_range.int8_range = 39999...39999
+ assert @first_range.save
+ assert @first_range.reload
+ assert_nil @first_range.int8_range
+ end
+
def test_update_tsvector
new_text_vector = "'new' 'text' 'vector'"
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
- assert @first_tsvector.text_vector = new_text_vector
+ @first_tsvector.text_vector = new_text_vector
assert @first_tsvector.save
assert @first_tsvector.reload
- assert_equal @first_tsvector.text_vector, new_text_vector
+ assert_equal new_text_vector, @first_tsvector.text_vector
end
def test_number_values
@@ -171,31 +487,31 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def test_update_integer_array
new_value = [32800,95000,29350,17000]
- assert @first_array.commission_by_quarter = new_value
+ @first_array.commission_by_quarter = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.commission_by_quarter, new_value
- assert @first_array.commission_by_quarter = new_value
+ 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 @first_array.commission_by_quarter, new_value
+ assert_equal new_value, @first_array.commission_by_quarter
end
def test_update_text_array
new_value = ['robby','robert','rob','robbie']
- assert @first_array.nicknames = new_value
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.nicknames, new_value
- assert @first_array.nicknames = new_value
+ assert_equal new_value, @first_array.nicknames
+ @first_array.nicknames = new_value
assert @first_array.save
assert @first_array.reload
- assert_equal @first_array.nicknames, new_value
+ assert_equal new_value, @first_array.nicknames
end
def test_update_money
new_value = BigDecimal.new('123.45')
- assert @first_money.wealth = new_value
+ @first_money.wealth = new_value
assert @first_money.save
assert @first_money.reload
assert_equal new_value, @first_money.wealth
@@ -204,28 +520,28 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def test_update_number
new_single = 789.012
new_double = 789012.345
- assert @first_number.single = new_single
- assert @first_number.double = new_double
+ @first_number.single = new_single
+ @first_number.double = new_double
assert @first_number.save
assert @first_number.reload
- assert_equal @first_number.single, new_single
- assert_equal @first_number.double, new_double
+ assert_equal new_single, @first_number.single
+ assert_equal new_double, @first_number.double
end
def test_update_time
- assert @first_time.time_interval = '2 years 3 minutes'
+ @first_time.time_interval = '2 years 3 minutes'
assert @first_time.save
assert @first_time.reload
- assert_equal @first_time.time_interval, '2 years 00:03:00'
+ 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'
- assert @first_network_address.cidr_address = new_cidr_address
- assert @first_network_address.inet_address = new_inet_address
- assert @first_network_address.mac_address = new_mac_address
+ @first_network_address.cidr_address = new_cidr_address
+ @first_network_address.inet_address = new_inet_address
+ @first_network_address.mac_address = new_mac_address
assert @first_network_address.save
assert @first_network_address.reload
assert_equal @first_network_address.cidr_address, new_cidr_address
@@ -235,54 +551,66 @@ class PostgresqlDataTypeTest < ActiveRecord::TestCase
def test_update_bit_string
new_bit_string = '11111111'
- new_bit_string_varying = 'FF'
- assert @first_bit_string.bit_string = new_bit_string
- assert @first_bit_string.bit_string_varying = new_bit_string_varying
+ 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 @first_bit_string.bit_string, new_bit_string
+ 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
- assert @first_oid.obj_id = new_value
+ @first_oid.obj_id = new_value
assert @first_oid.save
assert @first_oid.reload
- assert_equal @first_oid.obj_id, new_value
+ assert_equal new_value, @first_oid.obj_id
end
def test_timestamp_with_zone_values_with_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
+ with_timezone_config default: :utc, aware_attributes: true do
+ @connection.reconnect!
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
-
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
def test_timestamp_with_zone_values_without_rails_time_zone_support
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
- old_default_tz = ActiveRecord::Base.default_timezone
-
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
-
- @connection.reconnect!
-
- @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
- assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ with_timezone_config default: :local, aware_attributes: false do
+ @connection.reconnect!
+ # make sure to use a non-UTC time zone
+ @connection.execute("SET time zone 'America/Jamaica'", 'SCHEMA')
+
+ @first_timestamp_with_zone = PostgresqlTimestampWithZone.find(1)
+ assert_equal Time.utc(2010,1,1, 11,0,0), @first_timestamp_with_zone.time
+ assert_instance_of Time, @first_timestamp_with_zone.time
+ end
ensure
- ActiveRecord::Base.default_timezone = old_default_tz
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
@connection.reconnect!
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/explain_test.rb b/activerecord/test/cases/adapters/postgresql/explain_test.rb
index 619d581d5f..0b61f61572 100644
--- a/activerecord/test/cases/adapters/postgresql/explain_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/explain_test.rb
@@ -22,13 +22,6 @@ module ActiveRecord
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
-
- def test_dont_explain_for_set_search_path
- queries = Thread.current[:available_queries_for_explain] = []
- ActiveRecord::Base.connection.schema_search_path = "public"
- assert queries.empty?
- end
-
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/hstore_test.rb b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
index 23bafde17b..de724486c2 100644
--- a/activerecord/test/cases/adapters/postgresql/hstore_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/hstore_test.rb
@@ -7,19 +7,30 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlHstoreTest < ActiveRecord::TestCase
class Hstore < ActiveRecord::Base
self.table_name = 'hstores'
+
+ store_accessor :settings, :language, :timezone
end
def setup
@connection = ActiveRecord::Base.connection
- begin
- @connection.transaction do
- @connection.create_table('hstores') do |t|
- t.hstore 'tags', :default => ''
- end
- end
- rescue ActiveRecord::StatementInvalid
+
+ unless @connection.supports_extensions?
return skip "do not test on PG without hstore"
end
+
+ unless @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
+ @connection.transaction do
+ @connection.create_table('hstores') do |t|
+ t.hstore 'tags', :default => ''
+ t.hstore 'settings'
+ end
+ end
@column = Hstore.columns.find { |c| c.name == 'tags' }
end
@@ -27,10 +38,48 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
@connection.execute 'drop table if exists hstores'
end
+ def test_hstore_included_in_extensions
+ assert @connection.respond_to?(:extensions), "connection should have a list of extensions"
+ assert @connection.extensions.include?('hstore'), "extension list should include hstore"
+ end
+
+ def test_disable_enable_hstore
+ assert @connection.extension_enabled?('hstore')
+ @connection.disable_extension 'hstore'
+ assert_not @connection.extension_enabled?('hstore')
+ @connection.enable_extension 'hstore'
+ assert @connection.extension_enabled?('hstore')
+ ensure
+ # Restore column(s) dropped by `drop extension hstore cascade;`
+ load_schema
+ end
+
def test_column
assert_equal :hstore, @column.type
end
+ def test_change_table_supports_hstore
+ @connection.transaction do
+ @connection.change_table('hstores') do |t|
+ t.hstore 'users', default: ''
+ end
+ Hstore.reset_column_information
+ column = Hstore.columns.find { |c| c.name == 'users' }
+ assert_equal :hstore, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ Hstore.reset_column_information
+ end
+
+ def test_cast_value_on_write
+ x = Hstore.new tags: {"bool" => true, "number" => 5}
+ assert_equal({"bool" => "true", "number" => "5"}, x.tags)
+ x.save
+ assert_equal({"bool" => "true", "number" => "5"}, x.reload.tags)
+ end
+
def test_type_cast_hstore
assert @column
@@ -44,6 +93,24 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_equal({'c'=>'}','"a"'=>'b "a b'}, @column.type_cast(%q(c=>"}", "\"a\""=>"b \"a b")))
end
+ def test_with_store_accessors
+ x = Hstore.new(language: "fr", timezone: "GMT")
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ x.save!
+ x = Hstore.first
+ assert_equal "fr", x.language
+ assert_equal "GMT", x.timezone
+
+ x.language = "de"
+ x.save!
+
+ x = Hstore.first
+ assert_equal "de", x.language
+ assert_equal "GMT", x.timezone
+ end
+
def test_gen1
assert_equal(%q(" "=>""), @column.class.hstore_to_string({' '=>''}))
end
@@ -140,6 +207,10 @@ class PostgresqlHstoreTest < ActiveRecord::TestCase
assert_cycle('ca' => 'cà', 'ac' => 'àc')
end
+ def test_multiline
+ assert_cycle("a\nb" => "c\nd")
+ end
+
private
def assert_cycle hash
# test creation
diff --git a/activerecord/test/cases/adapters/postgresql/json_test.rb b/activerecord/test/cases/adapters/postgresql/json_test.rb
index d64037eec0..c33c7ef968 100644
--- a/activerecord/test/cases/adapters/postgresql/json_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/json_test.rb
@@ -7,6 +7,8 @@ require 'active_record/connection_adapters/postgresql_adapter'
class PostgresqlJSONTest < ActiveRecord::TestCase
class JsonDataType < ActiveRecord::Base
self.table_name = 'json_data_type'
+
+ store_accessor :settings, :resolution
end
def setup
@@ -15,6 +17,7 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
@connection.transaction do
@connection.create_table('json_data_type') do |t|
t.json 'payload', :default => {}
+ t.json 'settings'
end
end
rescue ActiveRecord::StatementInvalid
@@ -31,6 +34,28 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
assert_equal :json, @column.type
end
+ def test_change_table_supports_json
+ @connection.transaction do
+ @connection.change_table('json_data_type') do |t|
+ t.json 'users', default: '{}'
+ end
+ JsonDataType.reset_column_information
+ column = JsonDataType.columns.find { |c| c.name == 'users' }
+ assert_equal :json, column.type
+
+ raise ActiveRecord::Rollback # reset the schema change
+ end
+ ensure
+ JsonDataType.reset_column_information
+ end
+
+ def test_cast_value_on_write
+ x = JsonDataType.new payload: {"string" => "foo", :symbol => :bar}
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.payload)
+ x.save
+ assert_equal({"string" => "foo", "symbol" => "bar"}, x.reload.payload)
+ end
+
def test_type_cast_json
assert @column
@@ -68,4 +93,32 @@ class PostgresqlJSONTest < ActiveRecord::TestCase
x = JsonDataType.first
assert_equal(nil, x.payload)
end
+
+ def test_select_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ assert_equal(['v0', {'k1' => 'v1'}], x.payload)
+ end
+
+ def test_rewrite_array_json_value
+ @connection.execute %q|insert into json_data_type (payload) VALUES ('["v0",{"k1":"v1"}]')|
+ x = JsonDataType.first
+ x.payload = ['v1', {'k2' => 'v2'}, 'v3']
+ assert x.save!
+ end
+
+ def test_with_store_accessors
+ x = JsonDataType.new(resolution: "320×480")
+ assert_equal "320×480", x.resolution
+
+ x.save!
+ x = JsonDataType.first
+ assert_equal "320×480", x.resolution
+
+ x.resolution = "640×1136"
+ x.save!
+
+ x = JsonDataType.first
+ assert_equal "640×1136", x.resolution
+ end
end
diff --git a/activerecord/test/cases/adapters/postgresql/ltree_test.rb b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
new file mode 100644
index 0000000000..5d12ca75ca
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/ltree_test.rb
@@ -0,0 +1,41 @@
+# 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
+ self.table_name = 'ltrees'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.transaction do
+ @connection.create_table('ltrees') do |t|
+ t.ltree 'path'
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ skip "do not test on PG without ltree"
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists ltrees'
+ end
+
+ def test_column
+ column = Ltree.columns_hash['path']
+ assert_equal :ltree, column.type
+ end
+
+ def test_write
+ ltree = Ltree.new(path: '1.2.3.4')
+ assert ltree.save!
+ end
+
+ def test_select
+ @connection.execute "insert into ltrees (path) VALUES ('1.2.3')"
+ ltree = Ltree.first
+ assert_equal '1.2.3', ltree.path
+ 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 f1362dd15f..8b017760b1 100644
--- a/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/postgresql_adapter_test.rb
@@ -10,6 +10,15 @@ module ActiveRecord
@connection.exec_query('create table ex(id serial primary key, number integer, data character varying(255))')
end
+ def test_valid_column
+ column = @connection.columns('ex').find { |col| col.name == 'id' }
+ assert @connection.valid_type?(column.type)
+ end
+
+ def test_invalid_column
+ assert_not @connection.valid_type?(:foobar)
+ end
+
def test_primary_key
assert_equal 'id', @connection.primary_key('ex')
end
@@ -216,9 +225,44 @@ module ActiveRecord
assert_equal "(number > 100)", index.where
end
- def test_distinct_with_nulls
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls first"])
- assert_equal "DISTINCT posts.title, posts.updater_id AS alias_0", @connection.distinct("posts.title", ["posts.updater_id desc nulls last"])
+ def test_columns_for_distinct_zero_orders
+ assert_equal "posts.id",
+ @connection.columns_for_distinct("posts.id", [])
+ end
+
+ def test_columns_for_distinct_one_order
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", ["posts.created_at desc"])
+ end
+
+ def test_columns_for_distinct_few_orders
+ assert_equal "posts.id, posts.created_at AS alias_0, posts.position AS alias_1",
+ @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "posts.position asc"])
+ end
+
+ def test_columns_for_distinct_blank_not_nil_orders
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", ["posts.created_at desc", "", " "])
+ end
+
+ def test_columns_for_distinct_with_arel_order
+ order = Object.new
+ def order.to_sql
+ "posts.created_at desc"
+ end
+ assert_equal "posts.id, posts.created_at AS alias_0",
+ @connection.columns_for_distinct("posts.id", [order])
+ end
+
+ def test_columns_for_distinct_with_nulls
+ assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls first"])
+ assert_equal "posts.title, posts.updater_id AS alias_0", @connection.columns_for_distinct("posts.title", ["posts.updater_id desc nulls last"])
+ end
+
+ def test_raise_error_when_cannot_translate_exception
+ assert_raise TypeError do
+ @connection.send(:log, nil) { @connection.execute(nil) }
+ end
end
private
diff --git a/activerecord/test/cases/adapters/postgresql/quoting_test.rb b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
index 685f0ea74f..1122f8b9a1 100644
--- a/activerecord/test/cases/adapters/postgresql/quoting_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/quoting_test.rb
@@ -44,6 +44,19 @@ module ActiveRecord
c = Column.new(nil, 1, 'float')
assert_equal "'Infinity'", @conn.quote(infinity, c)
end
+
+ def test_quote_cast_numeric
+ fixnum = 666
+ c = Column.new(nil, nil, 'varchar')
+ assert_equal "'666'", @conn.quote(fixnum, c)
+ c = Column.new(nil, nil, 'text')
+ assert_equal "'666'", @conn.quote(fixnum, c)
+ end
+
+ def test_quote_time_usec
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0))
+ assert_equal "'1970-01-01 00:00:00.000000'", @conn.quote(Time.at(0).to_datetime)
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/schema_test.rb b/activerecord/test/cases/adapters/postgresql/schema_test.rb
index cd31900d4e..e8dd188ec8 100644
--- a/activerecord/test/cases/adapters/postgresql/schema_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/schema_test.rb
@@ -11,16 +11,19 @@ class SchemaTest < ActiveRecord::TestCase
INDEX_B_NAME = 'b_index_things_on_different_columns_in_each_schema'
INDEX_C_NAME = 'c_index_full_text_search'
INDEX_D_NAME = 'd_index_things_on_description_desc'
+ INDEX_E_NAME = 'e_index_things_on_name_vector'
INDEX_A_COLUMN = 'name'
INDEX_B_COLUMN_S1 = 'email'
INDEX_B_COLUMN_S2 = 'moment'
INDEX_C_COLUMN = %q{(to_tsvector('english', coalesce(things.name, '')))}
INDEX_D_COLUMN = 'description'
+ INDEX_E_COLUMN = 'name_vector'
COLUMNS = [
'id integer',
'name character varying(50)',
'email character varying(50)',
'description character varying(100)',
+ 'name_vector tsvector',
'moment timestamp without time zone default now()'
]
PK_TABLE_NAME = 'table_with_pk'
@@ -61,6 +64,8 @@ class SchemaTest < ActiveRecord::TestCase
@connection.execute "CREATE INDEX #{INDEX_C_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_C_COLUMN});"
@connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
@connection.execute "CREATE INDEX #{INDEX_D_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING btree (#{INDEX_D_COLUMN} DESC);"
+ @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
+ @connection.execute "CREATE INDEX #{INDEX_E_NAME} ON #{SCHEMA2_NAME}.#{TABLE_NAME} USING gin (#{INDEX_E_COLUMN});"
@connection.execute "CREATE TABLE #{SCHEMA_NAME}.#{PK_TABLE_NAME} (id serial primary key)"
@connection.execute "CREATE SEQUENCE #{SCHEMA_NAME}.#{UNMATCHED_SEQUENCE_NAME}"
@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))"
@@ -236,15 +241,15 @@ class SchemaTest < ActiveRecord::TestCase
end
def test_dump_indexes_for_schema_one
- do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema(SCHEMA_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_dump_indexes_for_schema_two
- do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema(SCHEMA2_NAME, INDEX_A_COLUMN, INDEX_B_COLUMN_S2, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_dump_indexes_for_schema_multiple_schemas_in_search_path
- do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN)
+ do_dump_index_tests_for_schema("public, #{SCHEMA_NAME}", INDEX_A_COLUMN, INDEX_B_COLUMN_S1, INDEX_D_COLUMN, INDEX_E_COLUMN)
end
def test_with_uppercase_index_name
@@ -344,15 +349,20 @@ class SchemaTest < ActiveRecord::TestCase
@connection.schema_search_path = "'$user', public"
end
- def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name)
+ def do_dump_index_tests_for_schema(this_schema_name, first_index_column_name, second_index_column_name, third_index_column_name, fourth_index_column_name)
with_schema_search_path(this_schema_name) do
indexes = @connection.indexes(TABLE_NAME).sort_by {|i| i.name}
- assert_equal 3,indexes.size
+ assert_equal 4,indexes.size
do_dump_index_assertions_for_one_index(indexes[0], INDEX_A_NAME, first_index_column_name)
do_dump_index_assertions_for_one_index(indexes[1], INDEX_B_NAME, second_index_column_name)
do_dump_index_assertions_for_one_index(indexes[2], INDEX_D_NAME, third_index_column_name)
+ do_dump_index_assertions_for_one_index(indexes[3], INDEX_E_NAME, fourth_index_column_name)
+ indexes.select{|i| i.name != INDEX_E_NAME}.each do |index|
+ assert_equal :btree, index.using
+ end
+ assert_equal :gin, indexes.select{|i| i.name == INDEX_E_NAME}[0].using
assert_equal :desc, indexes.select{|i| i.name == INDEX_D_NAME}[0].orders[INDEX_D_COLUMN]
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/sql_types_test.rb b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
new file mode 100644
index 0000000000..d7d40f6385
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/sql_types_test.rb
@@ -0,0 +1,18 @@
+require "cases/helper"
+
+class SqlTypesTest < ActiveRecord::TestCase
+ def test_binary_types
+ assert_equal 'bytea', type_to_sql(:binary, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ type_to_sql :binary, 4294967295
+ end
+ assert_equal 'text', type_to_sql(:text, 100_000)
+ assert_raise ActiveRecord::ActiveRecordError do
+ type_to_sql :text, 4294967295
+ end
+ end
+
+ def type_to_sql(*args)
+ ActiveRecord::Base.connection.type_to_sql(*args)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
index f1c4b85126..c5fd40accc 100644
--- a/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/statement_pool_test.rb
@@ -1,38 +1,40 @@
require 'cases/helper'
-module ActiveRecord::ConnectionAdapters
- class PostgreSQLAdapter < AbstractAdapter
- class InactivePGconn
- def query(*args)
- raise PGError
- end
+module ActiveRecord
+ module ConnectionAdapters
+ class PostgreSQLAdapter < AbstractAdapter
+ class InactivePGconn
+ def query(*args)
+ raise PGError
+ end
- def status
- PGconn::CONNECTION_BAD
+ def status
+ PGconn::CONNECTION_BAD
+ end
end
- end
- class StatementPoolTest < ActiveRecord::TestCase
- def test_cache_is_per_pid
- return skip('must support fork') unless Process.respond_to?(:fork)
+ class StatementPoolTest < ActiveRecord::TestCase
+ def test_cache_is_per_pid
+ return skip('must support fork') unless Process.respond_to?(:fork)
- cache = StatementPool.new nil, 10
- cache['foo'] = 'bar'
- assert_equal 'bar', cache['foo']
+ cache = StatementPool.new nil, 10
+ cache['foo'] = 'bar'
+ assert_equal 'bar', cache['foo']
- pid = fork {
- lookup = cache['foo'];
- exit!(!lookup)
- }
+ pid = fork {
+ lookup = cache['foo'];
+ exit!(!lookup)
+ }
- Process.waitpid pid
- assert $?.success?, 'process should exit successfully'
- end
+ Process.waitpid pid
+ assert $?.success?, 'process should exit successfully'
+ end
- def test_dealloc_does_not_raise_on_inactive_connection
- cache = StatementPool.new InactivePGconn.new, 10
- cache['foo'] = 'bar'
- assert_nothing_raised { cache.clear }
+ def test_dealloc_does_not_raise_on_inactive_connection
+ cache = StatementPool.new InactivePGconn.new, 10
+ cache['foo'] = 'bar'
+ assert_nothing_raised { cache.clear }
+ end
end
end
end
diff --git a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
index 26507ad654..dbc69a529c 100644
--- a/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
+++ b/activerecord/test/cases/adapters/postgresql/timestamp_test.rb
@@ -1,7 +1,16 @@
require 'cases/helper'
require 'models/developer'
+require 'models/topic'
class TimestampTest < ActiveRecord::TestCase
+ fixtures :topics
+
+ def test_group_by_date
+ keys = Topic.group("date_trunc('month', created_at)").count.keys
+ assert_operator keys.length, :>, 0
+ keys.each { |k| assert_kind_of Time, k }
+ end
+
def test_load_infinity_and_beyond
unless current_adapter?(:PostgreSQLAdapter)
return skip("only tested on postgresql")
@@ -75,6 +84,15 @@ class TimestampTest < ActiveRecord::TestCase
assert_equal '4', pg_datetime_precision('foos', 'updated_at')
end
+ def test_bc_timestamp
+ unless current_adapter?(:PostgreSQLAdapter)
+ return skip("only tested on postgresql")
+ end
+ date = Date.new(0) - 1.second
+ Developer.create!(:name => "aaron", :updated_at => date)
+ assert_equal date, Developer.find_by_name("aaron").updated_at
+ end
+
private
def pg_datetime_precision(table_name, column_name)
diff --git a/activerecord/test/cases/adapters/postgresql/uuid_test.rb b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
new file mode 100644
index 0000000000..a753a23c09
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/uuid_test.rb
@@ -0,0 +1,136 @@
+# encoding: utf-8
+
+require "cases/helper"
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlUUIDTest < ActiveRecord::TestCase
+ class UUID < ActiveRecord::Base
+ self.table_name = 'pg_uuids'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+
+ unless @connection.supports_extensions?
+ return skip "do not test on PG without uuid-ossp"
+ end
+
+ unless @connection.extension_enabled?('uuid-ossp')
+ @connection.enable_extension 'uuid-ossp'
+ @connection.commit_db_transaction
+ end
+
+ @connection.reconnect!
+
+ @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
+ end
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists pg_uuids'
+ end
+
+ def test_id_is_uuid
+ assert_equal :uuid, UUID.columns_hash['id'].type
+ assert UUID.primary_key
+ end
+
+ def test_id_has_a_default
+ u = UUID.create
+ assert_not_nil u.id
+ end
+
+ def test_auto_create_uuid
+ u = UUID.create
+ u.reload
+ assert_not_nil u.other_uuid
+ end
+
+ def test_pk_and_sequence_for_uuid_primary_key
+ pk, seq = @connection.pk_and_sequence_for('pg_uuids')
+ assert_equal 'id', pk
+ assert_equal nil, seq
+ end
+
+ def test_schema_dumper_for_uuid_primary_key
+ schema = StringIO.new
+ ActiveRecord::SchemaDumper.dump(@connection, schema)
+ assert_match(/\bcreate_table "pg_uuids", id: :uuid, default: "uuid_generate_v1\(\)"/, schema.string)
+ assert_match(/t\.uuid "other_uuid", default: "uuid_generate_v4\(\)"/, schema.string)
+ end
+end
+
+class PostgresqlUUIDTestNilDefault < ActiveRecord::TestCase
+ class UUID < ActiveRecord::Base
+ self.table_name = 'pg_uuids'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.reconnect!
+
+ @connection.transaction do
+ @connection.create_table('pg_uuids', id: false) do |t|
+ t.primary_key :id, :uuid, default: nil
+ t.string 'name'
+ end
+ end
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists pg_uuids'
+ end
+
+ def test_id_allows_default_override_via_nil
+ col_desc = @connection.execute("SELECT pg_get_expr(d.adbin, d.adrelid) as default
+ FROM pg_attribute a
+ LEFT JOIN pg_attrdef d ON a.attrelid = d.adrelid AND a.attnum = d.adnum
+ WHERE a.attname='id' AND a.attrelid = 'pg_uuids'::regclass").first
+ assert_nil col_desc["default"]
+ end
+end
+
+class PostgresqlUUIDTestInverseOf < ActiveRecord::TestCase
+ class UuidPost < ActiveRecord::Base
+ self.table_name = 'pg_uuid_posts'
+ has_many :uuid_comments, inverse_of: :uuid_post
+ end
+
+ class UuidComment < ActiveRecord::Base
+ self.table_name = 'pg_uuid_comments'
+ belongs_to :uuid_post
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ @connection.reconnect!
+
+ @connection.transaction do
+ @connection.create_table('pg_uuid_posts', id: :uuid) do |t|
+ t.string 'title'
+ end
+ @connection.create_table('pg_uuid_comments', id: :uuid) do |t|
+ t.uuid :uuid_post_id, default: 'uuid_generate_v4()'
+ t.string 'content'
+ end
+ end
+ end
+
+ def teardown
+ @connection.transaction do
+ @connection.execute 'drop table if exists pg_uuid_comments'
+ @connection.execute 'drop table if exists pg_uuid_posts'
+ end
+ end
+
+ def test_collection_association_with_uuid
+ post = UuidPost.create!
+ comment = post.uuid_comments.create!
+ assert post.uuid_comments.find(comment.id)
+ end
+end
diff --git a/activerecord/test/cases/adapters/postgresql/xml_test.rb b/activerecord/test/cases/adapters/postgresql/xml_test.rb
new file mode 100644
index 0000000000..bf14b378d8
--- /dev/null
+++ b/activerecord/test/cases/adapters/postgresql/xml_test.rb
@@ -0,0 +1,38 @@
+# encoding: utf-8
+
+require 'cases/helper'
+require 'active_record/base'
+require 'active_record/connection_adapters/postgresql_adapter'
+
+class PostgresqlXMLTest < ActiveRecord::TestCase
+ class XmlDataType < ActiveRecord::Base
+ self.table_name = 'xml_data_type'
+ end
+
+ def setup
+ @connection = ActiveRecord::Base.connection
+ begin
+ @connection.transaction do
+ @connection.create_table('xml_data_type') do |t|
+ t.xml 'payload', default: {}
+ end
+ end
+ rescue ActiveRecord::StatementInvalid
+ return skip "do not test on PG without xml"
+ end
+ @column = XmlDataType.columns.find { |c| c.name == 'payload' }
+ end
+
+ def teardown
+ @connection.execute 'drop table if exists xml_data_type'
+ end
+
+ def test_column
+ assert_equal :xml, @column.type
+ end
+
+ def test_null_xml
+ @connection.execute %q|insert into xml_data_type (payload) VALUES(null)|
+ assert_nil XmlDataType.first.payload
+ end
+end
diff --git a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
index 74288a98d1..e78cb88562 100644
--- a/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/copy_table_test.rb
@@ -1,7 +1,7 @@
require "cases/helper"
class CopyTableTest < ActiveRecord::TestCase
- fixtures :customers, :companies, :comments
+ fixtures :customers, :companies, :comments, :binaries
def setup
@connection = ActiveRecord::Base.connection
@@ -32,6 +32,11 @@ class CopyTableTest < ActiveRecord::TestCase
end
end
+ def test_copy_table_allows_to_pass_options_to_create_table
+ @connection.create_table('blocker_table')
+ test_copy_table('customers', 'blocker_table', force: true)
+ end
+
def test_copy_table_with_index
test_copy_table('comments', 'comments_with_index') do
@connection.add_index('comments_with_index', ['post_id', 'type'])
@@ -43,11 +48,13 @@ class CopyTableTest < ActiveRecord::TestCase
end
def test_copy_table_without_primary_key
- test_copy_table('developers_projects', 'programmers_projects')
+ test_copy_table('developers_projects', 'programmers_projects') do
+ assert_nil @connection.primary_key('programmers_projects')
+ end
end
def test_copy_table_with_id_col_that_is_not_primary_key
- test_copy_table('goofy_string_id', 'goofy_string_id2') do |from, to, options|
+ test_copy_table('goofy_string_id', 'goofy_string_id2') do
original_id = @connection.columns('goofy_string_id').detect{|col| col.name == 'id' }
copied_id = @connection.columns('goofy_string_id2').detect{|col| col.name == 'id' }
assert_equal original_id.type, copied_id.type
@@ -58,13 +65,17 @@ class CopyTableTest < ActiveRecord::TestCase
end
def test_copy_table_with_unconventional_primary_key
- test_copy_table('owners', 'owners_unconventional') do |from, to, options|
+ test_copy_table('owners', 'owners_unconventional') do
original_pk = @connection.primary_key('owners')
copied_pk = @connection.primary_key('owners_unconventional')
assert_equal original_pk, copied_pk
end
end
+ def test_copy_table_with_binary_column
+ test_copy_table 'binaries', 'binaries2'
+ end
+
protected
def copy_table(from, to, options = {})
@connection.copy_table(from, to, {:temporary => true}.merge(options))
@@ -79,7 +90,7 @@ protected
end
def table_indexes_without_name(table)
- @connection.indexes('comments_with_index').delete(:name)
+ @connection.indexes(table).delete(:name)
end
def row_count(table)
diff --git a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
index 2ba9143cd5..a7b2764fc1 100644
--- a/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/quoting_test.rb
@@ -17,7 +17,7 @@ module ActiveRecord
@conn.extend(Module.new { def logger; end })
column = Struct.new(:type, :name).new(:string, "foo")
binary = SecureRandom.hex
- expected = binary.dup.encode!('utf-8')
+ expected = binary.dup.encode!(Encoding::UTF_8)
assert_equal expected, @conn.type_cast(binary, column)
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
index 4e26c5dda1..ce7c869eec 100644
--- a/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_adapter_test.rb
@@ -21,13 +21,26 @@ module ActiveRecord
)
eosql
- @conn.extend(LogIntercepter)
- @conn.intercepted = true
+ @subscriber = SQLSubscriber.new
+ ActiveSupport::Notifications.subscribe('sql.active_record', @subscriber)
+ end
+
+ def test_valid_column
+ column = @conn.columns('items').find { |col| col.name == 'id' }
+ assert @conn.valid_type?(column.type)
+ 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.
+ def test_invalid_column
+ assert @conn.valid_type?(:foobar)
end
def teardown
- @conn.intercepted = false
- @conn.logged = []
+ ActiveSupport::Notifications.unsubscribe(@subscriber)
+ super
end
def test_column_types
@@ -154,6 +167,12 @@ module ActiveRecord
DualEncoding.connection.drop_table('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
+ end
+
def test_execute
@conn.execute "INSERT INTO items (number) VALUES (10)"
records = @conn.execute "SELECT * FROM items"
@@ -237,7 +256,7 @@ module ActiveRecord
def test_tables_logs_name
assert_logged [['SCHEMA', []]] do
@conn.tables('hello')
- assert_not_nil @conn.logged.first.shift
+ assert_not_nil @subscriber.logged.first.shift
end
end
@@ -249,7 +268,7 @@ module ActiveRecord
def test_table_exists_logs_name
assert @conn.table_exists?('items')
- assert_equal 'SCHEMA', @conn.logged[0][1]
+ assert_equal 'SCHEMA', @subscriber.logged[0][1]
end
def test_columns
@@ -287,10 +306,10 @@ module ActiveRecord
end
def test_indexes_logs
- assert_difference('@conn.logged.length') do
+ assert_difference('@subscriber.logged.length') do
@conn.indexes('items')
end
- assert_match(/items/, @conn.logged.last.first)
+ assert_match(/items/, @subscriber.logged.last.first)
end
def test_no_indexes
@@ -335,11 +354,23 @@ module ActiveRecord
assert_nil @conn.primary_key('failboat')
end
+ def test_supports_extensions
+ assert_not @conn.supports_extensions?, 'does not support extensions'
+ end
+
+ def test_respond_to_enable_extension
+ assert @conn.respond_to?(:enable_extension)
+ end
+
+ def test_respond_to_disable_extension
+ assert @conn.respond_to?(:disable_extension)
+ end
+
private
def assert_logged logs
yield
- assert_equal logs, @conn.logged
+ assert_equal logs, @subscriber.logged
end
end
diff --git a/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
new file mode 100644
index 0000000000..5a4fe63580
--- /dev/null
+++ b/activerecord/test/cases/adapters/sqlite3/sqlite3_create_folder_test.rb
@@ -0,0 +1,21 @@
+# encoding: utf-8
+require "cases/helper"
+require 'models/owner'
+
+module ActiveRecord
+ module ConnectionAdapters
+ class SQLite3CreateFolder < ActiveRecord::TestCase
+ def test_sqlite_creates_directory
+ Dir.mktmpdir do |dir|
+ dir = Pathname.new(dir)
+ @conn = Base.sqlite3_connection :database => dir.join("db/foo.sqlite3"),
+ :adapter => 'sqlite3',
+ :timeout => 100
+
+ assert Dir.exists? dir.join('db')
+ assert File.exist? dir.join('db/foo.sqlite3')
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/aggregations_test.rb b/activerecord/test/cases/aggregations_test.rb
index 48b06a767f..5536702f58 100644
--- a/activerecord/test/cases/aggregations_test.rb
+++ b/activerecord/test/cases/aggregations_test.rb
@@ -1,6 +1,5 @@
require "cases/helper"
require 'models/customer'
-require 'active_support/core_ext/exception'
class AggregationsTest < ActiveRecord::TestCase
fixtures :customers
@@ -26,7 +25,7 @@ class AggregationsTest < ActiveRecord::TestCase
def test_immutable_value_objects
customers(:david).balance = Money.new(100)
- assert_raise(ActiveSupport::FrozenObjectError) { customers(:david).balance.instance_eval { @amount = 20 } }
+ assert_raise(RuntimeError) { customers(:david).balance.instance_eval { @amount = 20 } }
end
def test_inferred_mapping
@@ -142,7 +141,6 @@ class AggregationsTest < ActiveRecord::TestCase
end
class OverridingAggregationsTest < ActiveRecord::TestCase
- class Name; end
class DifferentName; end
class Person < ActiveRecord::Base
diff --git a/activerecord/test/cases/ar_schema_test.rb b/activerecord/test/cases/ar_schema_test.rb
index b2eac0349b..500df52cd8 100644
--- a/activerecord/test/cases/ar_schema_test.rb
+++ b/activerecord/test/cases/ar_schema_test.rb
@@ -7,10 +7,14 @@ if ActiveRecord::Base.connection.supports_migrations?
def setup
@connection = ActiveRecord::Base.connection
+ ActiveRecord::SchemaMigration.drop_table
end
def teardown
@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_schema_define
@@ -28,6 +32,24 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_equal 7, ActiveRecord::Migrator::current_version
end
+ def test_schema_define_w_table_name_prefix
+ table_name = ActiveRecord::SchemaMigration.table_name
+ ActiveRecord::Base.table_name_prefix = "nep_"
+ ActiveRecord::SchemaMigration.table_name = "nep_#{table_name}"
+ ActiveRecord::Schema.define(:version => 7) do
+ create_table :fruits do |t|
+ t.column :color, :string
+ t.column :fruit_size, :string # NOTE: "size" is reserved in Oracle
+ t.column :texture, :string
+ t.column :flavor, :string
+ end
+ end
+ assert_equal 7, ActiveRecord::Migrator::current_version
+ ensure
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::SchemaMigration.table_name = table_name
+ end
+
def test_schema_raises_an_error_for_invalid_column_type
assert_raise NoMethodError do
ActiveRecord::Schema.define(:version => 8) do
@@ -45,5 +67,4 @@ if ActiveRecord::Base.connection.supports_migrations?
assert_nothing_raised { @connection.select_all "SELECT * FROM fruits" }
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 5f7825783b..a79f145e31 100644
--- a/activerecord/test/cases/associations/belongs_to_associations_test.rb
+++ b/activerecord/test/cases/associations/belongs_to_associations_test.rb
@@ -1,4 +1,4 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/developer'
require 'models/project'
require 'models/company'
@@ -14,6 +14,8 @@ require 'models/sponsor'
require 'models/member'
require 'models/essay'
require 'models/toy'
+require 'models/invoice'
+require 'models/line_item'
class BelongsToAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :topics,
@@ -21,9 +23,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
:posts, :tags, :taggings, :comments, :sponsors, :members
def test_belongs_to
- Client.find(3).firm.name
- assert_equal companies(:first_firm).name, Client.find(3).firm.name
- assert_not_nil Client.find(3).firm, "Microsoft should have a firm"
+ firm = Client.find(3).firm
+ assert_not_nil firm
+ assert_equal companies(:first_firm).name, firm.name
end
def test_belongs_to_with_primary_key
@@ -33,7 +35,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_belongs_to_with_primary_key_joins_on_correct_column
sql = Client.joins(:firm_with_primary_key).to_sql
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_no_match(/`firm_with_primary_keys_companies`\.`id`/, sql)
assert_match(/`firm_with_primary_keys_companies`\.`name`/, sql)
elsif current_adapter?(:OracleAdapter)
@@ -63,6 +65,13 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_id_assignment
+ apple = Firm.create("name" => "Apple")
+ citibank = Account.create("credit_limit" => 10)
+ citibank.firm_id = apple
+ assert_nil citibank.firm_id
+ end
+
def test_natural_assignment_with_primary_key
apple = Firm.create("name" => "Apple")
citibank = Client.create("name" => "Primary key client")
@@ -109,6 +118,34 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal apple.id, citibank.firm_id
end
+ def test_building_the_belonging_object_with_implicit_sti_base_class
+ account = Account.new
+ company = account.build_firm
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_explicit_sti_base_class
+ account = Account.new
+ company = account.build_firm(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_belonging_object_with_sti_subclass
+ account = Account.new
+ company = account.build_firm(:type => "Firm")
+ assert_kind_of Firm, company, "Expected #{company.class} to be a Firm"
+ end
+
+ def test_building_the_belonging_object_with_an_invalid_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "InvalidType") }
+ end
+
+ def test_building_the_belonging_object_with_an_unrelated_type
+ account = Account.new
+ assert_raise(ActiveRecord::SubclassNotFound) { account.build_firm(:type => "Account") }
+ end
+
def test_building_the_belonging_object_with_primary_key
client = Client.create(:name => "Primary key client")
apple = client.build_firm_with_primary_key("name" => "Apple")
@@ -197,15 +234,15 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_assigning_nil
- p = Post.find(1)
- c = Comment.find(1)
+ post = Post.find(1)
+ comment = Comment.find(1)
- assert_equal p.id, c.post_id
- assert_equal 2, Post.find(p.id).comments.size
+ assert_equal post.id, comment.post_id
+ assert_equal 2, Post.find(post.id).comments.size
- c.post = nil
+ comment.post = nil
- assert_equal 1, Post.find(p.id).comments.size
+ assert_equal 1, Post.find(post.id).comments.size
end
def test_belongs_to_with_primary_key_counter
@@ -228,56 +265,56 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_belongs_to_counter_with_reassigning
- t1 = Topic.create("title" => "t1")
- t2 = Topic.create("title" => "t2")
- r1 = Reply.new("title" => "r1", "content" => "r1")
- r1.topic = t1
+ topic1 = Topic.create("title" => "t1")
+ topic2 = Topic.create("title" => "t2")
+ reply1 = Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
- assert r1.save
- assert_equal 1, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.topic = Topic.find(t2.id)
+ reply1.topic = Topic.find(topic2.id)
assert_no_queries do
- r1.topic = t2
+ reply1.topic = topic2
end
- assert r1.save
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 1, Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 1, Topic.find(topic2.id).replies.size
- r1.topic = nil
+ reply1.topic = nil
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.topic = t1
+ reply1.topic = topic1
- assert_equal 1, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 1, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
- r1.destroy
+ reply1.destroy
- assert_equal 0, Topic.find(t1.id).replies.size
- assert_equal 0, Topic.find(t2.id).replies.size
+ assert_equal 0, Topic.find(topic1.id).replies.size
+ assert_equal 0, Topic.find(topic2.id).replies.size
end
def test_belongs_to_reassign_with_namespaced_models_and_counters
- t1 = Web::Topic.create("title" => "t1")
- t2 = Web::Topic.create("title" => "t2")
- r1 = Web::Reply.new("title" => "r1", "content" => "r1")
- r1.topic = t1
+ topic1 = Web::Topic.create("title" => "t1")
+ topic2 = Web::Topic.create("title" => "t2")
+ reply1 = Web::Reply.new("title" => "r1", "content" => "r1")
+ reply1.topic = topic1
- assert r1.save
- assert_equal 1, Web::Topic.find(t1.id).replies.size
- assert_equal 0, Web::Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 1, Web::Topic.find(topic1.id).replies.size
+ assert_equal 0, Web::Topic.find(topic2.id).replies.size
- r1.topic = Web::Topic.find(t2.id)
+ reply1.topic = Web::Topic.find(topic2.id)
- assert r1.save
- assert_equal 0, Web::Topic.find(t1.id).replies.size
- assert_equal 1, Web::Topic.find(t2.id).replies.size
+ assert reply1.save
+ assert_equal 0, Web::Topic.find(topic1.id).replies.size
+ assert_equal 1, Web::Topic.find(topic2.id).replies.size
end
def test_belongs_to_counter_after_save
@@ -289,12 +326,51 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
- def test_belongs_to_counter_after_update_attributes
- topic = Topic.create!(:title => "37s")
- topic.replies.create!(:title => "re: 37s", :content => "rails")
+ def test_belongs_to_with_touch_option_on_touch
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(1) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_removed_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = nil
+
+ assert_queries(2) { line_item.touch }
+ end
+
+ def test_belongs_to_with_touch_option_on_update
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.update amount: 10 }
+ end
+
+ def test_belongs_to_with_touch_option_on_destroy
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ assert_queries(2) { line_item.destroy }
+ end
+
+ def test_belongs_to_with_touch_option_on_touch_and_reassigned_parent
+ line_item = LineItem.create!
+ Invoice.create!(line_items: [line_item])
+
+ line_item.invoice = Invoice.create!
+
+ assert_queries(3) { line_item.touch }
+ end
+
+ def test_belongs_to_counter_after_update
+ topic = Topic.create!(title: "37s")
+ topic.replies.create!(title: "re: 37s", content: "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
- topic.update_attributes(:title => "37signals")
+ topic.update(title: "37signals")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
@@ -303,7 +379,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
topic.replies.create!(:title => "re: 37s", :content => "rails")
assert_equal 1, Topic.find(topic.id)[:replies_count]
- topic.update_columns(content: "rails is wonderfull")
+ topic.update_columns(content: "rails is wonderful")
assert_equal 1, Topic.find(topic.id)[:replies_count]
end
@@ -332,9 +408,9 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_new_record_with_foreign_key_but_no_object
- c = Client.new("firm_id" => 1)
+ client = Client.new("firm_id" => 1)
# sometimes tests on Oracle fail if ORDER BY is not provided therefore add always :order with :first
- assert_equal Firm.all.merge!(:order => "id").first, c.firm_with_basic_id
+ assert_equal Firm.all.merge!(:order => "id").first, client.firm_with_basic_id
end
def test_setting_foreign_key_after_nil_target_loaded
@@ -356,8 +432,7 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
def test_dont_find_target_when_foreign_key_is_null
tagging = taggings(:thinking_general)
- queries = assert_sql { tagging.super_tag }
- assert_equal 0, queries.length
+ assert_queries(0) { tagging.super_tag }
end
def test_field_name_same_as_foreign_key
@@ -379,6 +454,26 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal 15, topic.replies.size
end
+ def test_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.destroy
+ assert_equal 4, topic.reload[:replies_count]
+
+ reply.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]
@@ -510,6 +605,8 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
end
def test_dependent_delete_and_destroy_with_belongs_to
+ AuthorAddress.destroyed_author_address_ids.clear
+
author_address = author_addresses(:david_address)
author_address_extra = author_addresses(:david_address_extra)
assert_equal [], AuthorAddress.destroyed_author_address_ids
@@ -539,6 +636,11 @@ class BelongsToAssociationsTest < ActiveRecord::TestCase
assert_equal new_firm.name, "Apple"
end
+ def test_attributes_are_set_without_error_when_initialized_from_belongs_to_association_with_array_in_where_clause
+ new_account = Account.where(:credit_limit => [ 50, 60 ]).new
+ assert_nil new_account.credit_limit
+ end
+
def test_reassigning_the_parent_id_updates_the_object
client = companies(:second_client)
diff --git a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
index 80bca7f63e..811d91f849 100644
--- a/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
+++ b/activerecord/test/cases/associations/cascaded_eager_loading_test.rb
@@ -52,12 +52,10 @@ class CascadedEagerLoadingTest < ActiveRecord::TestCase
def test_cascaded_eager_association_loading_with_join_for_count
categories = Category.joins(:categorizations).includes([{:posts=>:comments}, :authors])
- assert_nothing_raised do
- assert_equal 4, categories.count
- assert_equal 4, categories.to_a.count
- assert_equal 3, categories.count(:distinct => true)
- assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
- end
+ assert_equal 4, categories.count
+ assert_equal 4, categories.to_a.count
+ assert_equal 3, categories.distinct.count
+ assert_equal 3, categories.to_a.uniq.size # Must uniq since instantiating with inner joins will get dupes
end
def test_cascaded_eager_association_loading_with_duplicated_includes
diff --git a/activerecord/test/cases/associations/eager_test.rb b/activerecord/test/cases/associations/eager_test.rb
index 124bf65d3a..498a4e8144 100644
--- a/activerecord/test/cases/associations/eager_test.rb
+++ b/activerecord/test/cases/associations/eager_test.rb
@@ -4,6 +4,7 @@ require 'models/tagging'
require 'models/tag'
require 'models/comment'
require 'models/author'
+require 'models/essay'
require 'models/category'
require 'models/company'
require 'models/person'
@@ -24,7 +25,7 @@ require 'models/categorization'
require 'models/sponsor'
class EagerAssociationTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :authors, :author_addresses, :categories, :categories_posts,
+ fixtures :posts, :comments, :authors, :essays, :author_addresses, :categories, :categories_posts,
:companies, :accounts, :tags, :taggings, :people, :readers, :categorizations,
:owners, :pets, :author_favorites, :jobs, :references, :subscribers, :subscriptions, :books,
:developers, :projects, :developers_projects, :members, :memberships, :clubs, :sponsors
@@ -73,6 +74,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
+ def test_has_many_through_with_order
+ authors = Author.includes(:favorite_authors).to_a
+ assert_no_queries { authors.map(&:favorite_authors) }
+ end
+
def test_with_two_tables_in_from_without_getting_double_quoted
posts = Post.select("posts.*").from("authors, posts").eager_load(:comments).where("posts.author_id = authors.id").order("posts.id").to_a
assert_equal 2, posts.first.comments.size
@@ -93,31 +99,31 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_preloading_has_many_in_multiple_queries_with_more_ids_than_database_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.all.merge!(:includes=>:comments).to_a
assert_equal 11, posts.size
end
def test_preloading_has_many_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.all.merge!(:includes=>:comments).to_a
assert_equal 11, posts.size
end
def test_preloading_habtm_in_multiple_queries_with_more_ids_than_database_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(5)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(5)
posts = Post.all.merge!(:includes=>:categories).to_a
assert_equal 11, posts.size
end
def test_preloading_habtm_in_one_queries_when_database_has_no_limit_on_ids_it_can_handle
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
posts = Post.all.merge!(:includes=>:categories).to_a
assert_equal 11, posts.size
end
def test_load_associated_records_in_one_query_when_adapter_has_no_limit
- Post.connection.expects(:in_clause_length).at_least_once.returns(nil)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(nil)
post = posts(:welcome)
assert_queries(2) do
@@ -126,7 +132,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_associated_records_in_several_queries_when_many_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(1)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(1)
post1, post2 = posts(:welcome), posts(:thinking)
assert_queries(3) do
@@ -135,7 +141,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_load_associated_records_in_one_query_when_a_few_ids_passed
- Post.connection.expects(:in_clause_length).at_least_once.returns(3)
+ Comment.connection.expects(:in_clause_length).at_least_once.returns(3)
post = posts(:welcome)
assert_queries(2) do
@@ -188,7 +194,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
end
- def test_finding_with_includes_on_has_one_assocation_with_same_include_includes_only_once
+ def test_finding_with_includes_on_has_one_association_with_same_include_includes_only_once
author = authors(:david)
post = author.post_about_thinking_with_last_comment
last_comment = post.last_comment
@@ -212,8 +218,9 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_association_with_same_include_includes_only_once
post = posts(:welcome)
- post.update_attributes!(:author => nil)
- post = assert_queries(1) { Post.all.merge!(:includes => {:author_with_address => :author_address}).find(post.id) } # find the post, then find the author which is null so no query for the author or address
+ post.update!(author: nil)
+ post = assert_queries(1) { Post.all.merge!(includes: {author_with_address: :author_address}).find(post.id) }
+ # find the post, then find the author which is null so no query for the author or address
assert_no_queries do
assert_equal nil, post.author_with_address
end
@@ -221,7 +228,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_finding_with_includes_on_null_belongs_to_polymorphic_association
sponsor = sponsors(:moustache_club_sponsor_for_groucho)
- sponsor.update_attributes!(:sponsorable => nil)
+ sponsor.update!(sponsorable: nil)
sponsor = assert_queries(1) { Sponsor.all.merge!(:includes => :sponsorable).find(sponsor.id) }
assert_no_queries do
assert_equal nil, sponsor.sponsorable
@@ -244,7 +251,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_nil Post.all.merge!(:includes => :author).find(posts(:authorless).id).author
end
- def test_nested_loading_with_no_associations
+ # Regression test for 21c75e5
+ def test_nested_loading_does_not_raise_exception_when_association_does_not_exist
assert_nothing_raised do
Post.all.merge!(:includes => {:author => :author_addresss}).find(posts(:authorless).id)
end
@@ -296,7 +304,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_foreign_keys
pets = Pet.all.merge!(:includes => :owner).to_a
- assert_equal 3, pets.length
+ assert_equal 4, pets.length
end
def test_eager_association_loading_with_belongs_to
@@ -339,9 +347,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_unquoted_table_name
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ['posts.id = ?',4]).to_a
- end
+ Comment.includes(:post).references(:posts).where('posts.id = ?', 4)
end
end
@@ -360,9 +366,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_conditions_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :where => ["#{quoted_posts_id} = ?",4]).to_a
- end
+ Comment.includes(:post).references(:posts).where("#{quoted_posts_id} = ?", 4)
end
end
@@ -375,9 +379,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_association_loading_with_belongs_to_and_order_string_with_quoted_table_name
quoted_posts_id= Comment.connection.quote_table_name('posts') + '.' + Comment.connection.quote_column_name('id')
assert_nothing_raised do
- ActiveSupport::Deprecation.silence do
- Comment.all.merge!(:includes => :post, :order => quoted_posts_id).to_a
- end
+ Comment.includes(:post).references(:posts).order(quoted_posts_id)
end
end
@@ -461,7 +463,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
posts_with_comments = people(:michael).posts.merge(:includes => :comments, :order => 'posts.id').to_a
posts_with_author = people(:michael).posts.merge(:includes => :author, :order => 'posts.id').to_a
posts_with_comments_and_author = people(:michael).posts.merge(:includes => [ :comments, :author ], :order => 'posts.id').to_a
- assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum += post.comments.size }
+ assert_equal 2, posts_with_comments.inject(0) { |sum, post| sum + post.comments.size }
assert_equal authors(:david), assert_no_queries { posts_with_author.first.author }
assert_equal authors(:david), assert_no_queries { posts_with_comments_and_author.first.author }
end
@@ -517,7 +519,7 @@ class EagerAssociationTest < ActiveRecord::TestCase
def test_eager_with_has_many_and_limit
posts = Post.all.merge!(:order => 'posts.id asc', :includes => [ :author, :comments ], :limit => 2).to_a
assert_equal 2, posts.size
- assert_equal 3, posts.inject(0) { |sum, post| sum += post.comments.size }
+ assert_equal 3, posts.inject(0) { |sum, post| sum + post.comments.size }
end
def test_eager_with_has_many_and_limit_and_conditions
@@ -541,15 +543,11 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_has_many_and_limit_and_conditions_array_on_the_eagers
- posts = ActiveSupport::Deprecation.silence do
- Post.all.merge!(:includes => [ :author, :comments ], :limit => 2, :where => [ "authors.name = ?", 'David' ]).to_a
- end
+ posts = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David')
assert_equal 2, posts.size
- count = ActiveSupport::Deprecation.silence do
- Post.count(:include => [ :author, :comments ], :limit => 2, :conditions => [ "authors.name = ?", 'David' ])
- end
- assert_equal count, posts.size
+ count = Post.includes(:author, :comments).limit(2).references(:author).where("authors.name = ?", 'David').count
+ assert_equal posts.size, count
end
def test_eager_with_has_many_and_limit_and_high_offset
@@ -616,8 +614,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
general = categories.find { |c| c == categories(:general) }
technology = categories.find { |c| c == categories(:technology) }
- post1 = general.posts.to_a.find { |p| p == posts(:welcome) }
- post2 = technology.posts.to_a.find { |p| p == posts(:welcome) }
+ post1 = general.posts.to_a.find { |p| p == welcome }
+ post2 = technology.posts.to_a.find { |p| p == welcome }
assert_equal post1.object_id, post2.object_id
end
@@ -750,6 +748,8 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_eager_with_default_scope_as_block
+ # warm up the habtm cache
+ EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first.projects
developer = EagerDeveloperWithBlockDefaultScope.where(:name => 'David').first
projects = Project.order(:id).to_a
assert_no_queries do
@@ -944,6 +944,12 @@ class EagerAssociationTest < ActiveRecord::TestCase
assert_equal 3, Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a.size
end
+ def test_dont_create_temporary_active_record_instances
+ Developer.instance_count = 0
+ developers = Developer.all.merge!(:includes => 'projects', :where => { 'developers_projects.access_level' => 1 }, :limit => 5).to_a
+ assert_equal developers.count, Developer.instance_count
+ end
+
def test_order_on_join_table_with_include_and_limit
assert_equal 5, Developer.all.merge!(:includes => 'projects', :order => 'developers_projects.joined_on DESC', :limit => 5).to_a.size
end
@@ -984,10 +990,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
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 1, post.lazy_readers.to_a.size
assert_equal 2, post.lazy_readers_skimmers_or_not.to_a.size
-
+
post_with_readers = Post.includes(:lazy_readers_skimmers_or_not).find(post.id)
assert_equal 2, post_with_readers.lazy_readers_skimmers_or_not.to_a.size
end
@@ -1133,6 +1139,10 @@ class EagerAssociationTest < ActiveRecord::TestCase
end
def test_deep_including_through_habtm
+ # warm up habtm cache
+ posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
+ posts[0].categories[0].categorizations.length
+
posts = Post.all.merge!(:includes => {:categories => :categorizations}, :order => "posts.id").to_a
assert_no_queries { assert_equal 2, posts[0].categories[0].categorizations.length }
assert_no_queries { assert_equal 1, posts[0].categories[1].categorizations.length }
@@ -1155,4 +1165,33 @@ class EagerAssociationTest < ActiveRecord::TestCase
Post.where('1 = 0').scoping { Comment.preload(:post).find(1).post }
)
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 }
+ assert_no_queries { assert_equal 5, author.posts.size, "should not cache a subset of the association" }
+ end
+
+ test "preloading a through association twice does not reset it" do
+ members = Member.includes(current_membership: :club).includes(:club).to_a
+ assert_no_queries {
+ assert_equal 3, members.map(&:current_membership).map(&:club).size
+ }
+ end
+
+ test "works in combination with order(:symbol) and reorder(:symbol)" do
+ author = Author.includes(:posts).references(:posts).order(:name).find_by('posts.title IS NOT NULL')
+ assert_equal authors(:bob), author
+
+ author = Author.includes(:posts).references(:posts).reorder(:name).find_by('posts.title IS NOT NULL')
+ assert_equal authors(:bob), author
+ end
+
+ test "preloading with a polymorphic association and using the existential predicate" do
+ assert_equal authors(:david), authors(:david).essays.includes(:writer).first.writer
+
+ assert_nothing_raised do
+ authors(:david).essays.includes(:writer).any?
+ end
+ end
end
diff --git a/activerecord/test/cases/associations/extension_test.rb b/activerecord/test/cases/associations/extension_test.rb
index da767a2a7e..f8f2832ab1 100644
--- a/activerecord/test/cases/associations/extension_test.rb
+++ b/activerecord/test/cases/associations/extension_test.rb
@@ -59,9 +59,11 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
end
def test_extension_name
- assert_equal 'DeveloperAssociationNameAssociationExtension', extension_name(Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
- assert_equal 'MyApplication::Business::DeveloperAssociationNameAssociationExtension', extension_name(MyApplication::Business::Developer)
+ extend!(Developer)
+ extend!(MyApplication::Business::Developer)
+
+ assert Object.const_get 'DeveloperAssociationNameAssociationExtension'
+ assert MyApplication::Business.const_get 'DeveloperAssociationNameAssociationExtension'
end
def test_proxy_association_after_scoped
@@ -72,9 +74,7 @@ class AssociationsExtensionsTest < ActiveRecord::TestCase
private
- def extension_name(model)
- builder = ActiveRecord::Associations::Builder::HasMany.new(model, :association_name, nil, {}) { }
- builder.send(:wrap_block_extension)
- builder.extension_module.name
+ def extend!(model)
+ ActiveRecord::Associations::Builder::HasMany.define_extensions(model, :association_name) { }
end
end
diff --git a/activerecord/test/cases/associations/habtm_join_table_test.rb b/activerecord/test/cases/associations/habtm_join_table_test.rb
deleted file mode 100644
index fe2b82f2c1..0000000000
--- a/activerecord/test/cases/associations/habtm_join_table_test.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-require 'cases/helper'
-
-class MyReader < ActiveRecord::Base
- has_and_belongs_to_many :my_books
-end
-
-class MyBook < ActiveRecord::Base
- has_and_belongs_to_many :my_readers
-end
-
-class HabtmJoinTableTest < ActiveRecord::TestCase
- def setup
- ActiveRecord::Base.connection.create_table :my_books, :force => true do |t|
- t.string :name
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_books)
-
- ActiveRecord::Base.connection.create_table :my_readers, :force => true do |t|
- t.string :name
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_readers)
-
- ActiveRecord::Base.connection.create_table :my_books_my_readers, :force => true do |t|
- t.integer :my_book_id
- t.integer :my_reader_id
- end
- assert ActiveRecord::Base.connection.table_exists?(:my_books_my_readers)
- end
-
- def teardown
- ActiveRecord::Base.connection.drop_table :my_books
- ActiveRecord::Base.connection.drop_table :my_readers
- ActiveRecord::Base.connection.drop_table :my_books_my_readers
- 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 42f5b69d4e..be928ec8ee 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
@@ -65,19 +65,6 @@ class DeveloperWithSymbolsForKeys < ActiveRecord::Base
:foreign_key => "developer_id"
end
-class DeveloperWithCounterSQL < ActiveRecord::Base
- self.table_name = 'developers'
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :projects,
- :class_name => "DeveloperWithCounterSQL",
- :join_table => "developers_projects",
- :association_foreign_key => "project_id",
- :foreign_key => "developer_id",
- :counter_sql => proc { "SELECT COUNT(*) AS count_all FROM projects INNER JOIN developers_projects ON projects.id = developers_projects.project_id WHERE developers_projects.developer_id =#{id}" }
- end
-end
-
class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :categories, :posts, :categories_posts, :developers, :projects, :developers_projects,
:parrots, :pirates, :parrots_pirates, :treasures, :price_estimates, :tags, :taggings
@@ -316,7 +303,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
dev.projects << projects(:active_record)
assert_equal 3, dev.projects.size
- assert_equal 1, dev.projects.uniq.size
+ assert_equal 1, dev.projects.distinct.size
end
def test_uniq_before_the_fact
@@ -364,31 +351,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 0, david.projects(true).size
end
- def test_deleting_with_sql
- david = Developer.find(1)
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(david)
- assert_equal 2, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_array_with_sql
- active_record = Project.find(1)
- active_record.developers.reload
- assert_equal 3, active_record.developers_by_sql.size
-
- active_record.developers_by_sql.delete(Developer.all)
- assert_equal 0, active_record.developers_by_sql(true).size
- end
-
- def test_deleting_all_with_sql
- project = Project.find(1)
- project.developers_by_sql.delete_all
- assert_equal 0, project.developers_by_sql.size
- end
-
def test_deleting_all
david = Developer.find(1)
david.projects.reload
@@ -475,13 +437,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert george.treasures(true).empty?
end
- def test_deprecated_push_with_attributes_was_removed
- jamis = developers(:jamis)
- assert_raise(NoMethodError) do
- jamis.projects.push_with_attributes(projects(:action_controller), :joined_on => Date.today)
- end
- end
-
def test_associations_with_conditions
assert_equal 3, projects(:active_record).developers.size
assert_equal 1, projects(:active_record).developers_named_david.size
@@ -537,25 +492,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert ! project.developers.include?(developer)
end
- def test_find_in_association_with_custom_finder_sql
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id), "SQL find"
-
- active_record = projects(:active_record)
- active_record.developers_with_finder_sql.reload
- assert_equal developers(:david), active_record.developers_with_finder_sql.find(developers(:david).id), "Ruby find"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_multiple_interpolations
- # interpolate once:
- assert_equal [developers(:david), developers(:jamis), developers(:poor_jamis)], projects(:active_record).developers_with_finder_sql, "first interpolation"
- # interpolate again, for a different project id
- assert_equal [developers(:david)], projects(:action_controller).developers_with_finder_sql, "second interpolation"
- end
-
- def test_find_in_association_with_custom_finder_sql_and_string_id
- assert_equal developers(:david), projects(:active_record).developers_with_finder_sql.find(developers(:david).id.to_s), "SQL find"
- end
-
def test_find_with_merged_options
assert_equal 1, projects(:active_record).limited_developers.size
assert_equal 1, projects(:active_record).limited_developers.to_a.size
@@ -570,9 +506,9 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal high_id_jamis, projects(:active_record).developers.find_by_name('Jamis')
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_developers = projects(:active_record).developers.order('projects.id')
- assert_equal ['projects.id', 'developers.name desc, developers.id desc'], ordered_developers.order_values
+ assert_equal ['developers.name desc, developers.id desc', 'projects.id'], ordered_developers.order_values
end
def test_dynamic_find_all_should_respect_readonly_access
@@ -669,16 +605,24 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_join_table_alias
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
:includes => {:projects => :developers},
- :where => 'developers_projects_join.joined_on IS NOT NULL'
+ :where => 'projects_developers_projects_join.joined_on IS NOT NULL'
).to_a.size
)
end
def test_join_with_group
+ # FIXME: `references` has no impact on the aliases generated for the join
+ # query. The fact that we pass `:developers_projects_join` to `references`
+ # and that the SQL string contains `developers_projects_join` is merely a
+ # coincidence.
group = Developer.columns.inject([]) do |g, c|
g << "developers.#{c.name}"
g << "developers_projects_2.#{c.name}"
@@ -688,7 +632,7 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal(
3,
Developer.references(:developers_projects_join).merge(
- :includes => {:projects => :developers}, :where => 'developers_projects_join.joined_on IS NOT NULL',
+ :includes => {:projects => :developers}, :where => 'projects_developers_projects_join.joined_on IS NOT NULL',
:group => group.join(",")
).to_a.size
)
@@ -702,12 +646,12 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
end
def test_find_scoped_grouped
- assert_equal 5, categories(:general).posts_grouped_by_title.size
- assert_equal 1, categories(:technology).posts_grouped_by_title.size
+ assert_equal 5, categories(:general).posts_grouped_by_title.to_a.size
+ assert_equal 1, categories(:technology).posts_grouped_by_title.to_a.size
end
def test_find_scoped_grouped_having
- assert_equal 2, projects(:active_record).well_payed_salary_groups.size
+ assert_equal 2, projects(:active_record).well_payed_salary_groups.to_a.size
assert projects(:active_record).well_payed_salary_groups.all? { |g| g.salary > 10000 }
end
@@ -774,12 +718,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal project, developer.projects.first
end
- def test_self_referential_habtm_without_foreign_key_set_should_raise_exception
- assert_raise(ActiveRecord::HasAndBelongsToManyAssociationForeignKeyNeeded) {
- SelfMember.new.friends
- }
- end
-
def test_dynamic_find_should_respect_association_include
# SQL error in sort clause if :include is not included
# due to Unknown column 'authors.id'
@@ -791,27 +729,6 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, david.projects.count
end
- def test_count_with_counter_sql
- developer = DeveloperWithCounterSQL.create(:name => 'tekin')
- developer.project_ids = [projects(:active_record).id]
- developer.save
- developer.reload
- assert_equal 1, developer.projects.count
- end
-
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Developer.new.projects.count
- end
- end
-
- unless current_adapter?(:PostgreSQLAdapter)
- def test_count_with_finder_sql
- assert_equal 3, projects(:active_record).developers_with_finder_sql.count
- assert_equal 3, projects(:active_record).developers_with_multiline_finder_sql.count
- end
- end
-
def test_association_proxy_transaction_method_starts_transaction_in_association_class
Post.expects(:transaction)
Category.first.posts.transaction do
@@ -851,15 +768,14 @@ class HasAndBelongsToManyAssociationsTest < ActiveRecord::TestCase
assert project.developers.include?(developer)
end
- test ":insert_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :insert_sql => 'lol' }
+ test "has and belongs to many associations on new records use null relations" do
+ projects = Developer.new.projects
+ assert_no_queries do
+ assert_equal [], projects
+ assert_equal [], projects.where(title: 'omg')
+ assert_equal [], projects.pluck(:title)
+ assert_equal 0, projects.count
+ end
end
- test ":delete_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- def klass.name; 'Foo'; end
- assert_deprecated { klass.has_and_belongs_to_many :posts, :delete_sql => 'lol' }
- 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 50c23c863f..dfc8a68e8c 100644
--- a/activerecord/test/cases/associations/has_many_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_associations_test.rb
@@ -19,79 +19,9 @@ require 'models/line_item'
require 'models/car'
require 'models/bulb'
require 'models/engine'
-
-class HasManyAssociationsTestForCountWithFinderSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithCountSql < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :counter_sql => "SELECT COUNT(*) line_items.* from line_items"
- end
- end
- def test_should_fail
- assert_raise(ArgumentError) do
- Invoice.create.custom_line_items.count(:conditions => {:amount => 0})
- end
- end
-end
-
-class HasManyAssociationsTestForCountWithVariousFinderSqls < ActiveRecord::TestCase
- class Invoice < ActiveRecord::Base
- ActiveSupport::Deprecation.silence do
- has_many :custom_line_items, :class_name => 'LineItem', :finder_sql => "SELECT DISTINCT line_items.amount from line_items"
- has_many :custom_full_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.invoice_id, line_items.amount from line_items"
- has_many :custom_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT * from line_items"
- has_many :custom_qualified_star_line_items, :class_name => 'LineItem', :finder_sql => "SELECT line_items.* from line_items"
- end
- end
-
- def test_should_count_distinct_results
- invoice = Invoice.new
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.custom_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 1, invoice.custom_line_items.count
- end
-
- def test_should_count_results_with_multiple_fields
- invoice = Invoice.new
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.custom_full_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_full_line_items.count
- end
-
- def test_should_count_results_with_star
- invoice = Invoice.new
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_star_line_items.count
- end
-
- def test_should_count_results_with_qualified_star
- invoice = Invoice.new
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.custom_qualified_star_line_items << LineItem.new(:amount => 0)
- invoice.save!
-
- assert_equal 2, invoice.custom_qualified_star_line_items.count
- end
-end
+require 'models/categorization'
+require 'models/minivan'
+require 'models/speedometer'
class HasManyAssociationsTestForReorderWithJoinDependency < ActiveRecord::TestCase
fixtures :authors, :posts, :comments
@@ -108,12 +38,31 @@ end
class HasManyAssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :categories, :companies, :developers, :projects,
:developers_projects, :topics, :authors, :comments,
- :people, :posts, :readers, :taggings, :cars, :essays
+ :people, :posts, :readers, :taggings, :cars, :essays,
+ :categorizations
def setup
Client.destroyed_client_ids.clear
end
+ def test_anonymous_has_many
+ developer = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers'
+ dev = self
+
+ developer_project = Class.new(ActiveRecord::Base) {
+ self.table_name = 'developers_projects'
+ belongs_to :developer, :class => dev
+ }
+ has_many :developer_projects, :class => developer_project, :foreign_key => 'developer_id'
+ }
+ dev = developer.first
+ named = Developer.find(dev.id)
+ assert_operator dev.developer_projects.count, :>, 0
+ assert_equal named.projects.map(&:id).sort,
+ dev.developer_projects.map(&:project_id).sort
+ end
+
def test_create_from_association_should_respect_default_scope
car = Car.create(:name => 'honda')
assert_equal 'honda', car.name
@@ -131,6 +80,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'exotic', bulb.name
end
+ def test_build_from_association_should_respect_scope
+ author = Author.new
+
+ post = author.thinking_posts.build
+ assert_equal 'So I was thinking', post.title
+ end
+
def test_create_from_association_with_nil_values_should_work
car = Car.create(:name => 'honda')
@@ -144,6 +100,51 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 'defaulty', bulb.name
end
+ def test_do_not_call_callbacks_for_delete_all
+ car = Car.create(:name => 'honda')
+ car.funky_bulbs.create!
+ assert_nothing_raised { car.reload.funky_bulbs.delete_all }
+ assert_equal 0, Bulb.count, "bulbs should have been deleted using :delete_all strategey"
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.companies.build(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.companies.build(:type => "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.companies.build(:type => "Account") }
+ end
+
+ test "building the association with an array" do
+ speedometer = Speedometer.new(speedometer_id: "a")
+ data = [{name: "first"}, {name: "second"}]
+ speedometer.minivans.build(data)
+
+ assert_equal 2, speedometer.minivans.size
+ assert speedometer.save
+ assert_equal ["first", "second"], speedometer.reload.minivans.map(&:name)
+ end
+
def test_association_keys_bypass_attribute_protection
car = Car.create(:name => 'honda')
@@ -262,12 +263,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal firm.limited_clients.length, firm.limited_clients.count
end
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Person.new.readers.count
- end
- end
-
def test_finding
assert_equal 2, Firm.all.merge!(:order => "id").first.clients.length
end
@@ -276,21 +271,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 2, Firm.order(:id).find{|f| f.id > 0}.clients.length
end
- def test_find_with_blank_conditions
- [[], {}, nil, ""].each do |blank|
- assert_equal 2, Firm.all.merge!(:order => "id").first.clients.where(blank).to_a.size
- end
- end
-
def test_find_many_with_merged_options
assert_equal 1, companies(:first_firm).limited_clients.size
assert_equal 1, companies(:first_firm).limited_clients.to_a.size
assert_equal 2, companies(:first_firm).limited_clients.limit(nil).to_a.size
end
- def test_find_should_prepend_to_association_order
+ def test_find_should_append_to_association_order
ordered_clients = companies(:first_firm).clients_sorted_desc.order('companies.id')
- assert_equal ['companies.id', 'id DESC'], ordered_clients.order_values
+ assert_equal ['id DESC', 'companies.id'], ordered_clients.order_values
end
def test_dynamic_find_should_respect_association_order
@@ -327,37 +316,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal "Summit", Firm.all.merge!(:order => "id").first.clients_using_primary_key.first.name
end
- def test_finding_using_sql
- firm = Firm.order("id").first
- first_client = firm.clients_using_sql.first
- assert_not_nil first_client
- assert_equal "Microsoft", first_client.name
- assert_equal 1, firm.clients_using_sql.size
- assert_equal 1, Firm.order("id").first.clients_using_sql.size
- end
-
- def test_finding_using_sql_take_into_account_only_uniq_ids
- firm = Firm.order("id").first
- client = firm.clients_using_sql.first
- assert_equal client, firm.clients_using_sql.find(client.id, client.id)
- assert_equal client, firm.clients_using_sql.find(client.id, client.id.to_s)
- end
-
- def test_counting_using_sql
- assert_equal 1, Firm.order("id").first.clients_using_counter_sql.size
- assert Firm.order("id").first.clients_using_counter_sql.any?
- assert_equal 0, Firm.order("id").first.clients_using_zero_counter_sql.size
- assert !Firm.order("id").first.clients_using_zero_counter_sql.any?
- end
-
- def test_counting_non_existant_items_using_sql
- assert_equal 0, Firm.order("id").first.no_clients_using_counter_sql.size
- end
-
- def test_counting_using_finder_sql
- assert_equal 2, Firm.find(4).clients_using_sql.count
- end
-
def test_belongs_to_sanity
c = Client.new
assert_nil c.firm
@@ -385,20 +343,16 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::RecordNotFound) { firm.clients.find(2, 99) }
end
- def test_find_string_ids_when_using_finder_sql
- firm = Firm.order("id").first
+ def test_find_ids_and_inverse_of
+ force_signal37_to_load_all_clients_of_firm
- client = firm.clients_using_finder_sql.find("2")
+ firm = companies(:first_firm)
+ client = firm.clients_of_firm.find(3)
assert_kind_of Client, client
- client_ary = firm.clients_using_finder_sql.find(["2"])
+ client_ary = firm.clients_of_firm.find([3])
assert_kind_of Array, client_ary
assert_equal client, client_ary.first
-
- client_ary = firm.clients_using_finder_sql.find("2", "3")
- assert_kind_of Array, client_ary
- assert_equal 2, client_ary.size
- assert client_ary.include?(client)
end
def test_find_all
@@ -582,6 +536,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_inverse_on_before_validate
+ firm = companies(:first_firm)
+ assert_queries(1) do
+ firm.clients_of_firm << Client.new("name" => "Natural Company")
+ end
+ end
+
def test_new_aliased_to_build
company = companies(:first_firm)
new_client = assert_no_queries { company.clients_of_firm.new("name" => "Another Client") }
@@ -609,6 +570,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal 3, company.clients_of_firm.size
end
+ def test_collection_not_empty_after_building
+ company = companies(:first_firm)
+ assert_predicate company.contracts, :empty?
+ company.contracts.build
+ assert_not_predicate company.contracts, :empty?
+ end
+
def test_collection_size_twice_for_regressions
post = posts(:thinking)
assert_equal 0, post.readers.size
@@ -728,6 +696,15 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal topic.replies.to_a.size, topic.replies_count
end
+ def test_pushing_association_updates_counter_cache
+ topic = Topic.order("id ASC").first
+ reply = Reply.create!
+
+ assert_difference "topic.reload.replies_count", 1 do
+ topic.replies << reply
+ end
+ end
+
def test_deleting_updates_counter_cache_without_dependent_option
post = posts(:welcome)
@@ -754,6 +731,45 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_custom_named_counter_cache
+ topic = topics(:first)
+
+ assert_difference "topic.reload.replies_count", -1 do
+ topic.approved_replies.clear
+ end
+ end
+
+ def test_calling_update_attributes_on_id_changes_the_counter_cache
+ topic = Topic.order("id ASC").first
+ original_count = topic.replies.to_a.size
+ assert_equal original_count, topic.replies_count
+
+ first_reply = topic.replies.first
+ first_reply.update_attributes(:parent_id => nil)
+ assert_equal original_count - 1, topic.reload.replies_count
+
+ first_reply.update_attributes(:parent_id => topic.id)
+ assert_equal original_count, topic.reload.replies_count
+ end
+
+ def test_calling_update_attributes_changing_ids_doesnt_change_counter_cache
+ topic1 = Topic.find(1)
+ topic2 = Topic.find(3)
+ original_count1 = topic1.replies.to_a.size
+ original_count2 = topic2.replies.to_a.size
+
+ reply1 = topic1.replies.first
+ reply2 = topic2.replies.first
+
+ reply1.update_attributes(:parent_id => topic2.id)
+ assert_equal original_count1 - 1, topic1.reload.replies_count
+ assert_equal original_count2 + 1, topic2.reload.replies_count
+
+ reply2.update_attributes(:parent_id => topic1.id)
+ assert_equal original_count1, topic1.reload.replies_count
+ assert_equal original_count2, topic2.reload.replies_count
+ end
+
def test_deleting_a_collection
force_signal37_to_load_all_clients_of_firm
companies(:first_firm).clients_of_firm.create("name" => "Another Client")
@@ -765,13 +781,13 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_delete_all
force_signal37_to_load_all_clients_of_firm
- companies(:first_firm).clients_of_firm.create("name" => "Another Client")
- clients = companies(:first_firm).clients_of_firm.to_a
+ companies(:first_firm).dependent_clients_of_firm.create("name" => "Another Client")
+ clients = companies(:first_firm).dependent_clients_of_firm.to_a
assert_equal 2, clients.count
- deleted = companies(:first_firm).clients_of_firm.delete_all
- assert_equal clients.sort_by(&:id), deleted.sort_by(&:id)
- assert_equal 0, companies(:first_firm).clients_of_firm.size
- assert_equal 0, companies(:first_firm).clients_of_firm(true).size
+
+ assert_difference "Client.count", -(clients.count) do
+ companies(:first_firm).dependent_clients_of_firm.delete_all
+ end
end
def test_delete_all_with_not_yet_loaded_association_collection
@@ -845,18 +861,33 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm = companies(:first_firm)
client_id = firm.dependent_clients_of_firm.first.id
assert_equal 1, firm.dependent_clients_of_firm.size
+ assert_equal 1, Client.find_by_id(client_id).client_of
- # :dependent means destroy is called on each client
+ # :delete_all is called on each client since the dependent options is :destroy
firm.dependent_clients_of_firm.clear
assert_equal 0, firm.dependent_clients_of_firm.size
assert_equal 0, firm.dependent_clients_of_firm(true).size
- assert_equal [client_id], Client.destroyed_client_ids[firm.id]
+ assert_equal [], Client.destroyed_client_ids[firm.id]
# Should be destroyed since the association is dependent.
assert_nil Client.find_by_id(client_id)
end
+ def test_delete_all_with_option_delete_all
+ firm = companies(:first_firm)
+ client_id = firm.dependent_clients_of_firm.first.id
+ firm.dependent_clients_of_firm.delete_all(:delete_all)
+ assert_nil Client.find_by_id(client_id)
+ end
+
+ def test_delete_all_accepts_limited_parameters
+ firm = companies(:first_firm)
+ assert_raise(ArgumentError) do
+ firm.dependent_clients_of_firm.delete_all(:destroy)
+ end
+ end
+
def test_clearing_an_exclusively_dependent_association_collection
firm = companies(:first_firm)
client_id = firm.exclusively_dependent_clients_of_firm.first.id
@@ -1104,21 +1135,6 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal num_accounts, Account.count
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.companies.create(:name => 'child')
-
- assert !firm.companies.empty?
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.companies.exists?(:name => 'child')
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :posts, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.companies.create(:name => 'child')
@@ -1145,7 +1161,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
def test_included_in_collection
- assert companies(:first_firm).clients.include?(Client.find(2))
+ assert_equal true, companies(:first_firm).clients.include?(Client.find(2))
+ end
+
+ def test_included_in_collection_for_new_records
+ client = Client.create(:name => 'Persisted')
+ assert_nil client.client_of
+ assert_equal false, Firm.new.clients_of_firm.include?(client),
+ 'includes a client that does not belong to any firm'
end
def test_adding_array_and_collection
@@ -1172,7 +1195,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.save
firm.reload
assert_equal 2, firm.clients.length
- assert !firm.clients.include?(:first_client)
+ assert_equal false, firm.clients.include?(:first_client)
end
def test_replace_failure
@@ -1233,24 +1256,44 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [readers(:michael_welcome).id], posts(:welcome).readers_with_person_ids
end
- def test_get_ids_for_unloaded_finder_sql_associations_loads_them
- company = companies(:first_firm)
- assert !company.clients_using_sql.loaded?
- assert_equal [companies(:second_client).id], company.clients_using_sql_ids
- assert company.clients_using_sql.loaded?
- end
-
def test_get_ids_for_ordered_association
assert_equal [companies(:second_client).id, companies(:first_client).id], companies(:first_firm).clients_ordered_by_name_ids
end
+ def test_get_ids_for_association_on_new_record_does_not_try_to_find_records
+ Company.columns # Load schema information so we don't query below
+ Contract.columns # if running just this test.
+
+ company = Company.new
+ assert_queries(0) do
+ company.contract_ids
+ end
+
+ assert_equal [], company.contract_ids
+ end
+
+ def test_set_ids_for_association_on_new_record_applies_association_correctly
+ contract_a = Contract.create!
+ contract_b = Contract.create!
+ Contract.create! # another contract
+ company = Company.new(:name => "Some Company")
+
+ company.contract_ids = [contract_a.id, contract_b.id]
+ assert_equal [contract_a.id, contract_b.id], company.contract_ids
+ assert_equal [contract_a, contract_b], company.contracts
+
+ company.save!
+ assert_equal company, contract_a.reload.company
+ assert_equal company, contract_b.reload.company
+ end
+
def test_assign_ids_ignoring_blanks
firm = Firm.create!(:name => 'Apple')
firm.client_ids = [companies(:first_client).id, nil, companies(:second_client).id, '']
firm.save!
assert_equal 2, firm.clients(true).size
- assert firm.clients.include?(companies(:second_client))
+ assert_equal true, firm.clients.include?(companies(:second_client))
end
def test_get_ids_for_through
@@ -1284,7 +1327,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_no_queries do
assert firm.clients.loaded?
- assert firm.clients.include?(client)
+ assert_equal true, firm.clients.include?(client)
end
end
@@ -1295,28 +1338,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
firm.reload
assert ! firm.clients.loaded?
assert_queries(1) do
- assert firm.clients.include?(client)
+ assert_equal true, firm.clients.include?(client)
end
assert ! firm.clients.loaded?
end
- def test_include_loads_collection_if_target_uses_finder_sql
- firm = companies(:first_firm)
- client = firm.clients_using_sql.first
-
- firm.reload
- assert ! firm.clients_using_sql.loaded?
- assert firm.clients_using_sql.include?(client)
- assert firm.clients_using_sql.loaded?
- end
-
-
def test_include_returns_false_for_non_matching_record_to_verify_scoping
firm = companies(:first_firm)
client = Client.create!(:name => 'Not Associated')
assert ! firm.clients.loaded?
- assert ! firm.clients.include?(client)
+ assert_equal false, firm.clients.include?(client)
end
def test_calling_first_or_last_on_association_should_not_load_association
@@ -1390,6 +1422,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal david.essays, Essay.where(writer_id: "David")
end
+ def test_has_many_assignment_with_custom_primary_key
+ david = people(:david)
+
+ assert_equal ["A Modest Proposal"], david.essays.map(&:name)
+ david.essays = [Essay.create!(name: "Remote Work" )]
+ assert_equal ["Remote Work"], david.essays.map(&:name)
+ end
+
def test_blank_custom_primary_key_on_new_record_should_not_run_queries
author = Author.new
assert !author.essays.loaded?
@@ -1399,15 +1439,17 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- def test_calling_first_or_last_with_integer_on_association_should_load_association
+ def test_calling_first_or_last_with_integer_on_association_should_not_load_association
firm = companies(:first_firm)
+ firm.clients.create(:name => 'Foo')
+ assert !firm.clients.loaded?
- assert_queries 1 do
+ assert_queries 2 do
firm.clients.first(2)
firm.clients.last(2)
end
- assert firm.clients.loaded?
+ assert !firm.clients.loaded?
end
def test_calling_many_should_count_instead_of_loading_association
@@ -1523,7 +1565,7 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
def test_include_method_in_has_many_association_should_return_true_for_instance_added_with_build
post = Post.new
comment = post.comments.build
- assert post.comments.include?(comment)
+ assert_equal true, post.comments.include?(comment)
end
def test_load_target_respects_protected_attributes
@@ -1570,6 +1612,14 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal [tagging], post.taggings
end
+ def test_build_with_polymorphic_has_many_does_not_allow_to_override_type_and_id
+ welcome = posts(:welcome)
+ tagging = welcome.taggings.build(:taggable_id => 99, :taggable_type => 'ShouldNotChange')
+
+ assert_equal welcome.id, tagging.taggable_id
+ assert_equal 'Post', tagging.taggable_type
+ end
+
def test_dont_call_save_callbacks_twice_on_has_many
firm = companies(:first_firm)
contract = firm.contracts.create!
@@ -1585,6 +1635,22 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
assert_equal car.id, bulb.attributes_after_initialize['car_id']
end
+ def test_attributes_are_set_when_initialized_from_has_many_null_relationship
+ car = Car.new name: 'honda'
+ bulb = car.bulbs.where(name: 'headlight').first_or_initialize
+ assert_equal 'headlight', bulb.name
+ end
+
+ def test_attributes_are_set_when_initialized_from_polymorphic_has_many_null_relationship
+ post = Post.new title: 'title', body: 'bar'
+ tag = Tag.create!(name: 'foo')
+
+ tagging = post.taggings.where(tag: tag).first_or_initialize
+
+ assert_equal tag.id, tagging.tag_id
+ assert_equal 'Post', tagging.taggable_type
+ end
+
def test_replace
car = Car.create(:name => 'honda')
bulb1 = car.bulbs.create
@@ -1639,13 +1705,56 @@ class HasManyAssociationsTest < ActiveRecord::TestCase
end
end
- test ":finder_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :finder_sql => 'lol' }
+ test "has many associations on new records use null relations" do
+ post = Post.new
+
+ assert_no_queries do
+ assert_equal [], post.comments
+ assert_equal [], post.comments.where(body: 'omg')
+ assert_equal [], post.comments.pluck(:body)
+ assert_equal 0, post.comments.sum(:id)
+ assert_equal 0, post.comments.count
+ end
+ end
+
+ test "collection proxy respects default scope" do
+ author = authors(:mary)
+ assert !author.first_posts.exists?
end
- test ":counter_sql is deprecated" do
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_many :foo, :counter_sql => 'lol' }
+ test "association with extend option" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend.author
+ assert_equal "hello", post.comments_with_extend.greeting
+ end
+
+ test "association with extend option with multiple extensions" do
+ post = posts(:welcome)
+ assert_equal "lifo", post.comments_with_extend_2.author
+ assert_equal "hello", post.comments_with_extend_2.greeting
+ end
+
+ test "delete record with complex joins" do
+ david = authors(:david)
+
+ post = david.posts.first
+ post.type = 'PostWithSpecialCategorization'
+ post.save
+
+ categorization = post.categorizations.first
+ categorization.special = true
+ categorization.save
+
+ assert_not_equal [], david.posts_with_special_categorizations
+ david.posts_with_special_categorizations = []
+ assert_equal [], david.posts_with_special_categorizations
+ end
+
+ test "does not duplicate associations when used with natural primary keys" do
+ speedometer = Speedometer.create!(id: '4')
+ speedometer.minivans.create!(minivan_id: 'a-van-red' ,name: 'a van', color: 'red')
+
+ assert_equal 1, speedometer.minivans.to_a.size, "Only one association should be present:\n#{speedometer.minivans.to_a}"
+ assert_equal 1, speedometer.reload.minivans.to_a.size
end
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 b2a5d9d6f7..c450b1beb5 100644
--- a/activerecord/test/cases/associations/has_many_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_many_through_associations_test.rb
@@ -5,6 +5,7 @@ require 'models/reference'
require 'models/job'
require 'models/reader'
require 'models/comment'
+require 'models/rating'
require 'models/tag'
require 'models/tagging'
require 'models/author'
@@ -27,7 +28,8 @@ require 'models/club'
class HasManyThroughAssociationsTest < ActiveRecord::TestCase
fixtures :posts, :readers, :people, :comments, :authors, :categories, :taggings, :tags,
:owners, :pets, :toys, :jobs, :references, :companies, :members, :author_addresses,
- :subscribers, :books, :subscriptions, :developers, :categorizations, :essays
+ :subscribers, :books, :subscriptions, :developers, :categorizations, :essays,
+ :categories_posts, :clubs, :memberships
# Dummies to force column loads so query counts are clean.
def setup
@@ -35,6 +37,136 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
Reader.create :person_id => 0, :post_id => 0
end
+ def test_preload_sti_rhs_class
+ developers = Developer.includes(:firms).all.to_a
+ assert_no_queries do
+ developers.each { |d| d.firms }
+ end
+ end
+
+ def test_preload_sti_middle_relation
+ club = Club.create!(name: 'Aaron cool banana club')
+ member1 = Member.create!(name: 'Aaron')
+ member2 = Member.create!(name: 'Cat')
+
+ SuperMembership.create! club: club, member: member1
+ CurrentMembership.create! club: club, member: member2
+
+ club1 = Club.includes(:members).find_by_id club.id
+ assert_equal [member1, member2].sort_by(&:id),
+ club1.members.sort_by(&:id)
+ end
+
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_ordered_habtm
+ person_prime = Class.new(ActiveRecord::Base) do
+ def self.name; 'Person'; end
+
+ has_many :readers
+ has_many :posts, -> { order('posts.id DESC') }, :through => :readers
+ end
+ posts = person_prime.includes(:posts).first.posts
+
+ assert_operator posts.length, :>, 1
+ posts.each_cons(2) do |left,right|
+ assert_operator left.id, :>, right.id
+ end
+ end
+
+ def test_singleton_has_many_through
+ book = make_model "Book"
+ subscription = make_model "Subscription"
+ subscriber = make_model "Subscriber"
+
+ subscriber.primary_key = 'nick'
+ subscription.belongs_to :book, class: book
+ subscription.belongs_to :subscriber, class: subscriber
+
+ book.has_many :subscriptions, class: subscription
+ book.has_many :subscribers, through: :subscriptions, class: subscriber
+
+ anonbook = book.first
+ namebook = Book.find anonbook.id
+
+ assert_operator anonbook.subscribers.count, :>, 0
+ anonbook.subscribers.each do |s|
+ assert_instance_of subscriber, s
+ end
+ assert_equal namebook.subscribers.map(&:id).sort,
+ anonbook.subscribers.map(&:id).sort
+ end
+
+ def test_no_pk_join_table_append
+ lesson, _, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+ end
+
+ def test_no_pk_join_table_delete
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ louis = student.new(:name => "Louis Reasoner")
+ sicp.students << ben
+ sicp.students << louis
+ assert sicp.save!
+
+ sicp.students.reload
+ assert_operator lesson_student.count, :>=, 2
+ assert_no_difference('student.count') do
+ assert_difference('lesson_student.count', -2) do
+ sicp.students.destroy(*student.all.to_a)
+ end
+ end
+ end
+
+ def test_no_pk_join_model_callbacks
+ lesson, lesson_student, student = make_no_pk_hm_t
+
+ after_destroy_called = false
+ lesson_student.after_destroy do
+ after_destroy_called = true
+ end
+
+ sicp = lesson.new(:name => "SICP")
+ ben = student.new(:name => "Ben Bitdiddle")
+ sicp.students << ben
+ assert sicp.save!
+
+ sicp.students.reload
+ sicp.students.destroy(*student.all.to_a)
+ assert after_destroy_called, "after destroy should be called"
+ end
+
+ def make_no_pk_hm_t
+ lesson = make_model 'Lesson'
+ student = make_model 'Student'
+
+ lesson_student = make_model 'LessonStudent'
+ lesson_student.table_name = 'lessons_students'
+
+ lesson_student.belongs_to :lesson, :class => lesson
+ lesson_student.belongs_to :student, :class => student
+ lesson.has_many :lesson_students, :class => lesson_student
+ lesson.has_many :students, :through => :lesson_students, :class => student
+ [lesson, lesson_student, student]
+ end
+
+ def test_pk_is_not_required_for_join
+ post = Post.includes(:scategories).first
+ post2 = Post.includes(:categories).first
+
+ assert_operator post.categories.length, :>, 0
+ assert_equal post2.categories, post.categories
+ end
+
def test_include?
person = Person.new
post = Post.new
@@ -57,6 +189,47 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert post.reload.people(true).include?(person)
end
+ def test_delete_all_for_with_dependent_option_destroy
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_destroy.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_destroy.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_nullify
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_nullify.count
+
+ assert_no_difference 'Job.count' do
+ assert_no_difference 'Reference.count' do
+ person.reload.jobs_with_dependent_nullify.delete_all
+ end
+ end
+ end
+
+ def test_delete_all_for_with_dependent_option_delete_all
+ person = people(:david)
+ assert_equal 1, person.jobs_with_dependent_delete_all.count
+
+ assert_no_difference 'Job.count' do
+ assert_difference 'Reference.count', -1 do
+ person.reload.jobs_with_dependent_delete_all.delete_all
+ end
+ end
+ end
+
+ def test_concat
+ person = people(:david)
+ post = posts(:thinking)
+ post.people.concat [person]
+ assert_equal 1, post.people.size
+ assert_equal 1, post.people(true).size
+ end
+
def test_associate_existing_record_twice_should_add_to_target_twice
post = posts(:thinking)
person = people(:david)
@@ -330,6 +503,17 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
end
end
+ def test_update_counter_caches_on_replace_association
+ post = posts(:welcome)
+ tag = post.tags.create!(:name => 'doomed')
+ tag.tagged_posts << posts(:thinking)
+
+ tag.tagged_posts = []
+ post.reload
+
+ assert_equal(post.taggings.count, post.taggings_count)
+ end
+
def test_replace_association
assert_queries(4){posts(:welcome);people(:david);people(:michael); posts(:welcome).people(true)}
@@ -571,8 +755,13 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal post.author.author_favorites, post.author_favorites
end
+ def test_merge_join_association_with_has_many_through_association_proxy
+ author = authors(:mary)
+ assert_nothing_raised { author.comments.ratings.to_sql }
+ end
+
def test_has_many_association_through_a_has_many_association_with_nonstandard_primary_keys
- assert_equal 1, owners(:blackbeard).toys.count
+ assert_equal 2, owners(:blackbeard).toys.count
end
def test_find_on_has_many_association_collection_with_include_and_conditions
@@ -596,7 +785,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
sarah = Person.create!(:first_name => 'Sarah', :primary_contact_id => people(:susan).id, :gender => 'F', :number1_fan_id => 1)
john = Person.create!(:first_name => 'John', :primary_contact_id => sarah.id, :gender => 'M', :number1_fan_id => 1)
assert_equal sarah.agents, [john]
- assert_equal people(:susan).agents.map(&:agents).flatten, people(:susan).agents_of_agents
+ assert_equal people(:susan).agents.flat_map(&:agents), people(:susan).agents_of_agents
end
def test_associate_existing_with_nonstandard_primary_key_on_belongs_to
@@ -695,7 +884,7 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
def test_can_update_through_association
assert_nothing_raised do
- people(:michael).posts.first.update_attributes!(:title => "Can write")
+ people(:michael).posts.first.update!(title: "Can write")
end
end
@@ -766,12 +955,6 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
assert_equal 1, authors(:mary).categories.general.count
end
- def test_counting_should_not_fire_sql_if_parent_is_unsaved
- assert_no_queries do
- assert_equal 0, Person.new.posts.count
- end
- end
-
def test_has_many_through_belongs_to_should_update_when_the_through_foreign_key_changes
post = posts(:eager_other)
@@ -876,4 +1059,34 @@ class HasManyThroughAssociationsTest < ActiveRecord::TestCase
post = tags(:general).tagged_posts.create! :title => "foo", :body => "bar"
assert_equal [tags(:general)], post.reload.tags
end
+
+ def test_has_many_through_obeys_order_on_through_association
+ owner = owners(:blackbeard)
+ assert owner.toys.to_sql.include?("pets.name desc")
+ assert_equal ["parrot", "bulbul"], owner.toys.map { |r| r.pet.name }
+ end
+
+ test "has many through associations on new records use null relations" do
+ person = Person.new
+
+ assert_no_queries do
+ assert_equal [], person.posts
+ assert_equal [], person.posts.where(body: 'omg')
+ assert_equal [], person.posts.pluck(:body)
+ assert_equal 0, person.posts.sum(:tags_count)
+ assert_equal 0, person.posts.count
+ end
+ end
+
+ test "has many through with default scope on the target" do
+ person = people(:michael)
+ assert_equal [posts(:thinking)], person.first_posts
+
+ readers(:michael_authorless).update(first_post_id: 1)
+ assert_equal [posts(:thinking)], person.reload.first_posts
+ end
+
+ def test_has_many_through_with_includes_in_through_association_scope
+ assert_not_empty posts(:welcome).author_address_extra_with_address
+ end
end
diff --git a/activerecord/test/cases/associations/has_one_associations_test.rb b/activerecord/test/cases/associations/has_one_associations_test.rb
index 2d3cb654df..cdd386187b 100644
--- a/activerecord/test/cases/associations/has_one_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_associations_test.rb
@@ -6,6 +6,8 @@ require 'models/ship'
require 'models/pirate'
require 'models/car'
require 'models/bulb'
+require 'models/author'
+require 'models/post'
class HasOneAssociationsTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false unless supports_savepoints?
@@ -156,22 +158,6 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_nothing_raised { firm.destroy }
end
- def test_restrict
- firm = RestrictedFirm.create!(:name => 'restrict')
- firm.create_account(:credit_limit => 10)
-
- assert_not_nil firm.account
-
- assert_raise(ActiveRecord::DeleteRestrictionError) { firm.destroy }
- assert RestrictedFirm.exists?(:name => 'restrict')
- assert firm.account.present?
- end
-
- def test_restrict_is_deprecated
- klass = Class.new(ActiveRecord::Base)
- assert_deprecated { klass.has_one :post, dependent: :restrict }
- end
-
def test_restrict_with_exception
firm = RestrictedWithExceptionFirm.create!(:name => 'restrict')
firm.create_account(:credit_limit => 10)
@@ -206,6 +192,40 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_equal account, firm.account
end
+ def test_build_association_dont_create_transaction
+ assert_no_queries {
+ Firm.new.build_account
+ }
+ end
+
+ def test_building_the_associated_object_with_implicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_explicit_sti_base_class
+ firm = DependentFirm.new
+ company = firm.build_company(:type => "Company")
+ assert_kind_of Company, company, "Expected #{company.class} to be a Company"
+ end
+
+ def test_building_the_associated_object_with_sti_subclass
+ firm = DependentFirm.new
+ company = firm.build_company(:type => "Client")
+ assert_kind_of Client, company, "Expected #{company.class} to be a Client"
+ end
+
+ def test_building_the_associated_object_with_an_invalid_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Invalid") }
+ end
+
+ def test_building_the_associated_object_with_an_unrelated_type
+ firm = DependentFirm.new
+ assert_raise(ActiveRecord::SubclassNotFound) { firm.build_company(:type => "Account") }
+ end
+
def test_build_and_create_should_not_happen_within_scope
pirate = pirates(:blackbeard)
scoped_count = pirate.association(:foo_bulb).scope.where_values.count
@@ -485,5 +505,34 @@ class HasOneAssociationsTest < ActiveRecord::TestCase
assert_no_queries { company.account = nil }
account = Account.find(2)
assert_queries { company.account = account }
+
+ assert_no_queries { Firm.new.account = account }
+ end
+
+ def test_has_one_assignment_triggers_save_on_change
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.build_ship(name: 'old name')
+ ship.save!
+
+ ship.name = 'new name'
+ assert ship.changed?
+ assert_queries(2) do
+ # One query for updating name and second query for updating pirate_id
+ pirate.ship = ship
+ end
+
+ assert_equal 'new name', pirate.ship.reload.name
+ end
+
+ def test_has_one_autosave_with_primary_key_manually_set
+ post = Post.create(id: 1234, title: "Some title", body: 'Some content')
+ author = Author.new(id: 33, name: 'Hank Moody')
+
+ author.post = post
+ author.save
+ author.reload
+
+ assert_not_nil author.post
+ assert_equal author.post, post
end
end
diff --git a/activerecord/test/cases/associations/has_one_through_associations_test.rb b/activerecord/test/cases/associations/has_one_through_associations_test.rb
index 90c557e886..f2723f2e18 100644
--- a/activerecord/test/cases/associations/has_one_through_associations_test.rb
+++ b/activerecord/test/cases/associations/has_one_through_associations_test.rb
@@ -191,6 +191,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
def test_preloading_has_one_through_on_belongs_to
+ MemberDetail.delete_all
assert_not_nil @member.member_type
@organization = organizations(:nsa)
@member_detail = MemberDetail.new
@@ -201,7 +202,7 @@ class HasOneThroughAssociationsTest < ActiveRecord::TestCase
end
@new_detail = @member_details[0]
assert @new_detail.send(:association, :member_type).loaded?
- assert_not_nil assert_no_queries { @new_detail.member_type }
+ assert_no_queries { @new_detail.member_type }
end
def test_save_of_record_with_loaded_has_one_through
diff --git a/activerecord/test/cases/associations/inner_join_association_test.rb b/activerecord/test/cases/associations/inner_join_association_test.rb
index 4f246f575e..9fe5ff50d9 100644
--- a/activerecord/test/cases/associations/inner_join_association_test.rb
+++ b/activerecord/test/cases/associations/inner_join_association_test.rb
@@ -41,15 +41,20 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert_no_match(/WHERE/i, sql)
end
+ def test_join_association_conditions_support_string_and_arel_expressions
+ assert_equal 0, Author.joins(:welcome_posts_with_comment).count
+ assert_equal 1, Author.joins(:welcome_posts_with_comments).count
+ end
+
def test_join_conditions_allow_nil_associations
authors = Author.includes(:essays).where(:essays => {:id => nil})
assert_equal 2, authors.count
end
- def test_find_with_implicit_inner_joins_honors_readonly_without_select
- authors = Author.joins(:posts).to_a
- assert !authors.empty?, "expected authors to be non-empty"
- assert authors.all? {|a| a.readonly? }, "expected all authors to be readonly"
+ def test_find_with_implicit_inner_joins_without_select_does_not_imply_readonly
+ authors = Author.joins(:posts)
+ assert_not authors.empty?, "expected authors to be non-empty"
+ assert authors.none? {|a| a.readonly? }, "expected no authors to be readonly"
end
def test_find_with_implicit_inner_joins_honors_readonly_with_select
@@ -82,7 +87,7 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
def test_calculate_honors_implicit_inner_joins_and_distinct_and_conditions
real_count = Author.all.to_a.select {|a| a.posts.any? {|p| p.title =~ /^Welcome/} }.length
- authors_with_welcoming_post_titles = Author.all.merge!(:joins => :posts, :where => "posts.title like 'Welcome%'").calculate(:count, 'authors.id', :distinct => true)
+ authors_with_welcoming_post_titles = Author.all.merge!(joins: :posts, where: "posts.title like 'Welcome%'").distinct.calculate(:count, 'authors.id')
assert_equal real_count, authors_with_welcoming_post_titles, "inner join and conditions should have only returned authors posting titles starting with 'Welcome'"
end
@@ -104,4 +109,12 @@ class InnerJoinAssociationTest < ActiveRecord::TestCase
assert !posts(:welcome).tags.empty?
assert Post.joins(:misc_tags).where(:id => posts(:welcome).id).empty?
end
+
+ test "the default scope of the target is applied when joining associations" do
+ author = Author.create! name: "Jon"
+ author.categorizations.create!
+ author.categorizations.create! special: true
+
+ assert_equal [author], Author.where(id: author).joins(:special_categorizations)
+ end
end
diff --git a/activerecord/test/cases/associations/inverse_associations_test.rb b/activerecord/test/cases/associations/inverse_associations_test.rb
index aad48e7ce9..893030345f 100644
--- a/activerecord/test/cases/associations/inverse_associations_test.rb
+++ b/activerecord/test/cases/associations/inverse_associations_test.rb
@@ -5,6 +5,102 @@ require 'models/interest'
require 'models/zine'
require 'models/club'
require 'models/sponsor'
+require 'models/rating'
+require 'models/comment'
+require 'models/car'
+require 'models/bulb'
+require 'models/mixed_case_monkey'
+
+class AutomaticInverseFindingTests < ActiveRecord::TestCase
+ fixtures :ratings, :comments, :cars
+
+ def test_has_one_and_belongs_to_should_find_inverse_automatically_on_multiple_word_name
+ monkey_reflection = MixedCaseMonkey.reflect_on_association(:man)
+ man_reflection = Man.reflect_on_association(:mixed_case_monkey)
+
+ assert_respond_to monkey_reflection, :has_inverse?
+ assert monkey_reflection.has_inverse?, "The monkey reflection should have an inverse"
+ assert_equal man_reflection, monkey_reflection.inverse_of, "The monkey reflection's inverse should be the man reflection"
+
+ assert_respond_to man_reflection, :has_inverse?
+ assert man_reflection.has_inverse?, "The man reflection should have an inverse"
+ assert_equal monkey_reflection, man_reflection.inverse_of, "The man reflection's inverse should be the monkey reflection"
+ end
+
+ def test_has_one_and_belongs_to_should_find_inverse_automatically
+ car_reflection = Car.reflect_on_association(:bulb)
+ bulb_reflection = Bulb.reflect_on_association(:car)
+
+ assert_respond_to car_reflection, :has_inverse?
+ assert car_reflection.has_inverse?, "The Car reflection should have an inverse"
+ assert_equal bulb_reflection, car_reflection.inverse_of, "The Car reflection's inverse should be the Bulb reflection"
+
+ assert_respond_to bulb_reflection, :has_inverse?
+ assert bulb_reflection.has_inverse?, "The Bulb reflection should have an inverse"
+ assert_equal car_reflection, bulb_reflection.inverse_of, "The Bulb reflection's inverse should be the Car reflection"
+ end
+
+ def test_has_many_and_belongs_to_should_find_inverse_automatically
+ comment_reflection = Comment.reflect_on_association(:ratings)
+ rating_reflection = Rating.reflect_on_association(:comment)
+
+ assert_respond_to comment_reflection, :has_inverse?
+ assert comment_reflection.has_inverse?, "The Comment reflection should have an inverse"
+ assert_equal rating_reflection, comment_reflection.inverse_of, "The Comment reflection's inverse should be the Rating reflection"
+ end
+
+ def test_has_one_and_belongs_to_automatic_inverse_shares_objects
+ car = Car.first
+ bulb = Bulb.create!(car: car)
+
+ assert_equal car.bulb, bulb, "The Car's bulb should be the original bulb"
+
+ car.bulb.color = "Blue"
+ assert_equal car.bulb.color, bulb.color, "Changing the bulb's color on the car association should change the bulb's color"
+
+ bulb.color = "Red"
+ assert_equal bulb.color, car.bulb.color, "Changing the bulb's color should change the bulb's color on the car association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_rating
+ comment = Comment.first
+ rating = Rating.create!(comment: comment)
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Brogramming is the act of programming, like a bro."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Broseiden is the king of the sea of bros."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_has_many_and_belongs_to_automatic_inverse_shares_objects_on_comment
+ rating = Rating.create!
+ comment = Comment.first
+ rating.comment = comment
+
+ assert_equal rating.comment, comment, "The Rating's comment should be the original Comment"
+
+ rating.comment.body = "Brogramming is the act of programming, like a bro."
+ assert_equal rating.comment.body, comment.body, "Changing the Comment's body on the association should change the original Comment's body"
+
+ comment.body = "Broseiden is the king of the sea of bros."
+ assert_equal comment.body, rating.comment.body, "Changing the original Comment's body should change the Comment's body on the association"
+ end
+
+ def test_polymorphic_and_has_many_through_relationships_should_not_have_inverses
+ sponsor_reflection = Sponsor.reflect_on_association(:sponsorable)
+
+ assert_respond_to sponsor_reflection, :has_inverse?
+ assert !sponsor_reflection.has_inverse?, "A polymorphic association should not find an inverse automatically"
+
+ club_reflection = Club.reflect_on_association(:members)
+
+ assert_respond_to club_reflection, :has_inverse?
+ assert !club_reflection.has_inverse?, "A has_many_through association should not find an inverse automatically"
+ end
+end
class InverseAssociationTests < ActiveRecord::TestCase
def test_should_allow_for_inverse_of_options_in_associations
@@ -235,6 +331,22 @@ class InverseHasManyTests < ActiveRecord::TestCase
assert_equal m.name, i.man.name, "Name of man should be the same after changes to newly-created-child-owned instance"
end
+ def test_parent_instance_should_be_shared_within_create_block_of_new_child
+ man = Man.first
+ interest = man.interests.build 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"
+ end
+
+ def test_parent_instance_should_be_shared_within_build_block_of_new_child
+ man = Man.first
+ interest = man.interests.build 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"
+ end
+
def test_parent_instance_should_be_shared_with_poked_in_child
m = men(:gordon)
i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
@@ -261,13 +373,92 @@ class InverseHasManyTests < ActiveRecord::TestCase
def test_parent_instance_should_be_shared_with_first_and_last_child
man = Man.first
+
assert man.interests.first.man.equal? man
assert man.interests.last.man.equal? man
end
+ def test_parent_instance_should_be_shared_with_first_n_and_last_n_children
+ man = Man.first
+
+ interests = man.interests.first(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+
+ interests = man.interests.last(2)
+ assert interests[0].man.equal? man
+ assert interests[1].man.equal? man
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id
+ man = Man.create!
+ interest = Interest.create!
+ man.interests = [interest]
+
+ assert interest.equal?(man.interests.first), "The inverse association should use the interest already created and held in memory"
+ assert interest.equal?(man.interests.find(interest.id)), "The inverse association should use the interest already created and held in memory"
+ assert man.equal?(man.interests.first.man), "Two inversion should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+ end
+
+ def test_parent_instance_should_find_child_instance_using_child_instance_id_when_created
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ assert man.equal?(man.interests.first.man), "Two inverses should lead back to the same object that was originally held"
+ assert man.equal?(man.interests.find(interest.id).man), "Two inversions should lead back to the same object that was originally held"
+
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match before the name is changed"
+ man.name = "Ben Bitdiddle"
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the parent name is changed"
+ man.interests.find(interest.id).man.name = "Alyssa P. Hacker"
+ assert_equal man.name, man.interests.find(interest.id).man.name, "The name of the man should match after the child name is changed"
+ end
+
+ def test_find_on_child_instance_with_id_should_not_load_all_child_records
+ man = Man.create!
+ interest = Interest.create!(man: man)
+
+ man.interests.find(interest.id)
+ assert_not man.interests.loaded?
+ end
+
+ def test_raise_record_not_found_error_when_invalid_ids_are_passed
+ # delete all interest records to ensure that hard coded invalid_id(s)
+ # are indeed invalid.
+ Interest.delete_all
+
+ man = Man.create!
+
+ invalid_id = 245324523
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_id) }
+
+ invalid_ids = [8432342, 2390102913, 2453245234523452]
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find(invalid_ids) }
+ end
+
+ def test_raise_record_not_found_error_when_no_ids_are_passed
+ man = Man.create!
+
+ assert_raise(ActiveRecord::RecordNotFound) { man.interests.find() }
+ end
+
def test_trying_to_use_inverses_that_dont_exist_should_raise_an_error
assert_raise(ActiveRecord::InverseOfAssociationNotFoundError) { Man.first.secret_interests }
end
+
+ def test_child_instance_should_point_to_parent_without_saving
+ man = Man.new
+ i = Interest.create(:topic => 'Industrial Revolution Re-enactment')
+
+ man.interests << i
+ assert_not_nil i.man
+
+ i.man.name = "Charles"
+ assert_equal i.man.name, man.name
+
+ assert !man.persisted?
+ end
end
class InverseBelongsToTests < ActiveRecord::TestCase
@@ -412,6 +603,18 @@ class InversePolymorphicBelongsToTests < ActiveRecord::TestCase
assert_equal face.description, new_man.polymorphic_face.description, "Description of face should be the same after changes to replaced-parent-owned instance"
end
+ def test_inversed_instance_should_not_be_reloaded_after_stale_state_changed
+ new_man = Man.new
+ face = Face.new
+ new_man.face = face
+
+ old_inversed_man = face.man
+ new_man.save!
+ new_inversed_man = face.man
+
+ assert_equal old_inversed_man.object_id, new_inversed_man.object_id
+ end
+
def test_should_not_try_to_set_inverse_instances_when_the_inverse_is_a_has_many
i = interests(:llama_wrangling)
m = i.polymorphic_man
diff --git a/activerecord/test/cases/associations/join_dependency_test.rb b/activerecord/test/cases/associations/join_dependency_test.rb
deleted file mode 100644
index 08c166dc33..0000000000
--- a/activerecord/test/cases/associations/join_dependency_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require "cases/helper"
-require 'models/edge'
-
-class JoinDependencyTest < ActiveRecord::TestCase
- def test_column_names_with_alias_handles_nil_primary_key
- assert_equal Edge.column_names, ActiveRecord::Associations::JoinDependency::JoinBase.new(Edge).column_names_with_alias.map(&:first)
- end
-end \ No newline at end of file
diff --git a/activerecord/test/cases/associations/join_model_test.rb b/activerecord/test/cases/associations/join_model_test.rb
index 9b00c21b52..aabeea025f 100644
--- a/activerecord/test/cases/associations/join_model_test.rb
+++ b/activerecord/test/cases/associations/join_model_test.rb
@@ -397,14 +397,14 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
end
def test_has_many_through_polymorphic_has_many
- assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.uniq.sort_by { |t| t.id }
+ assert_equal taggings(:welcome_general, :thinking_general), authors(:david).taggings.distinct.sort_by { |t| t.id }
end
def test_include_has_many_through_polymorphic_has_many
author = Author.includes(:taggings).find authors(:david).id
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
- assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
+ assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
end
end
@@ -443,8 +443,8 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
def test_has_many_through_uses_conditions_specified_on_the_has_many_association
author = Author.first
- assert_present author.comments
- assert_blank author.nonexistant_comments
+ assert author.comments.present?
+ assert author.nonexistant_comments.blank?
end
def test_has_many_through_uses_correct_attributes
@@ -464,7 +464,7 @@ class AssociationsJoinModelTest < ActiveRecord::TestCase
assert saved_post.reload.tags(true).include?(new_tag)
- new_post = Post.new(:title => "Association replacmenet works!", :body => "You best believe it.")
+ new_post = Post.new(:title => "Association replacement works!", :body => "You best believe it.")
saved_tag = tags(:general)
new_post.tags << saved_tag
diff --git a/activerecord/test/cases/associations/nested_through_associations_test.rb b/activerecord/test/cases/associations/nested_through_associations_test.rb
index 03d99d19f6..8ef351cda8 100644
--- a/activerecord/test/cases/associations/nested_through_associations_test.rb
+++ b/activerecord/test/cases/associations/nested_through_associations_test.rb
@@ -186,7 +186,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
members = assert_queries(4) { Member.includes(:organization_member_details_2).to_a.sort_by(&:id) }
groucho_details, other_details = member_details(:groucho), member_details(:some_other_guy)
- assert_no_queries do
+ # postgresql test if randomly executed then executes "SHOW max_identifier_length". Hence
+ # the need to ignore certain predefined sqls that deal with system calls.
+ assert_no_queries(ignore_none: false) do
assert_equal [groucho_details, other_details], members.first.organization_member_details_2.sort_by(&:id)
end
end
@@ -212,7 +214,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload
- authors = assert_queries(3) { Author.includes(:post_categories).to_a.sort_by(&:id) }
+ authors = assert_queries(4) { Author.includes(:post_categories).to_a.sort_by(&:id) }
general, cooking = categories(:general), categories(:cooking)
assert_no_queries do
@@ -221,6 +223,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_and_belongs_to_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:post_categories).first
+
assert_includes_and_joins_equal(
Author.where('categories.id' => categories(:cooking).id),
[authors(:bob)], :post_categories
@@ -237,7 +242,8 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload
- categories = assert_queries(3) { Category.includes(:post_comments).to_a.sort_by(&:id) }
+ Category.includes(:post_comments).to_a # preheat cache
+ categories = assert_queries(4) { Category.includes(:post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -246,6 +252,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_and_belongs_to_many_with_has_many_source_reflection_preload_via_joins
+ # preload table schemas
+ Category.joins(:post_comments).first
+
assert_includes_and_joins_equal(
Category.where('comments.id' => comments(:more_greetings).id).order('categories.id'),
[categories(:general), categories(:technology)], :post_comments
@@ -262,7 +271,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload
- authors = assert_queries(5) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
+ authors = assert_queries(6) { Author.includes(:category_post_comments).to_a.sort_by(&:id) }
greetings, more = comments(:greetings), comments(:more_greetings)
assert_no_queries do
@@ -271,6 +280,9 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
end
def test_has_many_through_has_many_with_has_many_through_habtm_source_reflection_preload_via_joins
+ # preload table schemas
+ Author.joins(:category_post_comments).first
+
assert_includes_and_joins_equal(
Author.where('comments.id' => comments(:does_it_hurt).id).order('authors.id'),
[authors(:david), authors(:mary)], :category_post_comments
@@ -360,7 +372,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
prev_default_scope = Club.default_scopes
[:includes, :preload, :joins, :eager_load].each do |q|
- Club.default_scopes = [Club.send(q, :category)]
+ Club.default_scopes = [proc { Club.send(q, :category) }]
assert_equal categories(:general), members(:groucho).reload.club_category
end
ensure
@@ -401,7 +413,7 @@ class NestedThroughAssociationsTest < ActiveRecord::TestCase
# Mary and Bob both have posts in misc, but they are the only ones.
authors = Author.joins(:similar_posts).where('posts.id' => posts(:misc_by_bob).id)
- assert_equal [authors(:mary), authors(:bob)], authors.uniq.sort_by(&:id)
+ assert_equal [authors(:mary), authors(:bob)], authors.distinct.sort_by(&:id)
# Check the polymorphism of taggings is being observed correctly (in both joins)
authors = Author.joins(:similar_posts).where('taggings.taggable_type' => 'FakeModel')
diff --git a/activerecord/test/cases/associations_test.rb b/activerecord/test/cases/associations_test.rb
index c0f1945cec..48e6fc5cd4 100644
--- a/activerecord/test/cases/associations_test.rb
+++ b/activerecord/test/cases/associations_test.rb
@@ -18,6 +18,8 @@ require 'models/ship'
require 'models/liquid'
require 'models/molecule'
require 'models/electron'
+require 'models/man'
+require 'models/interest'
class AssociationsTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :developers, :projects, :developers_projects,
@@ -95,7 +97,7 @@ class AssociationsTest < ActiveRecord::TestCase
def test_force_reload
firm = Firm.new("name" => "A New Firm, Inc")
firm.save
- firm.clients.each {|c|} # forcing to load all clients
+ firm.clients.each {} # forcing to load all clients
assert firm.clients.empty?, "New firm shouldn't have client objects"
assert_equal 0, firm.clients.size, "New firm should have 0 clients"
@@ -172,6 +174,18 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal 1, josh.posts.size
end
+ def test_append_behaves_like_push
+ josh = Author.new(:name => "Josh")
+ josh.posts.append Post.new(:title => "New on Edge", :body => "More cool stuff!")
+ assert josh.posts.loaded?
+ assert_equal 1, josh.posts.size
+ end
+
+ def test_prepend_is_not_defined
+ josh = Author.new(:name => "Josh")
+ assert_raises(NoMethodError) { josh.posts.prepend Post.new }
+ end
+
def test_save_on_parent_does_not_load_target
david = developers(:david)
@@ -203,7 +217,7 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert_equal post.body, "More cool stuff!"
end
- def test_reload_returns_assocition
+ def test_reload_returns_association
david = developers(:david)
assert_nothing_raised do
assert_equal david.projects, david.projects.reload.reload
@@ -225,10 +239,25 @@ class AssociationProxyTest < ActiveRecord::TestCase
assert david.projects.scope.is_a?(ActiveRecord::Relation)
assert_equal david.projects, david.projects.scope
end
+
+ test "proxy object is cached" do
+ david = developers(:david)
+ assert david.projects.equal?(david.projects)
+ end
+
+ test "inverses get set of subsets of the association" do
+ man = Man.create
+ man.interests.create
+
+ man = Man.find(man.id)
+
+ assert_queries(1) do
+ assert_equal man, man.interests.where("1=1").first.man
+ end
+ end
end
class OverridingAssociationsTest < ActiveRecord::TestCase
- class Person < ActiveRecord::Base; end
class DifferentPerson < ActiveRecord::Base; end
class PeopleList < ActiveRecord::Base
@@ -249,7 +278,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_habtm_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_and_belongs_to_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_and_belongs_to_many
assert_equal([], callbacks)
end
@@ -257,7 +286,7 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
def test_has_many_association_redefinition_callbacks_should_differ_and_not_inherited
# redeclared association on AR descendant should not inherit callbacks from superclass
callbacks = PeopleList.before_add_for_has_many
- assert_equal([:enlist], callbacks)
+ assert_equal(1, callbacks.length)
callbacks = DifferentPeopleList.before_add_for_has_many
assert_equal([], callbacks)
end
@@ -289,6 +318,14 @@ class OverridingAssociationsTest < ActiveRecord::TestCase
DifferentPeopleList.reflect_on_association(:has_one)
)
end
+
+ def test_requires_symbol_argument
+ assert_raises ArgumentError do
+ Class.new(Post) do
+ belongs_to "author"
+ end
+ end
+ end
end
class GeneratedMethodsTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/attribute_methods/read_test.rb b/activerecord/test/cases/attribute_methods/read_test.rb
index da5d9d8c2a..c0659fddef 100644
--- a/activerecord/test/cases/attribute_methods/read_test.rb
+++ b/activerecord/test/cases/attribute_methods/read_test.rb
@@ -11,19 +11,10 @@ module ActiveRecord
def setup
@klass = Class.new do
def self.superclass; Base; end
- def self.active_record_super; Base; end
def self.base_class; self; end
- extend ActiveRecord::Configuration
include ActiveRecord::AttributeMethods
- def self.define_attribute_methods
- # Created in the inherited/included hook for "proper" ARs
- @attribute_methods_mutex ||= Mutex.new
-
- super
- end
-
def self.column_names
%w{ one two three }
end
@@ -58,9 +49,9 @@ module ActiveRecord
end
def test_attribute_methods_generated?
- assert(!@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert_not @klass.method_defined?(:one)
@klass.define_attribute_methods
- assert(@klass.attribute_methods_generated?, 'attribute_methods_generated?')
+ assert @klass.method_defined?(:one)
end
end
end
diff --git a/activerecord/test/cases/attribute_methods/serialization_test.rb b/activerecord/test/cases/attribute_methods/serialization_test.rb
new file mode 100644
index 0000000000..75de773961
--- /dev/null
+++ b/activerecord/test/cases/attribute_methods/serialization_test.rb
@@ -0,0 +1,29 @@
+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 c2b58fd7d1..9c66ed354e 100644
--- a/activerecord/test/cases/attribute_methods_test.rb
+++ b/activerecord/test/cases/attribute_methods_test.rb
@@ -12,6 +12,8 @@ require 'models/contact'
require 'models/keyboard'
class AttributeMethodsTest < ActiveRecord::TestCase
+ include InTimeZone
+
fixtures :topics, :developers, :companies, :computers
def setup
@@ -25,6 +27,14 @@ class AttributeMethodsTest < ActiveRecord::TestCase
ActiveRecord::Base.send(:attribute_method_matchers).concat(@old_matchers)
end
+ def test_attribute_for_inspect
+ t = topics(:first)
+ t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
+
+ assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
+ assert_equal '"The First Topic Now Has A Title With\nNewlines And ..."', t.attribute_for_inspect(:title)
+ end
+
def test_attribute_present
t = Topic.new
t.title = "hello there!"
@@ -67,7 +77,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_boolean_attributes
- assert ! Topic.find(1).approved?
+ assert !Topic.find(1).approved?
assert Topic.find(2).approved?
end
@@ -82,7 +92,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_set_attributes_without_hash
topic = Topic.new
- assert_nothing_raised { topic.attributes = '' }
+ assert_raise(ArgumentError) { topic.attributes = '' }
end
def test_integers_as_nil
@@ -128,6 +138,7 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal '10', keyboard.id_before_type_cast
assert_equal nil, keyboard.read_attribute_before_type_cast('id')
assert_equal '10', keyboard.read_attribute_before_type_cast('key_number')
+ assert_equal '10', keyboard.read_attribute_before_type_cast(:key_number)
end
# Syck calls respond_to? before actually calling initialize
@@ -139,13 +150,10 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_respond_to topic, :title
end
- # IRB inspects the return value of "MyModel.allocate"
- # by inspecting it.
+ # IRB inspects the return value of "MyModel.allocate".
def test_allocated_object_can_be_inspected
topic = Topic.allocate
- topic.instance_eval { @attributes = nil }
- assert_nothing_raised { topic.inspect }
- assert topic.inspect, "#<Topic not initialized>"
+ assert_equal "#<Topic not initialized>", topic.inspect
end
def test_array_content
@@ -157,8 +165,8 @@ class AttributeMethodsTest < ActiveRecord::TestCase
end
def test_read_attributes_before_type_cast
- category = Category.new({:name=>"Test categoty", :type => nil})
- category_attrs = {"name"=>"Test categoty", "id" => nil, "type" => nil, "categorizations_count" => nil}
+ category = Category.new({:name=>"Test category", :type => nil})
+ category_attrs = {"name"=>"Test category", "id" => nil, "type" => nil, "categorizations_count" => nil}
assert_equal category_attrs , category.attributes_before_type_cast
end
@@ -287,6 +295,12 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_equal "Don't change the topic", topic[:title]
end
+ def test_read_attribute_raises_missing_attribute_error_when_not_exists
+ computer = Computer.select('id').first
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:developer] }
+ assert_raises(ActiveModel::MissingAttributeError) { computer[:extendedWarranty] }
+ end
+
def test_read_attribute_when_false
topic = topics(:first)
topic.approved = false
@@ -305,26 +319,17 @@ class AttributeMethodsTest < ActiveRecord::TestCase
def test_read_write_boolean_attribute
topic = Topic.new
- # puts ""
- # puts "New Topic"
- # puts topic.inspect
topic.approved = "false"
- # puts "Expecting false"
- # puts topic.inspect
assert !topic.approved?, "approved should be false"
+
topic.approved = "false"
- # puts "Expecting false"
- # puts topic.inspect
assert !topic.approved?, "approved should be false"
+
topic.approved = "true"
- # puts "Expecting true"
- # puts topic.inspect
assert topic.approved?, "approved should be true"
+
topic.approved = "true"
- # puts "Expecting true"
- # puts topic.inspect
assert topic.approved?, "approved should be true"
- # puts ""
end
def test_overridden_write_attribute
@@ -714,6 +719,15 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert_raise(ActiveRecord::UnknownAttributeError) { @target.new.attributes = { :title => "Ants in pants" } }
end
+ def test_bulk_update_raise_unknown_attribute_errro
+ error = assert_raises(ActiveRecord::UnknownAttributeError) {
+ @target.new(:hello => "world")
+ }
+ assert @target, error.record
+ assert "hello", error.attribute
+ 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)
@@ -745,21 +759,6 @@ class AttributeMethodsTest < ActiveRecord::TestCase
assert subklass.method_defined?(:id), "subklass is missing id method"
end
- def test_dispatching_column_attributes_through_method_missing_deprecated
- Topic.define_attribute_methods
-
- topic = Topic.new(:id => 5)
- topic.id = 5
-
- topic.method(:id).owner.send(:undef_method, :id)
-
- assert_deprecated do
- assert_equal 5, topic.id
- end
- ensure
- Topic.undefine_attribute_methods
- end
-
def test_read_attribute_with_nil_should_not_asplode
assert_equal nil, Topic.new.read_attribute(nil)
end
@@ -787,27 +786,13 @@ class AttributeMethodsTest < ActiveRecord::TestCase
private
def cached_columns
- Topic.columns.find_all { |column|
- !Topic.serialized_attributes.include? column.name
- }.map(&:name)
+ Topic.columns.map(&:name) - Topic.serialized_attributes.keys
end
def time_related_columns_on_topic
Topic.columns.select { |c| [:time, :date, :datetime, :timestamp].include?(c.type) }
end
- def in_time_zone(zone)
- old_zone = Time.zone
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
-
- Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
- ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
- yield
- ensure
- Time.zone = old_zone
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- end
-
def privatize(method_signature)
@target.class_eval(<<-private_method, __FILE__, __LINE__ + 1)
private
diff --git a/activerecord/test/cases/autosave_association_test.rb b/activerecord/test/cases/autosave_association_test.rb
index 4afd33f273..517d2674a7 100644
--- a/activerecord/test/cases/autosave_association_test.rb
+++ b/activerecord/test/cases/autosave_association_test.rb
@@ -144,7 +144,7 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
firm = Firm.first
firm.account = Account.first
- assert_queries(Firm.partial_updates? ? 0 : 1) { firm.save! }
+ assert_queries(Firm.partial_writes? ? 0 : 1) { firm.save! }
firm = Firm.first.dup
firm.account = Account.first
@@ -161,16 +161,16 @@ class TestDefaultAutosaveAssociationOnAHasOneAssociation < ActiveRecord::TestCas
end
def test_callbacks_firing_order_on_update
- eye = Eye.create(:iris_attributes => {:color => 'honey'})
- eye.update_attributes(:iris_attributes => {:color => 'green'})
+ eye = Eye.create(iris_attributes: {color: 'honey'})
+ eye.update(iris_attributes: {color: 'green'})
assert_equal [true, false], eye.after_update_callbacks_stack
end
def test_callbacks_firing_order_on_save
- eye = Eye.create(:iris_attributes => {:color => 'honey'})
+ eye = Eye.create(iris_attributes: {color: 'honey'})
assert_equal [false, false], eye.after_save_callbacks_stack
- eye.update_attributes(:iris_attributes => {:color => 'blue'})
+ eye.update(iris_attributes: {color: 'blue'})
assert_equal [false, false, false, false], eye.after_save_callbacks_stack
end
end
@@ -439,7 +439,7 @@ class TestDefaultAutosaveAssociationOnAHasManyAssociation < ActiveRecord::TestCa
end
def test_assign_ids_for_through_a_belongs_to
- post = Post.new(:title => "Assigning IDs works!", :body => "You heared it here first, folks!")
+ post = Post.new(:title => "Assigning IDs works!", :body => "You heard it here first, folks!")
post.person_ids = [people(:david).id, people(:michael).id]
post.save
post.reload
@@ -566,7 +566,7 @@ class TestDefaultAutosaveAssociationOnNewRecord < ActiveRecord::TestCase
end
class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
- self.use_transactional_fixtures = false unless supports_savepoints?
+ self.use_transactional_fixtures = false
def setup
super
@@ -705,6 +705,13 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
ids.each { |id| assert_nil klass.find_by_id(id) }
end
+ def test_should_not_resave_destroyed_association
+ @pirate.birds.create!(name: :parrot)
+ @pirate.birds.first.destroy
+ @pirate.save!
+ assert @pirate.reload.birds.empty?
+ end
+
def test_should_skip_validation_on_has_many_if_marked_for_destruction
2.times { |i| @pirate.birds.create!(:name => "birds_#{i}") }
@@ -764,6 +771,20 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
assert_equal 2, @pirate.birds.reload.length
end
+ def test_should_save_new_record_that_has_same_value_as_existing_record_marked_for_destruction_on_field_that_has_unique_index
+ Bird.connection.add_index :birds, :name, unique: true
+
+ 3.times { |i| @pirate.birds.create(name: "unique_birds_#{i}") }
+
+ @pirate.birds[0].mark_for_destruction
+ @pirate.birds.build(name: @pirate.birds[0].name)
+ @pirate.save!
+
+ assert_equal 3, @pirate.birds.reload.length
+ ensure
+ Bird.connection.remove_index :birds, column: :name
+ end
+
# Add and remove callbacks tests for association collections.
%w{ method proc }.each do |callback_type|
define_method("test_should_run_add_callback_#{callback_type}s_for_has_many") do
@@ -846,8 +867,10 @@ class TestDestroyAsPartOfAutosaveAssociation < ActiveRecord::TestCase
@pirate.parrots.each { |parrot| parrot.mark_for_destruction }
assert @pirate.save
- assert_queries(0) do
- assert @pirate.save
+ Pirate.transaction do
+ assert_queries(0) do
+ assert @pirate.save
+ end
end
end
@@ -1335,7 +1358,7 @@ class TestAutosaveAssociationValidationsOnAHasOneAssociation < ActiveRecord::Tes
assert !@pirate.valid?
end
- test "should not automatically asd validate associations without :validate => true" do
+ test "should not automatically add validate associations without :validate => true" do
assert @pirate.valid?
@pirate.non_validated_ship.name = ''
assert @pirate.valid?
@@ -1417,10 +1440,6 @@ class TestAutosaveAssociationValidationMethodsGeneration < ActiveRecord::TestCas
test "should generate validation methods for HABTM associations with :validate => true" do
assert_respond_to @pirate, :validate_associated_records_for_parrots
end
-
- test "should not generate validation methods for HABTM associations without :validate => true" do
- assert !@pirate.respond_to?(:validate_associated_records_for_non_validated_parrots)
- end
end
class TestAutosaveAssociationWithTouch < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/base_test.rb b/activerecord/test/cases/base_test.rb
index 0f859bf452..82b20e8cee 100644
--- a/activerecord/test/cases/base_test.rb
+++ b/activerecord/test/cases/base_test.rb
@@ -1,4 +1,7 @@
+# encoding: utf-8
+
require "cases/helper"
+require 'active_support/concurrency/latch'
require 'models/post'
require 'models/author'
require 'models/topic'
@@ -21,11 +24,10 @@ require 'models/parrot'
require 'models/person'
require 'models/edge'
require 'models/joke'
-require 'models/bulb'
require 'models/bird'
-require 'models/teapot'
+require 'models/car'
+require 'models/bulb'
require 'rexml/document'
-require 'active_support/core_ext/exception'
class FirstAbstractClass < ActiveRecord::Base
self.abstract_class = true
@@ -76,12 +78,6 @@ end
class BasicsTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
def test_generated_methods_modules
modules = Computer.ancestors
assert modules.include?(Computer::GeneratedFeatureMethods)
@@ -124,7 +120,7 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil Edge.primary_key
end
- unless current_adapter?(:PostgreSQLAdapter,:OracleAdapter,:SQLServerAdapter)
+ unless current_adapter?(:PostgreSQLAdapter, :OracleAdapter, :SQLServerAdapter)
def test_limit_with_comma
assert Topic.limit("1,2").to_a
end
@@ -141,19 +137,19 @@ class BasicsTest < ActiveRecord::TestCase
end
end
- def test_limit_should_sanitize_sql_injection_for_limit_without_comas
+ def test_limit_should_sanitize_sql_injection_for_limit_without_commas
assert_raises(ArgumentError) do
Topic.limit("1 select * from schema").to_a
end
end
- def test_limit_should_sanitize_sql_injection_for_limit_with_comas
+ def test_limit_should_sanitize_sql_injection_for_limit_with_commas
assert_raises(ArgumentError) do
Topic.limit("1, 7 procedure help()").to_a
end
end
- unless current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ unless current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_limit_should_allow_sql_literal
assert_equal 1, Topic.limit(Arel.sql('2-1')).to_a.length
end
@@ -222,7 +218,7 @@ class BasicsTest < ActiveRecord::TestCase
)
# For adapters which support microsecond resolution.
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:SQLite3Adapter)
+ if current_adapter?(:PostgreSQLAdapter, :SQLite3Adapter)
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
@@ -232,7 +228,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_local_time_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time = Time.local(2000)
topic = Topic.create('written_on' => time)
saved_time = Topic.find(topic.id).reload.written_on
@@ -245,7 +241,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -260,18 +256,20 @@ class BasicsTest < ActiveRecord::TestCase
def test_preserving_time_objects_with_utc_time_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- time = Time.utc(2000)
- topic = Topic.create('written_on' => time)
- saved_time = Topic.find(topic.id).reload.written_on
- assert_equal time, saved_time
- assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
- assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ with_timezone_config default: :local do
+ time = Time.utc(2000)
+ topic = Topic.create('written_on' => time)
+ saved_time = Topic.find(topic.id).reload.written_on
+ assert_equal time, saved_time
+ assert_equal [0, 0, 0, 1, 1, 2000, 6, 1, false, "UTC"], time.to_a
+ assert_equal [0, 0, 19, 31, 12, 1999, 5, 365, false, "EST"], saved_time.to_a
+ end
end
end
def test_preserving_time_objects_with_time_with_zone_conversion_to_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
Time.use_zone 'Central Time (US & Canada)' do
time = Time.zone.local(2000)
topic = Topic.create('written_on' => time)
@@ -300,13 +298,11 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_initialize_with_invalid_attribute
- begin
- Topic.new({ "title" => "test",
- "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
- rescue ActiveRecord::MultiparameterAssignmentErrors => ex
- assert_equal(1, ex.errors.size)
- assert_equal("last_read", ex.errors[0].attribute)
- end
+ Topic.new({ "title" => "test",
+ "last_read(1i)" => "2005", "last_read(2i)" => "2", "last_read(3i)" => "31"})
+ rescue ActiveRecord::MultiparameterAssignmentErrors => ex
+ assert_equal(1, ex.errors.size)
+ assert_equal("last_read", ex.errors[0].attribute)
end
def test_create_after_initialize_without_block
@@ -321,27 +317,12 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal(true, cb.frickinawesome)
end
- def test_first_or_create
- parrot = Bird.first_or_create(:color => 'green', :name => 'parrot')
- assert parrot.persisted?
- the_same_parrot = Bird.first_or_create(:color => 'yellow', :name => 'macaw')
- assert_equal parrot, the_same_parrot
- end
-
- def test_first_or_create_bang
- assert_raises(ActiveRecord::RecordInvalid) { Bird.first_or_create! }
- parrot = Bird.first_or_create!(:color => 'green', :name => 'parrot')
- assert parrot.persisted?
- the_same_parrot = Bird.first_or_create!(:color => 'yellow', :name => 'macaw')
- assert_equal parrot, the_same_parrot
- end
-
- def test_first_or_initialize
- parrot = Bird.first_or_initialize(:color => 'green', :name => 'parrot')
- assert_kind_of Bird, parrot
- assert !parrot.persisted?
- assert parrot.new_record?
- assert parrot.valid?
+ def test_create_after_initialize_with_array_param
+ cbs = CustomBulb.create([{ name: 'Dude' }, { name: 'Bob' }])
+ assert_equal 'Dude', cbs[0].name
+ assert_equal 'Bob', cbs[1].name
+ assert cbs[0].frickinawesome
+ assert !cbs[1].frickinawesome
end
def test_load
@@ -471,7 +452,7 @@ class BasicsTest < ActiveRecord::TestCase
Post.reset_table_name
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_update_all_with_order_and_limit
assert_equal 1, Topic.limit(1).order('id DESC').update_all(:content => 'bulk updated!')
end
@@ -508,25 +489,25 @@ class BasicsTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_utc_as_time_zone
- Topic.default_timezone = :utc
- attributes = { "bonus_time" => "5:42:00AM" }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time" => "5:42:00AM" }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_utc_as_time_zone_and_new
- Topic.default_timezone = :utc
- attributes = { "bonus_time(1i)"=>"2000",
- "bonus_time(2i)"=>"1",
- "bonus_time(3i)"=>"1",
- "bonus_time(4i)"=>"10",
- "bonus_time(5i)"=>"35",
- "bonus_time(6i)"=>"50" }
- topic = Topic.new(attributes)
- assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
- Topic.default_timezone = :local
+ with_timezone_config default: :utc do
+ attributes = { "bonus_time(1i)"=>"2000",
+ "bonus_time(2i)"=>"1",
+ "bonus_time(3i)"=>"1",
+ "bonus_time(4i)"=>"10",
+ "bonus_time(5i)"=>"35",
+ "bonus_time(6i)"=>"50" }
+ topic = Topic.new(attributes)
+ assert_equal Time.utc(2000, 1, 1, 10, 35, 50), topic.bonus_time
+ end
end
end
@@ -573,11 +554,27 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [ Topic.find(1) ], [ Topic.find(2).topic ] & [ Topic.find(1) ]
end
- def test_comparison
- topic_1 = Topic.create!
- topic_2 = Topic.create!
+ def test_create_without_prepared_statement
+ topic = Topic.connection.unprepared_statement do
+ Topic.create(:title => 'foo')
+ end
+
+ assert_equal topic, Topic.find(topic.id)
+ end
+
+ def test_destroy_without_prepared_statement
+ topic = Topic.create(title: 'foo')
+ Topic.connection.unprepared_statement do
+ Topic.find(topic.id).destroy
+ end
- assert_equal [topic_2, topic_1].sort, [topic_1, topic_2]
+ 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
@@ -586,6 +583,13 @@ class BasicsTest < ActiveRecord::TestCase
assert_nil topic <=> category
end
+ def test_comparison_with_different_objects_in_array
+ topic = Topic.create
+ assert_raises(ArgumentError) do
+ [1, topic].sort
+ end
+ end
+
def test_readonly_attributes
assert_equal Set.new([ 'title' , 'comments_count' ]), ReadonlyTitlePost.readonly_attributes
@@ -593,16 +597,30 @@ class BasicsTest < ActiveRecord::TestCase
post.reload
assert_equal "cannot change this", post.title
- post.update_attributes(:title => "try to change", :body => "changed")
+ post.update(title: "try to change", body: "changed")
post.reload
assert_equal "cannot change this", post.title
assert_equal "changed", post.body
end
- def test_attr_readonly_is_class_level_setting
- post = ReadonlyTitlePost.new
- assert_raise(NoMethodError) { post._attr_readonly = [:title] }
- assert_deprecated { post._attr_readonly }
+ def test_unicode_column_name
+ Weird.reset_column_information
+ weird = Weird.create(:ãªã¾ãˆ => 'ãŸã“焼ãä»®é¢')
+ assert_equal 'ãŸã“焼ãä»®é¢', weird.ãªã¾ãˆ
+ end
+
+ def test_respect_internal_encoding
+ if current_adapter?(:PostgreSQLAdapter)
+ skip 'pg does not respect internal encoding and always returns utf8'
+ end
+ old_default_internal = Encoding.default_internal
+ silence_warnings { Encoding.default_internal = "EUC-JP" }
+
+ Weird.reset_column_information
+
+ assert_equal ["EUC-JP"], Weird.columns.map {|c| c.name.encoding.name }.uniq
+ ensure
+ silence_warnings { Encoding.default_internal = old_default_internal }
end
def test_non_valid_identifier_column_name
@@ -617,16 +635,24 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 'value2', weird.read_attribute('a$b')
end
+ def test_group_weirds_by_from
+ Weird.create('a$b' => 'value', :from => 'aaron')
+ count = Weird.group(Weird.arel_table[:from]).count
+ assert_equal 1, count['aaron']
+ end
+
def test_attributes_on_dummy_time
# Oracle, and Sybase do not have a TIME datatype.
return true if current_adapter?(:OracleAdapter, :SybaseAdapter)
- attributes = {
- "bonus_time" => "5:42:00AM"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ with_timezone_config default: :local do
+ attributes = {
+ "bonus_time" => "5:42:00AM"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2000, 1, 1, 5, 42, 0), topic.bonus_time
+ end
end
def test_attributes_on_dummy_time_with_invalid_time
@@ -814,19 +840,18 @@ class BasicsTest < ActiveRecord::TestCase
# TODO: extend defaults tests to other databases!
if current_adapter?(:PostgreSQLAdapter)
def test_default
- tz = Default.default_timezone
- Default.default_timezone = :local
- default = Default.new
- Default.default_timezone = tz
-
- # fixed dates / times
- assert_equal Date.new(2004, 1, 1), default.fixed_date
- assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
-
- # char types
- assert_equal 'Y', default.char1
- assert_equal 'a varchar field', default.char2
- assert_equal 'a text field', default.char3
+ with_timezone_config default: :local do
+ default = Default.new
+
+ # fixed dates / times
+ assert_equal Date.new(2004, 1, 1), default.fixed_date
+ assert_equal Time.local(2004, 1,1,0,0,0,0), default.fixed_time
+
+ # char types
+ assert_equal 'Y', default.char1
+ assert_equal 'a varchar field', default.char2
+ assert_equal 'a text field', default.char3
+ end
end
class Geometric < ActiveRecord::Base; end
@@ -851,7 +876,7 @@ class BasicsTest < ActiveRecord::TestCase
# Reload and check that we have all the geometric attributes.
h = Geometric.find(g.id)
- assert_equal '(5,6.1)', h.a_point
+ assert_equal [5.0, 6.1], h.a_point
assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
assert_equal '[(2,3),(5.5,7),(8.5,11)]', h.a_path
@@ -880,7 +905,7 @@ class BasicsTest < ActiveRecord::TestCase
# Reload and check that we have all the geometric attributes.
h = Geometric.find(g.id)
- assert_equal '(5,6.1)', h.a_point
+ assert_equal [5.0, 6.1], h.a_point
assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
@@ -891,6 +916,29 @@ class BasicsTest < ActiveRecord::TestCase
objs = Geometric.find_by_sql ["select isclosed(a_path) from geometrics where id = ?", g.id]
assert_equal true, objs[0].isclosed
+
+ # test native ruby formats when defining the geometric types
+ g = Geometric.new(
+ :a_point => [5.0, 6.1],
+ #:a_line => '((2.0, 3), (5.5, 7.0))' # line type is currently unsupported in postgresql
+ :a_line_segment => '((2.0, 3), (5.5, 7.0))',
+ :a_box => '(2.0, 3), (5.5, 7.0)',
+ :a_path => '((2.0, 3), (5.5, 7.0), (8.5, 11.0))', # ( ) is a closed path
+ :a_polygon => '2.0, 3, 5.5, 7.0, 8.5, 11.0',
+ :a_circle => '((5.3, 10.4), 2)'
+ )
+
+ assert g.save
+
+ # Reload and check that we have all the geometric attributes.
+ h = Geometric.find(g.id)
+
+ assert_equal [5.0, 6.1], h.a_point
+ assert_equal '[(2,3),(5.5,7)]', h.a_line_segment
+ assert_equal '(5.5,7),(2,3)', h.a_box # reordered to store upper right corner then bottom left corner
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_path
+ assert_equal '((2,3),(5.5,7),(8.5,11))', h.a_polygon
+ assert_equal '<(5.3,10.4),2>', h.a_circle
end
end
@@ -995,7 +1043,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_reload_with_exclusive_scope
dev = DeveloperCalledDavid.first
- dev.update_attributes!( :name => "NotDavid" )
+ dev.update!(name: "NotDavid" )
assert_equal dev, dev.reload
end
@@ -1035,7 +1083,7 @@ class BasicsTest < ActiveRecord::TestCase
Joke.reset_sequence_name
end
- def test_dont_clear_inheritnce_column_when_setting_explicitly
+ def test_dont_clear_inheritance_column_when_setting_explicitly
Joke.inheritance_column = "my_type"
before_inherit = Joke.inheritance_column
@@ -1102,7 +1150,7 @@ class BasicsTest < ActiveRecord::TestCase
res6 = Post.count_by_sql "SELECT COUNT(DISTINCT p.id) FROM posts p, comments co WHERE p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id"
res7 = nil
assert_nothing_raised do
- res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").count(distinct: true)
+ res7 = Post.where("p.#{QUOTED_TYPE} = 'Post' AND p.id=co.post_id").joins("p, comments co").select("p.id").distinct.count
end
assert_equal res6, res7
end
@@ -1153,8 +1201,8 @@ class BasicsTest < ActiveRecord::TestCase
end
def test_find_keeps_multiple_group_values
- combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at').to_a
- assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at']).to_a
+ combined = Developer.all.merge!(:group => 'developers.name, developers.salary, developers.id, developers.created_at, developers.updated_at, developers.created_on, developers.updated_on').to_a
+ assert_equal combined, Developer.all.merge!(:group => ['developers.name', 'developers.salary', 'developers.id', 'developers.created_at', 'developers.updated_at', 'developers.created_on', 'developers.updated_on']).to_a
end
def test_find_symbol_ordered_last
@@ -1196,10 +1244,6 @@ class BasicsTest < ActiveRecord::TestCase
# Concrete subclasses an abstract class which has a type column.
assert !SubStiPost.descends_from_active_record?
-
- assert Teapot.descends_from_active_record?
- assert !OtherTeapot.descends_from_active_record?
- assert CoolTeapot.descends_from_active_record?
end
def test_find_on_abstract_base_class_doesnt_use_type_condition
@@ -1221,93 +1265,6 @@ class BasicsTest < ActiveRecord::TestCase
assert_no_queries { assert true }
end
- def test_to_param_should_return_string
- assert_kind_of String, Client.first.to_param
- end
-
- def test_to_param_returns_id_even_if_not_persisted
- client = Client.new
- client.id = 1
- assert_equal "1", client.to_param
- end
-
- def test_inspect_class
- assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
- assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
- assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
- end
-
- def test_inspect_instance
- topic = topics(:first)
- assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", content: "Have a nice day", important: nil, approved: false, replies_count: 1, parent_id: nil, parent_title: nil, type: nil, group: nil, created_at: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
- end
-
- def test_inspect_new_instance
- assert_match(/Topic id: nil/, Topic.new.inspect)
- end
-
- def test_inspect_limited_select_instance
- assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect
- assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect
- end
-
- def test_inspect_class_without_table
- assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
- end
-
- def test_attribute_for_inspect
- t = topics(:first)
- t.title = "The First Topic Now Has A Title With\nNewlines And More Than 50 Characters"
-
- assert_equal %("#{t.written_on.to_s(:db)}"), t.attribute_for_inspect(:written_on)
- assert_equal '"The First Topic Now Has A Title With\nNewlines And M..."', t.attribute_for_inspect(:title)
- end
-
- def test_becomes
- assert_kind_of Reply, topics(:first).becomes(Reply)
- assert_equal "The First Topic", topics(:first).becomes(Reply).title
- end
-
- def test_becomes_includes_errors
- company = Company.new(:name => nil)
- assert !company.valid?
- original_errors = company.errors
- client = company.becomes(Client)
- assert_equal original_errors, client.errors
- end
-
- def test_silence_sets_log_level_to_error_in_block
- original_logger = ActiveRecord::Base.logger
-
- assert_deprecated do
- log = StringIO.new
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
- ActiveRecord::Base.logger.level = Logger::DEBUG
- ActiveRecord::Base.silence do
- ActiveRecord::Base.logger.warn "warn"
- ActiveRecord::Base.logger.error "error"
- end
- assert_equal "error\n", log.string
- end
- ensure
- ActiveRecord::Base.logger = original_logger
- end
-
- def test_silence_sets_log_level_back_to_level_before_yield
- original_logger = ActiveRecord::Base.logger
-
- assert_deprecated do
- log = StringIO.new
- ActiveRecord::Base.logger = ActiveSupport::Logger.new(log)
- ActiveRecord::Base.logger.level = Logger::WARN
- ActiveRecord::Base.silence do
- end
- assert_equal Logger::WARN, ActiveRecord::Base.logger.level
- end
- ensure
- ActiveRecord::Base.logger = original_logger
- end
-
def test_benchmark_with_log_level
original_logger = ActiveRecord::Base.logger
log = StringIO.new
@@ -1359,9 +1316,9 @@ class BasicsTest < ActiveRecord::TestCase
def test_clear_cache!
# preheat cache
- c1 = Post.connection.schema_cache.columns['posts']
+ c1 = Post.connection.schema_cache.columns('posts')
ActiveRecord::Base.clear_cache!
- c2 = Post.connection.schema_cache.columns['posts']
+ c2 = Post.connection.schema_cache.columns('posts')
assert_not_equal c1, c2
end
@@ -1370,9 +1327,9 @@ class BasicsTest < ActiveRecord::TestCase
UnloadablePost.send(:current_scope=, UnloadablePost.all)
UnloadablePost.unloadable
- assert_not_nil Thread.current[:UnloadablePost_current_scope]
+ assert_not_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
ActiveSupport::Dependencies.remove_unloadable_constants!
- assert_nil Thread.current[:UnloadablePost_current_scope]
+ assert_nil ActiveRecord::Scoping::ScopeRegistry.value_for(:current_scope, "UnloadablePost")
ensure
Object.class_eval{ remove_const :UnloadablePost } if defined?(UnloadablePost)
end
@@ -1402,6 +1359,36 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal 1, post.comments.length
end
+ def test_marshal_between_processes
+ skip "can't marshal between processes when using an in-memory db" if in_memory_db?
+ skip "fork isn't supported" unless Process.respond_to?(:fork)
+
+ # Define a new model to ensure there are no caches
+ if self.class.const_defined?("Post", false)
+ flunk "there should be no post constant"
+ end
+
+ self.class.const_set("Post", Class.new(ActiveRecord::Base) {
+ has_many :comments
+ })
+
+ rd, wr = IO.pipe
+
+ ActiveRecord::Base.connection_handler.clear_all_connections!
+
+ fork do
+ rd.close
+ post = Post.new
+ post.comments.build
+ wr.write Marshal.dump(post)
+ wr.close
+ end
+
+ wr.close
+ assert Marshal.load rd.read
+ rd.close
+ end
+
def test_marshalling_new_record_round_trip_with_associations
post = Post.new
post.comments.build
@@ -1424,27 +1411,11 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal [], AbstractCompany.attribute_names
end
- def test_cache_key_for_existing_record_is_not_timezone_dependent
- ActiveRecord::Base.time_zone_aware_attributes = true
-
- Time.zone = "UTC"
- utc_key = Developer.first.cache_key
-
- Time.zone = "EST"
- est_key = Developer.first.cache_key
-
- assert_equal utc_key, est_key
- end
-
- def test_cache_key_format_for_existing_record_with_updated_at
- dev = Developer.first
- assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
- end
-
- def test_cache_key_format_for_existing_record_with_nil_updated_at
- dev = Developer.first
- dev.update_columns(updated_at: nil)
- assert_match(/\/#{dev.id}$/, dev.cache_key)
+ def test_touch_should_raise_error_on_a_new_object
+ company = Company.new(:rating => 1, :name => "37signals", :firm_name => "37signals")
+ assert_raises(ActiveRecord::ActiveRecordError) do
+ company.touch :updated_at
+ end
end
def test_uniq_delegates_to_scoped
@@ -1453,13 +1424,10 @@ class BasicsTest < ActiveRecord::TestCase
assert_equal scope, Bird.uniq
end
- def test_active_record_super
- assert_equal ActiveRecord::Model, ActiveRecord::Base.active_record_super
- assert_equal ActiveRecord::Base, Topic.active_record_super
- assert_equal Topic, ImportantTopic.active_record_super
- assert_equal ActiveRecord::Model, Teapot.active_record_super
- assert_equal Teapot, OtherTeapot.active_record_super
- assert_equal ActiveRecord::Model, CoolTeapot.active_record_super
+ def test_distinct_delegates_to_scoped
+ scope = stub
+ Bird.stubs(:all).returns(mock(:distinct => scope))
+ assert_equal scope, Bird.distinct
end
def test_table_name_with_2_abstract_subclasses
@@ -1468,7 +1436,7 @@ class BasicsTest < ActiveRecord::TestCase
def test_column_types_typecast
topic = Topic.first
- refute_equal 't.lo', topic.author_name
+ assert_not_equal 't.lo', topic.author_name
attrs = topic.attributes.dup
attrs.delete 'id'
@@ -1526,4 +1494,60 @@ class BasicsTest < ActiveRecord::TestCase
klass = Class.new(ActiveRecord::Base)
assert_equal ['foo'], klass.all.merge!(select: 'foo').select_values
end
+
+ test "connection_handler can be overridden" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ thread_connection_handler = nil
+
+ t = Thread.new do
+ klass.connection_handler = new_handler
+ thread_connection_handler = klass.connection_handler
+ end
+ t.join
+
+ assert_equal klass.connection_handler, orig_handler
+ assert_equal thread_connection_handler, new_handler
+ end
+
+ test "new threads get default the default connection handler" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ handler = nil
+
+ t = Thread.new do
+ handler = klass.connection_handler
+ end
+ t.join
+
+ assert_equal handler, orig_handler
+ assert_equal klass.connection_handler, orig_handler
+ assert_equal klass.default_connection_handler, orig_handler
+ end
+
+ test "changing a connection handler in a main thread does not poison the other threads" do
+ klass = Class.new(ActiveRecord::Base)
+ orig_handler = klass.connection_handler
+ new_handler = ActiveRecord::ConnectionAdapters::ConnectionHandler.new
+ after_handler = nil
+ latch1 = ActiveSupport::Concurrency::Latch.new
+ latch2 = ActiveSupport::Concurrency::Latch.new
+
+ t = Thread.new do
+ klass.connection_handler = new_handler
+ latch1.release
+ latch2.await
+ after_handler = klass.connection_handler
+ end
+
+ latch1.await
+
+ klass.connection_handler = orig_handler
+ latch2.release
+ t.join
+
+ assert_equal after_handler, new_handler
+ assert_equal orig_handler, klass.connection_handler
+ end
end
diff --git a/activerecord/test/cases/batches_test.rb b/activerecord/test/cases/batches_test.rb
index 12d5245fbd..38c2560d69 100644
--- a/activerecord/test/cases/batches_test.rb
+++ b/activerecord/test/cases/batches_test.rb
@@ -11,21 +11,39 @@ class EachTest < ActiveRecord::TestCase
Post.count('id') # preheat arel's table cache
end
- def test_each_should_excecute_one_query_per_batch
- assert_queries(Post.count + 1) do
+ def test_each_should_execute_one_query_per_batch
+ assert_queries(@total + 1) do
Post.find_each(:batch_size => 1) do |post|
assert_kind_of Post, post
end
end
end
- def test_each_should_not_return_query_chain_and_execcute_only_one_query
+ def test_each_should_not_return_query_chain_and_execute_only_one_query
assert_queries(1) do
result = Post.find_each(:batch_size => 100000){ }
assert_nil result
end
end
+ def test_each_should_return_an_enumerator_if_no_block_is_present
+ assert_queries(1) do
+ Post.find_each(:batch_size => 100000).with_index do |post, index|
+ assert_kind_of Post, post
+ assert_kind_of Integer, index
+ end
+ end
+ end
+
+ def test_each_enumerator_should_execute_one_query_per_batch
+ assert_queries(@total + 1) do
+ Post.find_each(:batch_size => 1).with_index do |post, index|
+ assert_kind_of Post, post
+ assert_kind_of Integer, index
+ end
+ end
+ end
+
def test_each_should_raise_if_select_is_set_without_id
assert_raise(RuntimeError) do
Post.select(:title).find_each(:batch_size => 1) { |post| post }
@@ -50,8 +68,18 @@ class EachTest < ActiveRecord::TestCase
Post.order("title").find_each { |post| post }
end
+ def test_logger_not_required
+ previous_logger = ActiveRecord::Base.logger
+ ActiveRecord::Base.logger = nil
+ assert_nothing_raised do
+ Post.limit(1).find_each { |post| post }
+ end
+ ensure
+ ActiveRecord::Base.logger = previous_logger
+ end
+
def test_find_in_batches_should_return_batches
- assert_queries(Post.count + 1) do
+ assert_queries(@total + 1) do
Post.find_in_batches(:batch_size => 1) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
@@ -60,7 +88,7 @@ class EachTest < ActiveRecord::TestCase
end
def test_find_in_batches_should_start_from_the_start_option
- assert_queries(Post.count) do
+ assert_queries(@total) do
Post.find_in_batches(:batch_size => 1, :start => 2) do |batch|
assert_kind_of Array, batch
assert_kind_of Post, batch.first
@@ -68,15 +96,13 @@ class EachTest < ActiveRecord::TestCase
end
end
- def test_find_in_batches_shouldnt_excute_query_unless_needed
- post_count = Post.count
-
+ def test_find_in_batches_shouldnt_execute_query_unless_needed
assert_queries(2) do
- Post.find_in_batches(:batch_size => post_count) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(:batch_size => @total) {|batch| assert_kind_of Array, batch }
end
assert_queries(1) do
- Post.find_in_batches(:batch_size => post_count + 1) {|batch| assert_kind_of Array, batch }
+ Post.find_in_batches(:batch_size => @total + 1) {|batch| assert_kind_of Array, batch }
end
end
@@ -136,4 +162,12 @@ class EachTest < ActiveRecord::TestCase
assert_equal nick_order_subscribers[1..-1].map(&:id), subscribers.map(&:id)
end
+
+ def test_find_in_batches_should_use_any_column_as_primary_key_when_start_is_not_specified
+ assert_queries(Subscriber.count + 1) do
+ Subscriber.find_each(:batch_size => 1) do |subscriber|
+ assert_kind_of Subscriber, subscriber
+ end
+ end
+ end
end
diff --git a/activerecord/test/cases/binary_test.rb b/activerecord/test/cases/binary_test.rb
index 25d2896ab0..9a486cf8b8 100644
--- a/activerecord/test/cases/binary_test.rb
+++ b/activerecord/test/cases/binary_test.rb
@@ -23,7 +23,7 @@ unless current_adapter?(:SybaseAdapter, :DB2Adapter, :FirebirdAdapter)
# Mysql adapter doesn't properly encode things, so we have to do it
if current_adapter?(:MysqlAdapter)
- name.force_encoding('UTF-8')
+ name.force_encoding(Encoding::UTF_8)
end
assert_equal 'ã„ãŸã ãã¾ã™ï¼', name
end
diff --git a/activerecord/test/cases/calculations_test.rb b/activerecord/test/cases/calculations_test.rb
index 6cb6c469d2..2c41656b3d 100644
--- a/activerecord/test/cases/calculations_test.rb
+++ b/activerecord/test/cases/calculations_test.rb
@@ -6,6 +6,10 @@ require 'models/edge'
require 'models/organization'
require 'models/possession'
require 'models/topic'
+require 'models/reply'
+require 'models/minivan'
+require 'models/speedometer'
+require 'models/ship_part'
Company.has_many :accounts
@@ -25,14 +29,19 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal 53.0, value
end
+ def test_should_resolve_aliased_attributes
+ assert_equal 318, Account.sum(:available_credit)
+ end
+
def test_should_return_decimal_average_of_integer_field
value = Account.average(:id)
assert_equal 3.5, value
end
def test_should_return_integer_average_if_db_returns_such
- Account.connection.stubs :select_value => 3
- value = Account.average(:id)
+ ShipPart.delete_all
+ ShipPart.create!(:id => 3, :name => 'foo')
+ value = ShipPart.average(:id)
assert_equal 3, value
end
@@ -92,25 +101,24 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_order_by_grouped_field
- c = Account.all.merge!(:group => :firm_id, :order => "firm_id").sum(:credit_limit)
+ c = Account.group(:firm_id).order("firm_id").sum(:credit_limit)
assert_equal [1, 2, 6, 9], c.keys.compact
end
def test_should_order_by_calculation
- c = Account.all.merge!(:group => :firm_id, :order => "sum_credit_limit desc, firm_id").sum(:credit_limit)
+ c = Account.group(:firm_id).order("sum_credit_limit desc, firm_id").sum(:credit_limit)
assert_equal [105, 60, 53, 50, 50], c.keys.collect { |k| c[k] }
assert_equal [6, 2, 9, 1], c.keys.compact
end
def test_should_limit_calculation
- c = Account.all.merge!(:where => "firm_id IS NOT NULL",
- :group => :firm_id, :order => "firm_id", :limit => 2).sum(:credit_limit)
+ c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").limit(2).sum(:credit_limit)
assert_equal [1, 2], c.keys.compact
end
def test_should_limit_calculation_with_offset
- c = Account.all.merge!(:where => "firm_id IS NOT NULL", :group => :firm_id,
- :order => "firm_id", :limit => 2, :offset => 1).sum(:credit_limit)
+ c = Account.where("firm_id IS NOT NULL").group(:firm_id).order("firm_id").
+ limit(2).offset(1).sum(:credit_limit)
assert_equal [2, 6], c.keys.compact
end
@@ -159,9 +167,17 @@ class CalculationsTest < ActiveRecord::TestCase
assert_no_match(/OFFSET/, queries.first)
end
+ def test_count_on_invalid_columns_raises
+ e = assert_raises(ActiveRecord::StatementInvalid) {
+ Account.select("credit_limit, firm_name").count
+ }
+
+ assert_match %r{accounts}i, e.message
+ assert_match "credit_limit, firm_name", e.message
+ end
+
def test_should_group_by_summed_field_having_condition
- c = Account.all.merge!(:group => :firm_id,
- :having => 'sum(credit_limit) > 50').sum(:credit_limit)
+ c = Account.group(:firm_id).having('sum(credit_limit) > 50').sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
@@ -196,17 +212,15 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_summed_field_with_conditions
- c = Account.all.merge!(:where => 'firm_id > 1',
- :group => :firm_id).sum(:credit_limit)
+ c = Account.where('firm_id > 1').group(:firm_id).sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_equal 60, c[2]
end
def test_should_group_by_summed_field_with_conditions_and_having
- c = Account.all.merge!(:where => 'firm_id > 1',
- :group => :firm_id,
- :having => 'sum(credit_limit) > 60').sum(:credit_limit)
+ c = Account.where('firm_id > 1').group(:firm_id).
+ having('sum(credit_limit) > 60').sum(:credit_limit)
assert_nil c[1]
assert_equal 105, c[6]
assert_nil c[2]
@@ -239,21 +253,12 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_group_by_association_with_non_numeric_foreign_key
- ActiveRecord::Base.connection.expects(:select_all).returns([{"count_all" => 1, "firm_id" => "ABC"}])
-
- firm = mock()
- firm.expects(:id).returns("ABC")
- firm.expects(:class).returns(Firm)
- Company.expects(:find).with(["ABC"]).returns([firm])
+ Speedometer.create! id: 'ABC'
+ Minivan.create! id: 'OMG', speedometer_id: 'ABC'
- column = mock()
- column.expects(:name).at_least_once.returns(:firm_id)
- column.expects(:type_cast).with("ABC").returns("ABC")
- Account.expects(:columns).at_least_once.returns([column])
-
- c = Account.group(:firm).count(:all)
+ c = Minivan.group(:speedometer).count(:all)
first_key = c.keys.first
- assert_equal Firm, first_key.class
+ assert_equal Speedometer, first_key.class
assert_equal 1, c[first_key]
end
@@ -310,8 +315,8 @@ class CalculationsTest < ActiveRecord::TestCase
end
def test_should_count_selected_field_with_include
- assert_equal 6, Account.includes(:firm).count(:distinct => true)
- assert_equal 4, Account.includes(:firm).select(:credit_limit).count(:distinct => true)
+ assert_equal 6, Account.includes(:firm).distinct.count
+ assert_equal 4, Account.includes(:firm).distinct.select(:credit_limit).count
end
def test_should_not_perform_joined_include_by_default
@@ -327,7 +332,7 @@ class CalculationsTest < ActiveRecord::TestCase
def test_should_count_scoped_select
Account.update_all("credit_limit = NULL")
- assert_equal 0, Account.all.merge!(:select => "credit_limit").count
+ assert_equal 0, Account.select("credit_limit").count
end
def test_should_count_scoped_select_with_options
@@ -335,28 +340,37 @@ class CalculationsTest < ActiveRecord::TestCase
Account.last.update_columns('credit_limit' => 49)
Account.first.update_columns('credit_limit' => 51)
- assert_equal 1, Account.all.merge!(:select => "credit_limit").where('credit_limit >= 50').count
+ assert_equal 1, Account.select("credit_limit").where('credit_limit >= 50').count
end
def test_should_count_manual_select_with_include
- assert_equal 6, Account.all.merge!(:select => "DISTINCT accounts.id", :includes => :firm).count
+ assert_equal 6, Account.select("DISTINCT accounts.id").includes(:firm).count
end
def test_count_with_column_parameter
assert_equal 5, Account.count(:firm_id)
end
+ def test_count_with_distinct
+ assert_equal 4, Account.select(:credit_limit).distinct.count
+ assert_equal 4, Account.select(:credit_limit).uniq.count
+ end
+
+ def test_count_with_aliased_attribute
+ assert_equal 6, Account.count(:available_credit)
+ end
+
def test_count_with_column_and_options_parameter
assert_equal 2, Account.where("credit_limit = 50 AND firm_id IS NOT NULL").count(:firm_id)
end
def test_should_count_field_in_joined_table
assert_equal 5, Account.joins(:firm).count('companies.id')
- assert_equal 4, Account.joins(:firm).count('companies.id', :distinct => true)
+ assert_equal 4, Account.joins(:firm).distinct.count('companies.id')
end
def test_should_count_field_in_joined_table_with_group_by
- c = Account.all.merge!(:group => 'accounts.firm_id', :joins => :firm).count('companies.id')
+ c = Account.group('accounts.firm_id').joins(:firm).count('companies.id')
[1,6,2,9].each { |firm_id| assert c.keys.include?(firm_id) }
end
@@ -378,6 +392,10 @@ class CalculationsTest < ActiveRecord::TestCase
end
end
+ def test_sum_expression_returns_zero_when_no_records_to_sum
+ assert_equal 0, Account.where('1 = 2').sum("2 * credit_limit")
+ end
+
def test_count_with_from_option
assert_equal Company.count(:all), Company.from('companies').count(:all)
assert_equal Account.where("credit_limit = 50").count(:all),
@@ -386,32 +404,12 @@ class CalculationsTest < ActiveRecord::TestCase
Company.where(:type => "Firm").from('companies').count(:type)
end
- def test_count_with_block_acts_as_array
- accounts = Account.where('id > 0')
- assert_equal Account.count, accounts.count { true }
- assert_equal 0, accounts.count { false }
- assert_equal Account.where('credit_limit > 50').size, accounts.count { |account| account.credit_limit > 50 }
- assert_equal Account.count, Account.count { true }
- assert_equal 0, Account.count { false }
- end
-
- def test_sum_with_block_acts_as_array
- accounts = Account.where('id > 0')
- assert_equal Account.sum(:credit_limit), accounts.sum { |account| account.credit_limit }
- assert_equal Account.sum(:credit_limit) + Account.count, accounts.sum{ |account| account.credit_limit + 1 }
- assert_equal 0, accounts.sum { |account| 0 }
- end
-
def test_sum_with_from_option
assert_equal Account.sum(:credit_limit), Account.from('accounts').sum(:credit_limit)
assert_equal Account.where("credit_limit > 50").sum(:credit_limit),
Account.where("credit_limit > 50").from('accounts').sum(:credit_limit)
end
- def test_sum_array_compatibility
- assert_equal Account.sum(:credit_limit), Account.sum(&:credit_limit)
- end
-
def test_average_with_from_option
assert_equal Account.average(:credit_limit), Account.from('accounts').average(:credit_limit)
assert_equal Account.where("credit_limit > 50").average(:credit_limit),
@@ -433,34 +431,19 @@ class CalculationsTest < ActiveRecord::TestCase
def test_maximum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter)
- assert_equal "7", Company.includes(:contracts).maximum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).maximum(:developer_id)
end
def test_minimum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:PostgreSQLAdapter) || current_adapter?(:MysqlAdapter)
- assert_equal "7", Company.includes(:contracts).minimum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).minimum(:developer_id)
end
def test_sum_with_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
- # TODO: Investigate why PG isn't being typecast
- if current_adapter?(:MysqlAdapter) || current_adapter?(:PostgreSQLAdapter)
- assert_equal "7", Company.includes(:contracts).sum(:developer_id)
- else
- assert_equal 7, Company.includes(:contracts).sum(:developer_id)
- end
+ assert_equal 7, Company.includes(:contracts).sum(:developer_id)
end
@@ -481,7 +464,7 @@ class CalculationsTest < ActiveRecord::TestCase
approved_topics_count = Topic.group(:approved).count(:author_name)[true]
assert_equal approved_topics_count, 3
# Count the number of distinct authors for approved Topics
- distinct_authors_for_approved_count = Topic.group(:approved).count(:author_name, :distinct => true)[true]
+ distinct_authors_for_approved_count = Topic.group(:approved).distinct.count(:author_name)[true]
assert_equal distinct_authors_for_approved_count, 2
end
@@ -489,6 +472,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [1,2,3,4], Topic.order(:id).pluck(:id)
end
+ def test_pluck_without_column_names
+ assert_equal [[1, "Firm", 1, nil, "37signals", nil, 1, nil, ""]],
+ Company.order(:id).limit(1).pluck
+ end
+
def test_pluck_type_cast
topic = topics(:first)
relation = Topic.where(:id => topic.id)
@@ -507,6 +495,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal [contract.id], company.contracts.pluck(:id)
end
+ def test_pluck_on_aliased_attribute
+ assert_equal 'The First Topic', Topic.order(:id).pluck(:heading).first
+ end
+
def test_pluck_with_serialization
t = Topic.create!(:content => { :foo => :bar })
assert_equal [{:foo => :bar}], Topic.where(:id => t.id).pluck(:content)
@@ -546,6 +538,11 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal Company.all.map(&:id).sort, Company.ids.sort
end
+ def test_pluck_with_includes_limit_and_empty_result
+ assert_equal [], Topic.includes(:replies).limit(0).pluck(:id)
+ assert_equal [], Topic.includes(:replies).limit(1).where('0 = 1').pluck(:id)
+ end
+
def test_pluck_not_auto_table_name_prefix_if_column_included
Company.create!(:name => "test", :contracts => [Contract.new(:developer_id => 7)])
ids = Company.includes(:contracts).pluck(:developer_id)
@@ -583,4 +580,10 @@ class CalculationsTest < ActiveRecord::TestCase
assert_equal ["Over There"], Possession.pluck(:where)
end
+
+ def test_pluck_replaces_select_clause
+ taks_relation = Topic.select(:approved, :id).order(:id)
+ assert_equal [1,2,3,4], taks_relation.pluck(:id)
+ assert_equal [false, true, true, true], taks_relation.pluck(:approved)
+ end
end
diff --git a/activerecord/test/cases/callbacks_test.rb b/activerecord/test/cases/callbacks_test.rb
index 7457bafd4e..c8f56e3c73 100644
--- a/activerecord/test/cases/callbacks_test.rb
+++ b/activerecord/test/cases/callbacks_test.rb
@@ -43,7 +43,7 @@ class CallbackDeveloper < ActiveRecord::Base
end
class CallbackDeveloperWithFalseValidation < CallbackDeveloper
- before_validation proc { |model| model.history << [:before_validation, :returning_false]; return false }
+ before_validation proc { |model| model.history << [:before_validation, :returning_false]; false }
before_validation proc { |model| model.history << [:before_validation, :should_never_get_here] }
end
@@ -520,7 +520,7 @@ class CallbacksTest < ActiveRecord::TestCase
], david.history
end
- def test_inheritence_of_callbacks
+ def test_inheritance_of_callbacks
parent = ParentDeveloper.new
assert !parent.after_save_called
parent.save
diff --git a/activerecord/test/cases/clone_test.rb b/activerecord/test/cases/clone_test.rb
index d91646efca..5e43082c33 100644
--- a/activerecord/test/cases/clone_test.rb
+++ b/activerecord/test/cases/clone_test.rb
@@ -29,5 +29,12 @@ module ActiveRecord
topic.author_name = 'Aaron'
assert_equal 'Aaron', cloned.author_name
end
+
+ def test_freezing_a_cloned_model_does_not_freeze_clone
+ cloned = Topic.new
+ clone = cloned.clone
+ cloned.freeze
+ assert_not clone.frozen?
+ end
end
end
diff --git a/activerecord/test/cases/coders/yaml_column_test.rb b/activerecord/test/cases/coders/yaml_column_test.rb
index b874adc081..b72c54f97b 100644
--- a/activerecord/test/cases/coders/yaml_column_test.rb
+++ b/activerecord/test/cases/coders/yaml_column_test.rb
@@ -43,10 +43,20 @@ module ActiveRecord
assert_equal [], coder.load([])
end
- def test_load_swallows_yaml_exceptions
+ def test_load_doesnt_swallow_yaml_exceptions
coder = YAMLColumn.new
bad_yaml = '--- {'
- assert_equal bad_yaml, coder.load(bad_yaml)
+ assert_raises(Psych::SyntaxError) do
+ coder.load(bad_yaml)
+ end
+ end
+
+ def test_load_doesnt_handle_undefined_class_or_module
+ coder = YAMLColumn.new
+ missing_class_yaml = '--- !ruby/object:DoesNotExistAndShouldntEver {}\n'
+ assert_raises(ArgumentError) do
+ coder.load(missing_class_yaml)
+ end
end
end
end
diff --git a/activerecord/test/cases/column_definition_test.rb b/activerecord/test/cases/column_definition_test.rb
index bd2fbaa7db..dbb2f223cd 100644
--- a/activerecord/test/cases/column_definition_test.rb
+++ b/activerecord/test/cases/column_definition_test.rb
@@ -8,6 +8,7 @@ module ActiveRecord
def @adapter.native_database_types
{:string => "varchar"}
end
+ @viz = @adapter.schema_creation
end
def test_can_set_coder
@@ -35,25 +36,25 @@ module ActiveRecord
def test_should_not_include_default_clause_when_default_is_null
column = Column.new("title", nil, "varchar(20)")
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal "title varchar(20)", column_def.to_sql
+ assert_equal "title varchar(20)", @viz.accept(column_def)
end
def test_should_include_default_clause_when_default_is_present
column = Column.new("title", "Hello", "varchar(20)")
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello'}, @viz.accept(column_def)
end
def test_should_specify_not_null_if_null_option_is_false
column = Column.new("title", "Hello", "varchar(20)", false)
column_def = ColumnDefinition.new(
- @adapter, column.name, "string",
+ column.name, "string",
column.limit, column.precision, column.scale, column.default, column.null)
- assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, column_def.to_sql
+ assert_equal %Q{title varchar(20) DEFAULT 'Hello' NOT NULL}, @viz.accept(column_def)
end
if current_adapter?(:MysqlAdapter)
diff --git a/activerecord/test/cases/column_test.rb b/activerecord/test/cases/column_test.rb
index a7b63d15c9..2a6d8cc2ab 100644
--- a/activerecord/test/cases/column_test.rb
+++ b/activerecord/test/cases/column_test.rb
@@ -1,10 +1,14 @@
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')
@@ -14,15 +18,21 @@ module ActiveRecord
assert column.type_cast('TRUE')
assert column.type_cast('on')
assert column.type_cast('ON')
- assert !column.type_cast(false)
- assert !column.type_cast(0)
- assert !column.type_cast('0')
- assert !column.type_cast('f')
- assert !column.type_cast('F')
- assert !column.type_cast('false')
- assert !column.type_cast('FALSE')
- assert !column.type_cast('off')
- assert !column.type_cast('OFF')
+
+ # 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
@@ -33,26 +43,40 @@ module ActiveRecord
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_raises(NoMethodError) do
- column.type_cast([])
- end
- assert_raises(NoMethodError) do
- column.type_cast(true)
- end
- assert_raises(NoMethodError) do
- column.type_cast(false)
- end
+ 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(' ')
+ 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")
@@ -60,8 +84,10 @@ module ActiveRecord
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")
@@ -70,8 +96,10 @@ module ActiveRecord
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(' ')
+ 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")
@@ -82,6 +110,14 @@ module ActiveRecord
assert_equal 1800, column.type_cast(30.minutes)
assert_equal 7200, column.type_cast(2.hours)
end
+
+ def test_string_to_time_with_timezone
+ [:utc, :local].each do |zone|
+ with_timezone_config default: zone do
+ assert_equal Time.utc(2013, 9, 4, 0, 0, 0), Column.string_to_time("Wed, 04 Sep 2013 03:00:00 EAT")
+ end
+ end
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
index 3e3d6e2769..eb2fe5639b 100644
--- a/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
+++ b/activerecord/test/cases/connection_adapters/abstract_adapter_test.rb
@@ -2,6 +2,15 @@ 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
@@ -10,19 +19,18 @@ module ActiveRecord
end
def test_in_use?
- # FIXME: change to refute in Rails 4.0 / mt
- assert !adapter.in_use?, 'adapter is not 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 !adapter.lease, 'should not lease adapter'
+ assert_not adapter.lease, 'should not lease adapter'
end
def test_last_use
- assert !adapter.last_use
+ assert_not adapter.last_use
adapter.lease
assert adapter.last_use
end
@@ -31,7 +39,7 @@ module ActiveRecord
assert adapter.lease, 'lease adapter'
assert adapter.in_use?, 'adapter is in use'
adapter.expire
- assert !adapter.in_use?, 'adapter is in use'
+ assert_not adapter.in_use?, 'adapter is in use'
end
def test_close
@@ -45,7 +53,7 @@ module ActiveRecord
# Close should put the adapter back in the pool
adapter.close
- assert !adapter.in_use?
+ assert_not adapter.in_use?
assert_equal adapter, pool.connection
end
diff --git a/activerecord/test/cases/connection_adapters/connection_handler_test.rb b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
index 4467ddfc39..3e33b30144 100644
--- a/activerecord/test/cases/connection_adapters/connection_handler_test.rb
+++ b/activerecord/test/cases/connection_adapters/connection_handler_test.rb
@@ -4,11 +4,11 @@ module ActiveRecord
module ConnectionAdapters
class ConnectionHandlerTest < ActiveRecord::TestCase
def setup
- @klass = Class.new { include ActiveRecord::Tag }
- @subklass = Class.new(@klass) { include ActiveRecord::Tag }
+ @klass = Class.new(Base) { def self.name; 'klass'; end }
+ @subklass = Class.new(@klass) { def self.name; 'subklass'; end }
@handler = ConnectionHandler.new
- @handler.establish_connection @klass, Base.connection_pool.spec
+ @pool = @handler.establish_connection(@klass, Base.connection_pool.spec)
end
def test_retrieve_connection
@@ -36,13 +36,17 @@ module ActiveRecord
end
def test_retrieve_connection_pool_uses_superclass_pool_after_subclass_establish_and_remove
- @handler.establish_connection 'north america', Base.connection_pool.spec
- assert_same @handler.retrieve_connection_pool(@klass),
- @handler.retrieve_connection_pool(@subklass)
+ sub_pool = @handler.establish_connection(@subklass, Base.connection_pool.spec)
+ assert_same sub_pool, @handler.retrieve_connection_pool(@subklass)
@handler.remove_connection @subklass
- assert_same @handler.retrieve_connection_pool(@klass),
- @handler.retrieve_connection_pool(@subklass)
+ assert_same @pool, @handler.retrieve_connection_pool(@subklass)
+ end
+
+ def test_connection_pools
+ assert_deprecated do
+ assert_equal({ Base.connection_pool.spec => @pool }, @handler.connection_pools)
+ 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 541e983758..ecad7c942f 100644
--- a/activerecord/test/cases/connection_adapters/schema_cache_test.rb
+++ b/activerecord/test/cases/connection_adapters/schema_cache_test.rb
@@ -9,49 +9,46 @@ module ActiveRecord
end
def test_primary_key
- assert_equal 'id', @cache.primary_keys['posts']
+ assert_equal 'id', @cache.primary_keys('posts')
end
def test_primary_key_for_non_existent_table
- assert_nil @cache.primary_keys['omgponies']
+ assert_nil @cache.primary_keys('omgponies')
end
def test_caches_columns
- columns = @cache.columns['posts']
- assert_equal columns, @cache.columns['posts']
+ columns = @cache.columns('posts')
+ assert_equal columns, @cache.columns('posts')
end
def test_caches_columns_hash
- columns_hash = @cache.columns_hash['posts']
- assert_equal columns_hash, @cache.columns_hash['posts']
+ columns_hash = @cache.columns_hash('posts')
+ assert_equal columns_hash, @cache.columns_hash('posts')
end
def test_clearing
- @cache.columns['posts']
- @cache.columns_hash['posts']
- @cache.tables['posts']
- @cache.primary_keys['posts']
+ @cache.columns('posts')
+ @cache.columns_hash('posts')
+ @cache.tables('posts')
+ @cache.primary_keys('posts')
@cache.clear!
- assert_equal 0, @cache.columns.size
- assert_equal 0, @cache.columns_hash.size
- assert_equal 0, @cache.tables.size
- assert_equal 0, @cache.primary_keys.size
+ assert_equal 0, @cache.size
end
def test_dump_and_load
- @cache.columns['posts']
- @cache.columns_hash['posts']
- @cache.tables['posts']
- @cache.primary_keys['posts']
+ @cache.columns('posts')
+ @cache.columns_hash('posts')
+ @cache.tables('posts')
+ @cache.primary_keys('posts')
@cache = Marshal.load(Marshal.dump(@cache))
- assert_equal 12, @cache.columns['posts'].size
- assert_equal 12, @cache.columns_hash['posts'].size
- assert @cache.tables['posts']
- assert_equal 'id', @cache.primary_keys['posts']
+ assert_equal 12, @cache.columns('posts').size
+ assert_equal 12, @cache.columns_hash('posts').size
+ assert @cache.tables('posts')
+ assert_equal 'id', @cache.primary_keys('posts')
end
end
diff --git a/activerecord/test/cases/connection_management_test.rb b/activerecord/test/cases/connection_management_test.rb
index fe1b40d884..df17732fff 100644
--- a/activerecord/test/cases/connection_management_test.rb
+++ b/activerecord/test/cases/connection_management_test.rb
@@ -80,9 +80,9 @@ module ActiveRecord
end
def test_connections_closed_if_exception
- app = Class.new(App) { def call(env); raise; end }.new
+ app = Class.new(App) { def call(env); raise NotImplementedError; end }.new
explosive = ConnectionManagement.new(app)
- assert_raises(RuntimeError) { explosive.call(@env) }
+ assert_raises(NotImplementedError) { explosive.call(@env) }
assert !ActiveRecord::Base.connection_handler.active_connections?
end
diff --git a/activerecord/test/cases/connection_pool_test.rb b/activerecord/test/cases/connection_pool_test.rb
index 0718d0886f..2da51ea015 100644
--- a/activerecord/test/cases/connection_pool_test.rb
+++ b/activerecord/test/cases/connection_pool_test.rb
@@ -118,6 +118,7 @@ module ActiveRecord
connection = cs.first
@pool.remove connection
assert_respond_to t.join.value, :execute
+ connection.close
end
def test_reap_and_active
@@ -185,7 +186,7 @@ module ActiveRecord
assert_not_nil connection
threads = []
4.times do |i|
- threads << Thread.new(i) do |pool_count|
+ threads << Thread.new(i) do
connection = pool.connection
assert_not_nil connection
connection.close
@@ -327,6 +328,17 @@ module ActiveRecord
def test_pool_sets_connection_visitor
assert @pool.connection.visitor.is_a?(Arel::Visitors::ToSql)
end
+
+ # make sure exceptions are thrown when establish_connection
+ # is called with an anonymous class
+ def test_anonymous_class_exception
+ anonymous = Class.new(ActiveRecord::Base)
+ handler = ActiveRecord::Base.connection_handler
+
+ assert_raises(RuntimeError) {
+ handler.establish_connection anonymous, nil
+ }
+ end
end
end
end
diff --git a/activerecord/test/cases/connection_specification/resolver_test.rb b/activerecord/test/cases/connection_specification/resolver_test.rb
index 434d2b7ba5..c8dfc3244b 100644
--- a/activerecord/test/cases/connection_specification/resolver_test.rb
+++ b/activerecord/test/cases/connection_specification/resolver_test.rb
@@ -8,33 +8,54 @@ module ActiveRecord
Resolver.new(spec, {}).spec.config
end
+ def test_url_invalid_adapter
+ assert_raises(LoadError) do
+ resolve 'ridiculous://foo?encoding=utf8'
+ end
+ end
+
+ # The abstract adapter is used simply to bypass the bit of code that
+ # checks that the adapter file can be required in.
+
def test_url_host_no_db
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo?encoding=utf8'
+ spec = resolve 'abstract://foo?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :host => "foo",
- :encoding => "utf8" }, spec)
+ adapter: "abstract",
+ host: "foo",
+ encoding: "utf8" }, spec)
end
def test_url_host_db
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo/bar?encoding=utf8'
+ spec = resolve 'abstract://foo/bar?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :database => "bar",
- :host => "foo",
- :encoding => "utf8" }, spec)
+ adapter: "abstract",
+ database: "bar",
+ host: "foo",
+ encoding: "utf8" }, spec)
end
def test_url_port
- skip "only if mysql is available" unless defined?(MysqlAdapter)
- spec = resolve 'mysql://foo:123?encoding=utf8'
+ spec = resolve 'abstract://foo:123?encoding=utf8'
assert_equal({
- :adapter => "mysql",
- :port => 123,
- :host => "foo",
- :encoding => "utf8" }, spec)
+ adapter: "abstract",
+ port: 123,
+ host: "foo",
+ encoding: "utf8" }, spec)
+ end
+
+ def test_encoded_password
+ password = 'am@z1ng_p@ssw0rd#!'
+ encoded_password = URI.encode_www_form_component(password)
+ spec = resolve "abstract://foo:#{encoded_password}@localhost/bar"
+ assert_equal password, spec[:password]
+ end
+
+ def test_descriptive_error_message_when_adapter_is_missing
+ error = assert_raise(LoadError) do
+ resolve(adapter: 'non-existing')
+ end
+
+ assert_match "Could not load 'active_record/connection_adapters/non-existing_adapter'", error.message
end
end
end
diff --git a/activerecord/test/cases/core_test.rb b/activerecord/test/cases/core_test.rb
new file mode 100644
index 0000000000..2a52bf574c
--- /dev/null
+++ b/activerecord/test/cases/core_test.rb
@@ -0,0 +1,33 @@
+require 'cases/helper'
+require 'models/person'
+require 'models/topic'
+
+class NonExistentTable < ActiveRecord::Base; end
+
+class CoreTest < ActiveRecord::TestCase
+ fixtures :topics
+
+ def test_inspect_class
+ assert_equal 'ActiveRecord::Base', ActiveRecord::Base.inspect
+ assert_equal 'LoosePerson(abstract)', LoosePerson.inspect
+ assert_match(/^Topic\(id: integer, title: string/, Topic.inspect)
+ end
+
+ def test_inspect_instance
+ topic = topics(:first)
+ assert_equal %(#<Topic id: 1, title: "The First Topic", author_name: "David", author_email_address: "david@loudthinking.com", written_on: "#{topic.written_on.to_s(:db)}", bonus_time: "#{topic.bonus_time.to_s(:db)}", last_read: "#{topic.last_read.to_s(:db)}", 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: "#{topic.created_at.to_s(:db)}", updated_at: "#{topic.updated_at.to_s(:db)}">), topic.inspect
+ end
+
+ def test_inspect_new_instance
+ assert_match(/Topic id: nil/, Topic.new.inspect)
+ end
+
+ def test_inspect_limited_select_instance
+ assert_equal %(#<Topic id: 1>), Topic.all.merge!(:select => 'id', :where => 'id = 1').first.inspect
+ assert_equal %(#<Topic id: 1, title: "The First Topic">), Topic.all.merge!(:select => 'id, title', :where => 'id = 1').first.inspect
+ end
+
+ def test_inspect_class_without_table
+ assert_equal "NonExistentTable(Table doesn't exist)", NonExistentTable.inspect
+ end
+end
diff --git a/activerecord/test/cases/counter_cache_test.rb b/activerecord/test/cases/counter_cache_test.rb
index fc46a249c8..ee3d8a81c2 100644
--- a/activerecord/test/cases/counter_cache_test.rb
+++ b/activerecord/test/cases/counter_cache_test.rb
@@ -51,6 +51,13 @@ class CounterCacheTest < ActiveRecord::TestCase
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
+ Topic.reset_counters(@topic.id, :replies, :unique_replies)
+ end
+ end
+
test "reset counters with string argument" do
Topic.increment_counter('replies_count', @topic.id)
@@ -115,10 +122,25 @@ class CounterCacheTest < ActiveRecord::TestCase
end
end
+ test 'update multiple counters' do
+ assert_difference ['@topic.reload.replies_count', '@topic.reload.unique_replies_count'], 2 do
+ Topic.update_counters @topic.id, replies_count: 2, unique_replies_count: 2
+ end
+ end
+
+ test "update other counters on parent destroy" do
+ david, joanna = dog_lovers(:david, :joanna)
+ joanna = joanna # squelch a warning
+
+ assert_difference 'joanna.reload.dogs_count', -1 do
+ david.destroy
+ end
+ end
+
test "reset the right counter if two have the same foreign key" do
michael = people(:michael)
assert_nothing_raised(ActiveRecord::StatementInvalid) do
- Person.reset_counters(michael.id, :followers)
+ Person.reset_counters(michael.id, :friends_too)
end
end
@@ -131,4 +153,11 @@ class CounterCacheTest < ActiveRecord::TestCase
Subscriber.reset_counters(subscriber.id, 'books')
end
end
+
+ test "the passed symbol needs to be an association name" do
+ e = assert_raises(ArgumentError) do
+ Topic.reset_counters(@topic.id, :replies_count)
+ end
+ assert_equal "'Topic' has no association called 'replies_count'", e.message
+ end
end
diff --git a/activerecord/test/cases/date_time_test.rb b/activerecord/test/cases/date_time_test.rb
index 3deb0dac99..c0491bbee5 100644
--- a/activerecord/test/cases/date_time_test.rb
+++ b/activerecord/test/cases/date_time_test.rb
@@ -5,18 +5,18 @@ require 'models/task'
class DateTimeTest < ActiveRecord::TestCase
def test_saves_both_date_and_time
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
time_values = [1807, 2, 10, 15, 30, 45]
# create DateTime value with local time zone offset
- local_offset = Rational(Time.local_time(*time_values).utc_offset, 86400)
+ local_offset = Rational(Time.local(*time_values).utc_offset, 86400)
now = DateTime.civil(*(time_values + [local_offset]))
task = Task.new
task.starting = now
task.save!
- # check against Time.local_time, since some platforms will return a Time instead of a DateTime
- assert_equal Time.local_time(*time_values), Task.find(task.id).starting
+ # check against Time.local, since some platforms will return a Time instead of a DateTime
+ assert_equal Time.local(*time_values), Task.find(task.id).starting
end
end
end
diff --git a/activerecord/test/cases/defaults_test.rb b/activerecord/test/cases/defaults_test.rb
index 72f1c99ca0..7e3d91e08c 100644
--- a/activerecord/test/cases/defaults_test.rb
+++ b/activerecord/test/cases/defaults_test.rb
@@ -39,7 +39,32 @@ class DefaultTest < ActiveRecord::TestCase
end
end
-if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+class DefaultStringsTest < ActiveRecord::TestCase
+ class DefaultString < ActiveRecord::Base; end
+
+ setup do
+ @connection = ActiveRecord::Base.connection
+ @connection.create_table :default_strings do |t|
+ t.string :string_col, default: "Smith"
+ t.string :string_col_with_quotes, default: "O'Connor"
+ end
+ DefaultString.reset_column_information
+ end
+
+ def test_default_strings
+ assert_equal "Smith", DefaultString.new.string_col
+ end
+
+ def test_default_strings_containing_single_quotes
+ assert_equal "O'Connor", DefaultString.new.string_col_with_quotes
+ end
+
+ teardown do
+ @connection.drop_table :default_strings
+ end
+end
+
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class DefaultsTestWithoutTransactionalFixtures < ActiveRecord::TestCase
# ActiveRecord::Base#create! (and #save and other related methods) will
# open a new transaction. When in transactional fixtures mode, this will
@@ -51,11 +76,60 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
# We don't want that to happen, so we disable transactional fixtures here.
self.use_transactional_fixtures = false
- # MySQL 5 and higher is quirky with not null text/blob columns.
- # With MySQL Text/blob columns cannot have defaults. If the column is not
- # null MySQL will report that the column has a null default
- # but it behaves as though the column had a default of ''
- def test_mysql_text_not_null_defaults
+ def using_strict(strict)
+ connection = ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection connection.merge(strict: strict)
+ yield
+ ensure
+ ActiveRecord::Base.remove_connection
+ ActiveRecord::Base.establish_connection connection
+ end
+
+ # MySQL cannot have defaults on text/blob columns. It reports the
+ # default value as null.
+ #
+ # Despite this, in non-strict mode, MySQL will use an empty string
+ # as the default value of the field, if no other value is
+ # specified.
+ #
+ # Therefore, in non-strict mode, we want column.default to report
+ # an empty string as its default, to be consistent with that.
+ #
+ # In strict mode, column.default should be nil.
+ def test_mysql_text_not_null_defaults_non_strict
+ using_strict(false) do
+ with_text_blob_not_null_table do |klass|
+ assert_equal '', klass.columns_hash['non_null_blob'].default
+ assert_equal '', klass.columns_hash['non_null_text'].default
+
+ assert_nil klass.columns_hash['null_blob'].default
+ assert_nil klass.columns_hash['null_text'].default
+
+ instance = klass.create!
+
+ assert_equal '', instance.non_null_text
+ assert_equal '', instance.non_null_blob
+
+ assert_nil instance.null_text
+ assert_nil instance.null_blob
+ end
+ end
+ end
+
+ def test_mysql_text_not_null_defaults_strict
+ using_strict(true) do
+ with_text_blob_not_null_table do |klass|
+ assert_nil klass.columns_hash['non_null_blob'].default
+ assert_nil klass.columns_hash['non_null_text'].default
+ assert_nil klass.columns_hash['null_blob'].default
+ assert_nil klass.columns_hash['null_text'].default
+
+ assert_raises(ActiveRecord::StatementInvalid) { klass.create }
+ end
+ end
+ end
+
+ def with_text_blob_not_null_table
klass = Class.new(ActiveRecord::Base)
klass.table_name = 'test_mysql_text_not_null_defaults'
klass.connection.create_table klass.table_name do |t|
@@ -64,19 +138,8 @@ if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
t.column :null_text, :text, :null => true
t.column :null_blob, :blob, :null => true
end
- assert_equal '', klass.columns_hash['non_null_blob'].default
- assert_equal '', klass.columns_hash['non_null_text'].default
- assert_nil klass.columns_hash['null_blob'].default
- assert_nil klass.columns_hash['null_text'].default
-
- assert_nothing_raised do
- instance = klass.create!
- assert_equal '', instance.non_null_text
- assert_equal '', instance.non_null_blob
- assert_nil instance.null_text
- assert_nil instance.null_blob
- end
+ yield klass
ensure
klass.connection.drop_table(klass.table_name) rescue nil
end
diff --git a/activerecord/test/cases/deprecated_dynamic_methods_test.rb b/activerecord/test/cases/deprecated_dynamic_methods_test.rb
deleted file mode 100644
index dde36e7f72..0000000000
--- a/activerecord/test/cases/deprecated_dynamic_methods_test.rb
+++ /dev/null
@@ -1,592 +0,0 @@
-# This file should be deleted when activerecord-deprecated_finders is removed as
-# a dependency.
-#
-# It is kept for now as there is some fairly nuanced behaviour in the dynamic
-# finders so it is useful to keep this around to guard against regressions if
-# we need to change the code.
-
-require 'cases/helper'
-require 'models/topic'
-require 'models/reply'
-require 'models/customer'
-require 'models/post'
-require 'models/company'
-require 'models/author'
-require 'models/category'
-require 'models/comment'
-require 'models/person'
-require 'models/reader'
-
-class DeprecatedDynamicMethodsTest < ActiveRecord::TestCase
- fixtures :topics, :customers, :companies, :accounts, :posts, :categories, :categories_posts, :authors, :people, :comments, :readers
-
- def setup
- @deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- end
-
- def teardown
- ActiveSupport::Deprecation.behavior = @deprecation_behavior
- end
-
- def test_find_all_by_one_attribute
- topics = Topic.find_all_by_content("Have a nice day")
- assert_equal 2, topics.size
- assert topics.include?(topics(:first))
-
- assert_equal [], Topic.find_all_by_title("The First Topic!!")
- end
-
- def test_find_all_by_one_attribute_which_is_a_symbol
- topics = Topic.find_all_by_content("Have a nice day".to_sym)
- assert_equal 2, topics.size
- assert topics.include?(topics(:first))
-
- assert_equal [], Topic.find_all_by_title("The First Topic!!")
- end
-
- def test_find_all_by_one_attribute_that_is_an_aggregate
- balance = customers(:david).balance
- assert_kind_of Money, balance
- found_customers = Customer.find_all_by_balance(balance)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_two_attributes_that_are_both_aggregates
- balance = customers(:david).balance
- address = customers(:david).address
- assert_kind_of Money, balance
- assert_kind_of Address, address
- found_customers = Customer.find_all_by_balance_and_address(balance, address)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_two_attributes_with_one_being_an_aggregate
- balance = customers(:david).balance
- assert_kind_of Money, balance
- found_customers = Customer.find_all_by_balance_and_name(balance, customers(:david).name)
- assert_equal 1, found_customers.size
- assert_equal customers(:david), found_customers.first
- end
-
- def test_find_all_by_one_attribute_with_options
- topics = Topic.find_all_by_content("Have a nice day", :order => "id DESC")
- assert_equal topics(:first), topics.last
-
- topics = Topic.find_all_by_content("Have a nice day", :order => "id")
- assert_equal topics(:first), topics.first
- end
-
- def test_find_all_by_array_attribute
- assert_equal 2, Topic.find_all_by_title(["The First Topic", "The Second Topic of the day"]).size
- end
-
- def test_find_all_by_boolean_attribute
- topics = Topic.find_all_by_approved(false)
- assert_equal 1, topics.size
- assert topics.include?(topics(:first))
-
- topics = Topic.find_all_by_approved(true)
- assert_equal 3, topics.size
- assert topics.include?(topics(:second))
- end
-
- def test_find_all_by_nil_and_not_nil_attributes
- topics = Topic.find_all_by_last_read_and_author_name nil, "Mary"
- assert_equal 1, topics.size
- assert_equal "Mary", topics[0].author_name
- end
-
- def test_find_or_create_from_one_attribute
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name("38signals")
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name("38signals")
- assert sig38.persisted?
- end
-
- def test_find_or_create_from_two_attributes
- number_of_topics = Topic.count
- another = Topic.find_or_create_by_title_and_author_name("Another topic","John")
- assert_equal number_of_topics + 1, Topic.count
- assert_equal another, Topic.find_or_create_by_title_and_author_name("Another topic", "John")
- assert another.persisted?
- end
-
- def test_find_or_create_from_one_attribute_bang
- number_of_companies = Company.count
- assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name!("") }
- assert_equal number_of_companies, Company.count
- sig38 = Company.find_or_create_by_name!("38signals")
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name!("38signals")
- assert sig38.persisted?
- end
-
- def test_find_or_create_from_two_attributes_bang
- number_of_companies = Company.count
- assert_raises(ActiveRecord::RecordInvalid) { Company.find_or_create_by_name_and_firm_id!("", 17) }
- assert_equal number_of_companies, Company.count
- sig38 = Company.find_or_create_by_name_and_firm_id!("38signals", 17)
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name_and_firm_id!("38signals", 17)
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- end
-
- def test_find_or_create_from_two_attributes_with_one_being_an_aggregate
- number_of_customers = Customer.count
- created_customer = Customer.find_or_create_by_balance_and_name(Money.new(123), "Elizabeth")
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123), "Elizabeth")
- assert created_customer.persisted?
- end
-
- def test_find_or_create_from_one_attribute_and_hash
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- end
-
- def test_find_or_create_from_two_attributes_and_hash
- number_of_companies = Company.count
- sig38 = Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal number_of_companies + 1, Company.count
- assert_equal sig38, Company.find_or_create_by_name_and_firm_id({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert sig38.persisted?
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- end
-
- def test_find_or_create_from_one_aggregate_attribute
- number_of_customers = Customer.count
- created_customer = Customer.find_or_create_by_balance(Money.new(123))
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance(Money.new(123))
- assert created_customer.persisted?
- end
-
- def test_find_or_create_from_one_aggregate_attribute_and_hash
- number_of_customers = Customer.count
- balance = Money.new(123)
- name = "Elizabeth"
- created_customer = Customer.find_or_create_by_balance({:balance => balance, :name => name})
- assert_equal number_of_customers + 1, Customer.count
- assert_equal created_customer, Customer.find_or_create_by_balance({:balance => balance, :name => name})
- assert created_customer.persisted?
- assert_equal balance, created_customer.balance
- assert_equal name, created_customer.name
- end
-
- def test_find_or_initialize_from_one_attribute
- sig38 = Company.find_or_initialize_by_name("38signals")
- assert_equal "38signals", sig38.name
- assert !sig38.persisted?
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute
- new_customer = Customer.find_or_initialize_by_balance(Money.new(123))
- assert_equal 123, new_customer.balance.amount
- assert !new_customer.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_should_set_attribute
- c = Company.find_or_initialize_by_name_and_rating("Fortune 1000", 1000)
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_from_one_attribute_should_set_attribute
- c = Company.find_or_create_by_name_and_rating("Fortune 1000", 1000)
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_should_set_attribute_even_when_set_the_hash
- c = Company.find_or_initialize_by_rating(1000, {:name => "Fortune 1000"})
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_from_one_attribute_should_set_attribute_even_when_set_the_hash
- c = Company.find_or_create_by_rating(1000, {:name => "Fortune 1000"})
- assert_equal "Fortune 1000", c.name
- assert_equal 1000, c.rating
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_should_set_attributes_if_given_as_block
- c = Company.find_or_initialize_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert !c.persisted?
- end
-
- def test_find_or_create_should_set_attributes_if_given_as_block
- c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_create_should_work_with_block_on_first_call
- class << Company
- undef_method(:find_or_create_by_name) if method_defined?(:find_or_create_by_name)
- end
- c = Company.find_or_create_by_name(:name => "Fortune 1000") { |f| f.rating = 1000 }
- assert_equal "Fortune 1000", c.name
- assert_equal 1000.to_f, c.rating.to_f
- assert c.valid?
- assert c.persisted?
- end
-
- def test_find_or_initialize_from_two_attributes
- another = Topic.find_or_initialize_by_title_and_author_name("Another topic","John")
- assert_equal "Another topic", another.title
- assert_equal "John", another.author_name
- assert !another.persisted?
- end
-
- def test_find_or_initialize_from_two_attributes_but_passing_only_one
- assert_raise(ArgumentError) { Topic.find_or_initialize_by_title_and_author_name("Another topic") }
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute_and_one_not
- new_customer = Customer.find_or_initialize_by_balance_and_name(Money.new(123), "Elizabeth")
- assert_equal 123, new_customer.balance.amount
- assert_equal "Elizabeth", new_customer.name
- assert !new_customer.persisted?
- end
-
- def test_find_or_initialize_from_one_attribute_and_hash
- sig38 = Company.find_or_initialize_by_name({:name => "38signals", :firm_id => 17, :client_of => 23})
- assert_equal "38signals", sig38.name
- assert_equal 17, sig38.firm_id
- assert_equal 23, sig38.client_of
- assert !sig38.persisted?
- end
-
- def test_find_or_initialize_from_one_aggregate_attribute_and_hash
- balance = Money.new(123)
- name = "Elizabeth"
- new_customer = Customer.find_or_initialize_by_balance({:balance => balance, :name => name})
- assert_equal balance, new_customer.balance
- assert_equal name, new_customer.name
- assert !new_customer.persisted?
- end
-
- def test_find_last_by_one_attribute
- assert_equal Topic.last, Topic.find_last_by_title(Topic.last.title)
- assert_nil Topic.find_last_by_title("A title with no matches")
- end
-
- def test_find_last_by_invalid_method_syntax
- assert_raise(NoMethodError) { Topic.fail_to_find_last_by_title("The First Topic") }
- assert_raise(NoMethodError) { Topic.find_last_by_title?("The First Topic") }
- end
-
- def test_find_last_by_one_attribute_with_several_options
- assert_equal accounts(:signals37), Account.order('id DESC').where('id != ?', 3).find_last_by_credit_limit(50)
- end
-
- def test_find_last_by_one_missing_attribute
- assert_raise(NoMethodError) { Topic.find_last_by_undertitle("The Last Topic!") }
- end
-
- def test_find_last_by_two_attributes
- topic = Topic.last
- assert_equal topic, Topic.find_last_by_title_and_author_name(topic.title, topic.author_name)
- assert_nil Topic.find_last_by_title_and_author_name(topic.title, "Anonymous")
- end
-
- def test_find_last_with_limit_gives_same_result_when_loaded_and_unloaded
- scope = Topic.limit(2)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_last_with_limit_and_offset_gives_same_result_when_loaded_and_unloaded
- scope = Topic.offset(2).limit(2)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_last_with_offset_gives_same_result_when_loaded_and_unloaded
- scope = Topic.offset(3)
- unloaded_last = scope.last
- loaded_last = scope.to_a.last
- assert_equal loaded_last, unloaded_last
- end
-
- def test_find_all_by_nil_attribute
- topics = Topic.find_all_by_last_read nil
- assert_equal 3, topics.size
- assert topics.collect(&:last_read).all?(&:nil?)
- end
-
- def test_forwarding_to_dynamic_finders
- welcome = Post.find(1)
- assert_equal 4, Category.find_all_by_type('SpecialCategory').size
- assert_equal 0, welcome.categories.find_all_by_type('SpecialCategory').size
- assert_equal 2, welcome.categories.find_all_by_type('Category').size
- end
-
- def test_dynamic_find_all_should_respect_association_order
- assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.where("type = 'Client'").to_a
- assert_equal [companies(:second_client), companies(:first_client)], companies(:first_firm).clients_sorted_desc.find_all_by_type('Client')
- end
-
- def test_dynamic_find_all_should_respect_association_limit
- assert_equal 1, companies(:first_firm).limited_clients.where("type = 'Client'").to_a.length
- assert_equal 1, companies(:first_firm).limited_clients.find_all_by_type('Client').length
- end
-
- def test_dynamic_find_all_limit_should_override_association_limit
- assert_equal 2, companies(:first_firm).limited_clients.where("type = 'Client'").limit(9_000).to_a.length
- assert_equal 2, companies(:first_firm).limited_clients.find_all_by_type('Client', :limit => 9_000).length
- end
-
- def test_dynamic_find_last_without_specified_order
- assert_equal companies(:second_client), companies(:first_firm).unsorted_clients.find_last_by_type('Client')
- end
-
- def test_dynamic_find_or_create_from_two_attributes_using_an_association
- author = authors(:david)
- number_of_posts = Post.count
- another = author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body")
- assert_equal number_of_posts + 1, Post.count
- assert_equal another, author.posts.find_or_create_by_title_and_body("Another Post", "This is the Body")
- assert another.persisted?
- end
-
- def test_dynamic_find_all_should_respect_association_order_for_through
- assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.where("comments.type = 'SpecialComment'").to_a
- assert_equal [Comment.find(10), Comment.find(7), Comment.find(6), Comment.find(3)], authors(:david).comments_desc.find_all_by_type('SpecialComment')
- end
-
- def test_dynamic_find_all_should_respect_association_limit_for_through
- assert_equal 1, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").to_a.length
- assert_equal 1, authors(:david).limited_comments.find_all_by_type('SpecialComment').length
- end
-
- def test_dynamic_find_all_order_should_override_association_limit_for_through
- assert_equal 4, authors(:david).limited_comments.where("comments.type = 'SpecialComment'").limit(9_000).to_a.length
- assert_equal 4, authors(:david).limited_comments.find_all_by_type('SpecialComment', :limit => 9_000).length
- end
-
- def test_find_all_include_over_the_same_table_for_through
- assert_equal 2, people(:michael).posts.includes(:people).to_a.length
- end
-
- def test_find_or_create_by_resets_cached_counters
- person = Person.create! :first_name => 'tenderlove'
- post = Post.first
-
- assert_equal [], person.readers
- assert_nil person.readers.find_by_post_id(post.id)
-
- person.readers.find_or_create_by_post_id(post.id)
-
- assert_equal 1, person.readers.count
- assert_equal 1, person.readers.length
- assert_equal post, person.readers.first.post
- assert_equal person, person.readers.first.person
- end
-
- def test_find_or_initialize
- the_client = companies(:first_firm).clients.find_or_initialize_by_name("Yet another client")
- assert_equal companies(:first_firm).id, the_client.firm_id
- assert_equal "Yet another client", the_client.name
- assert !the_client.persisted?
- end
-
- def test_find_or_create_updates_size
- number_of_clients = companies(:first_firm).clients.size
- the_client = companies(:first_firm).clients.find_or_create_by_name("Yet another client")
- assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
- assert_equal the_client, companies(:first_firm).clients.find_or_create_by_name("Yet another client")
- assert_equal number_of_clients + 1, companies(:first_firm, :reload).clients.size
- end
-
- def test_find_or_initialize_updates_collection_size
- number_of_clients = companies(:first_firm).clients_of_firm.size
- companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client")
- assert_equal number_of_clients + 1, companies(:first_firm).clients_of_firm.size
- end
-
- def test_find_or_initialize_returns_the_instantiated_object
- client = companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client")
- assert_equal client, companies(:first_firm).clients_of_firm[-1]
- end
-
- def test_find_or_initialize_only_instantiates_a_single_object
- number_of_clients = Client.count
- companies(:first_firm).clients_of_firm.find_or_initialize_by_name("name" => "Another Client").save!
- companies(:first_firm).save!
- assert_equal number_of_clients+1, Client.count
- end
-
- def test_find_or_create_with_hash
- post = authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody')
- assert_equal post, authors(:david).posts.find_or_create_by_title(:title => 'Yet another post', :body => 'somebody')
- assert post.persisted?
- end
-
- def test_find_or_create_with_one_attribute_followed_by_hash
- post = authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody')
- assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post', :body => 'somebody')
- assert post.persisted?
- end
-
- def test_find_or_create_should_work_with_block
- post = authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'}
- assert_equal post, authors(:david).posts.find_or_create_by_title('Yet another post') {|p| p.body = 'somebody'}
- assert post.persisted?
- end
-
- def test_forwarding_to_dynamic_finders_2
- welcome = Post.find(1)
- assert_equal 4, Comment.find_all_by_type('Comment').size
- assert_equal 2, welcome.comments.find_all_by_type('Comment').size
- end
-
- def test_dynamic_find_all_by_attributes
- authors = Author.all
-
- davids = authors.find_all_by_name('David')
- assert_kind_of Array, davids
- assert_equal [authors(:david)], davids
- end
-
- def test_dynamic_find_or_initialize_by_attributes
- authors = Author.all
-
- lifo = authors.find_or_initialize_by_name('Lifo')
- assert_equal "Lifo", lifo.name
- assert !lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_initialize_by_name(:name => 'David')
- end
-
- def test_dynamic_find_or_create_by_attributes
- authors = Author.all
-
- lifo = authors.find_or_create_by_name('Lifo')
- assert_equal "Lifo", lifo.name
- assert lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_create_by_name(:name => 'David')
- end
-
- def test_dynamic_find_or_create_by_attributes_bang
- authors = Author.all
-
- assert_raises(ActiveRecord::RecordInvalid) { authors.find_or_create_by_name!('') }
-
- lifo = authors.find_or_create_by_name!('Lifo')
- assert_equal "Lifo", lifo.name
- assert lifo.persisted?
-
- assert_equal authors(:david), authors.find_or_create_by_name!(:name => 'David')
- end
-
- def test_finder_block
- t = Topic.first
- found = nil
- Topic.find_by_id(t.id) { |f| found = f }
- assert_equal t, found
- end
-
- def test_finder_block_nothing_found
- bad_id = Topic.maximum(:id) + 1
- assert_nil Topic.find_by_id(bad_id) { |f| raise }
- end
-
- def test_find_returns_block_value
- t = Topic.first
- x = Topic.find_by_id(t.id) { |f| "hi mom!" }
- assert_equal "hi mom!", x
- end
-
- def test_dynamic_finder_with_invalid_params
- assert_raise(ArgumentError) { Topic.find_by_title 'No Title', :join => "It should be `joins'" }
- end
-
- def test_find_by_one_attribute_with_order_option
- assert_equal accounts(:signals37), Account.find_by_credit_limit(50, :order => 'id')
- assert_equal accounts(:rails_core_account), Account.find_by_credit_limit(50, :order => 'id DESC')
- end
-
- def test_dynamic_find_by_attributes_should_yield_found_object
- david = authors(:david)
- yielded_value = nil
- Author.find_by_name(david.name) do |author|
- yielded_value = author
- end
- assert_equal david, yielded_value
- end
-end
-
-class DynamicScopeTest < ActiveRecord::TestCase
- fixtures :posts
-
- def setup
- @test_klass = Class.new(Post) do
- def self.name; "Post"; end
- end
- @deprecation_behavior = ActiveSupport::Deprecation.behavior
- ActiveSupport::Deprecation.behavior = :silence
- end
-
- def teardown
- ActiveSupport::Deprecation.behavior = @deprecation_behavior
- end
-
- def test_dynamic_scope
- assert_equal @test_klass.scoped_by_author_id(1).find(1), @test_klass.find(1)
- assert_equal @test_klass.scoped_by_author_id_and_title(1, "Welcome to the weblog").first, @test_klass.all.merge!(:where => { :author_id => 1, :title => "Welcome to the weblog"}).first
- end
-
- def test_dynamic_scope_should_create_methods_after_hitting_method_missing
- assert_blank @test_klass.methods.grep(/scoped_by_type/)
- @test_klass.scoped_by_type(nil)
- assert_present @test_klass.methods.grep(/scoped_by_type/)
- end
-
- def test_dynamic_scope_with_less_number_of_arguments
- assert_raise(ArgumentError){ @test_klass.scoped_by_author_id_and_title(1) }
- end
-end
-
-class DynamicScopeMatchTest < ActiveRecord::TestCase
- def test_scoped_by_no_match
- assert_nil ActiveRecord::DynamicMatchers::Method.match(nil, "not_scoped_at_all")
- end
-
- def test_scoped_by
- model = stub(attribute_aliases: {})
- match = ActiveRecord::DynamicMatchers::Method.match(model, "scoped_by_age_and_sex_and_location")
- assert_not_nil match
- assert_equal %w(age sex location), match.attribute_names
- end
-end
diff --git a/activerecord/test/cases/dirty_test.rb b/activerecord/test/cases/dirty_test.rb
index 7334514f9a..9d7f57bf85 100644
--- a/activerecord/test/cases/dirty_test.rb
+++ b/activerecord/test/cases/dirty_test.rb
@@ -12,7 +12,7 @@ class Pirate # Just reopening it, not defining it
after_update :check_changes
private
- # after_save/update in sweepers, observers, and the model itself
+ # after_save/update and the model itself
# can end up checking dirty status and acting on the results
def check_changes
if self.changed?
@@ -27,6 +27,8 @@ class NumericData < ActiveRecord::Base
end
class DirtyTest < ActiveRecord::TestCase
+ include InTimeZone
+
# Dummy to force column loads so query counts are clean.
def setup
Person.create :first_name => 'foo'
@@ -77,6 +79,8 @@ class DirtyTest < ActiveRecord::TestCase
assert pirate.created_on_changed?
assert_kind_of ActiveSupport::TimeWithZone, pirate.created_on_was
assert_equal old_created_on, pirate.created_on_was
+ pirate.created_on = old_created_on
+ assert !pirate.created_on_changed?
end
end
@@ -121,31 +125,30 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_time_attributes_changes_without_time_zone
+ with_timezone_config aware_attributes: false do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'pirates'
- target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
-
- target.time_zone_aware_attributes = false
-
- # New record - no changes.
- pirate = target.new
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # New record - no changes.
+ pirate = target.new
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Saved - no changes.
- pirate.catchphrase = 'arrrr, time zone!!'
- pirate.save!
- assert !pirate.created_on_changed?
- assert_nil pirate.created_on_change
+ # Saved - no changes.
+ pirate.catchphrase = 'arrrr, time zone!!'
+ pirate.save!
+ assert !pirate.created_on_changed?
+ assert_nil pirate.created_on_change
- # Change created_on.
- old_created_on = pirate.created_on
- pirate.created_on = Time.now + 1.day
- assert pirate.created_on_changed?
- # kind_of does not work because
- # ActiveSupport::TimeWithZone.name == 'Time'
- assert_instance_of Time, pirate.created_on_was
- assert_equal old_created_on, pirate.created_on_was
+ # Change created_on.
+ old_created_on = pirate.created_on
+ pirate.created_on = Time.now + 1.day
+ assert pirate.created_on_changed?
+ # kind_of does not work because
+ # ActiveSupport::TimeWithZone.name == 'Time'
+ assert_instance_of Time, pirate.created_on_was
+ assert_equal old_created_on, pirate.created_on_was
+ end
end
@@ -202,6 +205,22 @@ class DirtyTest < ActiveRecord::TestCase
end
end
+ def test_nullable_datetime_not_marked_as_changed_if_new_value_is_blank
+ in_time_zone 'Edinburgh' do
+ target = Class.new(ActiveRecord::Base)
+ target.table_name = 'topics'
+
+ topic = target.create
+ assert_nil topic.written_on
+
+ ["", nil].each do |value|
+ topic.written_on = value
+ assert_nil topic.written_on
+ assert !topic.written_on_changed?
+ end
+ end
+ end
+
def test_integer_zero_to_string_zero_not_marked_as_changed
pirate = Pirate.new
pirate.parrot_id = 0
@@ -226,6 +245,21 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.changed?
end
+ def test_float_zero_to_string_zero_not_marked_as_changed
+ data = NumericData.new :temperature => 0.0
+ data.save!
+
+ assert_not data.changed?
+
+ data.temperature = '0'
+ assert_empty data.changes
+
+ data.temperature = '0.0'
+ assert_empty data.changes
+
+ data.temperature = '0.00'
+ assert_empty data.changes
+ end
def test_zero_to_blank_marked_as_changed
pirate = Pirate.new
@@ -311,12 +345,12 @@ class DirtyTest < ActiveRecord::TestCase
pirate = Pirate.new(:catchphrase => 'foo')
old_updated_on = 1.hour.ago.beginning_of_day
- with_partial_updates Pirate, false do
+ with_partial_writes Pirate, false do
assert_queries(2) { 2.times { pirate.save! } }
Pirate.where(id: pirate.id).update_all(:updated_on => old_updated_on)
end
- with_partial_updates Pirate, true do
+ with_partial_writes Pirate, true do
assert_queries(0) { 2.times { pirate.save! } }
assert_equal old_updated_on, pirate.reload.updated_on
@@ -329,12 +363,12 @@ class DirtyTest < ActiveRecord::TestCase
person = Person.new(:first_name => 'foo')
old_lock_version = 1
- with_partial_updates Person, false do
+ with_partial_writes Person, false do
assert_queries(2) { 2.times { person.save! } }
Person.where(id: person.id).update_all(:first_name => 'baz')
end
- with_partial_updates Person, true do
+ with_partial_writes Person, true do
assert_queries(0) { 2.times { person.save! } }
assert_equal old_lock_version, person.reload.lock_version
@@ -408,8 +442,8 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.catchphrase_changed?
end
- def test_save_should_store_serialized_attributes_even_with_partial_updates
- with_partial_updates(Topic) do
+ def test_save_should_store_serialized_attributes_even_with_partial_writes
+ with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
topic.content[:b] = "b"
#assert topic.changed? # Known bug, will fail
@@ -421,7 +455,7 @@ class DirtyTest < ActiveRecord::TestCase
end
def test_save_always_should_update_timestamps_when_serialized_attributes_are_present
- with_partial_updates(Topic) do
+ with_partial_writes(Topic) do
topic = Topic.create!(:content => {:a => "a"})
topic.save!
@@ -434,8 +468,8 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- def test_save_should_not_save_serialized_attribute_with_partial_updates_if_not_present
- with_partial_updates(Topic) do
+ def test_save_should_not_save_serialized_attribute_with_partial_writes_if_not_present
+ with_partial_writes(Topic) do
Topic.create!(:author_name => 'Bill', :content => {:a => "a"})
topic = Topic.select('id, author_name').first
topic.update_columns author_name: 'John'
@@ -502,7 +536,7 @@ class DirtyTest < ActiveRecord::TestCase
assert !pirate.previous_changes.key?('created_on')
pirate = Pirate.find_by_catchphrase("Thar She Blows!")
- pirate.update_attributes(:catchphrase => "Ahoy!")
+ pirate.update(catchphrase: "Ahoy!")
assert_equal 2, pirate.previous_changes.size
assert_equal ["Thar She Blows!", "Ahoy!"], pirate.previous_changes['catchphrase']
@@ -536,23 +570,22 @@ class DirtyTest < ActiveRecord::TestCase
end
end
- def test_setting_time_attributes_with_time_zone_field_to_same_time_should_not_be_marked_as_a_change
+ def test_datetime_attribute_can_be_updated_with_fractional_seconds
in_time_zone 'Paris' do
target = Class.new(ActiveRecord::Base)
- target.table_name = 'pirates'
+ target.table_name = 'topics'
- created_on = Time.now
+ written_on = Time.utc(2012, 12, 1, 12, 0, 0).in_time_zone('Paris')
- pirate = target.create(:created_on => created_on)
- pirate.reload # Here mysql truncate the usec value to 0
+ topic = target.create(:written_on => written_on)
+ topic.written_on += 0.3
- pirate.created_on = created_on
- assert !pirate.created_on_changed?
+ assert topic.written_on_changed?, 'Fractional second update not detected'
end
end
test "partial insert" do
- with_partial_updates Person do
+ with_partial_writes Person do
jon = nil
assert_sql(/first_name/i) do
jon = Person.create! first_name: 'Jon'
@@ -568,7 +601,7 @@ class DirtyTest < ActiveRecord::TestCase
end
test "partial insert with empty values" do
- with_partial_updates Aircraft do
+ with_partial_writes Aircraft do
a = Aircraft.create!
a.reload
assert_not_nil a.id
@@ -576,12 +609,12 @@ class DirtyTest < ActiveRecord::TestCase
end
private
- def with_partial_updates(klass, on = true)
- old = klass.partial_updates?
- klass.partial_updates = on
+ def with_partial_writes(klass, on = true)
+ old = klass.partial_writes?
+ klass.partial_writes = on
yield
ensure
- klass.partial_updates = old
+ klass.partial_writes = old
end
def check_pirate_after_save_failure(pirate)
@@ -590,16 +623,4 @@ class DirtyTest < ActiveRecord::TestCase
assert_equal %w(parrot_id), pirate.changed
assert_nil pirate.parrot_id_was
end
-
- def in_time_zone(zone)
- old_zone = Time.zone
- old_tz = ActiveRecord::Base.time_zone_aware_attributes
-
- Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
- ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
- yield
- ensure
- Time.zone = old_zone
- ActiveRecord::Base.time_zone_aware_attributes = old_tz
- end
end
diff --git a/activerecord/test/cases/disconnected_test.rb b/activerecord/test/cases/disconnected_test.rb
new file mode 100644
index 0000000000..1fecfd077e
--- /dev/null
+++ b/activerecord/test/cases/disconnected_test.rb
@@ -0,0 +1,27 @@
+require "cases/helper"
+
+class TestRecord < ActiveRecord::Base
+end
+
+class TestDisconnectedAdapter < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ def setup
+ skip "in-memory database mustn't disconnect" if in_memory_db?
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def teardown
+ return if in_memory_db?
+ spec = ActiveRecord::Base.connection_config
+ ActiveRecord::Base.establish_connection(spec)
+ end
+
+ test "can't execute statements while disconnected" do
+ @connection.execute "SELECT count(*) from products"
+ @connection.disconnect!
+ assert_raises(ActiveRecord::StatementInvalid) do
+ @connection.execute "SELECT count(*) from products"
+ end
+ end
+end
diff --git a/activerecord/test/cases/dup_test.rb b/activerecord/test/cases/dup_test.rb
index 71b2b16608..1e6ccecfab 100644
--- a/activerecord/test/cases/dup_test.rb
+++ b/activerecord/test/cases/dup_test.rb
@@ -107,5 +107,30 @@ module ActiveRecord
assert Topic.after_initialize_called
end
+ def test_dup_validity_is_independent
+ repair_validations(Topic) do
+ Topic.validates_presence_of :title
+ topic = Topic.new("title" => "Literature")
+ topic.valid?
+
+ duped = topic.dup
+ duped.title = nil
+ assert duped.invalid?
+
+ topic.title = nil
+ duped.title = 'Mathematics'
+ assert topic.invalid?
+ assert duped.valid?
+ end
+ end
+
+ def test_dup_with_default_scope
+ prev_default_scopes = Topic.default_scopes
+ Topic.default_scopes = [proc { Topic.where(:approved => true) }]
+ topic = Topic.new(:approved => false)
+ assert !topic.dup.approved?, "should not be overridden by default scopes"
+ ensure
+ Topic.default_scopes = prev_default_scopes
+ end
end
end
diff --git a/activerecord/test/cases/explain_subscriber_test.rb b/activerecord/test/cases/explain_subscriber_test.rb
index b425967678..b00e2744b9 100644
--- a/activerecord/test/cases/explain_subscriber_test.rb
+++ b/activerecord/test/cases/explain_subscriber_test.rb
@@ -1,55 +1,59 @@
require 'cases/helper'
+require 'active_record/explain_subscriber'
+require 'active_record/explain_registry'
if ActiveRecord::Base.connection.supports_explain?
class ExplainSubscriberTest < ActiveRecord::TestCase
SUBSCRIBER = ActiveRecord::ExplainSubscriber.new
- def test_collects_nothing_if_available_queries_for_explain_is_nil
- with_queries(nil) do
- SUBSCRIBER.finish(nil, nil, {})
- assert_nil Thread.current[:available_queries_for_explain]
- end
+ def setup
+ ActiveRecord::ExplainRegistry.reset
+ ActiveRecord::ExplainRegistry.collect = true
end
def test_collects_nothing_if_the_payload_has_an_exception
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :exception => Exception.new)
- assert queries.empty?
- end
+ SUBSCRIBER.finish(nil, nil, exception: Exception.new)
+ assert queries.empty?
end
def test_collects_nothing_for_ignored_payloads
- with_queries([]) do |queries|
- ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
- SUBSCRIBER.finish(nil, nil, :name => ip)
- end
- assert queries.empty?
+ ActiveRecord::ExplainSubscriber::IGNORED_PAYLOADS.each do |ip|
+ SUBSCRIBER.finish(nil, nil, name: ip)
end
+ assert queries.empty?
+ end
+
+ def test_collects_nothing_if_collect_is_false
+ ActiveRecord::ExplainRegistry.collect = false
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select 1 from users', binds: [1, 2])
+ assert queries.empty?
end
def test_collects_pairs_of_queries_and_binds
sql = 'select 1 from users'
binds = [1, 2]
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => sql, :binds => binds)
- assert_equal 1, queries.size
- assert_equal sql, queries[0][0]
- assert_equal binds, queries[0][1]
- end
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: sql, binds: binds)
+ assert_equal 1, queries.size
+ assert_equal sql, queries[0][0]
+ assert_equal binds, queries[0][1]
end
- def test_collects_nothing_if_unexplained_sqls
- with_queries([]) do |queries|
- SUBSCRIBER.finish(nil, nil, :name => 'SQL', :sql => 'SHOW max_identifier_length')
- assert queries.empty?
- end
+ def test_collects_nothing_if_the_statement_is_not_whitelisted
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'SHOW max_identifier_length')
+ assert queries.empty?
+ end
+
+ def test_collects_nothing_if_the_statement_is_only_partially_matched
+ SUBSCRIBER.finish(nil, nil, name: 'SQL', sql: 'select_db yo_mama')
+ assert queries.empty?
+ end
+
+ def teardown
+ ActiveRecord::ExplainRegistry.reset
end
- def with_queries(queries)
- Thread.current[:available_queries_for_explain] = queries
- yield queries
- ensure
- Thread.current[:available_queries_for_explain] = nil
+ def queries
+ ActiveRecord::ExplainRegistry.queries
end
end
end
diff --git a/activerecord/test/cases/explain_test.rb b/activerecord/test/cases/explain_test.rb
index 6dce8ccdd1..6dac5db111 100644
--- a/activerecord/test/cases/explain_test.rb
+++ b/activerecord/test/cases/explain_test.rb
@@ -14,50 +14,13 @@ if ActiveRecord::Base.connection.supports_explain?
base.connection
end
- def test_logging_query_plan_with_logger
- base.logger.expects(:warn).with do |message|
- message.starts_with?('EXPLAIN for:')
- end
-
- with_threshold(0) do
- Car.where(:name => 'honda').to_a
- end
- end
-
- def test_logging_query_plan_without_logger
- original = base.logger
- base.logger = nil
-
- class << base.logger
- def warn; raise "Should not be called" end
- end
-
- with_threshold(0) do
- car = Car.where(:name => 'honda').first
- assert_equal 'honda', car.name
- end
- ensure
- base.logger = original
- end
-
- def test_collect_queries_for_explain
- base.auto_explain_threshold_in_seconds = nil
- queries = Thread.current[:available_queries_for_explain] = []
-
- with_threshold(0) do
- Car.where(:name => 'honda').to_a
- end
-
- sql, binds = queries[0]
- assert_match "SELECT", sql
- assert_match "honda", sql
- assert_equal [], binds
- ensure
- Thread.current[:available_queries_for_explain] = nil
+ def test_relation_explain
+ message = Car.where(:name => 'honda').explain
+ assert_match(/^EXPLAIN for:/, message)
end
def test_collecting_queries_for_explain
- result, queries = ActiveRecord::Base.collecting_queries_for_explain do
+ queries = ActiveRecord::Base.collecting_queries_for_explain do
Car.where(:name => 'honda').to_a
end
@@ -65,17 +28,6 @@ if ActiveRecord::Base.connection.supports_explain?
assert_match "SELECT", sql
assert_match "honda", sql
assert_equal [], binds
- assert_equal [cars(:honda)], result
- end
-
- def test_logging_query_plan_when_counting_by_sql
- base.logger.expects(:warn).with do |message|
- message.starts_with?('EXPLAIN for:')
- end
-
- with_threshold(0) do
- Car.count_by_sql "SELECT COUNT(*) FROM cars WHERE name = 'honda'"
- end
end
def test_exec_explain_with_no_binds
@@ -108,20 +60,13 @@ if ActiveRecord::Base.connection.supports_explain?
assert_equal expected, base.exec_explain(queries)
end
- def test_silence_auto_explain
- base.expects(:collecting_sqls_for_explain).never
+ def test_unsupported_connection_adapter
+ connection.stubs(:supports_explain?).returns(false)
+
base.logger.expects(:warn).never
- base.silence_auto_explain do
- with_threshold(0) { Car.all }
- end
- end
- def with_threshold(threshold)
- current_threshold = base.auto_explain_threshold_in_seconds
- base.auto_explain_threshold_in_seconds = threshold
- yield
- ensure
- base.auto_explain_threshold_in_seconds = current_threshold
+ Car.where(:name => 'honda').to_a
end
+
end
end
diff --git a/activerecord/test/cases/finder_respond_to_test.rb b/activerecord/test/cases/finder_respond_to_test.rb
index 9440cd429a..3ff22f222f 100644
--- a/activerecord/test/cases/finder_respond_to_test.rb
+++ b/activerecord/test/cases/finder_respond_to_test.rb
@@ -21,14 +21,9 @@ class FinderRespondToTest < ActiveRecord::TestCase
assert_respond_to Topic, :find_by_title
end
- def test_should_respond_to_find_all_by_one_attribute
- ensure_topic_method_is_not_cached(:find_all_by_title)
- assert_respond_to Topic, :find_all_by_title
- end
-
- def test_should_respond_to_find_all_by_two_attributes
- ensure_topic_method_is_not_cached(:find_all_by_title_and_author_name)
- assert_respond_to Topic, :find_all_by_title_and_author_name
+ def test_should_respond_to_find_by_with_bang
+ ensure_topic_method_is_not_cached(:find_by_title!)
+ assert_respond_to Topic, :find_by_title!
end
def test_should_respond_to_find_by_two_attributes
@@ -41,36 +36,6 @@ class FinderRespondToTest < ActiveRecord::TestCase
assert_respond_to Topic, :find_by_heading
end
- def test_should_respond_to_find_or_initialize_from_one_attribute
- ensure_topic_method_is_not_cached(:find_or_initialize_by_title)
- assert_respond_to Topic, :find_or_initialize_by_title
- end
-
- def test_should_respond_to_find_or_initialize_from_two_attributes
- ensure_topic_method_is_not_cached(:find_or_initialize_by_title_and_author_name)
- assert_respond_to Topic, :find_or_initialize_by_title_and_author_name
- end
-
- def test_should_respond_to_find_or_create_from_one_attribute
- ensure_topic_method_is_not_cached(:find_or_create_by_title)
- assert_respond_to Topic, :find_or_create_by_title
- end
-
- def test_should_respond_to_find_or_create_from_two_attributes
- ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name)
- assert_respond_to Topic, :find_or_create_by_title_and_author_name
- end
-
- def test_should_respond_to_find_or_create_from_one_attribute_bang
- ensure_topic_method_is_not_cached(:find_or_create_by_title!)
- assert_respond_to Topic, :find_or_create_by_title!
- end
-
- def test_should_respond_to_find_or_create_from_two_attributes_bang
- ensure_topic_method_is_not_cached(:find_or_create_by_title_and_author_name!)
- assert_respond_to Topic, :find_or_create_by_title_and_author_name!
- end
-
def test_should_not_respond_to_find_by_one_missing_attribute
assert !Topic.respond_to?(:find_by_undertitle)
end
diff --git a/activerecord/test/cases/finder_test.rb b/activerecord/test/cases/finder_test.rb
index d44ac21b05..4188b32731 100644
--- a/activerecord/test/cases/finder_test.rb
+++ b/activerecord/test/cases/finder_test.rb
@@ -11,14 +11,34 @@ require 'models/project'
require 'models/developer'
require 'models/customer'
require 'models/toy'
+require 'models/matey'
class FinderTest < ActiveRecord::TestCase
fixtures :companies, :topics, :entrants, :developers, :developers_projects, :posts, :comments, :accounts, :authors, :customers, :categories, :categorizations
+ def test_find_by_id_with_hash
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Post.find_by_id(:limit => 1)
+ end
+ end
+
+ def test_find_by_title_and_id_with_hash
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Post.find_by_title_and_id('foo', :limit => 1)
+ end
+ end
+
def test_find
assert_equal(topics(:first).title, Topic.find(1).title)
end
+ def test_symbols_table_ref
+ Post.first # warm up
+ x = Symbol.all_symbols.count
+ Post.where("title" => {"xxxqqqq" => "bar"})
+ assert_equal x, Symbol.all_symbols.count
+ end
+
# find should handle strings that come from URLs
# (example: Category.find(params[:id]))
def test_find_with_string
@@ -26,16 +46,19 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists
- assert Topic.exists?(1)
- assert Topic.exists?("1")
- assert Topic.exists?(:author_name => "David")
- assert Topic.exists?(:author_name => "Mary", :approved => true)
- assert Topic.exists?(["parent_id = ?", 1])
- assert !Topic.exists?(45)
- assert !Topic.exists?(Topic.new)
+ assert_equal true, Topic.exists?(1)
+ assert_equal true, Topic.exists?("1")
+ assert_equal true, Topic.exists?(title: "The First Topic")
+ assert_equal true, Topic.exists?(heading: "The First Topic")
+ assert_equal true, Topic.exists?(:author_name => "Mary", :approved => true)
+ assert_equal true, Topic.exists?(["parent_id = ?", 1])
+ assert_equal true, Topic.exists?(id: [1, 9999])
+
+ assert_equal false, Topic.exists?(45)
+ assert_equal false, Topic.exists?(Topic.new)
begin
- assert !Topic.exists?("foo")
+ assert_equal false, Topic.exists?("foo")
rescue ActiveRecord::StatementInvalid
# PostgreSQL complains about string comparison with integer field
rescue Exception
@@ -52,49 +75,62 @@ class FinderTest < ActiveRecord::TestCase
end
def test_exists_returns_true_with_one_record_and_no_args
- assert Topic.exists?
+ assert_equal true, Topic.exists?
end
def test_exists_returns_false_with_false_arg
- assert !Topic.exists?(false)
+ assert_equal false, Topic.exists?(false)
end
# exists? should handle nil for id's that come from URLs and always return false
# (example: Topic.exists?(params[:id])) where params[:id] is nil
def test_exists_with_nil_arg
- assert !Topic.exists?(nil)
- assert Topic.exists?
- assert !Topic.first.replies.exists?(nil)
- assert Topic.first.replies.exists?
+ assert_equal false, Topic.exists?(nil)
+ assert_equal true, Topic.exists?
+
+ assert_equal false, Topic.first.replies.exists?(nil)
+ assert_equal true, Topic.first.replies.exists?
end
# ensures +exists?+ runs valid SQL by excluding order value
def test_exists_with_order
- assert Topic.order(:id).uniq.exists?
+ assert_equal true, Topic.order(:id).distinct.exists?
end
def test_exists_with_includes_limit_and_empty_result
- assert !Topic.includes(:replies).limit(0).exists?
- assert !Topic.includes(:replies).limit(1).where('0 = 1').exists?
+ assert_equal false, Topic.includes(:replies).limit(0).exists?
+ assert_equal false, Topic.includes(:replies).limit(1).where('0 = 1').exists?
+ end
+
+ def test_exists_with_distinct_association_includes_and_limit
+ author = Author.first
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).limit(1).exists?
+ end
+
+ def test_exists_with_distinct_association_includes_limit_and_order
+ author = Author.first
+ assert_equal false, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(0).exists?
+ assert_equal true, author.unique_categorized_posts.includes(:special_comments).order('comments.taggings_count DESC').limit(1).exists?
end
def test_exists_with_empty_table_and_no_args_given
Topic.delete_all
- assert !Topic.exists?
+ assert_equal false, Topic.exists?
end
def test_exists_with_aggregate_having_three_mappings
existing_address = customers(:david).address
- assert Customer.exists?(:address => existing_address)
+ assert_equal true, Customer.exists?(:address => existing_address)
end
def test_exists_with_aggregate_having_three_mappings_with_one_difference
existing_address = customers(:david).address
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city, existing_address.country + "1"))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street, existing_address.city + "1", existing_address.country))
- assert !Customer.exists?(:address =>
+ assert_equal false, Customer.exists?(:address =>
Address.new(existing_address.street + "1", existing_address.city, existing_address.country))
end
@@ -443,7 +479,7 @@ class FinderTest < ActiveRecord::TestCase
def test_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getutc]).first
end
@@ -452,7 +488,7 @@ class FinderTest < ActiveRecord::TestCase
def test_hash_condition_utc_time_interpolation_with_default_timezone_local
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :local do
+ with_timezone_config default: :local do
topic = Topic.first
assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getutc}).first
end
@@ -461,7 +497,7 @@ class FinderTest < ActiveRecord::TestCase
def test_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
assert_equal topic, Topic.all.merge!(:where => ['written_on = ?', topic.written_on.getlocal]).first
end
@@ -470,7 +506,7 @@ class FinderTest < ActiveRecord::TestCase
def test_hash_condition_local_time_interpolation_with_default_timezone_utc
with_env_tz 'America/New_York' do
- with_active_record_default_timezone :utc do
+ with_timezone_config default: :utc do
topic = Topic.first
assert_equal topic, Topic.all.merge!(:where => {:written_on => topic.written_on.getlocal}).first
end
@@ -581,7 +617,7 @@ class FinderTest < ActiveRecord::TestCase
def test_named_bind_with_postgresql_type_casts
l = Proc.new { bind(":a::integer '2009-01-01'::date", :a => '10') }
assert_nothing_raised(&l)
- assert_equal "#{ActiveRecord::Base.quote_value('10')}::integer '2009-01-01'::date", l.call
+ assert_equal "#{ActiveRecord::Base.connection.quote('10')}::integer '2009-01-01'::date", l.call
end
def test_string_sanitation
@@ -610,6 +646,11 @@ class FinderTest < ActiveRecord::TestCase
assert_nil Topic.find_by_heading("The First Topic!")
end
+ def test_find_by_one_attribute_bang_with_blank_defined
+ blank_topic = BlankTopic.create(title: "The Blank One")
+ assert_equal blank_topic, BlankTopic.find_by_title!("The Blank One")
+ end
+
def test_find_by_one_attribute_with_conditions
assert_equal accounts(:rails_core_account), Account.where('firm_id = ?', 6).find_by_credit_limit(50)
end
@@ -816,6 +857,14 @@ class FinderTest < ActiveRecord::TestCase
rescue ActiveRecord::RecordNotFound => e
assert_equal 'Couldn\'t find Toy with name=Hello World!', e.message
end
+ ensure
+ Toy.reset_primary_key
+ end
+
+ def test_find_without_primary_key
+ assert_raises(ActiveRecord::UnknownPrimaryKey) do
+ Matey.find(1)
+ end
end
def test_finder_with_offset_string
@@ -837,11 +886,4 @@ class FinderTest < ActiveRecord::TestCase
ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
-
- def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
- yield
- ensure
- ActiveRecord::Base.default_timezone = old_zone
- end
end
diff --git a/activerecord/test/cases/fixtures_test.rb b/activerecord/test/cases/fixtures_test.rb
index b0b29f5f42..bffff07089 100644
--- a/activerecord/test/cases/fixtures_test.rb
+++ b/activerecord/test/cases/fixtures_test.rb
@@ -84,6 +84,12 @@ class FixturesTest < ActiveRecord::TestCase
assert fixtures.detect { |f| f.name == 'collections' }, "no fixtures named 'collections' in #{fixtures.map(&:name).inspect}"
end
+ def test_create_symbol_fixtures_is_deprecated
+ assert_deprecated do
+ ActiveRecord::FixtureSet.create_fixtures(FIXTURES_ROOT, :collections, :collections => 'Course') { Course.connection }
+ end
+ end
+
def test_attributes
topics = create_fixtures("topics").first
assert_equal("The First Topic", topics["first"]["title"])
@@ -190,11 +196,11 @@ class FixturesTest < ActiveRecord::TestCase
end
def test_empty_yaml_fixture
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", 'Account', FIXTURES_ROOT + "/naked/yml/accounts")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "accounts", Account, FIXTURES_ROOT + "/naked/yml/accounts")
end
def test_empty_yaml_fixture_with_a_comment_in_it
- assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', FIXTURES_ROOT + "/naked/yml/companies")
+ assert_not_nil ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, FIXTURES_ROOT + "/naked/yml/companies")
end
def test_nonexistent_fixture_file
@@ -204,19 +210,19 @@ class FixturesTest < ActiveRecord::TestCase
assert Dir[nonexistent_fixture_path+"*"].empty?
assert_raise(Errno::ENOENT) do
- ActiveRecord::FixtureSet.new( Account.connection, "companies", 'Company', nonexistent_fixture_path)
+ ActiveRecord::FixtureSet.new( Account.connection, "companies", Company, nonexistent_fixture_path)
end
end
def test_dirty_dirty_yaml_file
assert_raise(ActiveRecord::Fixture::FormatError) do
- ActiveRecord::FixtureSet.new( Account.connection, "courses", 'Course', FIXTURES_ROOT + "/naked/yml/courses")
+ ActiveRecord::FixtureSet.new( Account.connection, "courses", Course, FIXTURES_ROOT + "/naked/yml/courses")
end
end
def test_omap_fixtures
assert_nothing_raised do
- fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', 'Category', FIXTURES_ROOT + "/categories_ordered")
+ fixtures = ActiveRecord::FixtureSet.new(Account.connection, 'categories', Category, FIXTURES_ROOT + "/categories_ordered")
fixtures.each.with_index do |(name, fixture), i|
assert_equal "fixture_no_#{i}", name
@@ -245,6 +251,60 @@ class FixturesTest < ActiveRecord::TestCase
def test_serialized_fixtures
assert_equal ["Green", "Red", "Orange"], traffic_lights(:uk).state
end
+
+ def test_fixtures_are_set_up_with_database_env_variable
+ db_url_tmp = ENV['DATABASE_URL']
+ ENV['DATABASE_URL'] = "sqlite3:///:memory:"
+ ActiveRecord::Base.stubs(:configurations).returns({})
+ test_case = Class.new(ActiveRecord::TestCase) do
+ fixtures :accounts
+
+ def test_fixtures
+ assert accounts(:signals37)
+ end
+ end
+
+ result = test_case.new(:test_fixtures).run
+
+ assert result.passed?, "Expected #{result.name} to pass:\n#{result}"
+ ensure
+ ENV['DATABASE_URL'] = db_url_tmp
+ end
+end
+
+class HasManyThroughFixture < ActiveSupport::TestCase
+ def make_model(name)
+ Class.new(ActiveRecord::Base) { define_singleton_method(:name) { name } }
+ end
+
+ def test_has_many_through
+ pt = make_model "ParrotTreasure"
+ parrot = make_model "Parrot"
+ treasure = make_model "Treasure"
+
+ pt.table_name = "parrots_treasures"
+ pt.belongs_to :parrot, :class => parrot
+ pt.belongs_to :treasure, :class => treasure
+
+ parrot.has_many :parrot_treasures, :class => pt
+ parrot.has_many :treasures, :through => :parrot_treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ rows = fs.table_rows
+ assert_equal load_has_and_belongs_to_many['parrots_treasures'], rows['parrots_treasures']
+ end
+
+ def load_has_and_belongs_to_many
+ parrot = make_model "Parrot"
+ parrot.has_and_belongs_to_many :treasures
+
+ parrots = File.join FIXTURES_ROOT, 'parrots'
+
+ fs = ActiveRecord::FixtureSet.new parrot.connection, "parrots", parrot, parrots
+ fs.table_rows
+ end
end
if Account.connection.respond_to?(:reset_pk_sequence!)
@@ -433,7 +493,7 @@ class OverRideFixtureMethodTest < ActiveRecord::TestCase
end
class CheckSetTableNameFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -477,11 +537,6 @@ class CustomConnectionFixturesTest < ActiveRecord::TestCase
fixtures :courses
self.use_transactional_fixtures = false
- def test_connection
- assert_kind_of Course, courses(:ruby)
- assert_equal Course.connection, courses(:ruby).connection
- end
-
def test_leaky_destroy
assert_nothing_raised { courses(:ruby) }
courses(:ruby).destroy
@@ -521,7 +576,7 @@ class InvalidTableNameFixturesTest < ActiveRecord::TestCase
end
class CheckEscapedYamlFixturesTest < ActiveRecord::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class :funny_jokes => Joke
fixtures :funny_jokes
# Set to false to blow away fixtures cache and ensure our fixtures are loaded
# and thus takes into account our set_fixture_class
@@ -563,7 +618,7 @@ class FixturesBrokenRollbackTest < ActiveRecord::TestCase
end
private
- def load_fixtures
+ def load_fixtures(config)
raise 'argh'
end
end
@@ -573,7 +628,16 @@ class LoadAllFixturesTest < ActiveRecord::TestCase
fixtures :all
def test_all_there
- assert_equal %w(developers people tasks), fixture_table_names.sort
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
+ end
+end
+
+class LoadAllFixturesWithPathnameTest < ActiveRecord::TestCase
+ self.fixture_path = Pathname.new(FIXTURES_ROOT).join('all')
+ fixtures :all
+
+ def test_all_there
+ assert_equal %w(admin/accounts admin/users developers people tasks), fixture_table_names.sort
end
end
diff --git a/activerecord/test/cases/forbidden_attributes_protection_test.rb b/activerecord/test/cases/forbidden_attributes_protection_test.rb
index 9a2172f41e..981a75faf6 100644
--- a/activerecord/test/cases/forbidden_attributes_protection_test.rb
+++ b/activerecord/test/cases/forbidden_attributes_protection_test.rb
@@ -1,6 +1,7 @@
require 'cases/helper'
require 'active_support/core_ext/hash/indifferent_access'
require 'models/person'
+require 'models/company'
class ProtectedParams < ActiveSupport::HashWithIndifferentAccess
attr_accessor :permitted
@@ -40,10 +41,29 @@ class ForbiddenAttributesProtectionTest < ActiveRecord::TestCase
assert_equal 'm', person.gender
end
+ def test_forbidden_attributes_cannot_be_used_for_sti_inheritance_column
+ params = ProtectedParams.new(type: 'Client')
+ assert_raises(ActiveModel::ForbiddenAttributesError) do
+ Company.new(params)
+ end
+ end
+
+ def test_permitted_attributes_can_be_used_for_sti_inheritance_column
+ params = ProtectedParams.new(type: 'Client')
+ params.permit!
+ person = Company.new(params)
+ assert_equal person.class, Client
+ end
+
def test_regular_hash_should_still_be_used_for_mass_assignment
person = Person.new(first_name: 'Guille', gender: 'm')
assert_equal 'Guille', person.first_name
assert_equal 'm', person.gender
end
+
+ def test_blank_attributes_should_not_raise
+ person = Person.new
+ assert_nil person.assign_attributes(ProtectedParams.new({}))
+ end
end
diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb
index cff6689c15..34e8f1be0f 100644
--- a/activerecord/test/cases/helper.rb
+++ b/activerecord/test/cases/helper.rb
@@ -2,8 +2,7 @@ require File.expand_path('../../../../load_paths', __FILE__)
require 'config'
-gem 'minitest'
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'stringio'
require 'active_record'
@@ -16,14 +15,14 @@ require 'support/connection'
# TODO: Move all these random hacks into the ARTest namespace and into the support/ dir
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
# Connect to the database
ARTest.connect
-require 'support/mysql'
-
# Quote "type" if it's a reserved word for the current connection.
QUOTED_TYPE = ActiveRecord::Base.connection.quote_column_name('type')
@@ -50,11 +49,58 @@ ensure
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
-def with_active_record_default_timezone(zone)
- old_zone, ActiveRecord::Base.default_timezone = ActiveRecord::Base.default_timezone, zone
+def with_timezone_config(cfg)
+ verify_default_timezone_config
+
+ old_default_zone = ActiveRecord::Base.default_timezone
+ old_awareness = ActiveRecord::Base.time_zone_aware_attributes
+ old_zone = Time.zone
+
+ if cfg.has_key?(:default)
+ ActiveRecord::Base.default_timezone = cfg[:default]
+ end
+ if cfg.has_key?(:aware_attributes)
+ ActiveRecord::Base.time_zone_aware_attributes = cfg[:aware_attributes]
+ end
+ if cfg.has_key?(:zone)
+ Time.zone = cfg[:zone]
+ end
yield
ensure
- ActiveRecord::Base.default_timezone = old_zone
+ ActiveRecord::Base.default_timezone = old_default_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_awareness
+ Time.zone = old_zone
+end
+
+# This method makes sure that tests don't leak global state related to time zones.
+EXPECTED_ZONE = nil
+EXPECTED_DEFAULT_TIMEZONE = :utc
+EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES = false
+def verify_default_timezone_config
+ if Time.zone != EXPECTED_ZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `Time.zone` was leaked.
+ Expected: #{EXPECTED_ZONE}
+ Got: #{Time.zone}
+ MSG
+ end
+ if ActiveRecord::Base.default_timezone != EXPECTED_DEFAULT_TIMEZONE
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.default_timezone` was leaked.
+ Expected: #{EXPECTED_DEFAULT_TIMEZONE}
+ Got: #{ActiveRecord::Base.default_timezone}
+ MSG
+ end
+ if ActiveRecord::Base.time_zone_aware_attributes != EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES
+ $stderr.puts <<-MSG
+\n#{self.to_s}
+ Global state `ActiveRecord::Base.time_zone_aware_attributes` was leaked.
+ Expected: #{EXPECTED_TIME_ZONE_AWARE_ATTRIBUTES}
+ Got: #{ActiveRecord::Base.time_zone_aware_attributes}
+ MSG
+ end
end
unless ENV['FIXTURE_DEBUG']
@@ -94,7 +140,7 @@ def load_schema
load SCHEMA_ROOT + "/schema.rb"
- if File.exists?(adapter_specific_schema_file)
+ if File.exist?(adapter_specific_schema_file)
load adapter_specific_schema_file
end
ensure
@@ -120,18 +166,36 @@ class << Time
end
end
-module LogIntercepter
- attr_accessor :logged, :intercepted
- def self.extended(base)
- base.logged = []
- end
- def log(sql, name, binds = [], &block)
- if @intercepted
- @logged << [sql, name, binds]
- yield
- else
- super(sql, name,binds, &block)
- end
+class SQLSubscriber
+ attr_reader :logged
+ attr_reader :payloads
+
+ def initialize
+ @logged = []
+ @payloads = []
+ end
+
+ def start(name, id, payload)
+ @payloads << payload
+ @logged << [payload[:sql], payload[:name], payload[:binds]]
end
+
+ def finish(name, id, payload); end
end
+
+module InTimeZone
+ private
+
+ def in_time_zone(zone)
+ old_zone = Time.zone
+ old_tz = ActiveRecord::Base.time_zone_aware_attributes
+
+ Time.zone = zone ? ActiveSupport::TimeZone[zone] : nil
+ ActiveRecord::Base.time_zone_aware_attributes = !zone.nil?
+ yield
+ ensure
+ Time.zone = old_zone
+ ActiveRecord::Base.time_zone_aware_attributes = old_tz
+ end
+end
diff --git a/activerecord/test/cases/inclusion_test.rb b/activerecord/test/cases/inclusion_test.rb
deleted file mode 100644
index 8f095e4953..0000000000
--- a/activerecord/test/cases/inclusion_test.rb
+++ /dev/null
@@ -1,133 +0,0 @@
-require 'cases/helper'
-require 'models/teapot'
-
-class BasicInclusionModelTest < ActiveRecord::TestCase
- def test_basic_model
- Teapot.create!(:name => "Ronnie Kemper")
- assert_equal "Ronnie Kemper", Teapot.first.name
- end
-
- def test_initialization
- t = Teapot.new(:name => "Bob")
- assert_equal "Bob", t.name
- end
-
- def test_inherited_model
- teapot = CoolTeapot.create!(:name => "Bob")
- teapot.reload
-
- assert_equal "Bob", teapot.name
- assert_equal "mmm", teapot.aaahhh
- end
-
- def test_generated_feature_methods
- assert Teapot < Teapot::GeneratedFeatureMethods
- end
-
- def test_exists
- t = Teapot.create!(:name => "Ronnie Kemper")
- assert Teapot.exists?(t)
- end
-
- def test_predicate_builder
- t = Teapot.create!(:name => "Bob")
- assert_equal "Bob", Teapot.where(:id => [t]).first.name
- assert_equal "Bob", Teapot.where(:id => t).first.name
- end
-
- def test_nested_model
- assert_equal "ceiling_teapots", Ceiling::Teapot.table_name
- end
-end
-
-class InclusionUnitTest < ActiveRecord::TestCase
- def setup
- @klass = Class.new { include ActiveRecord::Model }
- end
-
- def test_non_abstract_class
- assert !@klass.abstract_class?
- end
-
- def test_abstract_class
- @klass.abstract_class = true
- assert @klass.abstract_class?
- end
-
- def test_establish_connection
- assert @klass.respond_to?(:establish_connection)
- assert ActiveRecord::Model.respond_to?(:establish_connection)
- end
-
- def test_adapter_connection
- name = "#{ActiveRecord::Base.connection_config[:adapter]}_connection"
- assert @klass.respond_to?(name)
- assert ActiveRecord::Model.respond_to?(name)
- end
-
- def test_connection_handler
- assert_equal ActiveRecord::Base.connection_handler, @klass.connection_handler
- end
-
- def test_mirrored_configuration
- ActiveRecord::Base.time_zone_aware_attributes = true
- assert @klass.time_zone_aware_attributes
- ActiveRecord::Base.time_zone_aware_attributes = false
- assert !@klass.time_zone_aware_attributes
- ensure
- ActiveRecord::Base.time_zone_aware_attributes = false
- end
-
- # Doesn't really test anything, but this is here to ensure warnings don't occur
- def test_included_twice
- @klass.send :include, ActiveRecord::Model
- end
-
- def test_deprecation_proxy
- proxy = ActiveRecord::Model::DeprecationProxy.new
-
- assert_equal ActiveRecord::Model.name, proxy.name
- assert_equal ActiveRecord::Base.superclass, assert_deprecated { proxy.superclass }
-
- sup, sup2 = nil, nil
- ActiveSupport.on_load(:__test_active_record_model_deprecation) do
- sup = superclass
- sup2 = send(:superclass)
- end
- assert_deprecated do
- ActiveSupport.run_load_hooks(:__test_active_record_model_deprecation, proxy)
- end
- assert_equal ActiveRecord::Base.superclass, sup
- assert_equal ActiveRecord::Base.superclass, sup2
- end
-
- test "including in deprecation proxy" do
- model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup
- proxy = ActiveRecord::Model::DeprecationProxy.new(model, base)
-
- mod = Module.new
- proxy.include mod
- assert model < mod
- end
-
- test "extending in deprecation proxy" do
- model, base = ActiveRecord::Model.dup, ActiveRecord::Base.dup
- proxy = ActiveRecord::Model::DeprecationProxy.new(model, base)
-
- mod = Module.new
- assert_deprecated { proxy.extend mod }
- assert base.singleton_class < mod
- end
-end
-
-class InclusionFixturesTest < ActiveRecord::TestCase
- fixtures :teapots
-
- def test_fixtured_record
- assert_equal "Bob", teapots(:bob).name
- end
-
- def test_timestamped_fixture
- assert_not_nil teapots(:bob).created_at
- end
-end
diff --git a/activerecord/test/cases/inheritance_test.rb b/activerecord/test/cases/inheritance_test.rb
index 8fded9159f..73cf99a5d7 100644
--- a/activerecord/test/cases/inheritance_test.rb
+++ b/activerecord/test/cases/inheritance_test.rb
@@ -4,7 +4,6 @@ require 'models/person'
require 'models/post'
require 'models/project'
require 'models/subscriber'
-require 'models/teapot'
require 'models/vegetables'
class InheritanceTest < ActiveRecord::TestCase
@@ -69,6 +68,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_company_descends_from_active_record
+ assert !ActiveRecord::Base.descends_from_active_record?
assert AbstractCompany.descends_from_active_record?, 'AbstractCompany should descend from ActiveRecord::Base'
assert Company.descends_from_active_record?, 'Company should descend from ActiveRecord::Base'
assert !Class.new(Company).descends_from_active_record?, 'Company subclass should not descend from ActiveRecord::Base'
@@ -81,10 +81,6 @@ class InheritanceTest < ActiveRecord::TestCase
assert_equal SubStiPost, SubStiPost.base_class
end
- def test_active_record_model_included_base_class
- assert_equal Teapot, Teapot.base_class
- end
-
def test_abstract_inheritance_base_class
assert_equal LoosePerson, LoosePerson.base_class
assert_equal LooseDescendant, LooseDescendant.base_class
@@ -93,11 +89,7 @@ class InheritanceTest < ActiveRecord::TestCase
end
def test_base_class_activerecord_error
- klass = Class.new {
- extend ActiveRecord::Configuration
- include ActiveRecord::Inheritance
- }
-
+ klass = Class.new { include ActiveRecord::Inheritance }
assert_raise(ActiveRecord::ActiveRecordError) { klass.base_class }
end
@@ -165,6 +157,58 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of Cabbage, savoy
end
+ def test_inheritance_new_with_default_class
+ company = Company.new
+ assert_equal Company, company.class
+ end
+
+ def test_inheritance_new_with_base_class
+ company = Company.new(:type => 'Company')
+ assert_equal Company, company.class
+ end
+
+ def test_inheritance_new_with_subclass
+ firm = Company.new(:type => 'Firm')
+ assert_equal Firm, firm.class
+ end
+
+ def test_new_with_abstract_class
+ e = assert_raises(NotImplementedError) do
+ AbstractCompany.new
+ end
+ assert_equal("AbstractCompany is an abstract class and can not be instantiated.", e.message)
+ end
+
+ def test_new_with_ar_base
+ e = assert_raises(NotImplementedError) do
+ ActiveRecord::Base.new
+ end
+ assert_equal("ActiveRecord::Base is an abstract class and can not be instantiated.", e.message)
+ end
+
+ def test_new_with_invalid_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'InvalidType') }
+ end
+
+ def test_new_with_unrelated_type
+ assert_raise(ActiveRecord::SubclassNotFound) { Company.new(:type => 'Account') }
+ end
+
+ def test_new_with_complex_inheritance
+ assert_nothing_raised { Client.new(type: 'VerySpecialClient') }
+ end
+
+ def test_new_with_autoload_paths
+ path = File.expand_path('../../models/autoloadable', __FILE__)
+ ActiveSupport::Dependencies.autoload_paths << path
+
+ firm = Company.new(:type => 'ExtraFirm')
+ assert_equal ExtraFirm, firm.class
+ ensure
+ ActiveSupport::Dependencies.autoload_paths.reject! { |p| p == path }
+ ActiveSupport::Dependencies.clear
+ end
+
def test_inheritance_condition
assert_equal 10, Company.count
assert_equal 2, Firm.count
@@ -269,8 +313,12 @@ class InheritanceTest < ActiveRecord::TestCase
assert_kind_of SpecialSubscriber, SpecialSubscriber.find("webster132")
assert_nothing_raised { s = SpecialSubscriber.new("name" => "And breaaaaathe!"); s.id = 'roger'; s.save }
end
-end
+ def test_scope_inherited_properly
+ assert_nothing_raised { Company.of_first_firm }
+ assert_nothing_raised { Client.of_first_firm }
+ end
+end
class InheritanceComputeTypeTest < ActiveRecord::TestCase
fixtures :companies
diff --git a/activerecord/test/cases/integration_test.rb b/activerecord/test/cases/integration_test.rb
new file mode 100644
index 0000000000..406aacb056
--- /dev/null
+++ b/activerecord/test/cases/integration_test.rb
@@ -0,0 +1,84 @@
+require 'cases/helper'
+require 'models/company'
+require 'models/developer'
+require 'models/car'
+require 'models/bulb'
+
+class IntegrationTest < ActiveRecord::TestCase
+ fixtures :companies, :developers
+
+ def test_to_param_should_return_string
+ assert_kind_of String, Client.first.to_param
+ end
+
+ def test_to_param_returns_nil_if_not_persisted
+ client = Client.new
+ assert_equal nil, client.to_param
+ end
+
+ def test_to_param_returns_id_if_not_persisted_but_id_is_set
+ client = Client.new
+ client.id = 1
+ assert_equal '1', client.to_param
+ end
+
+ def test_cache_key_for_existing_record_is_not_timezone_dependent
+ utc_key = Developer.first.cache_key
+
+ with_timezone_config zone: "EST" do
+ est_key = Developer.first.cache_key
+ assert_equal utc_key, est_key
+ end
+ end
+
+ def test_cache_key_format_for_existing_record_with_updated_at
+ dev = Developer.first
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_updated_at_and_custom_cache_timestamp_format
+ dev = CachedDeveloper.first
+ assert_equal "cached_developers/#{dev.id}-#{dev.updated_at.utc.to_s(:number)}", dev.cache_key
+ end
+
+ def test_cache_key_changes_when_child_touched
+ car = Car.create
+ Bulb.create(car: car)
+
+ key = car.cache_key
+ car.bulb.touch
+ car.reload
+ assert_not_equal key, car.cache_key
+ end
+
+ def test_cache_key_format_for_existing_record_with_nil_updated_timestamps
+ dev = Developer.first
+ dev.update_columns(updated_at: nil, updated_on: nil)
+ assert_match(/\/#{dev.id}$/, dev.cache_key)
+ end
+
+ def test_cache_key_for_updated_on
+ dev = Developer.first
+ dev.updated_at = nil
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_for_newer_updated_at
+ dev = Developer.first
+ dev.updated_at += 3600
+ assert_equal "developers/#{dev.id}-#{dev.updated_at.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_for_newer_updated_on
+ dev = Developer.first
+ dev.updated_on += 3600
+ assert_equal "developers/#{dev.id}-#{dev.updated_on.utc.to_s(:nsec)}", dev.cache_key
+ end
+
+ def test_cache_key_format_is_precise_enough
+ dev = Developer.first
+ key = dev.cache_key
+ dev.touch
+ assert_not_equal key, dev.cache_key
+ end
+end
diff --git a/activerecord/test/cases/invalid_connection_test.rb b/activerecord/test/cases/invalid_connection_test.rb
new file mode 100644
index 0000000000..f2d8f18ec7
--- /dev/null
+++ b/activerecord/test/cases/invalid_connection_test.rb
@@ -0,0 +1,22 @@
+require "cases/helper"
+
+class TestAdapterWithInvalidConnection < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
+
+ class Bird < ActiveRecord::Base
+ end
+
+ def setup
+ # Can't just use current adapter; sqlite3 will create a database
+ # file on the fly.
+ Bird.establish_connection adapter: 'mysql', database: 'i_do_not_exist'
+ end
+
+ def teardown
+ Bird.remove_connection
+ end
+
+ test "inspect on Model class does not raise" do
+ assert_equal "#{Bird.name}(no database connection)", Bird.inspect
+ end
+end
diff --git a/activerecord/test/cases/invertible_migration_test.rb b/activerecord/test/cases/invertible_migration_test.rb
index 8f1cdd47ea..428145d00b 100644
--- a/activerecord/test/cases/invertible_migration_test.rb
+++ b/activerecord/test/cases/invertible_migration_test.rb
@@ -17,6 +17,37 @@ module ActiveRecord
end
end
+ class InvertibleRevertMigration < SilentMigration
+ def change
+ revert do
+ create_table("horses") do |t|
+ t.column :content, :text
+ t.column :remind_at, :datetime
+ end
+ end
+ end
+ end
+
+ class InvertibleByPartsMigration < SilentMigration
+ attr_writer :test
+ def change
+ create_table("new_horses") do |t|
+ t.column :breed, :string
+ end
+ reversible do |dir|
+ @test.yield :both
+ dir.up { @test.yield :up }
+ dir.down { @test.yield :down }
+ end
+ revert do
+ create_table("horses") do |t|
+ t.column :content, :text
+ t.column :remind_at, :datetime
+ end
+ end
+ end
+ end
+
class NonInvertibleMigration < SilentMigration
def change
create_table("horses") do |t|
@@ -27,6 +58,24 @@ module ActiveRecord
end
end
+ class RemoveIndexMigration1 < SilentMigration
+ def self.up
+ create_table("horses") do |t|
+ t.column :name, :string
+ t.column :color, :string
+ t.index [:name, :color]
+ end
+ end
+ end
+
+ class RemoveIndexMigration2 < SilentMigration
+ def change
+ change_table("horses") do |t|
+ t.remove_index [:name, :color]
+ end
+ end
+ end
+
class LegacyMigration < ActiveRecord::Migration
def self.up
create_table("horses") do |t|
@@ -40,9 +89,28 @@ module ActiveRecord
end
end
+ class RevertWholeMigration < SilentMigration
+ def initialize(name = self.class.name, version = nil, migration)
+ @migration = migration
+ super(name, version)
+ end
+
+ def change
+ revert @migration
+ end
+ end
+
+ class NestedRevertWholeMigration < RevertWholeMigration
+ def change
+ revert { super }
+ end
+ end
+
def teardown
- if ActiveRecord::Base.connection.table_exists?("horses")
- ActiveRecord::Base.connection.drop_table("horses")
+ %w[horses new_horses].each do |table|
+ if ActiveRecord::Base.connection.table_exists?(table)
+ ActiveRecord::Base.connection.drop_table(table)
+ end
end
end
@@ -54,6 +122,16 @@ module ActiveRecord
end
end
+ def test_exception_on_removing_index_without_column_option
+ RemoveIndexMigration1.new.migrate(:up)
+ migration = RemoveIndexMigration2.new
+ migration.migrate(:up)
+
+ assert_raises(IrreversibleMigration) do
+ migration.migrate(:down)
+ end
+ end
+
def test_migrate_up
migration = InvertibleMigration.new
migration.migrate(:up)
@@ -67,6 +145,83 @@ module ActiveRecord
assert !migration.connection.table_exists?("horses")
end
+ def test_migrate_revert
+ migration = InvertibleMigration.new
+ revert = InvertibleRevertMigration.new
+ migration.migrate :up
+ revert.migrate :up
+ assert !migration.connection.table_exists?("horses")
+ revert.migrate :down
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert !migration.connection.table_exists?("horses")
+ end
+
+ def test_migrate_revert_by_part
+ InvertibleMigration.new.migrate :up
+ received = []
+ migration = InvertibleByPartsMigration.new
+ migration.test = ->(dir){
+ assert migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ received << dir
+ }
+ migration.migrate :up
+ assert_equal [:both, :up], received
+ assert !migration.connection.table_exists?("horses")
+ assert migration.connection.table_exists?("new_horses")
+ migration.migrate :down
+ assert_equal [:both, :up, :both, :down], received
+ assert migration.connection.table_exists?("horses")
+ assert !migration.connection.table_exists?("new_horses")
+ end
+
+ def test_migrate_revert_whole_migration
+ migration = InvertibleMigration.new
+ [LegacyMigration, InvertibleMigration].each do |klass|
+ revert = RevertWholeMigration.new(klass)
+ migration.migrate :up
+ revert.migrate :up
+ assert !migration.connection.table_exists?("horses")
+ revert.migrate :down
+ assert migration.connection.table_exists?("horses")
+ migration.migrate :down
+ assert !migration.connection.table_exists?("horses")
+ end
+ end
+
+ def test_migrate_nested_revert_whole_migration
+ revert = NestedRevertWholeMigration.new(InvertibleRevertMigration)
+ revert.migrate :down
+ assert revert.connection.table_exists?("horses")
+ revert.migrate :up
+ assert !revert.connection.table_exists?("horses")
+ end
+
+ def test_revert_order
+ block = Proc.new{|t| t.string :name }
+ recorder = ActiveRecord::Migration::CommandRecorder.new(ActiveRecord::Base.connection)
+ recorder.instance_eval do
+ create_table("apples", &block)
+ revert do
+ create_table("bananas", &block)
+ revert do
+ create_table("clementines")
+ create_table("dates")
+ end
+ create_table("elderberries")
+ end
+ revert do
+ create_table("figs")
+ create_table("grapes")
+ end
+ end
+ assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
+ [:create_table, ["clementines"], nil], [:create_table, ["dates"], nil],
+ [:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
+ [:drop_table, ["figs"], nil]], recorder.commands
+ end
+
def test_legacy_up
LegacyMigration.migrate :up
assert ActiveRecord::Base.connection.table_exists?("horses"), "horses should exist"
diff --git a/activerecord/test/cases/json_serialization_test.rb b/activerecord/test/cases/json_serialization_test.rb
index a86b165c78..a222675918 100644
--- a/activerecord/test/cases/json_serialization_test.rb
+++ b/activerecord/test/cases/json_serialization_test.rb
@@ -6,7 +6,21 @@ require 'models/tagging'
require 'models/tag'
require 'models/comment'
+module JsonSerializationHelpers
+ private
+
+ def set_include_root_in_json(value)
+ original_root_in_json = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = value
+ yield
+ ensure
+ ActiveRecord::Base.include_root_in_json = original_root_in_json
+ end
+end
+
class JsonSerializationTest < ActiveRecord::TestCase
+ include JsonSerializationHelpers
+
class NamespacedContact < Contact
column :name, :string
end
@@ -23,20 +37,24 @@ class JsonSerializationTest < ActiveRecord::TestCase
end
def test_should_demodulize_root_in_json
- @contact = NamespacedContact.new :name => 'whatever'
- json = @contact.to_json
- assert_match %r{^\{"namespaced_contact":\{}, json
+ set_include_root_in_json(true) do
+ @contact = NamespacedContact.new name: 'whatever'
+ json = @contact.to_json
+ assert_match %r{^\{"namespaced_contact":\{}, json
+ end
end
def test_should_include_root_in_json
- 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
+ set_include_root_in_json(true) do
+ 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
+ end
end
def test_should_encode_all_encodable_attributes
@@ -141,6 +159,8 @@ end
class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
fixtures :authors, :posts, :comments, :tags, :taggings
+ include JsonSerializationHelpers
+
def setup
@david = authors(:david)
@mary = authors(:mary)
@@ -227,23 +247,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_should_allow_only_option_for_list_of_authors
- ActiveRecord::Base.include_root_in_json = false
- authors = [@david, @mary]
- assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, :only => :name)
- ensure
- ActiveRecord::Base.include_root_in_json = true
+ set_include_root_in_json(false) do
+ authors = [@david, @mary]
+ assert_equal %([{"name":"David"},{"name":"Mary"}]), ActiveSupport::JSON.encode(authors, only: :name)
+ end
end
def test_should_allow_except_option_for_list_of_authors
- ActiveRecord::Base.include_root_in_json = false
- authors = [@david, @mary]
- encoded = ActiveSupport::JSON.encode(authors, :except => [
- :name, :author_address_id, :author_address_extra_id,
- :organization_id, :owned_essay_id
- ])
- assert_equal %([{"id":1},{"id":2}]), encoded
- ensure
- ActiveRecord::Base.include_root_in_json = true
+ set_include_root_in_json(false) do
+ authors = [@david, @mary]
+ encoded = ActiveSupport::JSON.encode(authors, except: [
+ :name, :author_address_id, :author_address_extra_id,
+ :organization_id, :owned_essay_id
+ ])
+ assert_equal %([{"id":1},{"id":2}]), encoded
+ end
end
def test_should_allow_includes_for_list_of_authors
@@ -262,17 +280,21 @@ class DatabaseConnectedJsonEncodingTest < ActiveRecord::TestCase
end
def test_should_allow_options_for_hash_of_authors
- authors_hash = {
- 1 => @david,
- 2 => @mary
- }
- assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, :only => [1, :name])
+ set_include_root_in_json(true) do
+ authors_hash = {
+ 1 => @david,
+ 2 => @mary
+ }
+ assert_equal %({"1":{"author":{"name":"David"}}}), ActiveSupport::JSON.encode(authors_hash, only: [1, :name])
+ end
end
def test_should_be_able_to_encode_relation
- authors_relation = Author.where(:id => [@david.id, @mary.id])
+ set_include_root_in_json(true) do
+ authors_relation = Author.where(id: [@david.id, @mary.id])
- json = ActiveSupport::JSON.encode authors_relation, :only => :name
- assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json
+ json = ActiveSupport::JSON.encode authors_relation, only: :name
+ assert_equal '[{"author":{"name":"David"}},{"author":{"name":"Mary"}}]', json
+ end
end
end
diff --git a/activerecord/test/cases/lifecycle_test.rb b/activerecord/test/cases/lifecycle_test.rb
deleted file mode 100644
index 0b78f2e46b..0000000000
--- a/activerecord/test/cases/lifecycle_test.rb
+++ /dev/null
@@ -1,256 +0,0 @@
-require 'cases/helper'
-require 'models/topic'
-require 'models/developer'
-require 'models/reply'
-require 'models/minimalistic'
-require 'models/comment'
-
-class SpecialDeveloper < Developer; end
-
-class DeveloperObserver < ActiveRecord::Observer
- def calls
- @calls ||= []
- end
-
- def before_save(developer)
- calls << developer
- end
-end
-
-class SalaryChecker < ActiveRecord::Observer
- observe :special_developer
- attr_accessor :last_saved
-
- def before_save(developer)
- return developer.salary > 80000
- end
-
- module Implementation
- def after_save(developer)
- self.last_saved = developer
- end
- end
- include Implementation
-
-end
-
-class TopicaAuditor < ActiveRecord::Observer
- observe :topic
-
- attr_reader :topic
-
- def after_find(topic)
- @topic = topic
- end
-end
-
-class TopicObserver < ActiveRecord::Observer
- attr_reader :topic
-
- def after_find(topic)
- @topic = topic
- end
-
- # Create an after_save callback, so a notify_observer hook is created
- # on :topic.
- def after_save(nothing)
- end
-end
-
-class MinimalisticObserver < ActiveRecord::Observer
- attr_reader :minimalistic
-
- def after_find(minimalistic)
- @minimalistic = minimalistic
- end
-end
-
-class MultiObserver < ActiveRecord::Observer
- attr_reader :record
-
- def self.observed_class() [ Topic, Developer ] end
-
- cattr_reader :last_inherited
- @@last_inherited = nil
-
- def observed_class_inherited_with_testing(subclass)
- observed_class_inherited_without_testing(subclass)
- @@last_inherited = subclass
- end
-
- alias_method_chain :observed_class_inherited, :testing
-
- def after_find(record)
- @record = record
- end
-end
-
-class ValidatedComment < Comment
- attr_accessor :callers
-
- before_validation :record_callers
-
- after_validation do
- record_callers
- end
-
- def record_callers
- callers << self.class if callers
- end
-end
-
-class ValidatedCommentObserver < ActiveRecord::Observer
- attr_accessor :callers
-
- def after_validation(model)
- callers << self.class if callers
- end
-end
-
-
-class AroundTopic < Topic
-end
-
-class AroundTopicObserver < ActiveRecord::Observer
- observe :around_topic
- def topic_ids
- @topic_ids ||= []
- end
-
- def around_save(topic)
- topic_ids << topic.id
- yield(topic)
- topic_ids << topic.id
- end
-end
-
-class LifecycleTest < ActiveRecord::TestCase
- fixtures :topics, :developers, :minimalistics
-
- def test_before_destroy
- topic = Topic.find(1)
- assert_difference 'Topic.count', -(1 + topic.replies.size) do
- topic.destroy
- end
- end
-
- def test_auto_observer
- topic_observer = TopicaAuditor.instance
- assert_nil TopicaAuditor.observed_class
- assert_equal [Topic], TopicaAuditor.observed_classes.to_a
-
- topic = Topic.find(1)
- assert_equal topic.title, topic_observer.topic.title
- end
-
- def test_inferred_auto_observer
- topic_observer = TopicObserver.instance
- assert_equal Topic, TopicObserver.observed_class
-
- topic = Topic.find(1)
- assert_equal topic.title, topic_observer.topic.title
- end
-
- def test_observing_two_classes
- multi_observer = MultiObserver.instance
-
- topic = Topic.find(1)
- assert_equal topic.title, multi_observer.record.title
-
- developer = Developer.find(1)
- assert_equal developer.name, multi_observer.record.name
- end
-
- def test_observing_subclasses
- multi_observer = MultiObserver.instance
-
- developer = SpecialDeveloper.find(1)
- assert_equal developer.name, multi_observer.record.name
-
- klass = Class.new(Developer)
- assert_equal klass, multi_observer.last_inherited
-
- developer = klass.find(1)
- assert_equal developer.name, multi_observer.record.name
- end
-
- def test_after_find_can_be_observed_when_its_not_defined_on_the_model
- observer = MinimalisticObserver.instance
- assert_equal Minimalistic, MinimalisticObserver.observed_class
-
- minimalistic = Minimalistic.find(1)
- assert_equal minimalistic, observer.minimalistic
- end
-
- def test_after_find_can_be_observed_when_its_defined_on_the_model
- observer = TopicObserver.instance
- assert_equal Topic, TopicObserver.observed_class
-
- topic = Topic.find(1)
- assert_equal topic, observer.topic
- end
-
- def test_invalid_observer
- assert_raise(ArgumentError) { Topic.observers = Object.new; Topic.instantiate_observers }
- end
-
- test "model callbacks fire before observers are notified" do
- callers = []
-
- comment = ValidatedComment.new
- comment.callers = ValidatedCommentObserver.instance.callers = callers
-
- comment.valid?
- assert_equal [ValidatedComment, ValidatedComment, ValidatedCommentObserver], callers,
- "model callbacks did not fire before observers were notified"
- end
-
- test "able to save developer" do
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.new :name => 'Roger', :salary => 100000
- assert developer.save, "developer with normal salary failed to save"
- end
-
- test "unable to save developer with low salary" do
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.new :name => 'Rookie', :salary => 50000
- assert !developer.save, "allowed to save a developer with too low salary"
- end
-
- test "able to call methods defined with included module" do # https://rails.lighthouseapp.com/projects/8994/tickets/6065-activerecordobserver-is-not-aware-of-method-added-by-including-modules
- SalaryChecker.instance # activate
- developer = SpecialDeveloper.create! :name => 'Roger', :salary => 100000
- assert_equal developer, SalaryChecker.instance.last_saved
- end
-
- test "around filter from observer should accept block" do
- observer = AroundTopicObserver.instance
- topic = AroundTopic.new
- topic.save
- assert_nil observer.topic_ids.first
- assert_not_nil observer.topic_ids.last
- end
-
- test "able to disable observers" do
- observer = DeveloperObserver.instance # activate
- observer.calls.clear
-
- ActiveRecord::Base.observers.disable DeveloperObserver do
- Developer.create! :name => 'Ancestor', :salary => 100000
- SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
- end
-
- assert_equal [], observer.calls
- end
-
- def test_observer_is_called_once
- observer = DeveloperObserver.instance # activate
- observer.calls.clear
-
- developer = Developer.create! :name => 'Ancestor', :salary => 100000
- special_developer = SpecialDeveloper.create! :name => 'Descendent', :salary => 100000
-
- assert_equal [developer, special_developer], observer.calls
- end
-
-end
diff --git a/activerecord/test/cases/locking_test.rb b/activerecord/test/cases/locking_test.rb
index 2392516395..a16ed963fe 100644
--- a/activerecord/test/cases/locking_test.rb
+++ b/activerecord/test/cases/locking_test.rb
@@ -8,6 +8,7 @@ require 'models/legacy_thing'
require 'models/reference'
require 'models/string_key_object'
require 'models/car'
+require 'models/bulb'
require 'models/engine'
require 'models/wheel'
require 'models/treasure'
@@ -16,6 +17,7 @@ class LockWithoutDefault < ActiveRecord::Base; end
class LockWithCustomColumnWithoutDefault < ActiveRecord::Base
self.table_name = :lock_without_defaults_cust
+ self.column_defaults # to test @column_defaults caching.
self.locking_column = :custom_lock_version
end
@@ -26,6 +28,18 @@ end
class OptimisticLockingTest < ActiveRecord::TestCase
fixtures :people, :legacy_things, :references, :string_key_objects, :peoples_treasures
+ def test_quote_value_passed_lock_col
+ p1 = Person.find(1)
+ assert_equal 0, p1.lock_version
+
+ Person.expects(:quote_value).with(0, Person.columns_hash[Person.locking_column]).returns('0').once
+
+ p1.first_name = 'anika2'
+ p1.save!
+
+ assert_equal 1, p1.lock_version
+ end
+
def test_non_integer_lock_existing
s1 = StringKeyObject.find("record1")
s2 = StringKeyObject.find("record1")
@@ -193,11 +207,19 @@ class OptimisticLockingTest < ActiveRecord::TestCase
def test_lock_without_default_sets_version_to_zero
t1 = LockWithoutDefault.new
assert_equal 0, t1.lock_version
+
+ t1.save
+ t1 = LockWithoutDefault.find(t1.id)
+ assert_equal 0, t1.lock_version
end
def test_lock_with_custom_column_without_default_sets_version_to_zero
t1 = LockWithCustomColumnWithoutDefault.new
assert_equal 0, t1.custom_lock_version
+
+ t1.save
+ t1 = LockWithCustomColumnWithoutDefault.find(t1.id)
+ assert_equal 0, t1.custom_lock_version
end
def test_readonly_attributes
@@ -207,7 +229,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
s.reload
assert_equal "unchangeable name", s.name
- s.update_attributes(:name => "changed name")
+ s.update(name: "changed name")
s.reload
assert_equal "unchangeable name", s.name
end
@@ -234,7 +256,7 @@ class OptimisticLockingTest < ActiveRecord::TestCase
car = Car.create!
assert_difference 'car.wheels.count' do
- car.wheels << Wheel.create!
+ car.wheels << Wheel.create!
end
assert_difference 'car.wheels.count', -1 do
car.destroy
@@ -250,6 +272,10 @@ class OptimisticLockingTest < ActiveRecord::TestCase
assert p.treasures.empty?
assert RichPerson.connection.select_all("SELECT * FROM peoples_treasures WHERE rich_person_id = 1").empty?
end
+
+ def test_quoted_locking_column_is_deprecated
+ assert_deprecated { ActiveRecord::Base.quoted_locking_column }
+ end
end
class OptimisticLockingWithSchemaChangeTest < ActiveRecord::TestCase
@@ -341,9 +367,6 @@ 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.)
-
-# TODO: The Sybase, and OpenBase adapters currently have no support for pessimistic locking
-
unless current_adapter?(:SybaseAdapter, :OpenBaseAdapter) || in_memory_db?
class PessimisticLockingTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
diff --git a/activerecord/test/cases/log_subscriber_test.rb b/activerecord/test/cases/log_subscriber_test.rb
index 70d00aecf9..3bdc5a1302 100644
--- a/activerecord/test/cases/log_subscriber_test.rb
+++ b/activerecord/test/cases/log_subscriber_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require "models/binary"
require "models/developer"
require "models/post"
require "active_support/log_subscriber/test_helper"
@@ -7,6 +8,19 @@ class LogSubscriberTest < ActiveRecord::TestCase
include ActiveSupport::LogSubscriber::TestHelper
include ActiveSupport::Logger::Severity
+ class TestDebugLogSubscriber < ActiveRecord::LogSubscriber
+ attr_reader :debugs
+
+ def initialize
+ @debugs = []
+ super
+ end
+
+ def debug message
+ @debugs << message
+ end
+ end
+
fixtures :posts
def setup
@@ -29,30 +43,34 @@ class LogSubscriberTest < ActiveRecord::TestCase
def test_schema_statements_are_ignored
event = Struct.new(:duration, :payload)
- logger = Class.new(ActiveRecord::LogSubscriber) {
- attr_accessor :debugs
-
- def initialize
- @debugs = []
- super
- end
-
- def debug message
- @debugs << message
- end
- }.new
+ logger = TestDebugLogSubscriber.new
assert_equal 0, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!' }))
+ logger.sql(event.new(0, sql: 'hi mom!'))
assert_equal 1, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!', :name => 'foo' }))
+ logger.sql(event.new(0, sql: 'hi mom!', name: 'foo'))
assert_equal 2, logger.debugs.length
- logger.sql(event.new(0, { :sql => 'hi mom!', :name => 'SCHEMA' }))
+ logger.sql(event.new(0, sql: 'hi mom!', name: 'SCHEMA'))
assert_equal 2, logger.debugs.length
end
+ def test_sql_statements_are_not_squeezed
+ event = Struct.new(:duration, :payload)
+ logger = TestDebugLogSubscriber.new
+ logger.sql(event.new(0, sql: 'ruby rails'))
+ assert_match(/ruby rails/, logger.debugs.first)
+ end
+
+ def test_ignore_binds_payload_with_nil_column
+ event = Struct.new(:duration, :payload)
+
+ logger = TestDebugLogSubscriber.new
+ logger.sql(event.new(0, sql: 'hi mom!', binds: [[nil, 1]]))
+ assert_equal 1, logger.debugs.length
+ end
+
def test_basic_query_logging
Developer.all.load
wait
@@ -100,4 +118,12 @@ class LogSubscriberTest < ActiveRecord::TestCase
def test_initializes_runtime
Thread.new { assert_equal 0, ActiveRecord::LogSubscriber.runtime }.join
end
+
+ def test_binary_data_is_not_logged
+ skip if current_adapter?(:Mysql2Adapter)
+
+ Binary.create(data: 'some binary data')
+ wait
+ assert_match(/<16 bytes of binary data>/, @logger.logged(:debug).join)
+ end
end
diff --git a/activerecord/test/cases/migration/change_schema_test.rb b/activerecord/test/cases/migration/change_schema_test.rb
index 17c1634444..e37dca856d 100644
--- a/activerecord/test/cases/migration/change_schema_test.rb
+++ b/activerecord/test/cases/migration/change_schema_test.rb
@@ -35,7 +35,7 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo id), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(id foo), connection.columns(:testings).map(&:name)
end
def test_create_table_with_not_null_column
@@ -50,7 +50,7 @@ module ActiveRecord
def test_create_table_with_defaults
# MySQL doesn't allow defaults on TEXT or BLOB columns.
- mysql = current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter)
+ mysql = current_adapter?(:MysqlAdapter, :Mysql2Adapter)
connection.create_table :testings do |t|
t.column :one, :string, :default => "hello"
@@ -74,6 +74,35 @@ module ActiveRecord
assert_equal "hello", five.default unless mysql
end
+ def test_add_column_with_array
+ if current_adapter?(:PostgreSQLAdapter)
+ connection.create_table :testings
+ connection.add_column :testings, :foo, :string, :array => true
+
+ columns = connection.columns(:testings)
+ array_column = columns.detect { |c| c.name == "foo" }
+
+ assert array_column.array
+ else
+ skip "array option only supported in PostgreSQLAdapter"
+ end
+ end
+
+ def test_create_table_with_array_column
+ if current_adapter?(:PostgreSQLAdapter)
+ connection.create_table :testings do |t|
+ t.string :foo, :array => true
+ end
+
+ columns = connection.columns(:testings)
+ array_column = columns.detect { |c| c.name == "foo" }
+
+ assert array_column.array
+ else
+ skip "array option only supported in PostgreSQLAdapter"
+ end
+ end
+
def test_create_table_with_limits
connection.create_table :testings do |t|
t.column :foo, :string, :limit => 255
@@ -99,7 +128,7 @@ module ActiveRecord
assert_equal 'smallint', one.sql_type
assert_equal 'integer', four.sql_type
assert_equal 'bigint', eight.sql_type
- elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_match 'int(11)', default.sql_type
assert_match 'tinyint', one.sql_type
assert_match 'int', four.sql_type
@@ -119,7 +148,7 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo testing_id), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(testing_id foo), connection.columns(:testings).map(&:name)
end
def test_create_table_with_primary_key_prefix_as_table_name
@@ -129,7 +158,27 @@ module ActiveRecord
t.column :foo, :string
end
- assert_equal %w(foo testingid), connection.columns(:testings).map(&:name).sort
+ assert_equal %w(testingid foo), connection.columns(:testings).map(&:name)
+ end
+
+ def test_create_table_raises_when_redefining_primary_key_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings do |t|
+ t.column :id, :string
+ end
+ end
+
+ assert_equal "you can't redefine the primary key column 'id'. To define a custom primary key, pass { id: false } to create_table.", error.message
+ end
+
+ def test_create_table_raises_when_redefining_custom_primary_key_column
+ error = assert_raise(ArgumentError) do
+ connection.create_table :testings, primary_key: :testing_id do |t|
+ t.column :testing_id, :string
+ end
+ end
+
+ assert_equal "you can't redefine the primary key column 'testing_id'. To define a custom primary key, pass { id: false } to create_table.", error.message
end
def test_create_table_with_timestamps_should_create_datetime_columns
@@ -215,7 +264,7 @@ module ActiveRecord
end
end
- def test_keeping_default_and_notnull_constaint_on_change
+ def test_keeping_default_and_notnull_constraints_on_change
connection.create_table :testings do |t|
t.column :title, :string
end
@@ -273,7 +322,7 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo)
- refute connection.column_exists?(:testings, :bar)
+ assert_not connection.column_exists?(:testings, :bar)
end
def test_column_exists_with_type
@@ -283,10 +332,10 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo, :string)
- refute connection.column_exists?(:testings, :foo, :integer)
+ assert_not connection.column_exists?(:testings, :foo, :integer)
assert connection.column_exists?(:testings, :bar, :decimal)
- refute connection.column_exists?(:testings, :bar, :integer)
+ assert_not connection.column_exists?(:testings, :bar, :integer)
end
def test_column_exists_with_definition
@@ -298,13 +347,13 @@ module ActiveRecord
end
assert connection.column_exists?(:testings, :foo, :string, limit: 100)
- refute connection.column_exists?(:testings, :foo, :string, limit: nil)
+ assert_not connection.column_exists?(:testings, :foo, :string, limit: nil)
assert connection.column_exists?(:testings, :bar, :decimal, precision: 8, scale: 2)
- refute connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil)
+ assert_not connection.column_exists?(:testings, :bar, :decimal, precision: nil, scale: nil)
assert connection.column_exists?(:testings, :taggable_id, :integer, null: false)
- refute connection.column_exists?(:testings, :taggable_id, :integer, null: true)
+ assert_not connection.column_exists?(:testings, :taggable_id, :integer, null: true)
assert connection.column_exists?(:testings, :taggable_type, :string, default: 'Photo')
- refute connection.column_exists?(:testings, :taggable_type, :string, default: nil)
+ assert_not connection.column_exists?(:testings, :taggable_type, :string, default: nil)
end
def test_column_exists_on_table_with_no_options_parameter_supplied
diff --git a/activerecord/test/cases/migration/change_table_test.rb b/activerecord/test/cases/migration/change_table_test.rb
index 4614be9650..8065541bfe 100644
--- a/activerecord/test/cases/migration/change_table_test.rb
+++ b/activerecord/test/cases/migration/change_table_test.rb
@@ -1,23 +1,11 @@
require "cases/migration/helper"
+require "minitest/mock"
module ActiveRecord
class Migration
class TableTest < ActiveRecord::TestCase
- class MockConnection < MiniTest::Mock
- def native_database_types
- {
- :string => 'varchar(255)',
- :integer => 'integer',
- }
- end
-
- def type_to_sql(type, limit, precision, scale)
- native_database_types[type]
- end
- end
-
def setup
- @connection = MockConnection.new
+ @connection = MiniTest::Mock.new
end
def teardown
@@ -98,26 +86,18 @@ module ActiveRecord
end
end
- def string_column
- @connection.native_database_types[:string]
- end
-
- def integer_column
- @connection.native_database_types[:integer]
- end
-
def test_integer_creates_integer_column
with_change_table do |t|
- @connection.expect :add_column, nil, [:delete_me, :foo, integer_column, {}]
- @connection.expect :add_column, nil, [:delete_me, :bar, integer_column, {}]
+ @connection.expect :add_column, nil, [:delete_me, :foo, :integer, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :integer, {}]
t.integer :foo, :bar
end
end
def test_string_creates_string_column
with_change_table do |t|
- @connection.expect :add_column, nil, [:delete_me, :foo, string_column, {}]
- @connection.expect :add_column, nil, [:delete_me, :bar, string_column, {}]
+ @connection.expect :add_column, nil, [:delete_me, :foo, :string, {}]
+ @connection.expect :add_column, nil, [:delete_me, :bar, :string, {}]
t.string :foo, :bar
end
end
@@ -164,6 +144,13 @@ module ActiveRecord
end
end
+ def test_rename_index_renames_index
+ with_change_table do |t|
+ @connection.expect :rename_index, nil, [:delete_me, :bar, :baz]
+ t.rename_index :bar, :baz
+ end
+ end
+
def test_change_changes_column
with_change_table do |t|
@connection.expect :change_column, nil, [:delete_me, :bar, :string, {}]
@@ -187,14 +174,14 @@ module ActiveRecord
def test_remove_drops_single_column
with_change_table do |t|
- @connection.expect :remove_column, nil, [:delete_me, :bar]
+ @connection.expect :remove_columns, nil, [:delete_me, :bar]
t.remove :bar
end
end
def test_remove_drops_multiple_columns
with_change_table do |t|
- @connection.expect :remove_column, nil, [:delete_me, :bar, :baz]
+ @connection.expect :remove_columns, nil, [:delete_me, :bar, :baz]
t.remove :bar, :baz
end
end
diff --git a/activerecord/test/cases/migration/column_attributes_test.rb b/activerecord/test/cases/migration/column_attributes_test.rb
index b88db384a0..aa606ac8bb 100644
--- a/activerecord/test/cases/migration/column_attributes_test.rb
+++ b/activerecord/test/cases/migration/column_attributes_test.rb
@@ -16,32 +16,23 @@ module ActiveRecord
end
def test_add_remove_single_field_using_string_arguments
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column 'test_models', 'last_name', :string
-
- TestModel.reset_column_information
-
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column 'test_models', 'last_name'
-
- TestModel.reset_column_information
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_add_remove_single_field_using_symbol_arguments
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
add_column :test_models, :last_name, :string
-
- TestModel.reset_column_information
- assert TestModel.column_methods_hash.key?(:last_name)
+ assert_column TestModel, :last_name
remove_column :test_models, :last_name
-
- TestModel.reset_column_information
- refute TestModel.column_methods_hash.key?(:last_name)
+ assert_no_column TestModel, :last_name
end
def test_unabstracted_database_dependent_types
@@ -168,26 +159,6 @@ module ActiveRecord
assert_equal Date, bob.favorite_day.class
end
- # Oracle adapter stores Time or DateTime with timezone value already in _before_type_cast column
- # therefore no timezone change is done afterwards when default timezone is changed
- unless current_adapter?(:OracleAdapter)
- # Test DateTime column and defaults, including timezone.
- # FIXME: moment of truth may be Time on 64-bit platforms.
- if bob.moment_of_truth.is_a?(DateTime)
-
- with_env_tz 'US/Eastern' do
- bob.reload
- assert_equal DateTime.local_offset, bob.moment_of_truth.offset
- assert_not_equal 0, bob.moment_of_truth.offset
- assert_not_equal "Z", bob.moment_of_truth.zone
- # US/Eastern is -5 hours from GMT
- assert_equal Rational(-5, 24), bob.moment_of_truth.offset
- assert_match(/\A-05:00\Z/, bob.moment_of_truth.zone)
- assert_equal DateTime::ITALY, bob.moment_of_truth.start
- end
- end
- end
-
assert_instance_of TrueClass, bob.male?
assert_kind_of BigDecimal, bob.wealth
end
diff --git a/activerecord/test/cases/migration/columns_test.rb b/activerecord/test/cases/migration/columns_test.rb
new file mode 100644
index 0000000000..2d7a7ec73a
--- /dev/null
+++ b/activerecord/test/cases/migration/columns_test.rb
@@ -0,0 +1,279 @@
+require "cases/migration/helper"
+
+module ActiveRecord
+ class Migration
+ class ColumnsTest < ActiveRecord::TestCase
+ include ActiveRecord::Migration::TestHelper
+
+ self.use_transactional_fixtures = false
+
+ # FIXME: this is more of an integration test with AR::Base and the
+ # schema modifications. Maybe we should move this?
+ def test_add_rename
+ add_column "test_models", "girlfriend", :string
+ TestModel.reset_column_information
+
+ TestModel.create :girlfriend => 'bobette'
+
+ rename_column "test_models", "girlfriend", "exgirlfriend"
+
+ TestModel.reset_column_information
+ bob = TestModel.first
+
+ assert_equal "bobette", bob.exgirlfriend
+ end
+
+ # FIXME: another integration test. We should decouple this from the
+ # AR::Base implementation.
+ def test_rename_column_using_symbol_arguments
+ add_column :test_models, :first_name, :string
+
+ TestModel.create :first_name => 'foo'
+
+ rename_column :test_models, :first_name, :nick_name
+ TestModel.reset_column_information
+ assert TestModel.column_names.include?("nick_name")
+ assert_equal ['foo'], TestModel.all.map(&:nick_name)
+ end
+
+ # FIXME: another integration test. We should decouple this from the
+ # AR::Base implementation.
+ def test_rename_column
+ add_column "test_models", "first_name", "string"
+
+ TestModel.create :first_name => 'foo'
+
+ rename_column "test_models", "first_name", "nick_name"
+ TestModel.reset_column_information
+ assert TestModel.column_names.include?("nick_name")
+ assert_equal ['foo'], TestModel.all.map(&:nick_name)
+ end
+
+ def test_rename_column_preserves_default_value_not_null
+ 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
+
+ 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
+ end
+
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
+ def test_mysql_rename_column_preserves_auto_increment
+ rename_column "test_models", "id", "id_test"
+ assert_equal "auto_increment", connection.columns("test_models").find { |c| c.name == "id_test" }.extra
+ end
+ end
+
+ def test_rename_nonexistent_column
+ exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ ActiveRecord::StatementInvalid
+ else
+ ActiveRecord::ActiveRecordError
+ end
+ assert_raise(exception) do
+ rename_column "test_models", "nonexistent", "should_fail"
+ end
+ end
+
+ def test_rename_column_with_sql_reserved_word
+ add_column 'test_models', 'first_name', :string
+ rename_column "test_models", "first_name", "group"
+
+ assert TestModel.column_names.include?("group")
+ end
+
+ def test_rename_column_with_an_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name
+
+ assert_equal 1, connection.indexes('test_models').size
+ rename_column "test_models", "hat_name", "name"
+
+ assert_equal ['index_test_models_on_name'], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_rename_column_with_multi_column_index
+ add_column "test_models", :hat_size, :integer
+ add_column "test_models", :hat_style, :string, limit: 100
+ add_index "test_models", ["hat_style", "hat_size"], unique: true
+
+ rename_column "test_models", "hat_size", 'size'
+ if current_adapter? :OracleAdapter
+ assert_equal ['i_test_models_hat_style_size'], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_hat_style_and_size'], connection.indexes('test_models').map(&:name)
+ end
+
+ rename_column "test_models", "hat_style", 'style'
+ if current_adapter? :OracleAdapter
+ assert_equal ['i_test_models_style_size'], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_style_and_size'], connection.indexes('test_models').map(&:name)
+ end
+ end
+
+ def test_rename_column_does_not_rename_custom_named_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name, :name => 'idx_hat_name'
+
+ assert_equal 1, connection.indexes('test_models').size
+ rename_column "test_models", "hat_name", "name"
+ assert_equal ['idx_hat_name'], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_remove_column_with_index
+ add_column "test_models", :hat_name, :string
+ add_index :test_models, :hat_name
+
+ assert_equal 1, connection.indexes('test_models').size
+ remove_column("test_models", "hat_name")
+ assert_equal 0, connection.indexes('test_models').size
+ end
+
+ def test_remove_column_with_multi_column_index
+ add_column "test_models", :hat_size, :integer
+ add_column "test_models", :hat_style, :string, :limit => 100
+ add_index "test_models", ["hat_style", "hat_size"], :unique => true
+
+ assert_equal 1, connection.indexes('test_models').size
+ remove_column("test_models", "hat_size")
+
+ # Every database and/or database adapter has their own behavior
+ # if it drops the multi-column index when any of the indexed columns dropped by remove_column.
+ if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
+ assert_equal [], connection.indexes('test_models').map(&:name)
+ else
+ assert_equal ['index_test_models_on_hat_style_and_hat_size'], connection.indexes('test_models').map(&:name)
+ end
+ end
+
+ def test_change_type_of_not_null_column
+ change_column "test_models", "updated_at", :datetime, :null => false
+ change_column "test_models", "updated_at", :datetime, :null => false
+
+ TestModel.reset_column_information
+ assert_equal false, TestModel.columns_hash['updated_at'].null
+ ensure
+ change_column "test_models", "updated_at", :datetime, :null => true
+ end
+
+ def test_change_column_nullability
+ add_column "test_models", "funny", :boolean
+ assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
+
+ change_column "test_models", "funny", :boolean, :null => false, :default => true
+
+ TestModel.reset_column_information
+ assert_not TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
+
+ change_column "test_models", "funny", :boolean, :null => true
+ TestModel.reset_column_information
+ assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
+ end
+
+ def test_change_column
+ add_column 'test_models', 'age', :integer
+ add_column 'test_models', 'approved', :boolean, :default => true
+
+ old_columns = connection.columns(TestModel.table_name)
+
+ assert old_columns.find { |c| c.name == 'age' && c.type == :integer }
+
+ change_column "test_models", "age", :string
+
+ new_columns = connection.columns(TestModel.table_name)
+
+ assert_not new_columns.find { |c| c.name == 'age' and c.type == :integer }
+ assert new_columns.find { |c| c.name == 'age' and c.type == :string }
+
+ old_columns = connection.columns(TestModel.table_name)
+ assert old_columns.find { |c|
+ c.name == 'approved' && c.type == :boolean && c.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 }
+ change_column :test_models, :approved, :boolean, :default => true
+ end
+
+ def test_change_column_with_nil_default
+ add_column "test_models", "contributor", :boolean, :default => true
+ assert TestModel.new.contributor?
+
+ change_column "test_models", "contributor", :boolean, :default => nil
+ TestModel.reset_column_information
+ assert_not TestModel.new.contributor?
+ assert_nil TestModel.new.contributor
+ end
+
+ def test_change_column_with_new_default
+ add_column "test_models", "administrator", :boolean, :default => true
+ assert TestModel.new.administrator?
+
+ change_column "test_models", "administrator", :boolean, :default => false
+ TestModel.reset_column_information
+ assert_not TestModel.new.administrator?
+ end
+
+ def test_change_column_with_custom_index_name
+ add_column "test_models", "category", :string
+ add_index :test_models, :category, name: 'test_models_categories_idx'
+
+ assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
+ change_column "test_models", "category", :string, null: false, default: 'article'
+
+ assert_equal ['test_models_categories_idx'], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_change_column_with_long_index_name
+ table_name_prefix = 'test_models_'
+ long_index_name = table_name_prefix + ('x' * (connection.allowed_index_name_length - table_name_prefix.length))
+ add_column "test_models", "category", :string
+ add_index :test_models, :category, name: long_index_name
+
+ change_column "test_models", "category", :string, null: false, default: 'article'
+
+ assert_equal [long_index_name], connection.indexes('test_models').map(&:name)
+ end
+
+ def test_change_column_default
+ add_column "test_models", "first_name", :string
+ connection.change_column_default "test_models", "first_name", "Tester"
+
+ assert_equal "Tester", TestModel.new.first_name
+ end
+
+ def test_change_column_default_to_null
+ add_column "test_models", "first_name", :string
+ connection.change_column_default "test_models", "first_name", nil
+ assert_nil TestModel.new.first_name
+ end
+
+ def test_remove_column_no_second_parameter_raises_exception
+ assert_raise(ArgumentError) { connection.remove_column("funny") }
+ end
+
+ def test_removing_and_renaming_column_preserves_custom_primary_key
+ connection.create_table "my_table", primary_key: "my_table_id", force: true do |t|
+ t.integer "col_one"
+ t.string "col_two", limit: 128, null: false
+ end
+
+ remove_column("my_table", "col_two")
+ rename_column("my_table", "col_one", "col_three")
+
+ assert_equal 'my_table_id', connection.primary_key('my_table')
+ 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 f2213ee6aa..1b205d372f 100644
--- a/activerecord/test/cases/migration/command_recorder_test.rb
+++ b/activerecord/test/cases/migration/command_recorder_test.rb
@@ -26,7 +26,7 @@ module ActiveRecord
}.new)
assert recorder.respond_to?(:create_table), 'respond_to? create_table'
recorder.send(:create_table, :horses)
- assert_equal [[:create_table, [:horses]]], recorder.commands
+ assert_equal [[:create_table, [:horses], nil]], recorder.commands
end
def test_unknown_commands_delegate
@@ -34,10 +34,15 @@ module ActiveRecord
assert_equal 'bar', recorder.foo
end
- def test_unknown_commands_raise_exception_if_they_cannot_delegate
- @recorder.record :execute, ['some sql']
+ def test_inverse_of_raise_exception_on_unknown_commands
assert_raises(ActiveRecord::IrreversibleMigration) do
- @recorder.inverse
+ @recorder.inverse_of :execute, ['some sql']
+ end
+ end
+
+ def test_irreversible_commands_raise_exception
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert{ @recorder.execute 'some sql' }
end
end
@@ -46,121 +51,206 @@ module ActiveRecord
assert_equal 1, @recorder.commands.length
end
- def test_inverse
- @recorder.record :create_table, [:system_settings]
- assert_equal 1, @recorder.inverse.length
+ def test_inverted_commands_are_reversed
+ @recorder.revert do
+ @recorder.record :create_table, [:hello]
+ @recorder.record :create_table, [:world]
+ end
+ tables = @recorder.commands.map{|_cmd, args, _block| args}
+ assert_equal [[:world], [:hello]], tables
+ end
- @recorder.record :rename_table, [:old, :new]
- assert_equal 2, @recorder.inverse.length
+ def test_revert_order
+ block = Proc.new{|t| t.string :name }
+ @recorder.instance_eval do
+ create_table("apples", &block)
+ revert do
+ create_table("bananas", &block)
+ revert do
+ create_table("clementines", &block)
+ create_table("dates")
+ end
+ create_table("elderberries")
+ end
+ revert do
+ create_table("figs", &block)
+ create_table("grapes")
+ end
+ end
+ assert_equal [[:create_table, ["apples"], block], [:drop_table, ["elderberries"], nil],
+ [:create_table, ["clementines"], block], [:create_table, ["dates"], nil],
+ [:drop_table, ["bananas"], block], [:drop_table, ["grapes"], nil],
+ [:drop_table, ["figs"], block]], @recorder.commands
end
- def test_inverted_commands_are_reveresed
- @recorder.record :create_table, [:hello]
- @recorder.record :create_table, [:world]
- tables = @recorder.inverse.map(&:last)
- assert_equal [[:world], [:hello]], tables
+ def test_invert_change_table
+ @recorder.revert do
+ @recorder.change_table :fruits do |t|
+ t.string :name
+ t.rename :kind, :cultivar
+ end
+ end
+ assert_equal [
+ [:rename_column, [:fruits, :cultivar, :kind]],
+ [:remove_column, [:fruits, :name, :string, {}], nil],
+ ], @recorder.commands
+
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.revert do
+ @recorder.change_table :fruits do |t|
+ t.remove :kind
+ end
+ end
+ end
end
def test_invert_create_table
- @recorder.record :create_table, [:system_settings]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:system_settings]], drop_table
+ @recorder.revert do
+ @recorder.record :create_table, [:system_settings]
+ end
+ drop_table = @recorder.commands.first
+ assert_equal [:drop_table, [:system_settings], nil], drop_table
end
- def test_invert_create_table_with_options
- @recorder.record :create_table, [:people_reminders, {:id => false}]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:people_reminders]], drop_table
+ def test_invert_create_table_with_options_and_block
+ block = Proc.new{}
+ drop_table = @recorder.inverse_of :create_table, [:people_reminders, id: false], &block
+ assert_equal [:drop_table, [:people_reminders, id: false], block], drop_table
+ end
+
+ def test_invert_drop_table
+ block = Proc.new{}
+ create_table = @recorder.inverse_of :drop_table, [:people_reminders, id: false], &block
+ assert_equal [:create_table, [:people_reminders, id: false], block], create_table
+ end
+
+ def test_invert_drop_table_without_a_block_nor_option
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :drop_table, [:people_reminders]
+ end
end
def test_invert_create_join_table
- @recorder.record :create_join_table, [:musics, :artists]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:artists_musics]], drop_table
+ drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists]
+ assert_equal [:drop_join_table, [:musics, :artists], nil], drop_join_table
end
def test_invert_create_join_table_with_table_name
- @recorder.record :create_join_table, [:musics, :artists, {:table_name => :catalog}]
- drop_table = @recorder.inverse.first
- assert_equal [:drop_table, [:catalog]], drop_table
+ drop_join_table = @recorder.inverse_of :create_join_table, [:musics, :artists, table_name: :catalog]
+ assert_equal [:drop_join_table, [:musics, :artists, table_name: :catalog], nil], drop_join_table
+ end
+
+ def test_invert_drop_join_table
+ block = Proc.new{}
+ create_join_table = @recorder.inverse_of :drop_join_table, [:musics, :artists, table_name: :catalog], &block
+ assert_equal [:create_join_table, [:musics, :artists, table_name: :catalog], block], create_join_table
end
def test_invert_rename_table
- @recorder.record :rename_table, [:old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_table, [:old, :new]
assert_equal [:rename_table, [:new, :old]], rename
end
def test_invert_add_column
- @recorder.record :add_column, [:table, :column, :type, {}]
- remove = @recorder.inverse.first
- assert_equal [:remove_column, [:table, :column]], remove
+ remove = @recorder.inverse_of :add_column, [:table, :column, :type, {}]
+ assert_equal [:remove_column, [:table, :column, :type, {}], nil], remove
+ end
+
+ def test_invert_remove_column
+ add = @recorder.inverse_of :remove_column, [:table, :column, :type, {}]
+ assert_equal [:add_column, [:table, :column, :type, {}], nil], add
+ end
+
+ def test_invert_remove_column_without_type
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :remove_column, [:table, :column]
+ end
end
def test_invert_rename_column
- @recorder.record :rename_column, [:table, :old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_column, [:table, :old, :new]
assert_equal [:rename_column, [:table, :new, :old]], rename
end
def test_invert_add_index
- @recorder.record :add_index, [:table, [:one, :two], {:options => true}]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two], options: true]
+ assert_equal [:remove_index, [:table, {column: [:one, :two], options: true}]], remove
end
def test_invert_add_index_with_name
- @recorder.record :add_index, [:table, [:one, :two], {:name => "new_index"}]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:name => "new_index"}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two], name: "new_index"]
+ assert_equal [:remove_index, [:table, {column: [:one, :two], name: "new_index"}]], remove
end
def test_invert_add_index_with_no_options
- @recorder.record :add_index, [:table, [:one, :two]]
- remove = @recorder.inverse.first
- assert_equal [:remove_index, [:table, {:column => [:one, :two]}]], remove
+ remove = @recorder.inverse_of :add_index, [:table, [:one, :two]]
+ assert_equal [:remove_index, [:table, {column: [:one, :two]}]], remove
+ end
+
+ def test_invert_remove_index
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], options: true}]
+ assert_equal [:add_index, [:table, [:one, :two], options: true]], add
+ end
+
+ def test_invert_remove_index_with_name
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two], name: "new_index"}]
+ assert_equal [:add_index, [:table, [:one, :two], name: "new_index"]], add
+ end
+
+ def test_invert_remove_index_with_no_special_options
+ add = @recorder.inverse_of :remove_index, [:table, {column: [:one, :two]}]
+ assert_equal [:add_index, [:table, [:one, :two], {}]], add
+ end
+
+ def test_invert_remove_index_with_no_column
+ assert_raises(ActiveRecord::IrreversibleMigration) do
+ @recorder.inverse_of :remove_index, [:table, name: "new_index"]
+ end
end
def test_invert_rename_index
- @recorder.record :rename_index, [:table, :old, :new]
- rename = @recorder.inverse.first
+ rename = @recorder.inverse_of :rename_index, [:table, :old, :new]
assert_equal [:rename_index, [:table, :new, :old]], rename
end
def test_invert_add_timestamps
- @recorder.record :add_timestamps, [:table]
- remove = @recorder.inverse.first
- assert_equal [:remove_timestamps, [:table]], remove
+ remove = @recorder.inverse_of :add_timestamps, [:table]
+ assert_equal [:remove_timestamps, [:table], nil], remove
end
def test_invert_remove_timestamps
- @recorder.record :remove_timestamps, [:table]
- add = @recorder.inverse.first
- assert_equal [:add_timestamps, [:table]], add
+ add = @recorder.inverse_of :remove_timestamps, [:table]
+ assert_equal [:add_timestamps, [:table], nil], add
end
def test_invert_add_reference
- @recorder.record :add_reference, [:table, :taggable, { polymorphic: true }]
- remove = @recorder.inverse.first
- assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }]], remove
+ remove = @recorder.inverse_of :add_reference, [:table, :taggable, { polymorphic: true }]
+ assert_equal [:remove_reference, [:table, :taggable, { polymorphic: true }], nil], remove
end
def test_invert_add_belongs_to_alias
- @recorder.record :add_belongs_to, [:table, :user]
- remove = @recorder.inverse.first
- assert_equal [:remove_reference, [:table, :user]], remove
+ remove = @recorder.inverse_of :add_belongs_to, [:table, :user]
+ assert_equal [:remove_reference, [:table, :user], nil], remove
end
def test_invert_remove_reference
- @recorder.record :remove_reference, [:table, :taggable, { polymorphic: true }]
- add = @recorder.inverse.first
- assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }]], add
+ add = @recorder.inverse_of :remove_reference, [:table, :taggable, { polymorphic: true }]
+ assert_equal [:add_reference, [:table, :taggable, { polymorphic: true }], nil], add
end
def test_invert_remove_belongs_to_alias
- @recorder.record :remove_belongs_to, [:table, :user]
- add = @recorder.inverse.first
- assert_equal [:add_reference, [:table, :user]], add
+ add = @recorder.inverse_of :remove_belongs_to, [:table, :user]
+ assert_equal [:add_reference, [:table, :user], nil], add
+ end
+
+ def test_invert_enable_extension
+ disable = @recorder.inverse_of :enable_extension, ['uuid-ossp']
+ assert_equal [:disable_extension, ['uuid-ossp'], nil], disable
+ end
+
+ def test_invert_disable_extension
+ enable = @recorder.inverse_of :disable_extension, ['uuid-ossp']
+ assert_equal [:enable_extension, ['uuid-ossp'], nil], enable
end
end
end
diff --git a/activerecord/test/cases/migration/create_join_table_test.rb b/activerecord/test/cases/migration/create_join_table_test.rb
index cd1b0e8b47..efaec0f823 100644
--- a/activerecord/test/cases/migration/create_join_table_test.rb
+++ b/activerecord/test/cases/migration/create_join_table_test.rb
@@ -35,6 +35,12 @@ module ActiveRecord
assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
end
+ def test_create_join_table_with_symbol_and_string
+ connection.create_join_table :artists, 'musics'
+
+ assert_equal %w(artist_id music_id), connection.columns(:artists_musics).map(&:name).sort
+ end
+
def test_create_join_table_with_the_proper_order
connection.create_join_table :videos, :musics
@@ -72,6 +78,48 @@ module ActiveRecord
assert_equal [%w(artist_id music_id)], connection.indexes(:artists_musics).map(&:columns)
end
+
+ def test_drop_join_table
+ connection.create_join_table :artists, :musics
+ connection.drop_join_table :artists, :musics
+
+ assert !connection.tables.include?('artists_musics')
+ end
+
+ def test_drop_join_table_with_strings
+ connection.create_join_table :artists, :musics
+ connection.drop_join_table 'artists', 'musics'
+
+ assert !connection.tables.include?('artists_musics')
+ end
+
+ def test_drop_join_table_with_the_proper_order
+ connection.create_join_table :videos, :musics
+ connection.drop_join_table :videos, :musics
+
+ assert !connection.tables.include?('musics_videos')
+ end
+
+ def test_drop_join_table_with_the_table_name
+ connection.create_join_table :artists, :musics, table_name: :catalog
+ connection.drop_join_table :artists, :musics, table_name: :catalog
+
+ assert !connection.tables.include?('catalog')
+ end
+
+ def test_drop_join_table_with_the_table_name_as_string
+ connection.create_join_table :artists, :musics, table_name: 'catalog'
+ connection.drop_join_table :artists, :musics, table_name: 'catalog'
+
+ assert !connection.tables.include?('catalog')
+ end
+
+ def test_drop_join_table_with_column_options
+ connection.create_join_table :artists, :musics, column_options: {null: true}
+ connection.drop_join_table :artists, :musics, column_options: {null: true}
+
+ assert !connection.tables.include?('artists_musics')
+ end
end
end
end
diff --git a/activerecord/test/cases/migration/helper.rb b/activerecord/test/cases/migration/helper.rb
index 768ebc5861..e28feedcf9 100644
--- a/activerecord/test/cases/migration/helper.rb
+++ b/activerecord/test/cases/migration/helper.rb
@@ -2,12 +2,10 @@ require "cases/helper"
module ActiveRecord
class Migration
- class << self
- attr_accessor :message_count
- end
+ class << self; attr_accessor :message_count; end
+ self.message_count = 0
def puts(text="")
- ActiveRecord::Migration.message_count ||= 0
ActiveRecord::Migration.message_count += 1
end
diff --git a/activerecord/test/cases/migration/index_test.rb b/activerecord/test/cases/migration/index_test.rb
index dd9492924c..04521a5f5a 100644
--- a/activerecord/test/cases/migration/index_test.rb
+++ b/activerecord/test/cases/migration/index_test.rb
@@ -35,7 +35,7 @@ module ActiveRecord
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
- refute connection.index_name_exists?(table_name, 'old_idx', false)
+ assert_not connection.index_name_exists?(table_name, 'old_idx', false)
assert connection.index_name_exists?(table_name, 'new_idx', true)
end
@@ -55,19 +55,31 @@ module ActiveRecord
assert_raise(ArgumentError) { connection.remove_index(table_name, "no_such_index") }
end
- def test_add_index_name_length_limit
- good_index_name = 'x' * connection.index_name_length
+ def test_add_index_works_with_long_index_names
+ connection.add_index(table_name, "foo", name: good_index_name)
+
+ assert connection.index_name_exists?(table_name, good_index_name, false)
+ connection.remove_index(table_name, name: good_index_name)
+ end
+
+ def test_add_index_does_not_accept_too_long_index_names
too_long_index_name = good_index_name + 'x'
- assert_raises(ArgumentError) {
- connection.add_index(table_name, "foo", :name => too_long_index_name)
+ e = assert_raises(ArgumentError) {
+ connection.add_index(table_name, "foo", name: too_long_index_name)
}
+ assert_match(/too long; the limit is #{connection.allowed_index_name_length} characters/, e.message)
- refute connection.index_name_exists?(table_name, too_long_index_name, false)
+ assert_not connection.index_name_exists?(table_name, too_long_index_name, false)
connection.add_index(table_name, "foo", :name => good_index_name)
+ end
+
+ def test_internal_index_with_name_matching_database_limit
+ good_index_name = 'x' * connection.index_name_length
+ connection.add_index(table_name, "foo", name: good_index_name, internal: true)
assert connection.index_name_exists?(table_name, good_index_name, false)
- connection.remove_index(table_name, :name => good_index_name)
+ connection.remove_index(table_name, name: good_index_name)
end
def test_index_symbol_names
@@ -75,7 +87,7 @@ module ActiveRecord
assert connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
connection.remove_index table_name, :name => :symbol_index_name
- refute connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
+ assert_not connection.index_exists?(table_name, :foo, :name => :symbol_index_name)
end
def test_index_exists
@@ -91,6 +103,12 @@ module ActiveRecord
assert connection.index_exists?(:testings, [:foo, :bar])
end
+ def test_valid_index_options
+ assert_raise ArgumentError do
+ connection.add_index :testings, :foo, unqiue: true
+ end
+ end
+
def test_unique_index_exists
connection.add_index :testings, :foo, :unique => true
@@ -180,6 +198,12 @@ module ActiveRecord
connection.remove_index("testings", "last_name")
assert !connection.index_exists?("testings", "last_name")
end
+
+ private
+ def good_index_name
+ 'x' * connection.allowed_index_name_length
+ end
+
end
end
end
diff --git a/activerecord/test/cases/migration/logger_test.rb b/activerecord/test/cases/migration/logger_test.rb
index ee0c20747e..97efb94b66 100644
--- a/activerecord/test/cases/migration/logger_test.rb
+++ b/activerecord/test/cases/migration/logger_test.rb
@@ -7,6 +7,7 @@ module ActiveRecord
self.use_transactional_fixtures = false
Migration = Struct.new(:name, :version) do
+ def disable_ddl_transaction; false end
def migrate direction
# do nothing
end
@@ -34,4 +35,3 @@ module ActiveRecord
end
end
end
-
diff --git a/activerecord/test/cases/migration/references_index_test.rb b/activerecord/test/cases/migration/references_index_test.rb
index 264a99f9ce..3ff89524fe 100644
--- a/activerecord/test/cases/migration/references_index_test.rb
+++ b/activerecord/test/cases/migration/references_index_test.rb
@@ -29,7 +29,7 @@ module ActiveRecord
t.references :foo
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_does_not_create_index_explicit
@@ -37,7 +37,7 @@ module ActiveRecord
t.references :foo, :index => false
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_creates_index_with_options
@@ -75,7 +75,7 @@ module ActiveRecord
t.references :foo
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_does_not_create_index_for_existing_table_explicit
@@ -84,7 +84,7 @@ module ActiveRecord
t.references :foo, :index => false
end
- refute connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
+ assert_not connection.index_exists?(table_name, :foo_id, :name => :index_testings_on_foo_id)
end
def test_creates_polymorphic_index_for_existing_table
diff --git a/activerecord/test/cases/migration/references_statements_test.rb b/activerecord/test/cases/migration/references_statements_test.rb
index 144302bd4a..e9545f2cce 100644
--- a/activerecord/test/cases/migration/references_statements_test.rb
+++ b/activerecord/test/cases/migration/references_statements_test.rb
@@ -22,7 +22,7 @@ module ActiveRecord
def test_does_not_create_reference_type_column
add_reference table_name, :taggable
- refute column_exists?(table_name, :taggable_type, :string)
+ assert_not column_exists?(table_name, :taggable_type, :string)
end
def test_creates_reference_type_column
@@ -37,7 +37,7 @@ module ActiveRecord
def test_does_not_create_reference_id_index
add_reference table_name, :user
- refute index_exists?(table_name, :user_id)
+ assert_not index_exists?(table_name, :user_id)
end
def test_creates_polymorphic_index
@@ -57,34 +57,34 @@ module ActiveRecord
def test_deletes_reference_id_column
remove_reference table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
end
def test_deletes_reference_id_index
remove_reference table_name, :supplier
- refute index_exists?(table_name, :supplier_id)
+ assert_not index_exists?(table_name, :supplier_id)
end
-
+
def test_does_not_delete_reference_type_column
with_polymorphic_column do
remove_reference table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
assert column_exists?(table_name, :supplier_type, :string)
end
end
-
+
def test_deletes_reference_type_column
with_polymorphic_column do
remove_reference table_name, :supplier, polymorphic: true
- refute column_exists?(table_name, :supplier_type, :string)
+ assert_not column_exists?(table_name, :supplier_type, :string)
end
end
def test_deletes_polymorphic_index
with_polymorphic_column do
remove_reference table_name, :supplier, polymorphic: true
- refute index_exists?(table_name, [:supplier_id, :supplier_type])
+ assert_not index_exists?(table_name, [:supplier_id, :supplier_type])
end
end
@@ -95,7 +95,7 @@ module ActiveRecord
def test_remove_belongs_to_alias
remove_belongs_to table_name, :supplier
- refute column_exists?(table_name, :supplier_id, :integer)
+ assert_not column_exists?(table_name, :supplier_id, :integer)
end
private
diff --git a/activerecord/test/cases/migration/rename_column_test.rb b/activerecord/test/cases/migration/rename_column_test.rb
deleted file mode 100644
index d1a85ee5e4..0000000000
--- a/activerecord/test/cases/migration/rename_column_test.rb
+++ /dev/null
@@ -1,194 +0,0 @@
-require "cases/migration/helper"
-
-module ActiveRecord
- class Migration
- class RenameColumnTest < ActiveRecord::TestCase
- include ActiveRecord::Migration::TestHelper
-
- self.use_transactional_fixtures = false
-
- # FIXME: this is more of an integration test with AR::Base and the
- # schema modifications. Maybe we should move this?
- def test_add_rename
- add_column "test_models", "girlfriend", :string
- TestModel.reset_column_information
-
- TestModel.create :girlfriend => 'bobette'
-
- rename_column "test_models", "girlfriend", "exgirlfriend"
-
- TestModel.reset_column_information
- bob = TestModel.first
-
- assert_equal "bobette", bob.exgirlfriend
- end
-
- # FIXME: another integration test. We should decouple this from the
- # AR::Base implementation.
- def test_rename_column_using_symbol_arguments
- add_column :test_models, :first_name, :string
-
- TestModel.create :first_name => 'foo'
-
- rename_column :test_models, :first_name, :nick_name
- TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
- assert_equal ['foo'], TestModel.all.map(&:nick_name)
- end
-
- # FIXME: another integration test. We should decouple this from the
- # AR::Base implementation.
- def test_rename_column
- add_column "test_models", "first_name", "string"
-
- TestModel.create :first_name => 'foo'
-
- rename_column "test_models", "first_name", "nick_name"
- TestModel.reset_column_information
- assert TestModel.column_names.include?("nick_name")
- assert_equal ['foo'], TestModel.all.map(&:nick_name)
- end
-
- def test_rename_column_preserves_default_value_not_null
- 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
-
- rename_column "test_models", "salary", "anual_salary"
-
- assert TestModel.column_names.include?("anual_salary")
- default_after = connection.columns("test_models").find { |c| c.name == "anual_salary" }.default
- assert_equal 70000, default_after
- end
-
- def test_rename_nonexistent_column
- exception = if current_adapter?(:PostgreSQLAdapter, :OracleAdapter)
- ActiveRecord::StatementInvalid
- else
- ActiveRecord::ActiveRecordError
- end
- assert_raise(exception) do
- rename_column "test_models", "nonexistent", "should_fail"
- end
- end
-
- def test_rename_column_with_sql_reserved_word
- add_column 'test_models', 'first_name', :string
- rename_column "test_models", "first_name", "group"
-
- assert TestModel.column_names.include?("group")
- end
-
- def test_rename_column_with_an_index
- add_column "test_models", :hat_name, :string
- add_index :test_models, :hat_name
-
- # FIXME: we should test that the index goes away
- rename_column "test_models", "hat_name", "name"
- end
-
- def test_remove_column_with_index
- add_column "test_models", :hat_name, :string
- add_index :test_models, :hat_name
-
- # FIXME: we should test that the index goes away
- remove_column("test_models", "hat_name")
- end
-
- def test_remove_column_with_multi_column_index
- add_column "test_models", :hat_size, :integer
- add_column "test_models", :hat_style, :string, :limit => 100
- add_index "test_models", ["hat_style", "hat_size"], :unique => true
-
- # FIXME: we should test that the index goes away
- remove_column("test_models", "hat_size")
- end
-
- # FIXME: we need to test that these calls do something
- def test_change_type_of_not_null_column
- change_column "test_models", "updated_at", :datetime, :null => false
- change_column "test_models", "updated_at", :datetime, :null => false
- change_column "test_models", "updated_at", :datetime, :null => true
- end
-
- def test_change_column_nullability
- add_column "test_models", "funny", :boolean
- assert TestModel.columns_hash["funny"].null, "Column 'funny' must initially allow nulls"
-
- change_column "test_models", "funny", :boolean, :null => false, :default => true
-
- TestModel.reset_column_information
- refute TestModel.columns_hash["funny"].null, "Column 'funny' must *not* allow nulls at this point"
-
- change_column "test_models", "funny", :boolean, :null => true
- TestModel.reset_column_information
- assert TestModel.columns_hash["funny"].null, "Column 'funny' must allow nulls again at this point"
- end
-
- def test_change_column
- add_column 'test_models', 'age', :integer
- add_column 'test_models', 'approved', :boolean, :default => true
-
- old_columns = connection.columns(TestModel.table_name)
-
- assert old_columns.find { |c| c.name == 'age' && c.type == :integer }
-
- change_column "test_models", "age", :string
-
- new_columns = connection.columns(TestModel.table_name)
-
- refute new_columns.find { |c| c.name == 'age' and c.type == :integer }
- assert new_columns.find { |c| c.name == 'age' and c.type == :string }
-
- old_columns = connection.columns(TestModel.table_name)
- assert old_columns.find { |c|
- c.name == 'approved' && c.type == :boolean && c.default == true
- }
-
- change_column :test_models, :approved, :boolean, :default => false
- new_columns = connection.columns(TestModel.table_name)
-
- refute 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 }
- change_column :test_models, :approved, :boolean, :default => true
- end
-
- def test_change_column_with_nil_default
- add_column "test_models", "contributor", :boolean, :default => true
- assert TestModel.new.contributor?
-
- change_column "test_models", "contributor", :boolean, :default => nil
- TestModel.reset_column_information
- refute TestModel.new.contributor?
- assert_nil TestModel.new.contributor
- end
-
- def test_change_column_with_new_default
- add_column "test_models", "administrator", :boolean, :default => true
- assert TestModel.new.administrator?
-
- change_column "test_models", "administrator", :boolean, :default => false
- TestModel.reset_column_information
- refute TestModel.new.administrator?
- end
-
- def test_change_column_default
- add_column "test_models", "first_name", :string
- connection.change_column_default "test_models", "first_name", "Tester"
-
- assert_equal "Tester", TestModel.new.first_name
- end
-
- def test_change_column_default_to_null
- add_column "test_models", "first_name", :string
- connection.change_column_default "test_models", "first_name", nil
- assert_nil TestModel.new.first_name
- end
-
- def test_remove_column_no_second_parameter_raises_exception
- assert_raise(ArgumentError) { connection.remove_column("funny") }
- end
- end
- end
-end
diff --git a/activerecord/test/cases/migration/rename_table_test.rb b/activerecord/test/cases/migration/rename_table_test.rb
index 21901bec3c..22dbd7c38b 100644
--- a/activerecord/test/cases/migration/rename_table_test.rb
+++ b/activerecord/test/cases/migration/rename_table_test.rb
@@ -63,7 +63,17 @@ module ActiveRecord
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")
- assert connection.indexes(:octopi).first.columns.include?("url")
+ index = connection.indexes(:octopi).first
+ assert index.columns.include?("url")
+ assert_equal 'index_octopi_on_url', index.name
+ end
+
+ def test_rename_table_does_not_rename_custom_named_index
+ add_index :test_models, :url, name: 'special_url_idx'
+
+ rename_table :test_models, :octopi
+
+ assert_equal ['special_url_idx'], connection.indexes(:octopi).map(&:name)
end
def test_rename_table_for_postgresql_should_also_rename_default_sequence
diff --git a/activerecord/test/cases/migration_test.rb b/activerecord/test/cases/migration_test.rb
index 3c0d2b18d9..acfde2a27a 100644
--- a/activerecord/test/cases/migration_test.rb
+++ b/activerecord/test/cases/migration_test.rb
@@ -24,15 +24,19 @@ class MigrationTest < ActiveRecord::TestCase
def setup
super
- %w(reminders people_reminders prefix_reminders_suffix).each do |table|
+ %w(reminders people_reminders prefix_reminders_suffix p_things_s).each do |table|
Reminder.connection.drop_table(table) rescue nil
end
Reminder.reset_column_information
ActiveRecord::Migration.verbose = true
ActiveRecord::Migration.message_count = 0
+ ActiveRecord::Base.connection.schema_cache.clear!
end
def teardown
+ ActiveRecord::Base.table_name_prefix = ""
+ ActiveRecord::Base.table_name_suffix = ""
+
ActiveRecord::Base.connection.initialize_schema_migrations_table
ActiveRecord::Base.connection.execute "DELETE FROM #{ActiveRecord::Migrator.schema_migrations_table_name}"
@@ -44,6 +48,7 @@ class MigrationTest < ActiveRecord::TestCase
%w(reminders people_reminders prefix_reminders_suffix).each do |table|
Reminder.connection.drop_table(table) rescue nil
end
+ Reminder.reset_table_name
Reminder.reset_column_information
%w(last_name key bio age height wealth birthday favorite_day
@@ -172,20 +177,18 @@ class MigrationTest < ActiveRecord::TestCase
end
def test_filtering_migrations
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert !Reminder.table_exists?
name_filter = lambda { |migration| migration.name == "ValidPeopleHaveLastNames" }
ActiveRecord::Migrator.up(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert Person.column_methods_hash.include?(:last_name)
+ assert_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
ActiveRecord::Migrator.down(MIGRATIONS_ROOT + "/valid", &name_filter)
- Person.reset_column_information
- assert !Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
end
@@ -232,11 +235,15 @@ class MigrationTest < ActiveRecord::TestCase
skip "not supported on #{ActiveRecord::Base.connection.class}"
end
- refute Person.column_methods_hash.include?(:last_name)
+ assert_no_column Person, :last_name
- migration = Struct.new(:name, :version) {
- def migrate(x); raise 'Something broke'; end
- }.new('zomg', 100)
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
@@ -244,30 +251,99 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "An error has occurred, this and all later migrations canceled:\n\nSomething broke", e.message
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
+ end
+
+ def test_migrator_one_up_with_exception_and_rollback_using_run
+ unless ActiveRecord::Base.connection.supports_ddl_transactions?
+ skip "not supported on #{ActiveRecord::Base.connection.class}"
+ end
+
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration) {
+ def version; 100 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 100)
+
+ e = assert_raise(StandardError) { migrator.run }
+
+ assert_equal "An error has occurred, this migration was canceled:\n\nSomething broke", e.message
+
+ assert_no_column Person, :last_name,
+ "On error, the Migrator should revert schema changes but it did not."
+ end
+
+ def test_migration_without_transaction
+ unless ActiveRecord::Base.connection.supports_ddl_transactions?
+ skip "not supported on #{ActiveRecord::Base.connection.class}"
+ end
+
+ assert_no_column Person, :last_name
+
+ migration = Class.new(ActiveRecord::Migration) {
+ self.disable_ddl_transaction!
+
+ def version; 101 end
+ def migrate(x)
+ add_column "people", "last_name", :string
+ raise 'Something broke'
+ end
+ }.new
+
+ migrator = ActiveRecord::Migrator.new(:up, [migration], 101)
+ e = assert_raise(StandardError) { migrator.migrate }
+ assert_equal "An error has occurred, all later migrations canceled:\n\nSomething broke", e.message
+
+ assert_column Person, :last_name,
+ "without ddl transactions, the Migrator should not rollback on error but it did."
+ ensure
Person.reset_column_information
- refute Person.column_methods_hash.include?(:last_name)
+ if Person.column_names.include?('last_name')
+ Person.connection.remove_column('people', 'last_name')
+ end
end
def test_schema_migrations_table_name
+ original_schema_migrations_table_name = ActiveRecord::Migrator.schema_migrations_table_name
+
+ assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
assert_equal "prefix_schema_migrations_suffix", ActiveRecord::Migrator.schema_migrations_table_name
+ ActiveRecord::Base.schema_migrations_table_name = "changed"
+ Reminder.reset_table_name
+ assert_equal "prefix_changed_suffix", ActiveRecord::Migrator.schema_migrations_table_name
ActiveRecord::Base.table_name_prefix = ""
ActiveRecord::Base.table_name_suffix = ""
Reminder.reset_table_name
- assert_equal "schema_migrations", ActiveRecord::Migrator.schema_migrations_table_name
+ assert_equal "changed", ActiveRecord::Migrator.schema_migrations_table_name
ensure
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
+ ActiveRecord::Base.schema_migrations_table_name = original_schema_migrations_table_name
+ Reminder.reset_table_name
end
- def test_proper_table_name
- assert_equal "table", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "table", ActiveRecord::Migrator.proper_table_name(:table)
- assert_equal "reminders", ActiveRecord::Migrator.proper_table_name(Reminder)
+ def test_proper_table_name_on_migrator
+ 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)
+ end
Reminder.reset_table_name
- assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ assert_deprecated do
+ assert_equal Reminder.table_name, ActiveRecord::Migrator.proper_table_name(Reminder)
+ end
# Use the model's own prefix/suffix if a model is given
ActiveRecord::Base.table_name_prefix = "ARprefix_"
@@ -275,7 +351,9 @@ class MigrationTest < ActiveRecord::TestCase
Reminder.table_name_prefix = 'prefix_'
Reminder.table_name_suffix = '_suffix'
Reminder.reset_table_name
- assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ assert_deprecated do
+ assert_equal "prefix_reminders_suffix", ActiveRecord::Migrator.proper_table_name(Reminder)
+ end
Reminder.table_name_prefix = ''
Reminder.table_name_suffix = ''
Reminder.reset_table_name
@@ -284,11 +362,39 @@ class MigrationTest < ActiveRecord::TestCase
ActiveRecord::Base.table_name_prefix = "prefix_"
ActiveRecord::Base.table_name_suffix = "_suffix"
Reminder.reset_table_name
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name('table')
- assert_equal "prefix_table_suffix", ActiveRecord::Migrator.proper_table_name(:table)
- ActiveRecord::Base.table_name_prefix = ""
- ActiveRecord::Base.table_name_suffix = ""
+ 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
+ migration = ActiveRecord::Migration.new
+ assert_equal "table", migration.proper_table_name('table')
+ assert_equal "table", migration.proper_table_name(:table)
+ assert_equal "reminders", migration.proper_table_name(Reminder)
+ Reminder.reset_table_name
+ assert_equal Reminder.table_name, migration.proper_table_name(Reminder)
+
+ # Use the model's own prefix/suffix if a model is given
+ ActiveRecord::Base.table_name_prefix = "ARprefix_"
+ ActiveRecord::Base.table_name_suffix = "_ARsuffix"
+ Reminder.table_name_prefix = 'prefix_'
+ Reminder.table_name_suffix = '_suffix'
+ Reminder.reset_table_name
+ assert_equal "prefix_reminders_suffix", migration.proper_table_name(Reminder)
+ Reminder.table_name_prefix = ''
+ Reminder.table_name_suffix = ''
Reminder.reset_table_name
+
+ # Use AR::Base's prefix/suffix if string or symbol is given
+ ActiveRecord::Base.table_name_prefix = "prefix_"
+ ActiveRecord::Base.table_name_suffix = "_suffix"
+ Reminder.reset_table_name
+ assert_equal "prefix_table_suffix", migration.proper_table_name('table', migration.table_name_options)
+ assert_equal "prefix_table_suffix", migration.proper_table_name(:table, migration.table_name_options)
end
def test_rename_table_with_prefix_and_suffix
@@ -307,8 +413,6 @@ class MigrationTest < ActiveRecord::TestCase
assert_equal "hello world", Thing.first.content
ensure
- ActiveRecord::Base.table_name_prefix = ''
- ActiveRecord::Base.table_name_suffix = ''
Thing.reset_table_name
Thing.reset_sequence_name
end
@@ -326,9 +430,6 @@ class MigrationTest < ActiveRecord::TestCase
WeNeedReminders.down
assert_raise(ActiveRecord::StatementInvalid) { Reminder.first }
ensure
- ActiveRecord::Base.table_name_prefix = ''
- ActiveRecord::Base.table_name_suffix = ''
- Reminder.reset_table_name
Reminder.reset_sequence_name
end
@@ -344,11 +445,7 @@ class MigrationTest < ActiveRecord::TestCase
columns = Person.connection.columns(:binary_testings)
data_column = columns.detect { |c| c.name == "data" }
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
- assert_equal '', data_column.default
- else
- assert_nil data_column.default
- end
+ assert_nil data_column.default
Person.connection.drop_table :binary_testings rescue nil
end
@@ -436,11 +533,29 @@ class ReservedWordsMigrationTest < ActiveRecord::TestCase
end
end
+class ExplicitlyNamedIndexMigrationTest < ActiveRecord::TestCase
+ def test_drop_index_by_name
+ connection = Person.connection
+ connection.create_table :values, force: true do |t|
+ t.integer :value
+ end
+
+ assert_nothing_raised ArgumentError do
+ connection.add_index :values, :value, name: 'a_different_name'
+ connection.remove_index :values, column: :value, name: 'a_different_name'
+ end
+
+ connection.drop_table :values rescue nil
+ end
+end
+
if ActiveRecord::Base.connection.supports_bulk_alter?
class BulkAlterTableMigrationsTest < ActiveRecord::TestCase
def setup
@connection = Person.connection
@connection.create_table(:delete_me, :force => true) {|t| }
+ Person.reset_column_information
+ Person.reset_sequence_name
end
def teardown
@@ -595,8 +710,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
@existing_migrations = Dir[@migrations_path + "/*.rb"]
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy"})
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
assert_equal [@migrations_path + "/4_people_have_hobbies.bukkits.rb", @migrations_path + "/5_people_have_descriptions.bukkits.rb"], copied.map(&:filename)
expected = "# This migration comes from bukkits (originally 1)"
@@ -619,10 +734,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
sources[:bukkits] = MIGRATIONS_ROOT + "/to_copy"
sources[:omg] = MIGRATIONS_ROOT + "/to_copy2"
ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/6_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/7_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/4_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/5_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/6_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/7_create_comments.omg.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
ActiveRecord::Migration.copy(@migrations_path, sources)
@@ -637,8 +752,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
expected = [@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb",
@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb"]
assert_equal expected, copied.map(&:filename)
@@ -662,10 +777,10 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, sources)
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101012_create_articles.omg.rb")
- assert File.exists?(@migrations_path + "/20100726101013_create_comments.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101012_create_articles.omg.rb")
+ assert File.exist?(@migrations_path + "/20100726101013_create_comments.omg.rb")
assert_equal 4, copied.length
files_count = Dir[@migrations_path + "/*.rb"].length
@@ -682,8 +797,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.travel_to(Time.utc(2010, 2, 20, 10, 10, 10)) do
ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010102_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100301010103_people_have_descriptions.bukkits.rb")
files_count = Dir[@migrations_path + "/*.rb"].length
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
@@ -694,6 +809,26 @@ class CopyMigrationsTest < ActiveRecord::TestCase
clear
end
+ def test_copying_migrations_preserving_magic_comments
+ ActiveRecord::Base.timestamped_migrations = false
+ @migrations_path = MIGRATIONS_ROOT + "/valid"
+ @existing_migrations = Dir[@migrations_path + "/*.rb"]
+
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ assert File.exist?(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")
+ assert_equal [@migrations_path + "/4_currencies_have_symbols.bukkits.rb"], copied.map(&:filename)
+
+ expected = "# coding: ISO-8859-15\n# This migration comes from bukkits (originally 1)"
+ assert_equal expected, IO.readlines(@migrations_path + "/4_currencies_have_symbols.bukkits.rb")[0..1].join.chomp
+
+ files_count = Dir[@migrations_path + "/*.rb"].length
+ copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/magic"})
+ assert_equal files_count, Dir[@migrations_path + "/*.rb"].length
+ assert copied.empty?
+ ensure
+ clear
+ end
+
def test_skipping_migrations
@migrations_path = MIGRATIONS_ROOT + "/valid_with_timestamps"
@existing_migrations = Dir[@migrations_path + "/*.rb"]
@@ -737,8 +872,8 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
@@ -752,11 +887,20 @@ class CopyMigrationsTest < ActiveRecord::TestCase
Time.travel_to(Time.utc(2010, 7, 26, 10, 10, 10)) do
copied = ActiveRecord::Migration.copy(@migrations_path, {:bukkits => MIGRATIONS_ROOT + "/to_copy_with_timestamps"})
- assert File.exists?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
- assert File.exists?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101010_people_have_hobbies.bukkits.rb")
+ assert File.exist?(@migrations_path + "/20100726101011_people_have_descriptions.bukkits.rb")
assert_equal 2, copied.length
end
ensure
clear
end
+
+ def test_check_pending_with_stdlib_logger
+ old, ActiveRecord::Base.logger = ActiveRecord::Base.logger, ::Logger.new($stdout)
+ quietly do
+ assert_nothing_raised { ActiveRecord::Migration::CheckPending.new(Proc.new {}).call({}) }
+ end
+ ensure
+ ActiveRecord::Base.logger = old
+ end
end
diff --git a/activerecord/test/cases/migrator_test.rb b/activerecord/test/cases/migrator_test.rb
index 1e16addcf3..3f9854200d 100644
--- a/activerecord/test/cases/migrator_test.rb
+++ b/activerecord/test/cases/migrator_test.rb
@@ -29,6 +29,7 @@ module ActiveRecord
def teardown
super
ActiveRecord::SchemaMigration.delete_all rescue nil
+ ActiveRecord::Migration.verbose = true
end
def test_migrator_with_duplicate_names
@@ -84,10 +85,10 @@ module ActiveRecord
end
end
- def test_deprecated_constructor
- assert_deprecated do
- ActiveRecord::Migrator.new(:up, MIGRATIONS_ROOT + "/valid")
- end
+ def test_finds_migrations_in_numbered_directory
+ migrations = ActiveRecord::Migrator.migrations [MIGRATIONS_ROOT + '/10_urban']
+ assert_equal 9, migrations[0].version
+ assert_equal 'AddExpressions', migrations[0].name
end
def test_relative_migrations
@@ -115,11 +116,11 @@ module ActiveRecord
ActiveRecord::Migrator.new(:up, pass_one).migrate
assert pass_one.first.went_up
- refute pass_one.first.went_down
+ assert_not pass_one.first.went_down
pass_two = [Sensor.new('One', 1), Sensor.new('Three', 3)]
ActiveRecord::Migrator.new(:up, pass_two).migrate
- refute pass_two[0].went_up
+ assert_not pass_two[0].went_up
assert pass_two[1].went_up
assert pass_two.all? { |x| !x.went_down }
@@ -129,7 +130,7 @@ module ActiveRecord
ActiveRecord::Migrator.new(:down, pass_three).migrate
assert pass_three[0].went_down
- refute pass_three[1].went_down
+ assert_not pass_three[1].went_down
assert pass_three[2].went_down
end
@@ -301,7 +302,7 @@ module ActiveRecord
_, migrator = migrator_class(3)
ActiveRecord::Base.connection.execute("DROP TABLE schema_migrations")
- refute ActiveRecord::Base.connection.table_exists?('schema_migrations')
+ assert_not ActiveRecord::Base.connection.table_exists?('schema_migrations')
migrator.migrate("valid", 1)
assert ActiveRecord::Base.connection.table_exists?('schema_migrations')
end
diff --git a/activerecord/test/cases/modules_test.rb b/activerecord/test/cases/modules_test.rb
index 08b3408665..9124105e6d 100644
--- a/activerecord/test/cases/modules_test.rb
+++ b/activerecord/test/cases/modules_test.rb
@@ -1,6 +1,7 @@
require "cases/helper"
require 'models/company_in_module'
require 'models/shop'
+require 'models/developer'
class ModulesTest < ActiveRecord::TestCase
fixtures :accounts, :companies, :projects, :developers, :collections, :products, :variants
diff --git a/activerecord/test/cases/multiparameter_attributes_test.rb b/activerecord/test/cases/multiparameter_attributes_test.rb
index 1209f5460f..b82409bfbe 100644
--- a/activerecord/test/cases/multiparameter_attributes_test.rb
+++ b/activerecord/test/cases/multiparameter_attributes_test.rb
@@ -5,12 +5,6 @@ require 'models/customer'
class MultiParameterAttributeTest < ActiveRecord::TestCase
fixtures :topics
- def setup
- ActiveRecord::Base.time_zone_aware_attributes = false
- ActiveRecord::Base.default_timezone = :local
- Time.zone = nil
- end
-
def test_multiparameter_attributes_on_date
attributes = { "last_read(1i)" => "2004", "last_read(2i)" => "6", "last_read(3i)" => "24" }
topic = Topic.find(1)
@@ -82,13 +76,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_no_date
@@ -148,13 +144,15 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_missing
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
- "written_on(5i)" => "12", "written_on(6i)" => "02"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ with_timezone_config default: :local do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "12", "written_on(3i)" => "12",
+ "written_on(5i)" => "12", "written_on(6i)" => "02"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 12, 12, 0, 12, 2), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_will_ignore_hour_if_blank
@@ -176,6 +174,7 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
topic.attributes = attributes
assert_nil topic.written_on
end
+
def test_multiparameter_attributes_on_time_with_seconds_will_ignore_date_if_empty
attributes = {
"written_on(1i)" => "", "written_on(2i)" => "", "written_on(3i)" => "",
@@ -187,56 +186,56 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
end
def test_multiparameter_attributes_on_time_with_utc
- ActiveRecord::Base.default_timezone = :utc
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ with_timezone_config default: :utc do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
- assert_equal Time.zone, topic.written_on.time_zone
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 23, 24, 0), topic.written_on
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on.time
+ assert_equal Time.zone, topic.written_on.time_zone
+ end
end
def test_multiparameter_attributes_on_time_with_time_zone_aware_attributes_false
- Time.zone = ActiveSupport::TimeZone[-28800]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :local, aware_attributes: false, zone: -28800 do
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
end
def test_multiparameter_attributes_on_time_with_skip_time_zone_conversion_for_attributes
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
- Topic.skip_time_zone_conversion_for_attributes = [:written_on]
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
- assert_equal false, topic.written_on.respond_to?(:time_zone)
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ Topic.skip_time_zone_conversion_for_attributes = [:written_on]
+ attributes = {
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => "00"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2004, 6, 24, 16, 24, 0), topic.written_on
+ assert_equal false, topic.written_on.respond_to?(:time_zone)
+ end
ensure
Topic.skip_time_zone_conversion_for_attributes = []
end
@@ -244,30 +243,31 @@ class MultiParameterAttributeTest < ActiveRecord::TestCase
# Oracle, and Sybase do not have a TIME datatype.
unless current_adapter?(:OracleAdapter, :SybaseAdapter)
def test_multiparameter_attributes_on_time_only_column_with_time_zone_aware_attributes_does_not_do_time_zone_conversion
- ActiveRecord::Base.time_zone_aware_attributes = true
- ActiveRecord::Base.default_timezone = :utc
- Time.zone = ActiveSupport::TimeZone[-28800]
+ with_timezone_config default: :utc, aware_attributes: true, zone: -28800 do
+ attributes = {
+ "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
+ "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ }
+ topic = Topic.find(1)
+ topic.attributes = attributes
+ assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
+ assert topic.bonus_time.utc?
+ end
+ end
+ end
+
+ def test_multiparameter_attributes_on_time_with_empty_seconds
+ with_timezone_config default: :local do
attributes = {
- "bonus_time(1i)" => "2000", "bonus_time(2i)" => "1", "bonus_time(3i)" => "1",
- "bonus_time(4i)" => "16", "bonus_time(5i)" => "24"
+ "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
+ "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
}
topic = Topic.find(1)
topic.attributes = attributes
- assert_equal Time.utc(2000, 1, 1, 16, 24, 0), topic.bonus_time
- assert topic.bonus_time.utc?
+ assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
end
end
- def test_multiparameter_attributes_on_time_with_empty_seconds
- attributes = {
- "written_on(1i)" => "2004", "written_on(2i)" => "6", "written_on(3i)" => "24",
- "written_on(4i)" => "16", "written_on(5i)" => "24", "written_on(6i)" => ""
- }
- topic = Topic.find(1)
- topic.attributes = attributes
- assert_equal Time.local(2004, 6, 24, 16, 24, 0), topic.written_on
- end
-
def test_multiparameter_attributes_setting_time_attribute
return skip "Oracle does not have TIME data type" if current_adapter? :OracleAdapter
diff --git a/activerecord/test/cases/multiple_db_test.rb b/activerecord/test/cases/multiple_db_test.rb
index 42461e8ecb..2e386a172a 100644
--- a/activerecord/test/cases/multiple_db_test.rb
+++ b/activerecord/test/cases/multiple_db_test.rb
@@ -96,12 +96,12 @@ class MultipleDbTest < ActiveRecord::TestCase
unless in_memory_db?
def test_associations_should_work_when_model_has_no_connection
begin
- ActiveRecord::Model.remove_connection
+ ActiveRecord::Base.remove_connection
assert_nothing_raised ActiveRecord::ConnectionNotEstablished do
College.first.courses.first
end
ensure
- ActiveRecord::Model.establish_connection 'arunit'
+ ActiveRecord::Base.establish_connection 'arunit'
end
end
end
diff --git a/activerecord/test/cases/named_scope_test.rb b/activerecord/test/cases/named_scope_test.rb
deleted file mode 100644
index bd121126e7..0000000000
--- a/activerecord/test/cases/named_scope_test.rb
+++ /dev/null
@@ -1,449 +0,0 @@
-require "cases/helper"
-require 'models/post'
-require 'models/topic'
-require 'models/comment'
-require 'models/reply'
-require 'models/author'
-require 'models/developer'
-
-class NamedScopeTest < ActiveRecord::TestCase
- fixtures :posts, :authors, :topics, :comments, :author_addresses
-
- def test_implements_enumerable
- assert !Topic.all.empty?
-
- assert_equal Topic.all.to_a, Topic.base
- assert_equal Topic.all.to_a, Topic.base.to_a
- assert_equal Topic.first, Topic.base.first
- assert_equal Topic.all.to_a, Topic.base.map { |i| i }
- end
-
- def test_found_items_are_cached
- all_posts = Topic.base
-
- assert_queries(1) do
- all_posts.collect
- all_posts.collect
- end
- end
-
- def test_reload_expires_cache_of_found_items
- all_posts = Topic.base
- all_posts.to_a
-
- new_post = Topic.create!
- assert !all_posts.include?(new_post)
- assert all_posts.reload.include?(new_post)
- end
-
- def test_delegates_finds_and_calculations_to_the_base_class
- assert !Topic.all.empty?
-
- assert_equal Topic.all.to_a, Topic.base.to_a
- assert_equal Topic.first, Topic.base.first
- assert_equal Topic.count, Topic.base.count
- assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
- end
-
- def test_method_missing_priority_when_delegating
- klazz = Class.new(ActiveRecord::Base) do
- self.table_name = "topics"
- scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) }
- scope :to, Proc.new { where('written_on <= ?', Time.now) }
- end
- assert_equal klazz.to.since.to_a, klazz.since.to.to_a
- end
-
- def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
- assert Topic.approved.respond_to?(:limit)
- assert Topic.approved.respond_to?(:count)
- assert Topic.approved.respond_to?(:length)
- end
-
- def test_respond_to_respects_include_private_parameter
- assert !Topic.approved.respond_to?(:tables_in_string)
- assert Topic.approved.respond_to?(:tables_in_string, true)
- end
-
- def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
- assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
-
- assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved
- assert_equal Topic.where(:approved => true).count, Topic.approved.count
- end
-
- def test_scopes_with_string_name_can_be_composed
- # NOTE that scopes defined with a string as a name worked on their own
- # but when called on another scope the other scope was completely replaced
- assert_equal Topic.replied.approved, Topic.replied.approved_as_string
- end
-
- def test_scopes_are_composable
- assert_equal((approved = Topic.all.merge!(:where => {:approved => true}).to_a), Topic.approved)
- assert_equal((replied = Topic.all.merge!(:where => 'replies_count > 0').to_a), Topic.replied)
- assert !(approved == replied)
- assert !(approved & replied).empty?
-
- assert_equal approved & replied, Topic.approved.replied
- end
-
- def test_procedural_scopes
- topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on)
- topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on)
- assert_not_equal topics_written_before_the_second, topics_written_before_the_third
-
- assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on)
- assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
- end
-
- def test_procedural_scopes_returning_nil
- all_topics = Topic.all
-
- assert_equal all_topics, Topic.written_before(nil)
- end
-
- def test_scope_with_object
- objects = Topic.with_object
- assert_operator objects.length, :>, 0
- assert objects.all?(&:approved?), 'all objects should be approved'
- end
-
- def test_has_many_associations_have_access_to_scopes
- assert_not_equal Post.containing_the_letter_a, authors(:david).posts
- assert !Post.containing_the_letter_a.empty?
-
- assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.containing_the_letter_a
- end
-
- def test_scope_with_STI
- assert_equal 3,Post.containing_the_letter_a.count
- assert_equal 1,SpecialPost.containing_the_letter_a.count
- end
-
- def test_has_many_through_associations_have_access_to_scopes
- assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
- assert !Comment.containing_the_letter_e.empty?
-
- assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
- end
-
- def test_scopes_honor_current_scopes_from_when_defined
- assert !Post.ranked_by_comments.limit_by(5).empty?
- assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
- assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
- assert_not_equal Post.top(5), authors(:david).posts.top(5)
- # Oracle sometimes sorts differently if WHERE condition is changed
- assert_equal authors(:david).posts.ranked_by_comments.limit_by(5).to_a.sort_by(&:id), authors(:david).posts.top(5).to_a.sort_by(&:id)
- assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
- end
-
- def test_active_records_have_scope_named__all__
- assert !Topic.all.empty?
-
- assert_equal Topic.all.to_a, Topic.base
- end
-
- def test_active_records_have_scope_named__scoped__
- scope = Topic.where("content LIKE '%Have%'")
- assert !scope.empty?
-
- assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
- end
-
- def test_first_and_last_should_allow_integers_for_limit
- assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
- assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
- end
-
- def test_first_and_last_should_not_use_query_when_results_are_loaded
- topics = Topic.base
- topics.reload # force load
- assert_no_queries do
- topics.first
- topics.last
- end
- end
-
- def test_empty_should_not_load_results
- topics = Topic.base
- assert_queries(2) do
- topics.empty? # use count query
- topics.collect # force load
- topics.empty? # use loaded (no query)
- end
- end
-
- def test_any_should_not_load_results
- topics = Topic.base
- assert_queries(2) do
- topics.any? # use count query
- topics.collect # force load
- topics.any? # use loaded (no query)
- end
- end
-
- def test_any_should_call_proxy_found_if_using_a_block
- topics = Topic.base
- assert_queries(1) do
- topics.expects(:empty?).never
- topics.any? { true }
- end
- end
-
- def test_any_should_not_fire_query_if_scope_loaded
- topics = Topic.base
- topics.collect # force load
- assert_no_queries { assert topics.any? }
- end
-
- def test_model_class_should_respond_to_any
- assert Topic.any?
- Topic.delete_all
- assert !Topic.any?
- end
-
- def test_many_should_not_load_results
- topics = Topic.base
- assert_queries(2) do
- topics.many? # use count query
- topics.collect # force load
- topics.many? # use loaded (no query)
- end
- end
-
- def test_many_should_call_proxy_found_if_using_a_block
- topics = Topic.base
- assert_queries(1) do
- topics.expects(:size).never
- topics.many? { true }
- end
- end
-
- def test_many_should_not_fire_query_if_scope_loaded
- topics = Topic.base
- topics.collect # force load
- assert_no_queries { assert topics.many? }
- end
-
- def test_many_should_return_false_if_none_or_one
- topics = Topic.base.where(:id => 0)
- assert !topics.many?
- topics = Topic.base.where(:id => 1)
- assert !topics.many?
- end
-
- def test_many_should_return_true_if_more_than_one
- assert Topic.base.many?
- end
-
- def test_model_class_should_respond_to_many
- Topic.delete_all
- assert !Topic.many?
- Topic.create!
- assert !Topic.many?
- Topic.create!
- assert Topic.many?
- end
-
- def test_should_build_on_top_of_scope
- topic = Topic.approved.build({})
- assert topic.approved
- end
-
- def test_should_build_new_on_top_of_scope
- topic = Topic.approved.new
- assert topic.approved
- end
-
- def test_should_create_on_top_of_scope
- topic = Topic.approved.create({})
- assert topic.approved
- end
-
- def test_should_create_with_bang_on_top_of_scope
- topic = Topic.approved.create!({})
- assert topic.approved
- end
-
- def test_should_build_on_top_of_chained_scopes
- topic = Topic.approved.by_lifo.build({})
- assert topic.approved
- assert_equal 'lifo', topic.author_name
- end
-
- def test_find_all_should_behave_like_select
- assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
- end
-
- def test_rand_should_select_a_random_object_from_proxy
- assert_kind_of Topic, Topic.approved.sample
- end
-
- def test_should_use_where_in_query_for_scope
- assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set
- end
-
- def test_size_should_use_count_when_results_are_not_loaded
- topics = Topic.base
- assert_queries(1) do
- assert_sql(/COUNT/i) { topics.size }
- end
- end
-
- def test_size_should_use_length_when_results_are_loaded
- topics = Topic.base
- topics.reload # force load
- assert_no_queries do
- topics.size # use loaded (no query)
- end
- end
-
- def test_should_not_duplicates_where_values
- where_values = Topic.where("1=1").scope_with_lambda.where_values
- assert_equal ["1=1"], where_values
- end
-
- def test_chaining_with_duplicate_joins
- join = "INNER JOIN comments ON comments.post_id = posts.id"
- post = Post.find(1)
- assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
- end
-
- def test_chaining_should_use_latest_conditions_when_creating
- post = Topic.rejected.new
- assert !post.approved?
-
- post = Topic.rejected.approved.new
- assert post.approved?
-
- post = Topic.approved.rejected.new
- assert !post.approved?
-
- post = Topic.approved.rejected.approved.new
- assert post.approved?
- end
-
- def test_chaining_should_use_latest_conditions_when_searching
- # Normal hash conditions
- assert_equal Topic.where(:approved => true).to_a, Topic.rejected.approved.to_a
- assert_equal Topic.where(:approved => false).to_a, Topic.approved.rejected.to_a
-
- # Nested hash conditions with same keys
- assert_equal [posts(:sti_comments)], Post.with_special_comments.with_very_special_comments.to_a
-
- # Nested hash conditions with different keys
- assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
- end
-
- def test_scopes_batch_finders
- assert_equal 3, Topic.approved.count
-
- assert_queries(4) do
- Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
- end
-
- assert_queries(2) do
- Topic.approved.find_in_batches(:batch_size => 2) do |group|
- group.each {|t| assert t.approved? }
- end
- end
- end
-
- def test_table_names_for_chaining_scopes_with_and_without_table_name_included
- assert_nothing_raised do
- Comment.for_first_post.for_first_author.to_a
- end
- end
-
- def test_scopes_on_relations
- # Topic.replied
- approved_topics = Topic.all.approved.order('id DESC')
- assert_equal topics(:fourth), approved_topics.first
-
- replied_approved_topics = approved_topics.replied
- assert_equal topics(:third), replied_approved_topics.first
- end
-
- def test_index_on_scope
- approved = Topic.approved.order('id ASC')
- assert_equal topics(:second), approved[0]
- assert approved.loaded?
- end
-
- def test_nested_scopes_queries_size
- assert_queries(1) do
- Topic.approved.by_lifo.replied.written_before(Time.now).to_a
- end
- end
-
- # Note: these next two are kinda odd because they are essentially just testing that the
- # query cache works as it should, but they are here for legacy reasons as they was previously
- # a separate cache on association proxies, and these show that that is not necessary.
- def test_scopes_are_cached_on_associations
- post = posts(:welcome)
-
- Post.cache do
- assert_queries(1) { post.comments.containing_the_letter_e.to_a }
- assert_no_queries { post.comments.containing_the_letter_e.to_a }
- end
- end
-
- def test_scopes_with_arguments_are_cached_on_associations
- post = posts(:welcome)
-
- Post.cache do
- one = assert_queries(1) { post.comments.limit_by(1).to_a }
- assert_equal 1, one.size
-
- two = assert_queries(1) { post.comments.limit_by(2).to_a }
- assert_equal 2, two.size
-
- assert_no_queries { post.comments.limit_by(1).to_a }
- assert_no_queries { post.comments.limit_by(2).to_a }
- end
- end
-
- def test_scopes_to_get_newest
- post = posts(:welcome)
- old_last_comment = post.comments.newest
- new_comment = post.comments.create(:body => "My new comment")
- assert_equal new_comment, post.comments.newest
- assert_not_equal old_last_comment, post.comments.newest
- end
-
- def test_scopes_are_reset_on_association_reload
- post = posts(:welcome)
-
- [:destroy_all, :reset, :delete_all].each do |method|
- before = post.comments.containing_the_letter_e
- post.association(:comments).send(method)
- assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
- end
- end
-
- def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
- assert_nothing_raised do
- require "models/without_table"
- end
- end
-
- def test_eager_scopes_are_deprecated
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
-
- assert_deprecated do
- klass.scope :welcome_2, klass.where(:id => posts(:welcome).id)
- end
- assert_equal [posts(:welcome).title], klass.welcome_2.map(&:title)
- end
-
- def test_eager_default_scope_relations_are_deprecated
- klass = Class.new(ActiveRecord::Base)
- klass.table_name = 'posts'
-
- assert_deprecated do
- klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
- end
- assert_equal [posts(:welcome).title], klass.all.map(&:title)
- end
-end
diff --git a/activerecord/test/cases/nested_attributes_test.rb b/activerecord/test/cases/nested_attributes_test.rb
index fe9eddbdec..2f89699df7 100644
--- a/activerecord/test/cases/nested_attributes_test.rb
+++ b/activerecord/test/cases/nested_attributes_test.rb
@@ -79,10 +79,10 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_should_disable_allow_destroy_by_default
Pirate.accepts_nested_attributes_for :ship
- pirate = Pirate.create!(:catchphrase => "Don' botharrr talkin' like one, savvy?")
- ship = pirate.create_ship(:name => 'Nights Dirty Lightning')
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ ship = pirate.create_ship(name: 'Nights Dirty Lightning')
- pirate.update_attributes(:ship_attributes => { '_destroy' => true, :id => ship.id })
+ pirate.update(ship_attributes: { '_destroy' => true, :id => ship.id })
assert_nothing_raised(ActiveRecord::RecordNotFound) { pirate.ship.reload }
end
@@ -125,33 +125,47 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_reject_if_with_a_proc_which_returns_true_always_for_has_one
Pirate.accepts_nested_attributes_for :ship, :reject_if => proc {|attributes| true }
- pirate = Pirate.new(:catchphrase => "Stop wastin' me time")
- ship = pirate.create_ship(:name => 's1')
- pirate.update_attributes({:ship_attributes => { :name => 's2', :id => ship.id } })
+ pirate = Pirate.new(catchphrase: "Stop wastin' me time")
+ ship = pirate.create_ship(name: 's1')
+ pirate.update({ship_attributes: { name: 's2', id: ship.id } })
assert_equal 's1', ship.reload.name
end
+ def test_reuse_already_built_new_record
+ pirate = Pirate.new
+ ship_built_first = pirate.build_ship
+ pirate.ship_attributes = { name: 'Ship 1' }
+ assert_equal ship_built_first.object_id, pirate.ship.object_id
+ end
+
+ def test_do_not_allow_assigning_foreign_key_when_reusing_existing_new_record
+ pirate = Pirate.create!(catchphrase: "Don' botharrr talkin' like one, savvy?")
+ pirate.build_ship
+ pirate.ship_attributes = { name: 'Ship 1', pirate_id: pirate.id + 1 }
+ assert_equal pirate.id, pirate.ship.pirate_id
+ end
+
def test_reject_if_with_a_proc_which_returns_true_always_for_has_many
Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }
- man = Man.create(:name => "John")
- interest = man.interests.create(:topic => 'photography')
- man.update_attributes({:interests_attributes => { :topic => 'gardening', :id => interest.id } })
+ man = Man.create(name: "John")
+ interest = man.interests.create(topic: 'photography')
+ man.update({interests_attributes: { topic: 'gardening', id: interest.id } })
assert_equal 'photography', interest.reload.topic
end
def test_destroy_works_independent_of_reject_if
Man.accepts_nested_attributes_for :interests, :reject_if => proc {|attributes| true }, :allow_destroy => true
- man = Man.create(:name => "Jon")
- interest = man.interests.create(:topic => 'the ladies')
- man.update_attributes({:interests_attributes => { :_destroy => "1", :id => interest.id } })
+ man = Man.create(name: "Jon")
+ interest = man.interests.create(topic: 'the ladies')
+ man.update({interests_attributes: { _destroy: "1", id: interest.id } })
assert man.reload.interests.empty?
end
def test_has_many_association_updating_a_single_record
Man.accepts_nested_attributes_for(:interests)
- man = Man.create(:name => 'John')
- interest = man.interests.create(:topic => 'photography')
- man.update_attributes({:interests_attributes => {:topic => 'gardening', :id => interest.id}})
+ man = Man.create(name: 'John')
+ interest = man.interests.create(topic: 'photography')
+ man.update({interests_attributes: {topic: 'gardening', id: interest.id}})
assert_equal 'gardening', interest.reload.topic
end
@@ -167,7 +181,7 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
def test_first_and_array_index_zero_methods_return_the_same_value_when_nested_attributes_are_set_to_update_existing_record
Man.accepts_nested_attributes_for(:interests)
man = Man.create(:name => "John")
- interest = man.interests.create :topic => 'gardning'
+ interest = man.interests.create :topic => 'gardening'
man = Man.find man.id
man.interests_attributes = [{:id => interest.id, :topic => 'gardening'}]
assert_equal man.interests.first.topic, man.interests[0].topic
@@ -185,6 +199,17 @@ class TestNestedAttributesInGeneral < ActiveRecord::TestCase
assert_equal "James", mean_pirate.parrot.name
assert_equal "blue", mean_pirate.parrot.color
end
+
+ def test_accepts_nested_attributes_for_can_be_overridden_in_subclasses
+ Pirate.accepts_nested_attributes_for(:parrot)
+
+ mean_pirate_class = Class.new(Pirate) do
+ accepts_nested_attributes_for :parrot
+ end
+ mean_pirate = mean_pirate_class.new
+ mean_pirate.parrot_attributes = { :name => "James" }
+ assert_equal "James", mean_pirate.parrot.name
+ end
end
class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@@ -273,8 +298,8 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
@pirate.ship.destroy
[1, '1', true, 'true'].each do |truth|
- ship = @pirate.reload.create_ship(:name => 'Mister Pablo')
- @pirate.update_attributes(:ship_attributes => { :id => ship.id, :_destroy => truth })
+ ship = @pirate.reload.create_ship(name: 'Mister Pablo')
+ @pirate.update(ship_attributes: { id: ship.id, _destroy: truth })
assert_nil @pirate.reload.ship
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(ship.id) }
@@ -283,7 +308,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
[nil, '0', 0, 'false', false].each do |not_truth|
- @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => not_truth })
+ @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: not_truth })
assert_equal @ship, @pirate.reload.ship
end
@@ -292,7 +317,7 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
Pirate.accepts_nested_attributes_for :ship, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
- @pirate.update_attributes(:ship_attributes => { :id => @pirate.ship.id, :_destroy => '1' })
+ @pirate.update(ship_attributes: { id: @pirate.ship.id, _destroy: '1' })
assert_equal @ship, @pirate.reload.ship
@@ -300,14 +325,14 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.ship_attributes = HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
+ @pirate.ship_attributes = ActiveSupport::HashWithIndifferentAccess.new(:id => @ship.id, :name => 'Davy Jones Gold Dagger')
assert @pirate.ship.persisted?
assert_equal 'Davy Jones Gold Dagger', @pirate.ship.name
end
- def test_should_work_with_update_attributes_as_well
- @pirate.update_attributes({ :catchphrase => 'Arr', :ship_attributes => { :id => @ship.id, :name => 'Mister Pablo' } })
+ def test_should_work_with_update_as_well
+ @pirate.update({ catchphrase: 'Arr', ship_attributes: { id: @ship.id, name: 'Mister Pablo' } })
@pirate.reload
assert_equal 'Arr', @pirate.catchphrase
@@ -331,22 +356,22 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
end
def test_should_accept_update_only_option
- @pirate.update_attributes(:update_only_ship_attributes => { :id => @pirate.ship.id, :name => 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { id: @pirate.ship.id, name: 'Mayflower' })
end
def test_should_create_new_model_when_nothing_is_there_and_update_only_is_true
@ship.delete
- @pirate.reload.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
+ @pirate.reload.update(update_only_ship_attributes: { name: 'Mayflower' })
assert_not_nil @pirate.ship
end
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower' })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower' })
assert_equal 'Mayflower', @ship.reload.name
assert_equal @ship, @pirate.reload.ship
@@ -354,9 +379,9 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id })
assert_equal 'Mayflower', @ship.reload.name
assert_equal @ship, @pirate.reload.ship
@@ -365,9 +390,9 @@ class TestNestedAttributesOnAHasOneAssociation < ActiveRecord::TestCase
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
Pirate.accepts_nested_attributes_for :update_only_ship, :update_only => true, :allow_destroy => true
@ship.delete
- @ship = @pirate.create_update_only_ship(:name => 'Nights Dirty Lightning')
+ @ship = @pirate.create_update_only_ship(name: 'Nights Dirty Lightning')
- @pirate.update_attributes(:update_only_ship_attributes => { :name => 'Mayflower', :id => @ship.id, :_destroy => true })
+ @pirate.update(update_only_ship_attributes: { name: 'Mayflower', id: @ship.id, _destroy: true })
assert_nil @pirate.reload.ship
assert_raise(ActiveRecord::RecordNotFound) { Ship.find(@ship.id) }
@@ -457,31 +482,29 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_destroy_an_existing_record_if_there_is_a_matching_id_and_destroy_is_truthy
@ship.pirate.destroy
[1, '1', true, 'true'].each do |truth|
- pirate = @ship.reload.create_pirate(:catchphrase => 'Arr')
- @ship.update_attributes(:pirate_attributes => { :id => pirate.id, :_destroy => truth })
+ pirate = @ship.reload.create_pirate(catchphrase: 'Arr')
+ @ship.update(pirate_attributes: { id: pirate.id, _destroy: truth })
assert_raise(ActiveRecord::RecordNotFound) { pirate.reload }
end
end
def test_should_unset_association_when_an_existing_record_is_destroyed
- @ship.reload
original_pirate_id = @ship.pirate.id
- @ship.attributes = {:pirate_attributes => {:id => @ship.pirate.id, :_destroy => true}}
- @ship.save!
+ @ship.update! pirate_attributes: { id: @ship.pirate.id, _destroy: true }
- assert_empty Pirate.where(["id = ?", original_pirate_id])
+ assert_empty Pirate.where(id: original_pirate_id)
assert_nil @ship.pirate_id
assert_nil @ship.pirate
@ship.reload
- assert_empty Pirate.where(["id = ?", original_pirate_id])
+ assert_empty Pirate.where(id: original_pirate_id)
assert_nil @ship.pirate_id
assert_nil @ship.pirate
end
def test_should_not_destroy_an_existing_record_if_destroy_is_not_truthy
[nil, '0', 0, 'false', false].each do |not_truth|
- @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => not_truth })
+ @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: not_truth })
assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
end
end
@@ -489,14 +512,14 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_not_destroy_an_existing_record_if_allow_destroy_is_false
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => false, :reject_if => proc { |attributes| attributes.empty? }
- @ship.update_attributes(:pirate_attributes => { :id => @ship.pirate.id, :_destroy => '1' })
+ @ship.update(pirate_attributes: { id: @ship.pirate.id, _destroy: '1' })
assert_nothing_raised(ActiveRecord::RecordNotFound) { @ship.pirate.reload }
-
+ ensure
Ship.accepts_nested_attributes_for :pirate, :allow_destroy => true, :reject_if => proc { |attributes| attributes.empty? }
end
- def test_should_work_with_update_attributes_as_well
- @ship.update_attributes({ :name => 'Mister Pablo', :pirate_attributes => { :catchphrase => 'Arr' } })
+ def test_should_work_with_update_as_well
+ @ship.update({ name: 'Mister Pablo', pirate_attributes: { catchphrase: 'Arr' } })
@ship.reload
assert_equal 'Mister Pablo', @ship.name
@@ -525,18 +548,18 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_update_existing_when_update_only_is_true_and_no_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr' })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr' })
assert_equal 'Arr', @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
end
def test_should_update_existing_when_update_only_is_true_and_id_is_given
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id })
assert_equal 'Arr', @pirate.reload.catchphrase
assert_equal @pirate, @ship.reload.update_only_pirate
@@ -545,9 +568,9 @@ class TestNestedAttributesOnABelongsToAssociation < ActiveRecord::TestCase
def test_should_destroy_existing_when_update_only_is_true_and_id_is_given_and_is_marked_for_destruction
Ship.accepts_nested_attributes_for :update_only_pirate, :update_only => true, :allow_destroy => true
@pirate.delete
- @pirate = @ship.create_update_only_pirate(:catchphrase => 'Aye')
+ @pirate = @ship.create_update_only_pirate(catchphrase: 'Aye')
- @ship.update_attributes(:update_only_pirate_attributes => { :catchphrase => 'Arr', :id => @pirate.id, :_destroy => true })
+ @ship.update(update_only_pirate_attributes: { catchphrase: 'Arr', id: @pirate.id, _destroy: true })
assert_raise(ActiveRecord::RecordNotFound) { @pirate.reload }
@@ -573,7 +596,7 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_take_a_hash_with_string_keys_and_assign_the_attributes_to_the_associated_models
@alternate_params[association_getter].stringify_keys!
- @pirate.update_attributes @alternate_params
+ @pirate.update @alternate_params
assert_equal ['Grace OMalley', 'Privateers Greed'], [@child_1.reload.name, @child_2.reload.name]
end
@@ -584,7 +607,7 @@ module NestedAttributesOnACollectionAssociationTests
end
def test_should_also_work_with_a_HashWithIndifferentAccess
- @pirate.send(association_setter, HashWithIndifferentAccess.new('foo' => HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
+ @pirate.send(association_setter, ActiveSupport::HashWithIndifferentAccess.new('foo' => ActiveSupport::HashWithIndifferentAccess.new(:id => @child_1.id, :name => 'Grace OMalley')))
@pirate.save
assert_equal 'Grace OMalley', @child_1.reload.name
end
@@ -619,10 +642,10 @@ module NestedAttributesOnACollectionAssociationTests
def test_should_refresh_saved_records_when_not_overwriting_unsaved_updates
@pirate.reload
- record = @pirate.class.reflect_on_association(@association_name).klass.new(:name => 'Grace OMalley')
+ record = @pirate.class.reflect_on_association(@association_name).klass.new(name: 'Grace OMalley')
@pirate.send(@association_name) << record
record.save!
- @pirate.send(@association_name).last.update_attributes!(:name => 'Polly')
+ @pirate.send(@association_name).last.update!(name: 'Polly')
assert_equal 'Polly', @pirate.send(@association_name).send(:load_target).last.name
end
@@ -709,17 +732,17 @@ module NestedAttributesOnACollectionAssociationTests
end
end
- def test_should_work_with_update_attributes_as_well
- @pirate.update_attributes(:catchphrase => 'Arr',
+ def test_should_work_with_update_as_well
+ @pirate.update(catchphrase: 'Arr',
association_getter => { 'foo' => { :id => @child_1.id, :name => 'Grace OMalley' }})
assert_equal 'Grace OMalley', @child_1.reload.name
end
def test_should_update_existing_records_and_add_new_ones_that_have_no_id
- @alternate_params[association_getter]['baz'] = { :name => 'Buccaneers Servant' }
+ @alternate_params[association_getter]['baz'] = { name: 'Buccaneers Servant' }
assert_difference('@pirate.send(@association_name).count', +1) do
- @pirate.update_attributes @alternate_params
+ @pirate.update @alternate_params
end
assert_equal ['Grace OMalley', 'Privateers Greed', 'Buccaneers Servant'].to_set, @pirate.reload.send(@association_name).map(&:name).to_set
end
@@ -741,7 +764,7 @@ module NestedAttributesOnACollectionAssociationTests
[nil, '', '0', 0, 'false', false].each do |false_variable|
@alternate_params[association_getter]['foo']['_destroy'] = false_variable
assert_no_difference('@pirate.send(@association_name).count') do
- @pirate.update_attributes(@alternate_params)
+ @pirate.update(@alternate_params)
end
end
end
@@ -774,30 +797,12 @@ module NestedAttributesOnACollectionAssociationTests
end
end
- def test_validate_presence_of_parent_fails_without_inverse_of
- Man.accepts_nested_attributes_for(:interests)
- Man.reflect_on_association(:interests).options.delete(:inverse_of)
- Interest.reflect_on_association(:man).options.delete(:inverse_of)
-
- repair_validations(Interest) do
- Interest.validates_presence_of(:man)
- assert_no_difference ['Man.count', 'Interest.count'] do
- man = Man.create(:name => 'John',
- :interests_attributes => [{:topic=>'Cars'}, {:topic=>'Sports'}])
- assert !man.errors[:"interests.man"].empty?
- end
- end
- ensure
- Man.reflect_on_association(:interests).options[:inverse_of] = :man
- Interest.reflect_on_association(:man).options[:inverse_of] = :interests
- end
-
def test_can_use_symbols_as_object_identifier
@pirate.attributes = { :parrots_attributes => { :foo => { :name => 'Lovely Day' }, :bar => { :name => 'Blown Away' } } }
assert_nothing_raised(NoMethodError) { @pirate.save! }
end
- def test_numeric_colum_changes_from_zero_to_no_empty_string
+ def test_numeric_column_changes_from_zero_to_no_empty_string
Man.accepts_nested_attributes_for(:interests)
repair_validations(Interest) do
@@ -805,7 +810,7 @@ module NestedAttributesOnACollectionAssociationTests
man = Man.create(name: 'John')
interest = man.interests.create(topic: 'bar', zine_id: 0)
assert interest.save
- assert !man.update_attributes({interests_attributes: { id: interest.id, zine_id: 'foo' }})
+ assert !man.update({interests_attributes: { id: interest.id, zine_id: 'foo' }})
end
end
@@ -936,18 +941,18 @@ class TestNestedAttributesWithNonStandardPrimaryKeys < ActiveRecord::TestCase
end
def test_should_update_existing_records_with_non_standard_primary_key
- @owner.update_attributes(@params)
+ @owner.update(@params)
assert_equal ['Foo', 'Bar'], @owner.pets.map(&:name)
end
- def test_attr_accessor_of_child_should_be_value_provided_during_update_attributes
+ def test_attr_accessor_of_child_should_be_value_provided_during_update
@owner = owners(:ashley)
@pet1 = pets(:chew)
attributes = {:pets_attributes => { "1"=> { :id => @pet1.id,
:name => "Foo2",
:current_user => "John",
:_destroy=>true }}}
- @owner.update_attributes(attributes)
+ @owner.update(attributes)
assert_equal 'John', Pet.after_destroy_output
end
diff --git a/activerecord/test/cases/nested_attributes_with_callbacks_test.rb b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
new file mode 100644
index 0000000000..43a69928b6
--- /dev/null
+++ b/activerecord/test/cases/nested_attributes_with_callbacks_test.rb
@@ -0,0 +1,144 @@
+require "cases/helper"
+require "models/pirate"
+require "models/bird"
+
+class NestedAttributesWithCallbacksTest < ActiveRecord::TestCase
+ Pirate.has_many(:birds_with_add_load,
+ :class_name => "Bird",
+ :before_add => proc { |p,b|
+ @@add_callback_called << b
+ p.birds_with_add_load.to_a
+ })
+ Pirate.has_many(:birds_with_add,
+ :class_name => "Bird",
+ :before_add => proc { |p,b| @@add_callback_called << b })
+
+ Pirate.accepts_nested_attributes_for(:birds_with_add_load,
+ :birds_with_add,
+ :allow_destroy => true)
+
+ def setup
+ @@add_callback_called = []
+ @pirate = Pirate.new.tap do |pirate|
+ pirate.catchphrase = "Don't call me!"
+ pirate.birds_attributes = [{:name => 'Bird1'},{:name => 'Bird2'}]
+ pirate.save!
+ end
+ @birds = @pirate.birds.to_a
+ end
+
+ def bird_to_update
+ @birds[0]
+ end
+
+ def bird_to_destroy
+ @birds[1]
+ end
+
+ def existing_birds_attributes
+ @birds.map do |bird|
+ bird.attributes.slice("id","name")
+ end
+ end
+
+ def new_birds
+ @pirate.birds_with_add.to_a - @birds
+ end
+
+ def new_bird_attributes
+ [{'name' => "New Bird"}]
+ end
+
+ def destroy_bird_attributes
+ [{'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ def update_new_and_destroy_bird_attributes
+ [{'id' => @birds[0].id.to_s, 'name' => 'New Name'},
+ {'name' => "New Bird"},
+ {'id' => bird_to_destroy.id.to_s, "_destroy" => true}]
+ end
+
+ # Characterizing when :before_add callback is called
+ test ":before_add called for new bird when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ test ":before_add called for new bird when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = new_bird_attributes
+ assert_new_bird_with_callback_called
+ end
+
+ def assert_new_bird_with_callback_called
+ assert_equal(1, new_birds.size)
+ assert_equal(new_birds, @@add_callback_called)
+ end
+
+ test ":before_add not called for identical assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for identical assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = existing_birds_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for destroy assignment when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ test ":before_add not called for deletion assignment when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = destroy_bird_attributes
+ assert_callbacks_not_called
+ end
+
+ def assert_callbacks_not_called
+ assert_empty new_birds
+ assert_empty @@add_callback_called
+ end
+
+ # Ensuring that the records in the association target are updated,
+ # whether the association is loaded before or not
+ test "Assignment updates records in target when not loaded" do
+ assert_not @pirate.birds_with_add.loaded?
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test "Assignment updates records in target when loaded" do
+ @pirate.birds_with_add.load_target
+ @pirate.birds_with_add_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add)
+ end
+
+ test("Assignment updates records in target when not loaded" +
+ " and callback loads target") do
+ assert_not @pirate.birds_with_add_load.loaded?
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ test("Assignment updates records in target when loaded" +
+ " and callback loads target") do
+ @pirate.birds_with_add_load.load_target
+ @pirate.birds_with_add_load_attributes = update_new_and_destroy_bird_attributes
+ assert_assignment_affects_records_in_target(:birds_with_add_load)
+ end
+
+ def assert_assignment_affects_records_in_target(association_name)
+ association = @pirate.send(association_name)
+ assert association.detect {|b| b == bird_to_update }.name_changed?,
+ 'Update record not updated'
+ assert association.detect {|b| b == bird_to_destroy }.marked_for_destruction?,
+ 'Destroy record not marked for destruction'
+ end
+end
diff --git a/activerecord/test/cases/persistence_test.rb b/activerecord/test/cases/persistence_test.rb
index b5f32a57b2..6cd3e2154e 100644
--- a/activerecord/test/cases/persistence_test.rb
+++ b/activerecord/test/cases/persistence_test.rb
@@ -12,14 +12,13 @@ require 'models/minimalistic'
require 'models/warehouse_thing'
require 'models/parrot'
require 'models/minivan'
+require 'models/owner'
require 'models/person'
require 'models/pet'
require 'models/toy'
require 'rexml/document'
-require 'active_support/core_ext/exception'
-
-class PersistencesTest < ActiveRecord::TestCase
+class PersistenceTest < ActiveRecord::TestCase
fixtures :topics, :companies, :developers, :projects, :computers, :accounts, :minimalistics, 'warehouse-things', :authors, :categorizations, :categories, :posts, :minivans, :pets, :toys
# Oracle UPDATE does not support ORDER BY
@@ -140,6 +139,19 @@ class PersistencesTest < ActiveRecord::TestCase
end
end
+ def test_becomes
+ assert_kind_of Reply, topics(:first).becomes(Reply)
+ assert_equal "The First Topic", topics(:first).becomes(Reply).title
+ end
+
+ def test_becomes_includes_errors
+ company = Company.new(:name => nil)
+ assert !company.valid?
+ original_errors = company.errors
+ client = company.becomes(Client)
+ assert_equal original_errors, client.errors
+ end
+
def test_delete_many
original_count = Topic.count
Topic.delete(deleting = [1, 2])
@@ -243,20 +255,20 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal "David", topic2.author_name
end
- def test_update
+ def test_update_object
topic = Topic.new
topic.title = "Another New Topic"
topic.written_on = "2003-12-12 23:23:00"
topic.save
- topicReloaded = Topic.find(topic.id)
- assert_equal("Another New Topic", topicReloaded.title)
+ topic_reloaded = Topic.find(topic.id)
+ assert_equal("Another New Topic", topic_reloaded.title)
- topicReloaded.title = "Updated topic"
- topicReloaded.save
+ topic_reloaded.title = "Updated topic"
+ topic_reloaded.save
- topicReloadedAgain = Topic.find(topic.id)
+ topic_reloaded_again = Topic.find(topic.id)
- assert_equal("Updated topic", topicReloadedAgain.title)
+ assert_equal("Updated topic", topic_reloaded_again.title)
end
def test_update_columns_not_equal_attributes
@@ -264,12 +276,12 @@ class PersistencesTest < ActiveRecord::TestCase
topic.title = "Still another topic"
topic.save
- topicReloaded = Topic.allocate
- topicReloaded.init_with(
+ topic_reloaded = Topic.allocate
+ topic_reloaded.init_with(
'attributes' => topic.attributes.merge('does_not_exist' => 'test')
)
- topicReloaded.title = 'A New Topic'
- assert_nothing_raised { topicReloaded.save }
+ topic_reloaded.title = 'A New Topic'
+ assert_nothing_raised { topic_reloaded.save }
end
def test_update_for_record_with_only_primary_key
@@ -280,12 +292,39 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_sti_type
assert_instance_of Reply, topics(:second)
- topic = topics(:second).becomes(Topic)
+ topic = topics(:second).becomes!(Topic)
assert_instance_of Topic, topic
topic.save!
assert_instance_of Topic, Topic.find(topic.id)
end
+ def test_preserve_original_sti_type
+ reply = topics(:second)
+ assert_equal "Reply", reply.type
+
+ topic = reply.becomes(Topic)
+ assert_equal "Reply", reply.type
+
+ assert_instance_of Topic, topic
+ assert_equal "Reply", topic.type
+ end
+
+ def test_update_after_create
+ klass = Class.new(Topic) do
+ def self.name; 'Topic'; end
+ after_create do
+ update_attribute("author_name", "David")
+ end
+ end
+ topic = klass.new
+ topic.title = "Another New Topic"
+ topic.save
+
+ topic_reloaded = Topic.find(topic.id)
+ assert_equal("Another New Topic", topic_reloaded.title)
+ assert_equal("David", topic_reloaded.author_name)
+ end
+
def test_delete
topic = Topic.find(1)
assert_equal topic, topic.delete, 'topic.delete did not return self'
@@ -354,7 +393,7 @@ class PersistencesTest < ActiveRecord::TestCase
client.delete
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_destroy_new_record
@@ -368,7 +407,7 @@ class PersistencesTest < ActiveRecord::TestCase
client.destroy
assert client.frozen?
assert_kind_of Firm, client.firm
- assert_raise(ActiveSupport::FrozenObjectError) { client.name = "something else" }
+ assert_raise(RuntimeError) { client.name = "something else" }
end
def test_update_attribute
@@ -380,10 +419,6 @@ class PersistencesTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attribute_does_not_choke_on_nil
- assert Topic.find(1).update_attributes(nil)
- end
-
def test_update_attribute_for_readonly_attribute
minivan = Minivan.find('m1')
assert_raises(ActiveRecord::ActiveRecordError) { minivan.update_attribute(:color, 'black') }
@@ -488,7 +523,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_column_with_one_changed_and_one_updated
t = Topic.order('id').limit(1).first
- title, author_name = t.title, t.author_name
+ author_name = t.author_name
t.author_name = 'John'
t.update_column(:title, 'super_title')
assert_equal 'John', t.author_name
@@ -501,6 +536,14 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal 'super_title', t.title
end
+ def test_update_column_with_default_scope
+ developer = DeveloperCalledDavid.first
+ developer.name = 'John'
+ developer.save!
+
+ assert developer.update_column(:name, 'Will'), 'did not update record due to default scope'
+ end
+
def test_update_columns
topic = Topic.find(1)
topic.update_columns({ "approved" => true, title: "Sebastian Topic" })
@@ -529,7 +572,7 @@ class PersistencesTest < ActiveRecord::TestCase
def test_update_columns_should_not_leave_the_object_dirty
topic = Topic.find(1)
- topic.update_attributes({ "content" => "Have a nice day", :author_name => "Jose" })
+ topic.update({ "content" => "Have a nice day", :author_name => "Jose" })
topic.reload
topic.update_columns({ content: "You too", "author_name" => "Sebastian" })
@@ -592,6 +635,43 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal 'super_title', t.title
end
+ def test_update_columns_changing_id
+ topic = Topic.find(1)
+ topic.update_columns(id: 123)
+ assert_equal 123, topic.id
+ topic.reload
+ assert_equal 123, topic.id
+ end
+
+ def test_update_columns_returns_boolean
+ topic = Topic.find(1)
+ assert_equal true, topic.update_columns(title: "New title")
+ end
+
+ def test_update_columns_with_default_scope
+ developer = DeveloperCalledDavid.first
+ developer.name = 'John'
+ developer.save!
+
+ assert developer.update_columns(name: 'Will'), 'did not update record due to default scope'
+ end
+
+ def test_update
+ topic = Topic.find(1)
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+
+ topic.update("approved" => true, "title" => "The First Topic Updated")
+ topic.reload
+ assert topic.approved?
+ assert_equal "The First Topic Updated", topic.title
+
+ topic.update(approved: false, title: "The First Topic")
+ topic.reload
+ assert !topic.approved?
+ assert_equal "The First Topic", topic.title
+ end
+
def test_update_attributes
topic = Topic.find(1)
assert !topic.approved?
@@ -602,10 +682,51 @@ class PersistencesTest < ActiveRecord::TestCase
assert topic.approved?
assert_equal "The First Topic Updated", topic.title
- topic.update_attributes(:approved => false, :title => "The First Topic")
+ topic.update_attributes(approved: false, title: "The First Topic")
topic.reload
assert !topic.approved?
assert_equal "The First Topic", topic.title
+
+ assert_raise(ActiveRecord::RecordNotUnique, ActiveRecord::StatementInvalid) do
+ topic.update_attributes(id: 3, title: "Hm is it possible?")
+ end
+ assert_not_equal "Hm is it possible?", Topic.find(3).title
+
+ topic.update_attributes(id: 1234)
+ assert_nothing_raised { topic.reload }
+ assert_equal topic.title, Topic.find(1234).title
+ end
+
+ def test_update_attributes_parameters
+ topic = Topic.find(1)
+ assert_nothing_raised do
+ topic.update_attributes({})
+ end
+
+ assert_raises(ArgumentError) do
+ topic.update_attributes(nil)
+ end
+ end
+
+ def test_update!
+ Reply.validates_presence_of(:title)
+ reply = Reply.find(2)
+ assert_equal "The Second Topic of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ reply.update!("title" => "The Second Topic of the day updated", "content" => "Have a nice evening")
+ reply.reload
+ assert_equal "The Second Topic of the day updated", reply.title
+ assert_equal "Have a nice evening", reply.content
+
+ reply.update!(title: "The Second Topic of the day", content: "Have a nice day")
+ reply.reload
+ assert_equal "The Second Topic of the day", reply.title
+ assert_equal "Have a nice day", reply.content
+
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update!(title: nil, content: "Have a nice evening") }
+ ensure
+ Reply.reset_callbacks(:validate)
end
def test_update_attributes!
@@ -619,12 +740,12 @@ class PersistencesTest < ActiveRecord::TestCase
assert_equal "The Second Topic of the day updated", reply.title
assert_equal "Have a nice evening", reply.content
- reply.update_attributes!(:title => "The Second Topic of the day", :content => "Have a nice day")
+ reply.update_attributes!(title: "The Second Topic of the day", content: "Have a nice day")
reply.reload
assert_equal "The Second Topic of the day", reply.title
assert_equal "Have a nice day", reply.content
- assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(:title => nil, :content => "Have a nice evening") }
+ assert_raise(ActiveRecord::RecordInvalid) { reply.update_attributes!(title: nil, content: "Have a nice evening") }
ensure
Reply.reset_callbacks(:validate)
end
diff --git a/activerecord/test/cases/pooled_connections_test.rb b/activerecord/test/cases/pooled_connections_test.rb
index 0a6354f5cc..626c6aeaf8 100644
--- a/activerecord/test/cases/pooled_connections_test.rb
+++ b/activerecord/test/cases/pooled_connections_test.rb
@@ -7,42 +7,26 @@ class PooledConnectionsTest < ActiveRecord::TestCase
def setup
@per_test_teardown = []
- @connection = ActiveRecord::Model.remove_connection
+ @connection = ActiveRecord::Base.remove_connection
end
def teardown
- ActiveRecord::Model.clear_all_connections!
- ActiveRecord::Model.establish_connection(@connection)
+ ActiveRecord::Base.clear_all_connections!
+ ActiveRecord::Base.establish_connection(@connection)
@per_test_teardown.each {|td| td.call }
end
- def checkout_connections
- ActiveRecord::Model.establish_connection(@connection.merge({:pool => 2, :checkout_timeout => 0.3}))
- @connections = []
- @timed_out = 0
-
- 4.times do
- Thread.new do
- begin
- @connections << ActiveRecord::Base.connection_pool.checkout
- rescue ActiveRecord::ConnectionTimeoutError
- @timed_out += 1
- end
- end.join
- end
- end
-
# Will deadlock due to lack of Monitor timeouts in 1.9
def checkout_checkin_connections(pool_size, threads)
- ActiveRecord::Model.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
+ ActiveRecord::Base.establish_connection(@connection.merge({:pool => pool_size, :checkout_timeout => 0.5}))
@connection_count = 0
@timed_out = 0
threads.times do
Thread.new do
begin
- conn = ActiveRecord::Model.connection_pool.checkout
+ conn = ActiveRecord::Base.connection_pool.checkout
sleep 0.1
- ActiveRecord::Model.connection_pool.checkin conn
+ ActiveRecord::Base.connection_pool.checkin conn
@connection_count += 1
rescue ActiveRecord::ConnectionTimeoutError
@timed_out += 1
@@ -55,13 +39,13 @@ class PooledConnectionsTest < ActiveRecord::TestCase
checkout_checkin_connections 1, 2
assert_equal 2, @connection_count
assert_equal 0, @timed_out
- assert_equal 1, ActiveRecord::Model.connection_pool.connections.size
+ assert_equal 1, ActiveRecord::Base.connection_pool.connections.size
end
private
def add_record(name)
- ActiveRecord::Model.connection_pool.with_connection { Project.create! :name => name }
+ ActiveRecord::Base.connection_pool.with_connection { Project.create! :name => name }
end
end unless current_adapter?(:FrontBase) || in_memory_db?
diff --git a/activerecord/test/cases/primary_keys_test.rb b/activerecord/test/cases/primary_keys_test.rb
index e6e50a4cd4..aa125c70c5 100644
--- a/activerecord/test/cases/primary_keys_test.rb
+++ b/activerecord/test/cases/primary_keys_test.rb
@@ -188,31 +188,31 @@ class PrimaryKeyWithNoConnectionTest < ActiveRecord::TestCase
def test_set_primary_key_with_no_connection
return skip("disconnect wipes in-memory db") if in_memory_db?
- connection = ActiveRecord::Model.remove_connection
+ connection = ActiveRecord::Base.remove_connection
model = Class.new(ActiveRecord::Base)
model.primary_key = 'foo'
assert_equal 'foo', model.primary_key
- ActiveRecord::Model.establish_connection(connection)
+ ActiveRecord::Base.establish_connection(connection)
assert_equal 'foo', model.primary_key
end
end
-if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
class PrimaryKeyWithAnsiQuotesTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
-
- def test_primaery_key_method_with_ansi_quotes
+
+ def test_primary_key_method_with_ansi_quotes
con = ActiveRecord::Base.connection
con.execute("SET SESSION sql_mode='ANSI_QUOTES'")
assert_equal "id", con.primary_key("topics")
ensure
con.reconnect!
end
-
+
end
end
diff --git a/activerecord/test/cases/query_cache_test.rb b/activerecord/test/cases/query_cache_test.rb
index 51a285a2b4..136fda664c 100644
--- a/activerecord/test/cases/query_cache_test.rb
+++ b/activerecord/test/cases/query_cache_test.rb
@@ -167,7 +167,7 @@ class QueryCacheTest < ActiveRecord::TestCase
# Oracle adapter returns count() as Fixnum or Float
if current_adapter?(:OracleAdapter)
assert_kind_of Numeric, Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
- elsif current_adapter?(:SQLite3Adapter) || current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:SQLite3Adapter, :Mysql2Adapter)
# Future versions of the sqlite3 adapter will return numeric
assert_instance_of Fixnum,
Task.connection.select_value("SELECT count(*) AS count_all FROM tasks")
@@ -184,6 +184,17 @@ class QueryCacheTest < ActiveRecord::TestCase
assert_queries(2) { task.lock!; task.lock! }
end
end
+
+ def test_cache_is_available_when_connection_is_connected
+ conf = ActiveRecord::Base.configurations
+
+ ActiveRecord::Base.configurations = {}
+ Task.cache do
+ assert_queries(1) { Task.find(1); Task.find(1) }
+ end
+ ensure
+ ActiveRecord::Base.configurations = conf
+ end
end
class QueryCacheExpiryTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/quoting_test.rb b/activerecord/test/cases/quoting_test.rb
index 3dd11ae89d..e2439b9a24 100644
--- a/activerecord/test/cases/quoting_test.rb
+++ b/activerecord/test/cases/quoting_test.rb
@@ -53,50 +53,40 @@ module ActiveRecord
end
def test_quoted_time_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = Time.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = Time.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_time_crazy
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :asdfasdf
- t = Time.now
- assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :asdfasdf do
+ t = Time.now
+ assert_equal t.getlocal.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quoted_datetime_utc
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :utc
- t = DateTime.now
- assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :utc do
+ t = DateTime.now
+ assert_equal t.getutc.to_s(:db), @quoter.quoted_date(t)
+ end
end
###
# DateTime doesn't define getlocal, so make sure it does nothing
def test_quoted_datetime_local
- before = ActiveRecord::Base.default_timezone
- ActiveRecord::Base.default_timezone = :local
- t = DateTime.now
- assert_equal t.to_s(:db), @quoter.quoted_date(t)
- ensure
- ActiveRecord::Base.default_timezone = before
+ with_timezone_config default: :local do
+ t = DateTime.now
+ assert_equal t.to_s(:db), @quoter.quoted_date(t)
+ end
end
def test_quote_with_quoted_id
@@ -194,25 +184,6 @@ module ActiveRecord
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:binary))
end
- def test_quote_binary_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- assert_equal "'foo'", @quoter.quote('lo\l', col)
- end
-
- def test_quote_as_mb_chars_binary_column_with_string_to_binary
- col = Class.new(FakeColumn) {
- def string_to_binary(value)
- 'foo'
- end
- }.new(:binary)
- string = ActiveSupport::Multibyte::Chars.new('lo\l')
- assert_equal "'foo'", @quoter.quote(string, col)
- end
-
def test_string_with_crazy_column
assert_equal "'lo\\\\l'", @quoter.quote('lo\l', FakeColumn.new(:foo))
end
diff --git a/activerecord/test/cases/readonly_test.rb b/activerecord/test/cases/readonly_test.rb
index df076c97b4..2afd25c989 100644
--- a/activerecord/test/cases/readonly_test.rb
+++ b/activerecord/test/cases/readonly_test.rb
@@ -1,4 +1,5 @@
require "cases/helper"
+require 'models/author'
require 'models/post'
require 'models/comment'
require 'models/developer'
@@ -7,7 +8,7 @@ require 'models/reader'
require 'models/person'
class ReadOnlyTest < ActiveRecord::TestCase
- fixtures :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
+ fixtures :authors, :posts, :comments, :developers, :projects, :developers_projects, :people, :readers
def test_cant_save_readonly_record
dev = Developer.find(1)
@@ -34,15 +35,12 @@ class ReadOnlyTest < ActiveRecord::TestCase
Developer.readonly.each { |d| assert d.readonly? }
end
+ def test_find_with_joins_option_does_not_imply_readonly
+ Developer.joins(' ').each { |d| assert_not d.readonly? }
+ Developer.joins(' ').readonly(true).each { |d| assert d.readonly? }
- def test_find_with_joins_option_implies_readonly
- # Blank joins don't count.
- Developer.joins(' ').each { |d| assert !d.readonly? }
- Developer.joins(' ').readonly(false).each { |d| assert !d.readonly? }
-
- # Others do.
- Developer.joins(', projects').each { |d| assert d.readonly? }
- Developer.joins(', projects').readonly(false).each { |d| assert !d.readonly? }
+ Developer.joins(', projects').each { |d| assert_not d.readonly? }
+ Developer.joins(', projects').readonly(true).each { |d| assert d.readonly? }
end
def test_has_many_find_readonly
@@ -87,7 +85,7 @@ class ReadOnlyTest < ActiveRecord::TestCase
# conflicting column names
unless current_adapter?(:OracleAdapter)
Post.joins(', developers').scoping do
- assert Post.find(1).readonly?
+ assert_not Post.find(1).readonly?
assert Post.readonly.find(1).readonly?
assert !Post.readonly(false).find(1).readonly?
end
diff --git a/activerecord/test/cases/reflection_test.rb b/activerecord/test/cases/reflection_test.rb
index a9d46f4fba..d7ad5ed29f 100644
--- a/activerecord/test/cases/reflection_test.rb
+++ b/activerecord/test/cases/reflection_test.rb
@@ -18,6 +18,11 @@ require 'models/subscription'
require 'models/tag'
require 'models/sponsor'
require 'models/edge'
+require 'models/hotel'
+require 'models/chef'
+require 'models/department'
+require 'models/cake_designer'
+require 'models/drink_designer'
class ReflectionTest < ActiveRecord::TestCase
include ActiveRecord::Reflection
@@ -35,18 +40,18 @@ class ReflectionTest < ActiveRecord::TestCase
def test_read_attribute_names
assert_equal(
- %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count parent_id parent_title type created_at updated_at ).sort,
+ %w( id title author_name author_email_address bonus_time written_on last_read content important group approved replies_count unique_replies_count parent_id parent_title type created_at updated_at ).sort,
@first.attribute_names.sort
)
end
def test_columns
- assert_equal 17, Topic.columns.length
+ assert_equal 18, Topic.columns.length
end
def test_columns_are_returned_in_the_order_they_were_declared
column_names = Topic.columns.map { |column| column.name }
- assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count parent_id parent_title type group created_at updated_at), column_names
+ assert_equal %w(id title author_name author_email_address written_on bonus_time last_read content important approved replies_count unique_replies_count parent_id parent_title type group created_at updated_at), column_names
end
def test_content_columns
@@ -186,14 +191,6 @@ class ReflectionTest < ActiveRecord::TestCase
ActiveRecord::Base.store_full_sti_class = true
end
- def test_reflection_of_all_associations
- # FIXME these assertions bust a lot
- assert_equal 39, Firm.reflect_on_all_associations.size
- assert_equal 29, Firm.reflect_on_all_associations(:has_many).size
- assert_equal 10, Firm.reflect_on_all_associations(:has_one).size
- assert_equal 0, Firm.reflect_on_all_associations(:belongs_to).size
- end
-
def test_reflection_should_not_raise_error_when_compared_to_other_object
assert_nothing_raised { Firm.reflections[:clients] == Object.new }
end
@@ -235,6 +232,17 @@ class ReflectionTest < ActiveRecord::TestCase
assert_equal expected, actual
end
+ def test_scope_chain_does_not_interfere_with_hmt_with_polymorphic_case
+ @hotel = Hotel.create!
+ @department = @hotel.departments.create!
+ @department.chefs.create!(employable: CakeDesigner.create!)
+ @department.chefs.create!(employable: DrinkDesigner.create!)
+
+ assert_equal 1, @hotel.cake_designers.size
+ assert_equal 1, @hotel.drink_designers.size
+ assert_equal 2, @hotel.chefs.size
+ end
+
def test_nested?
assert !Author.reflect_on_association(:comments).nested?
assert Author.reflect_on_association(:tags).nested?
@@ -260,8 +268,9 @@ class ReflectionTest < ActiveRecord::TestCase
reflection = ActiveRecord::Reflection::AssociationReflection.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { reflection.association_primary_key }
- through = ActiveRecord::Reflection::ThroughReflection.new(:fuu, :edge, nil, {}, Author)
- through.stubs(:source_reflection).returns(stub_everything(:options => {}, :class_name => 'Edge'))
+ through = Class.new(ActiveRecord::Reflection::ThroughReflection) {
+ define_method(:source_reflection) { reflection }
+ }.new(:fuu, :edge, nil, {}, Author)
assert_raises(ActiveRecord::UnknownPrimaryKey) { through.association_primary_key }
end
diff --git a/activerecord/test/cases/relation/delegation_test.rb b/activerecord/test/cases/relation/delegation_test.rb
new file mode 100644
index 0000000000..c171c5e14e
--- /dev/null
+++ b/activerecord/test/cases/relation/delegation_test.rb
@@ -0,0 +1,98 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class DelegationTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def assert_responds(target, method)
+ assert target.respond_to?(method)
+ assert_nothing_raised do
+ method_arity = target.to_a.method(method).arity
+
+ if method_arity.zero?
+ target.send(method)
+ elsif method_arity < 0
+ if method == :shuffle!
+ target.send(method)
+ else
+ target.send(method, 1)
+ end
+ else
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+
+ class DelegationAssociationTest < DelegationTest
+ def target
+ Post.first.comments
+ end
+
+ [:map, :collect].each do |method|
+ test "##{method} is delgated" do
+ assert_responds(target, method)
+ assert_equal(target.pluck(:body), target.send(method) {|post| post.body })
+ end
+
+ test "##{method}! is not delgated" do
+ assert_deprecated do
+ assert_responds(target, "#{method}!")
+ end
+ end
+ end
+
+ [:compact!, :flatten!, :reject!, :reverse!, :rotate!,
+ :shuffle!, :slice!, :sort!, :sort_by!].each do |method|
+ test "##{method} delegation is deprecated" do
+ assert_deprecated do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ [:select!, :uniq!].each do |method|
+ test "##{method} is implemented" do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ class DelegationRelationTest < DelegationTest
+ def target
+ Comment.where.not(body: nil)
+ end
+
+ [:map, :collect].each do |method|
+ test "##{method} is delgated" do
+ assert_responds(target, method)
+ assert_equal(target.pluck(:body), target.send(method) {|post| post.body })
+ end
+
+ test "##{method}! is not delgated" do
+ assert_deprecated do
+ assert_responds(target, "#{method}!")
+ end
+ end
+ end
+
+ [:compact!, :flatten!, :reject!, :reverse!, :rotate!,
+ :shuffle!, :slice!, :sort!, :sort_by!].each do |method|
+ test "##{method} delegation is deprecated" do
+ assert_deprecated do
+ assert_responds(target, method)
+ end
+ end
+ end
+
+ [:select!, :uniq!].each do |method|
+ test "##{method} is triggers an immutable error" do
+ assert_raises ActiveRecord::ImmutableRelation do
+ assert_responds(target, method)
+ end
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/mutation_test.rb b/activerecord/test/cases/relation/mutation_test.rb
new file mode 100644
index 0000000000..020fb24afa
--- /dev/null
+++ b/activerecord/test/cases/relation/mutation_test.rb
@@ -0,0 +1,148 @@
+require 'cases/helper'
+require 'models/post'
+
+module ActiveRecord
+ class RelationMutationTest < ActiveSupport::TestCase
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+ inherited self
+
+ def arel_table
+ Post.arel_table
+ end
+
+ def connection
+ Post.connection
+ end
+
+ def relation_delegate_class(klass)
+ self.class.relation_delegate_class(klass)
+ end
+ end
+
+ def relation
+ @relation ||= Relation.new FakeKlass.new('posts'), :b
+ end
+
+ (Relation::MULTI_VALUE_METHODS - [:references, :extending, :order]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal [:foo], relation.public_send("#{method}_values")
+ end
+ end
+
+ test '#order!' do
+ assert relation.order!('name ASC').equal?(relation)
+ assert_equal ['name ASC'], relation.order_values
+ end
+
+ test '#order! with symbol prepends the table name' do
+ assert relation.order!(:name).equal?(relation)
+ node = relation.order_values.first
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test '#order! on non-string does not attempt regexp match for references' do
+ obj = Object.new
+ obj.expects(:=~).never
+ assert relation.order!(obj)
+ assert_equal [obj], relation.order_values
+ end
+
+ test '#references!' do
+ assert relation.references!(:foo).equal?(relation)
+ assert relation.references_values.include?('foo')
+ end
+
+ test 'extending!' do
+ mod, mod2 = Module.new, Module.new
+
+ assert relation.extending!(mod).equal?(relation)
+ assert_equal [mod], relation.extending_values
+ assert relation.is_a?(mod)
+
+ relation.extending!(mod2)
+ assert_equal [mod, mod2], relation.extending_values
+ end
+
+ test 'extending! with empty args' do
+ relation.extending!
+ assert_equal [], relation.extending_values
+ end
+
+ (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
+ test "##{method}!" do
+ assert relation.public_send("#{method}!", :foo).equal?(relation)
+ assert_equal :foo, relation.public_send("#{method}_value")
+ end
+ end
+
+ test '#from!' do
+ assert relation.from!('foo').equal?(relation)
+ assert_equal ['foo', nil], relation.from_value
+ end
+
+ test '#lock!' do
+ assert relation.lock!('foo').equal?(relation)
+ assert_equal 'foo', relation.lock_value
+ end
+
+ test '#reorder!' do
+ relation = self.relation.order('foo')
+
+ assert relation.reorder!('bar').equal?(relation)
+ assert_equal ['bar'], relation.order_values
+ assert relation.reordering_value
+ end
+
+ test '#reorder! with symbol prepends the table name' do
+ assert relation.reorder!(:name).equal?(relation)
+ node = relation.order_values.first
+
+ assert node.ascending?
+ assert_equal :name, node.expr.name
+ assert_equal "posts", node.expr.relation.name
+ end
+
+ test 'reverse_order!' do
+ assert relation.reverse_order!.equal?(relation)
+ assert relation.reverse_order_value
+ relation.reverse_order!
+ assert !relation.reverse_order_value
+ end
+
+ test 'create_with!' do
+ assert relation.create_with!(foo: 'bar').equal?(relation)
+ assert_equal({foo: 'bar'}, relation.create_with_value)
+ end
+
+ test 'test_merge!' do
+ assert relation.merge!(where: :foo).equal?(relation)
+ assert_equal [:foo], relation.where_values
+ end
+
+ test 'merge with a proc' do
+ assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
+ end
+
+ test 'none!' do
+ assert relation.none!.equal?(relation)
+ assert_equal [NullRelation], relation.extending_values
+ assert relation.is_a?(NullRelation)
+ end
+
+ test 'distinct!' do
+ relation.distinct! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+
+ test 'uniq! was replaced by distinct!' do
+ relation.uniq! :foo
+ assert_equal :foo, relation.distinct_value
+ assert_equal :foo, relation.uniq_value # deprecated access
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/predicate_builder_test.rb b/activerecord/test/cases/relation/predicate_builder_test.rb
new file mode 100644
index 0000000000..14a8d97d36
--- /dev/null
+++ b/activerecord/test/cases/relation/predicate_builder_test.rb
@@ -0,0 +1,14 @@
+require "cases/helper"
+require 'models/topic'
+
+module ActiveRecord
+ class PredicateBuilderTest < ActiveRecord::TestCase
+ def test_registering_new_handlers
+ PredicateBuilder.register_handler(Regexp, proc do |column, value|
+ Arel::Nodes::InfixOperation.new('~', column, value.source)
+ end)
+
+ assert_match %r{["`]topics["`].["`]title["`] ~ 'rails'}i, Topic.where(title: /rails/).to_sql
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_chain_test.rb b/activerecord/test/cases/relation/where_chain_test.rb
new file mode 100644
index 0000000000..92d1e013e8
--- /dev/null
+++ b/activerecord/test/cases/relation/where_chain_test.rb
@@ -0,0 +1,80 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/comment'
+
+module ActiveRecord
+ class WhereChainTest < ActiveRecord::TestCase
+ fixtures :posts
+
+ def setup
+ super
+ @name = 'title'
+ 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)
+ end
+
+ def test_not_null
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], nil)
+ relation = Post.where.not(title: nil)
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_in
+ expected = Arel::Nodes::NotIn.new(Post.arel_table[@name], %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')
+ relation = Post.joins(:comments).where.not(comments: {title: 'hello'})
+ assert_equal(expected.to_sql, relation.where_values.first.to_sql)
+ end
+
+ 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)
+
+ expected = Arel::Nodes::NotEqual.new(Post.arel_table[@name], 'world')
+ assert_equal(expected, relation.where_values.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)
+
+ expected = Arel::Nodes::Equality.new(Post.arel_table[@name], 'world')
+ assert_equal(expected, relation.where_values.last)
+ end
+
+ def test_not_eq_with_string_parameter
+ expected = Arel::Nodes::Not.new("title = 'hello'")
+ relation = Post.where.not("title = 'hello'")
+ assert_equal([expected], relation.where_values)
+ end
+
+ def test_not_eq_with_array_parameter
+ expected = Arel::Nodes::Not.new("title = 'hello'")
+ relation = Post.where.not(['title = ?', 'hello'])
+ assert_equal([expected], relation.where_values)
+ end
+
+ 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])
+ 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])
+ end
+ end
+end
diff --git a/activerecord/test/cases/relation/where_test.rb b/activerecord/test/cases/relation/where_test.rb
index 9c0b139dbf..3e460fa3d6 100644
--- a/activerecord/test/cases/relation/where_test.rb
+++ b/activerecord/test/cases/relation/where_test.rb
@@ -5,10 +5,24 @@ require 'models/treasure'
require 'models/post'
require 'models/comment'
require 'models/edge'
+require 'models/topic'
module ActiveRecord
class WhereTest < ActiveRecord::TestCase
- fixtures :posts, :edges
+ fixtures :posts, :edges, :authors
+
+ def test_where_copies_bind_params
+ author = authors(:david)
+ posts = author.posts.where('posts.id != 1')
+ joined = Post.where(id: posts)
+
+ assert_operator joined.length, :>, 0
+
+ joined.each { |post|
+ assert_equal author, post.author
+ assert_not_equal 1, post.id
+ }
+ end
def test_belongs_to_shallow_where
author = Author.new
@@ -67,6 +81,38 @@ module ActiveRecord
assert_equal expected.to_sql, actual.to_sql
end
+ def test_decorated_polymorphic_where
+ treasure_decorator = Struct.new(:model) do
+ def self.method_missing(method, *args, &block)
+ Treasure.send(method, *args, &block)
+ end
+
+ def is_a?(klass)
+ model.is_a?(klass)
+ end
+
+ def method_missing(method, *args, &block)
+ model.send(method, *args, &block)
+ end
+ end
+
+ treasure = Treasure.new
+ treasure.id = 1
+ decorated_treasure = treasure_decorator.new(treasure)
+
+ expected = PriceEstimate.where(estimate_of_type: 'Treasure', estimate_of_id: 1)
+ actual = PriceEstimate.where(estimate_of: decorated_treasure)
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
+ def test_aliased_attribute
+ expected = Topic.where(heading: 'The First Topic')
+ actual = Topic.where(title: 'The First Topic')
+
+ assert_equal expected.to_sql, actual.to_sql
+ end
+
def test_where_error
assert_raises(ActiveRecord::StatementInvalid) do
Post.where(:id => { 'posts.author_id' => 10 }).first
@@ -82,8 +128,18 @@ module ActiveRecord
assert_equal 0, Post.where(:posts => {}).count
end
+ def test_where_with_table_name_and_empty_array
+ assert_equal 0, Post.where(:id => []).count
+ end
+
def test_where_with_empty_hash_and_no_foreign_key
assert_equal 0, Edge.where(:sink => {}).count
end
+
+ def test_where_with_blank_conditions
+ [[], {}, nil, ""].each do |blank|
+ assert_equal 4, Edge.where(blank).order("sink_id").to_a.size
+ end
+ end
end
end
diff --git a/activerecord/test/cases/relation_scoping_test.rb b/activerecord/test/cases/relation_scoping_test.rb
deleted file mode 100644
index d318dab1e1..0000000000
--- a/activerecord/test/cases/relation_scoping_test.rb
+++ /dev/null
@@ -1,518 +0,0 @@
-require "cases/helper"
-require 'models/post'
-require 'models/author'
-require 'models/developer'
-require 'models/project'
-require 'models/comment'
-require 'models/category'
-require 'models/person'
-require 'models/reference'
-
-class RelationScopingTest < ActiveRecord::TestCase
- fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
-
- def test_reverse_order
- assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
- end
-
- def test_reverse_order_with_arel_node
- assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order
- end
-
- def test_reverse_order_with_multiple_arel_nodes
- assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order
- end
-
- def test_reverse_order_with_arel_nodes_and_strings
- assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order
- end
-
- def test_double_reverse_order_produces_original_order
- assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order
- end
-
- def test_scoped_find
- Developer.where("name = 'David'").scoping do
- assert_nothing_raised { Developer.find(1) }
- end
- end
-
- def test_scoped_find_first
- developer = Developer.find(10)
- Developer.where("salary = 100000").scoping do
- assert_equal developer, Developer.order("name").first
- end
- end
-
- def test_scoped_find_last
- highest_salary = Developer.order("salary DESC").first
-
- Developer.order("salary").scoping do
- assert_equal highest_salary, Developer.last
- end
- end
-
- def test_scoped_find_last_preserves_scope
- lowest_salary = Developer.order("salary ASC").first
- highest_salary = Developer.order("salary DESC").first
-
- Developer.order("salary").scoping do
- assert_equal highest_salary, Developer.last
- assert_equal lowest_salary, Developer.first
- end
- end
-
- def test_scoped_find_combines_and_sanitizes_conditions
- Developer.where("salary = 9000").scoping do
- assert_equal developers(:poor_jamis), Developer.where("name = 'Jamis'").first
- end
- end
-
- def test_scoped_find_all
- Developer.where("name = 'David'").scoping do
- assert_equal [developers(:david)], Developer.all
- end
- end
-
- def test_scoped_find_select
- Developer.select("id, name").scoping do
- developer = Developer.where("name = 'David'").first
- assert_equal "David", developer.name
- assert !developer.has_attribute?(:salary)
- end
- end
-
- def test_scope_select_concatenates
- Developer.select("id, name").scoping do
- developer = Developer.select('salary').where("name = 'David'").first
- assert_equal 80000, developer.salary
- assert developer.has_attribute?(:id)
- assert developer.has_attribute?(:name)
- assert developer.has_attribute?(:salary)
- end
- end
-
- def test_scoped_count
- Developer.where("name = 'David'").scoping do
- assert_equal 1, Developer.count
- end
-
- Developer.where('salary = 100000').scoping do
- assert_equal 8, Developer.count
- assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count
- end
- end
-
- def test_scoped_find_include
- # with the include, will retrieve only developers for the given project
- scoped_developers = Developer.includes(:projects).scoping do
- Developer.where('projects.id' => 2).to_a
- end
- assert scoped_developers.include?(developers(:david))
- assert !scoped_developers.include?(developers(:jamis))
- assert_equal 1, scoped_developers.size
- end
-
- def test_scoped_find_joins
- scoped_developers = Developer.joins('JOIN developers_projects ON id = developer_id').scoping do
- Developer.where('developers_projects.project_id = 2').to_a
- end
-
- assert scoped_developers.include?(developers(:david))
- assert !scoped_developers.include?(developers(:jamis))
- assert_equal 1, scoped_developers.size
- assert_equal developers(:david).attributes, scoped_developers.first.attributes
- end
-
- def test_scoped_create_with_where
- new_comment = VerySpecialComment.where(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
- end
-
- assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
- end
-
- def test_scoped_create_with_create_with
- new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
- end
-
- assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
- end
-
- def test_scoped_create_with_create_with_has_higher_priority
- new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do
- VerySpecialComment.create :body => "Wonderful world"
- end
-
- assert_equal 1, new_comment.post_id
- assert Post.find(1).comments.include?(new_comment)
- end
-
- def test_ensure_that_method_scoping_is_correctly_restored
- begin
- Developer.where("name = 'Jamis'").scoping do
- raise "an exception"
- end
- rescue
- end
-
- assert !Developer.all.where_values.include?("name = 'Jamis'")
- end
-end
-
-class NestedRelationScopingTest < ActiveRecord::TestCase
- fixtures :authors, :developers, :projects, :comments, :posts
-
- def test_merge_options
- 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
- end
- end
- end
-
- def test_merge_inner_scope_has_priority
- Developer.limit(5).scoping do
- Developer.limit(10).scoping do
- assert_equal 10, Developer.all.size
- end
- end
- end
-
- def test_replace_options
- Developer.where(:name => 'David').scoping do
- Developer.unscoped do
- assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name]
- end
-
- assert_equal 'David', Developer.first[:name]
- end
- end
-
- def test_three_level_nested_exclusive_scoped_find
- Developer.where("name = 'Jamis'").scoping do
- assert_equal 'Jamis', Developer.first.name
-
- Developer.unscoped.where("name = 'David'") do
- assert_equal 'David', Developer.first.name
-
- Developer.unscoped.where("name = 'Maiha'") do
- assert_equal nil, Developer.first
- end
-
- # ensure that scoping is restored
- assert_equal 'David', Developer.first.name
- end
-
- # ensure that scoping is restored
- assert_equal 'Jamis', Developer.first.name
- end
- end
-
- def test_nested_scoped_create
- comment = Comment.create_with(:post_id => 1).scoping do
- Comment.create_with(:post_id => 2).scoping do
- Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
- end
- end
-
- assert_equal 2, comment.post_id
- end
-
- def test_nested_exclusive_scope_for_create
- comment = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do
- Comment.unscoped.create_with(:post_id => 1).scoping do
- assert_blank Comment.new.body
- Comment.create :body => "Hey guys"
- end
- end
-
- assert_equal 1, comment.post_id
- assert_equal 'Hey guys', comment.body
- end
-end
-
-class HasManyScopingTest< ActiveRecord::TestCase
- fixtures :comments, :posts, :people, :references
-
- def setup
- @welcome = Post.find(1)
- end
-
- def test_forwarding_of_static_methods
- assert_equal 'a comment...', Comment.what_are_you
- assert_equal 'a comment...', @welcome.comments.what_are_you
- end
-
- def test_forwarding_to_scoped
- assert_equal 4, Comment.search_by_type('Comment').size
- assert_equal 2, @welcome.comments.search_by_type('Comment').size
- end
-
- def test_nested_scope_finder
- Comment.where('1=0').scoping do
- assert_equal 0, @welcome.comments.count
- assert_equal 'a comment...', @welcome.comments.what_are_you
- end
-
- Comment.where('1=1').scoping do
- assert_equal 2, @welcome.comments.count
- assert_equal 'a comment...', @welcome.comments.what_are_you
- end
- end
-
- def test_should_maintain_default_scope_on_associations
- magician = BadReference.find(1)
- assert_equal [magician], people(:michael).bad_references
- end
-
- def test_should_default_scope_on_associations_is_overriden_by_association_conditions
- reference = references(:michael_unicyclist).becomes(BadReference)
- assert_equal [reference], people(:michael).fixed_bad_references
- end
-
- def test_should_maintain_default_scope_on_eager_loaded_associations
- michael = Person.where(:id => people(:michael).id).includes(:bad_references).first
- magician = BadReference.find(1)
- assert_equal [magician], michael.bad_references
- end
-end
-
-class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
- fixtures :posts, :categories, :categories_posts
-
- def setup
- @welcome = Post.find(1)
- end
-
- def test_forwarding_of_static_methods
- assert_equal 'a category...', Category.what_are_you
- assert_equal 'a category...', @welcome.categories.what_are_you
- end
-
- def test_nested_scope_finder
- Category.where('1=0').scoping do
- assert_equal 0, @welcome.categories.count
- assert_equal 'a category...', @welcome.categories.what_are_you
- end
-
- Category.where('1=1').scoping do
- assert_equal 2, @welcome.categories.count
- assert_equal 'a category...', @welcome.categories.what_are_you
- end
- end
-end
-
-class DefaultScopingTest < ActiveRecord::TestCase
- fixtures :developers, :posts
-
- def test_default_scope
- expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary }
- assert_equal expected, received
- end
-
- def test_default_scope_as_class_method
- assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all
- end
-
- def test_default_scope_as_class_method_referencing_scope
- assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all
- end
-
- def test_default_scope_as_block_referencing_scope
- assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_lambda
- assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_block
- assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all
- end
-
- def test_default_scope_with_callable
- assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all
- end
-
- def test_default_scope_is_unscoped_on_find
- assert_equal 1, DeveloperCalledDavid.count
- assert_equal 11, DeveloperCalledDavid.unscoped.count
- end
-
- def test_default_scope_is_unscoped_on_create
- assert_nil DeveloperCalledJamis.unscoped.create!.name
- end
-
- def test_default_scope_with_conditions_string
- assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
- assert_equal nil, DeveloperCalledDavid.create!.name
- end
-
- def test_default_scope_with_conditions_hash
- assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
- assert_equal 'Jamis', DeveloperCalledJamis.create!.name
- end
-
- def test_default_scoping_with_threads
- 2.times do
- Thread.new { assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC') }.join
- end
- end
-
- def test_default_scope_with_inheritance
- wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_default_scope_with_module_includes
- wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_default_scope_with_multiple_calls
- wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash
- assert_equal "Jamis", wheres[:name]
- assert_equal 50000, wheres[:salary]
- end
-
- def test_scope_overwrites_default
- expected = Developer.all.merge!(:order => ' name DESC, salary DESC').to_a.collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
- assert_equal expected, received
- end
-
- def test_reorder_overrides_default_scope_order
- expected = Developer.order('name DESC').collect { |dev| dev.name }
- received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name }
- assert_equal expected, received
- end
-
- def test_order_after_reorder_combines_orders
- expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
- received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
- assert_equal expected, received
- end
-
- def test_order_in_default_scope_should_not_prevail
- expected = Developer.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
- received = DeveloperOrderedBySalary.all.merge!(:order => 'salary').to_a.collect { |dev| dev.salary }
- assert_equal expected, received
- end
-
- def test_create_attribute_overwrites_default_scoping
- assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name
- assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary
- end
-
- def test_create_attribute_overwrites_default_values
- assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary
- assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary
- end
-
- def test_default_scope_attribute
- jamis = PoorDeveloperCalledJamis.new(:name => 'David')
- assert_equal 50000, jamis.salary
- end
-
- def test_where_attribute
- aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron')
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
- end
-
- def test_where_attribute_merge
- aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron')
- assert_equal 'Aaron', aaron.name
- end
-
- def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit
- posts_limit_offset = Post.limit(3).offset(2)
- posts_offset_limit = Post.offset(2).limit(3)
- assert_equal posts_limit_offset, posts_offset_limit
- end
-
- def test_create_with_merge
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge(
- PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
-
- aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).
- create_with(:name => 'Aaron').new
- assert_equal 20, aaron.salary
- assert_equal 'Aaron', aaron.name
- end
-
- def test_create_with_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new
- assert_equal 'Jamis', jamis.name
- end
-
- # FIXME: I don't know if this is *desired* behavior, but it is *today's*
- # behavior.
- def test_create_with_empty_hash_will_not_reset
- jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
- assert_equal 'Aaron', jamis.name
- end
-
- def test_unscoped_with_named_scope_should_not_have_default_scope
- assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
-
- assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis))
- assert_equal 10, DeveloperCalledJamis.unscoped.poor.length
- end
-
- def test_default_scope_select_ignored_by_aggregations
- assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count
- end
-
- def test_default_scope_select_ignored_by_grouped_aggregations
- assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }],
- DeveloperWithSelect.group(:salary).count
- end
-
- def test_default_scope_order_ignored_by_aggregations
- assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count
- end
-
- def test_default_scope_find_last
- assert DeveloperOrderedBySalary.count > 1, "need more than one row for test"
-
- lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id)
- assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last
- end
-
- def test_default_scope_include_with_count
- d = DeveloperWithIncludes.create!
- d.audit_logs.create! :message => 'foo'
-
- assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
- end
-
- def test_default_scope_is_threadsafe
- if in_memory_db?
- skip "in memory db can't share a db between threads"
- end
-
- threads = []
- assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
-
- threads << Thread.new do
- Thread.current[:long_default_scope] = true
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- end
- threads << Thread.new do
- assert_equal 1, ThreadsafeDeveloper.all.to_a.count
- end
- threads.each(&:join)
- end
-end
diff --git a/activerecord/test/cases/relation_test.rb b/activerecord/test/cases/relation_test.rb
index 6399111be6..70d113fb39 100644
--- a/activerecord/test/cases/relation_test.rb
+++ b/activerecord/test/cases/relation_test.rb
@@ -1,31 +1,37 @@
require "cases/helper"
require 'models/post'
require 'models/comment'
+require 'models/author'
+require 'models/rating'
module ActiveRecord
class RelationTest < ActiveRecord::TestCase
- fixtures :posts, :comments
+ fixtures :posts, :comments, :authors
- class FakeKlass < Struct.new(:table_name)
+ class FakeKlass < Struct.new(:table_name, :name)
+ extend ActiveRecord::Delegation::DelegateCache
+
+ inherited self
+
+ def self.connection
+ Post.connection
+ end
end
def test_construction
- relation = nil
- assert_nothing_raised do
- relation = Relation.new :a, :b
- end
- assert_equal :a, relation.klass
+ relation = Relation.new FakeKlass, :b
+ assert_equal FakeKlass, relation.klass
assert_equal :b, relation.table
assert !relation.loaded, 'relation is not loaded'
end
def test_responds_to_model_and_returns_klass
- relation = Relation.new :a, :b
- assert_equal :a, relation.model
+ relation = Relation.new FakeKlass, :b
+ assert_equal FakeKlass, relation.model
end
def test_initialize_single_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
(Relation::SINGLE_VALUE_METHODS - [:create_with]).each do |method|
assert_nil relation.send("#{method}_value"), method.to_s
end
@@ -33,19 +39,19 @@ module ActiveRecord
end
def test_multi_value_initialize
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
Relation::MULTI_VALUE_METHODS.each do |method|
assert_equal [], relation.send("#{method}_values"), method.to_s
end
end
def test_extensions
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal [], relation.extensions
end
def test_empty_where_values_hash
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal({}, relation.where_values_hash)
relation.where! :hello
@@ -74,12 +80,12 @@ module ActiveRecord
end
def test_table_name_delegates_to_klass
- relation = Relation.new FakeKlass.new('foo'), :b
- assert_equal 'foo', relation.table_name
+ relation = Relation.new FakeKlass.new('posts'), :b
+ assert_equal 'posts', relation.table_name
end
def test_scope_for_create
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal({}, relation.scope_for_create)
end
@@ -109,32 +115,38 @@ module ActiveRecord
assert_equal({}, relation.scope_for_create)
end
+ def test_bad_constants_raise_errors
+ assert_raises(NameError) do
+ ActiveRecord::Relation::HelloWorld
+ end
+ end
+
def test_empty_eager_loading?
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert !relation.eager_loading?
end
def test_eager_load_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation.eager_load! :b
assert relation.eager_loading?
end
def test_references_values
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
assert_equal [], relation.references_values
relation = relation.references(:foo).references(:omg, :lol)
assert_equal ['foo', 'omg', 'lol'], relation.references_values
end
def test_references_values_dont_duplicate
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation = relation.references(:foo).references(:foo)
assert_equal ['foo'], relation.references_values
end
test 'merging a hash into a relation' do
- relation = Relation.new :a, :b
+ relation = Relation.new FakeKlass, :b
relation = relation.merge where: :lol, readonly: true
assert_equal [:lol], relation.where_values
@@ -142,7 +154,7 @@ module ActiveRecord
end
test 'merging an empty hash into a relation' do
- assert_equal [], Relation.new(:a, :b).merge({}).where_values
+ assert_equal [], Relation.new(FakeKlass, :b).merge({}).where_values
end
test 'merging a hash with unknown keys raises' do
@@ -150,7 +162,7 @@ module ActiveRecord
end
test '#values returns a dup of the values' do
- relation = Relation.new(:a, :b).where! :foo
+ relation = Relation.new(FakeKlass, :b).where! :foo
values = relation.values
values[:where] = nil
@@ -158,103 +170,57 @@ module ActiveRecord
end
test 'relations can be created with a values hash' do
- relation = Relation.new(:a, :b, where: [:foo])
+ relation = Relation.new(FakeKlass, :b, where: [:foo])
assert_equal [:foo], relation.where_values
end
test 'merging a single where value' do
- relation = Relation.new(:a, :b)
+ relation = Relation.new(FakeKlass, :b)
relation.merge!(where: :foo)
assert_equal [:foo], relation.where_values
end
test 'merging a hash interpolates conditions' do
- klass = stub
- klass.stubs(:sanitize_sql).with(['foo = ?', 'bar']).returns('foo = bar')
+ klass = Class.new(FakeKlass) do
+ def self.sanitize_sql(args)
+ raise unless args == ['foo = ?', 'bar']
+ 'foo = bar'
+ end
+ end
relation = Relation.new(klass, :b)
relation.merge!(where: ['foo = ?', 'bar'])
assert_equal ['foo = bar'], relation.where_values
end
- end
-
- class RelationMutationTest < ActiveSupport::TestCase
- def relation
- @relation ||= Relation.new :a, :b
- end
- (Relation::MULTI_VALUE_METHODS - [:references, :extending]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal [:foo], relation.public_send("#{method}_values")
- end
+ def test_merging_readonly_false
+ relation = Relation.new FakeKlass, :b
+ readonly_false_relation = relation.readonly(false)
+ # test merging in both directions
+ assert_equal false, relation.merge(readonly_false_relation).readonly_value
+ assert_equal false, readonly_false_relation.merge(relation).readonly_value
end
- test '#references!' do
- assert relation.references!(:foo).equal?(relation)
- assert relation.references_values.include?('foo')
+ def test_relation_merging_with_merged_joins_as_symbols
+ special_comments_with_ratings = SpecialComment.joins(:ratings)
+ posts_with_special_comments_with_ratings = Post.group("posts.id").joins(:special_comments).merge(special_comments_with_ratings)
+ assert_equal 3, authors(:david).posts.merge(posts_with_special_comments_with_ratings).count.length
end
- test 'extending!' do
- mod, mod2 = Module.new, Module.new
+ def test_respond_to_for_non_selected_element
+ post = Post.select(:title).first
+ assert_equal false, post.respond_to?(:body), "post should not respond_to?(:body) since invoking it raises exception"
- assert relation.extending!(mod).equal?(relation)
- assert_equal [mod], relation.extending_values
- assert relation.is_a?(mod)
-
- relation.extending!(mod2)
- assert_equal [mod, mod2], relation.extending_values
- end
-
- test 'extending! with empty args' do
- relation.extending!
- assert_equal [], relation.extending_values
+ silence_warnings { post = Post.select("'title' as post_title").first }
+ assert_equal false, post.respond_to?(:title), "post should not respond_to?(:body) since invoking it raises exception"
end
- (Relation::SINGLE_VALUE_METHODS - [:from, :lock, :reordering, :reverse_order, :create_with]).each do |method|
- test "##{method}!" do
- assert relation.public_send("#{method}!", :foo).equal?(relation)
- assert_equal :foo, relation.public_send("#{method}_value")
- end
+ def test_relation_merging_with_merged_joins_as_strings
+ join_string = "LEFT OUTER JOIN #{Rating.quoted_table_name} ON #{SpecialComment.quoted_table_name}.id = #{Rating.quoted_table_name}.comment_id"
+ special_comments_with_ratings = SpecialComment.joins join_string
+ 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
- test '#from!' do
- assert relation.from!('foo').equal?(relation)
- assert_equal ['foo', nil], relation.from_value
- end
-
- test '#lock!' do
- assert relation.lock!('foo').equal?(relation)
- assert_equal 'foo', relation.lock_value
- end
-
- test '#reorder!' do
- relation = self.relation.order('foo')
-
- assert relation.reorder!('bar').equal?(relation)
- assert_equal ['bar'], relation.order_values
- assert relation.reordering_value
- end
-
- test 'reverse_order!' do
- assert relation.reverse_order!.equal?(relation)
- assert relation.reverse_order_value
- relation.reverse_order!
- assert !relation.reverse_order_value
- end
-
- test 'create_with!' do
- assert relation.create_with!(foo: 'bar').equal?(relation)
- assert_equal({foo: 'bar'}, relation.create_with_value)
- end
-
- test 'merge!' do
- assert relation.merge!(where: :foo).equal?(relation)
- assert_equal [:foo], relation.where_values
- end
-
- test 'merge with a proc' do
- assert_equal [:foo], relation.merge(-> { where(:foo) }).where_values
- end
end
end
diff --git a/activerecord/test/cases/relations_test.rb b/activerecord/test/cases/relations_test.rb
index fdbc0a3fdb..c9c7ac04b3 100644
--- a/activerecord/test/cases/relations_test.rb
+++ b/activerecord/test/cases/relations_test.rb
@@ -42,6 +42,11 @@ class RelationTest < ActiveRecord::TestCase
end
def test_two_scopes_with_includes_should_not_drop_any_include
+ # heat habtm cache
+ car = Car.incl_engines.incl_tyres.first
+ car.tyres.length
+ car.engines.length
+
car = Car.incl_engines.incl_tyres.first
assert_no_queries { car.tyres.length }
assert_no_queries { car.engines.length }
@@ -139,6 +144,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal relation.to_a, Topic.select('a.*').from(relation, :a).to_a
end
+ def test_finding_with_subquery_with_binds
+ relation = Post.first.comments
+ assert_equal relation.to_a, Comment.select('*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('subquery.*').from(relation).to_a
+ assert_equal relation.to_a, Comment.select('a.*').from(relation, :a).to_a
+ end
+
def test_finding_with_conditions
assert_equal ["David"], Author.where(:name => 'David').map(&:name)
assert_equal ['Mary'], Author.where(["name = ?", 'Mary']).map(&:name)
@@ -157,19 +169,23 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 4, topics.to_a.size
assert_equal topics(:first).title, topics.first.title
end
-
+
def test_finding_with_assoc_order
topics = Topic.order(:id => :desc)
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
-
+
def test_finding_with_reverted_assoc_order
topics = Topic.order(:id => :asc).reverse_order
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
-
+
+ def test_order_with_hash_and_symbol_generates_the_same_sql
+ assert_equal Topic.order(:id).to_sql, Topic.order(:id => :asc).to_sql
+ end
+
def test_raising_exception_on_invalid_hash_params
assert_raise(ArgumentError) { Topic.order(:name, "id DESC", :id => :DeSc) }
end
@@ -180,7 +196,7 @@ class RelationTest < ActiveRecord::TestCase
end
def test_finding_with_order_concatenated
- topics = Topic.order('title').order('author_name')
+ topics = Topic.order('author_name').order('title')
assert_equal 4, topics.to_a.size
assert_equal topics(:fourth).title, topics.first.title
end
@@ -258,7 +274,7 @@ class RelationTest < ActiveRecord::TestCase
def test_none_chained_to_methods_firing_queries_straight_to_db
assert_no_queries do
- assert_equal [], Developer.none.pluck(:id) # => uses select_all
+ assert_equal [], Developer.none.pluck(:id, :name)
assert_equal 0, Developer.none.delete_all
assert_equal 0, Developer.none.update_all(:name => 'David')
assert_equal 0, Developer.none.delete(1)
@@ -278,8 +294,9 @@ class RelationTest < ActiveRecord::TestCase
def test_null_relation_calculations_methods
assert_no_queries do
- assert_equal 0, Developer.none.count
- assert_equal nil, Developer.none.calculate(:average, 'salary')
+ assert_equal 0, Developer.none.count
+ assert_equal 0, Developer.none.calculate(:count, nil)
+ assert_equal nil, Developer.none.calculate(:average, 'salary')
end
end
@@ -288,6 +305,10 @@ class RelationTest < ActiveRecord::TestCase
assert_equal({}, Developer.none.where_values_hash)
end
+ def test_null_relation_where_values_hash
+ assert_equal({ 'salary' => 100_000 }, Developer.none.where(salary: 100_000).where_values_hash)
+ end
+
def test_joins_with_nil_argument
assert_nothing_raised { DependentFirm.joins(nil).first }
end
@@ -321,6 +342,22 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 1, person_with_reader_and_post.size
end
+ def test_no_arguments_to_query_methods_raise_errors
+ assert_raises(ArgumentError) { Topic.references() }
+ assert_raises(ArgumentError) { Topic.includes() }
+ assert_raises(ArgumentError) { Topic.preload() }
+ assert_raises(ArgumentError) { Topic.group() }
+ assert_raises(ArgumentError) { Topic.reorder() }
+ end
+
+ def test_blank_like_arguments_to_query_methods_dont_raise_errors
+ assert_nothing_raised { Topic.references([]) }
+ assert_nothing_raised { Topic.includes([]) }
+ assert_nothing_raised { Topic.preload([]) }
+ assert_nothing_raised { Topic.group([]) }
+ assert_nothing_raised { Topic.reorder([]) }
+ end
+
def test_scoped_responds_to_delegated_methods
relation = Topic.all
@@ -351,7 +388,7 @@ class RelationTest < ActiveRecord::TestCase
def test_respond_to_dynamic_finders
relation = Topic.all
- ["find_by_title", "find_by_title_and_author_name", "find_or_create_by_title", "find_or_initialize_by_title_and_author_name"].each do |method|
+ ["find_by_title", "find_by_title_and_author_name"].each do |method|
assert_respond_to relation, method, "Topic.all should respond to #{method.inspect}"
end
end
@@ -404,6 +441,13 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ def test_preload_applies_to_all_chained_preloaded_scopes
+ assert_queries(3) do
+ post = Post.with_comments.with_tags.first
+ assert post
+ end
+ end
+
def test_find_with_included_associations
assert_queries(2) do
posts = Post.includes(:comments).order('posts.id')
@@ -442,6 +486,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Developer.where(name: 'David').map(&:id).sort, developers
end
+ def test_includes_with_select
+ query = Post.select('comments_count AS ranking').order('ranking').includes(:comments)
+ .where(comments: { id: 1 })
+
+ assert_equal ['comments_count AS ranking'], query.select_values
+ assert_equal 1, query.to_a.size
+ end
+
def test_loading_with_one_association
posts = Post.preload(:comments)
post = posts.find { |p| p.id == 1 }
@@ -457,6 +509,14 @@ class RelationTest < ActiveRecord::TestCase
assert_equal Post.find(1).last_comment, post.last_comment
end
+ def test_to_sql_on_eager_join
+ expected = assert_sql {
+ Post.eager_load(:last_comment).order('comments.id DESC').to_a
+ }.first
+ actual = Post.eager_load(:last_comment).order('comments.id DESC').to_sql
+ assert_equal expected, actual
+ end
+
def test_loading_with_one_association_with_non_preload
posts = Post.eager_load(:last_comment).order('comments.id DESC')
post = posts.find { |p| p.id == 1 }
@@ -469,6 +529,7 @@ class RelationTest < ActiveRecord::TestCase
expected_taggings = taggings(:welcome_general, :thinking_general)
assert_no_queries do
+ assert_equal expected_taggings, author.taggings.distinct.sort_by { |t| t.id }
assert_equal expected_taggings, author.taggings.uniq.sort_by { |t| t.id }
end
@@ -509,7 +570,7 @@ class RelationTest < ActiveRecord::TestCase
def test_find_in_empty_array
authors = Author.all.where(:id => [])
- assert_blank authors.to_a
+ assert authors.to_a.blank?
end
def test_where_with_ar_object
@@ -573,6 +634,36 @@ class RelationTest < ActiveRecord::TestCase
relation = Author.where(:id => Author.where(:id => david.id))
assert_equal [david], relation.to_a
}
+
+ assert_queries(1) {
+ relation = Author.where('id in (?)', Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ }
+
+ assert_queries(1) do
+ relation = Author.where('id in (:author_ids)', author_ids: Author.where(id: david).select(:id))
+ assert_equal [david], relation.to_a
+ end
+ end
+
+ def test_find_all_using_where_with_relation_with_bound_values
+ david = authors(:david)
+ davids_posts = david.posts.order(:id).to_a
+
+ assert_queries(1) do
+ relation = Post.where(id: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (?)', david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as bind variables'
+ end
+
+ assert_queries(1) do
+ relation = Post.where('id in (:post_ids)', post_ids: david.posts.select(:id))
+ assert_equal davids_posts, relation.order(:id).to_a, 'should process Relation as named bind variables'
+ end
end
def test_find_all_using_where_with_relation_and_alternate_primary_key
@@ -691,6 +782,13 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [developers(:poor_jamis)], dev_with_count.to_a
end
+ def test_relation_to_sql
+ sql = Post.connection.unprepared_statement do
+ Post.first.comments.to_sql
+ end
+ assert_no_match(/\?/, sql)
+ end
+
def test_relation_merging_with_arel_equalities_keeps_last_equality
devs = Developer.where(Developer.arel_table[:salary].eq(80000)).merge(
Developer.where(Developer.arel_table[:salary].eq(9000))
@@ -723,7 +821,7 @@ class RelationTest < ActiveRecord::TestCase
def test_relation_merging_with_locks
devs = Developer.lock.where("salary >= 80000").order("id DESC").merge(Developer.limit(2))
- assert_present devs.locked
+ assert devs.locked.present?
end
def test_relation_merging_with_preload
@@ -759,11 +857,11 @@ class RelationTest < ActiveRecord::TestCase
def test_count_with_distinct
posts = Post.all
- assert_equal 3, posts.count(:comments_count, :distinct => true)
- assert_equal 11, posts.count(:comments_count, :distinct => false)
+ assert_equal 3, posts.distinct(true).count(:comments_count)
+ assert_equal 11, posts.distinct(false).count(:comments_count)
- assert_equal 3, posts.select(:comments_count).count(:distinct => true)
- assert_equal 11, posts.select(:comments_count).count(:distinct => false)
+ assert_equal 3, posts.distinct(true).select(:comments_count).count
+ assert_equal 11, posts.distinct(false).select(:comments_count).count
end
def test_count_explicit_columns
@@ -1058,6 +1156,39 @@ class RelationTest < ActiveRecord::TestCase
assert_equal 'parrot', parrot.name
end
+ def test_find_or_create_by
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.find_or_create_by(name: 'bob')
+ assert bird.persisted?
+
+ assert_equal bird, Bird.find_or_create_by(name: 'bob')
+ end
+
+ def test_find_or_create_by_with_create_with
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.create_with(color: 'green').find_or_create_by(name: 'bob')
+ assert bird.persisted?
+ assert_equal 'green', bird.color
+
+ assert_equal bird, Bird.create_with(color: 'blue').find_or_create_by(name: 'bob')
+ end
+
+ def test_find_or_create_by!
+ assert_raises(ActiveRecord::RecordInvalid) { Bird.find_or_create_by!(color: 'green') }
+ end
+
+ def test_find_or_initialize_by
+ assert_nil Bird.find_by(name: 'bob')
+
+ bird = Bird.find_or_initialize_by(name: 'bob')
+ assert bird.new_record?
+ bird.save!
+
+ assert_equal bird, Bird.find_or_initialize_by(name: 'bob')
+ end
+
def test_explicit_create_scope
hens = Bird.where(:name => 'hen')
assert_equal 'hen', hens.new.name
@@ -1110,20 +1241,20 @@ class RelationTest < ActiveRecord::TestCase
end
def test_default_scope_order_with_scope_order
- assert_equal 'honda', CoolCar.order_using_new_style.limit(1).first.name
- assert_equal 'honda', FastCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', CoolCar.order_using_new_style.limit(1).first.name
+ assert_equal 'zyke', FastCar.order_using_new_style.limit(1).first.name
end
def test_order_using_scoping
car1 = CoolCar.order('id DESC').scoping do
- CoolCar.all.merge!(:order => 'id asc').first
+ CoolCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car1.name
+ assert_equal 'zyke', car1.name
car2 = FastCar.order('id DESC').scoping do
- FastCar.all.merge!(:order => 'id asc').first
+ FastCar.all.merge!(order: 'id asc').first
end
- assert_equal 'honda', car2.name
+ assert_equal 'zyke', car2.name
end
def test_unscoped_block_style
@@ -1143,20 +1274,9 @@ class RelationTest < ActiveRecord::TestCase
assert_equal "id", Post.all.primary_key
end
- def test_eager_loading_with_conditions_on_joins
- scope = Post.includes(:comments)
-
- # This references the comments table, and so it should cause the comments to be eager
- # loaded via a JOIN, rather than by subsequent queries.
- scope = scope.joins(
- Post.arel_table.create_join(
- Post.arel_table,
- Post.arel_table.create_on(Comment.arel_table[:id].eq(3))
- )
- )
-
+ def test_disable_implicit_join_references_is_deprecated
assert_deprecated do
- assert scope.eager_loading?
+ ActiveRecord::Base.disable_implicit_join_references = true
end
end
@@ -1206,7 +1326,7 @@ class RelationTest < ActiveRecord::TestCase
assert_equal posts(:welcome), comments(:greetings).post
end
- def test_uniq
+ def test_distinct
tag1 = Tag.create(:name => 'Foo')
tag2 = Tag.create(:name => 'Foo')
@@ -1214,14 +1334,25 @@ class RelationTest < ActiveRecord::TestCase
assert_equal ['Foo', 'Foo'], query.map(&:name)
assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.distinct.map(&:name)
assert_equal ['Foo'], query.uniq.map(&:name)
end
assert_sql(/DISTINCT/) do
+ assert_equal ['Foo'], query.distinct(true).map(&:name)
assert_equal ['Foo'], query.uniq(true).map(&:name)
end
+ assert_equal ['Foo', 'Foo'], query.distinct(true).distinct(false).map(&:name)
assert_equal ['Foo', 'Foo'], query.uniq(true).uniq(false).map(&:name)
end
+ def test_doesnt_add_having_values_if_options_are_blank
+ scope = Post.having('')
+ assert_equal [], scope.having_values
+
+ scope = Post.having([])
+ assert_equal [], scope.having_values
+ end
+
def test_references_triggers_eager_loading
scope = Post.includes(:comments)
assert !scope.eager_loading?
@@ -1267,6 +1398,24 @@ class RelationTest < ActiveRecord::TestCase
assert_equal [], scope.references_values
end
+ def test_automatically_added_reorder_references
+ scope = Post.reorder('comments.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body', 'yaks.body')
+ assert_equal %w(comments yaks), scope.references_values
+
+ # Don't infer yaks, let's not go down that road again...
+ scope = Post.reorder('comments.body, yaks.body')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('comments.body asc')
+ assert_equal %w(comments), scope.references_values
+
+ scope = Post.reorder('foo(comments.body)')
+ assert_equal [], scope.references_values
+ end
+
def test_presence
topics = Topic.all
@@ -1358,6 +1507,15 @@ class RelationTest < ActiveRecord::TestCase
end
end
+ test "loaded relations cannot be mutated by extending!" do
+ relation = Post.all
+ relation.to_a
+
+ assert_raises(ActiveRecord::ImmutableRelation) do
+ relation.extending! Module.new
+ end
+ end
+
test "relations show the records in #inspect" do
relation = Post.limit(2)
assert_equal "#<ActiveRecord::Relation [#{Post.limit(2).map(&:inspect).join(', ')}]>", relation.inspect
@@ -1396,4 +1554,99 @@ class RelationTest < ActiveRecord::TestCase
end
assert_no_queries { relation.to_a }
end
+
+ test 'group with select and includes' do
+ authors_count = Post.select('author_id, COUNT(author_id) AS num_posts').
+ group('author_id').order('author_id').includes(:author).to_a
+
+ assert_no_queries do
+ result = authors_count.map do |post|
+ [post.num_posts, post.author.try(:name)]
+ end
+
+ expected = [[1, nil], [5, "David"], [3, "Mary"], [2, "Bob"]]
+ assert_equal expected, result
+ end
+ end
+
+ test "delegations do not leak to other classes" do
+ Topic.all.by_lifo
+ assert Topic.all.class.method_defined?(:by_lifo)
+ assert !Post.all.respond_to?(:by_lifo)
+ end
+
+ class OMGTopic < ActiveRecord::Base
+ self.table_name = 'topics'
+
+ def self.__omg__
+ "omgtopic"
+ end
+ end
+
+ test "delegations do not clash across classes" do
+ begin
+ class ::Array
+ def __omg__
+ "array"
+ end
+ end
+
+ assert_equal "array", Topic.all.__omg__
+ assert_equal "omgtopic", OMGTopic.all.__omg__
+ ensure
+ Array.send(:remove_method, :__omg__)
+ end
+ end
+
+ test "merge collapses wheres from the LHS only" do
+ left = Post.where(title: "omg").where(comments_count: 1)
+ right = Post.where(title: "wtf").where(title: "bbq")
+
+ expected = [left.where_values[1]] + right.where_values
+ merged = left.merge(right)
+
+ assert_equal expected, merged.where_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
+ left = Post.where(id: 10)
+
+ merged = left.merge(right)
+ assert_equal binds, merged.bind_values
+ end
+
+ def test_merging_reorders_bind_params
+ post = Post.first
+ 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]]
+
+ merged = left.merge(right)
+ assert_equal post, merged.first
+ end
end
diff --git a/activerecord/test/cases/result_test.rb b/activerecord/test/cases/result_test.rb
new file mode 100644
index 0000000000..b6c583dbf5
--- /dev/null
+++ b/activerecord/test/cases/result_test.rb
@@ -0,0 +1,32 @@
+require "cases/helper"
+
+module ActiveRecord
+ class ResultTest < ActiveRecord::TestCase
+ def result
+ Result.new(['col_1', 'col_2'], [
+ ['row 1 col 1', 'row 1 col 2'],
+ ['row 2 col 1', 'row 2 col 2']
+ ])
+ end
+
+ def test_to_hash_returns_row_hashes
+ assert_equal [
+ {'col_1' => 'row 1 col 1', 'col_2' => 'row 1 col 2'},
+ {'col_1' => 'row 2 col 1', 'col_2' => 'row 2 col 2'}
+ ], result.to_hash
+ end
+
+ def test_each_with_block_returns_row_hashes
+ result.each do |row|
+ assert_equal ['col_1', 'col_2'], row.keys
+ end
+ end
+
+ def test_each_without_block_returns_an_enumerator
+ result.each.with_index do |row, index|
+ assert_equal ['col_1', 'col_2'], row.keys
+ assert_kind_of Integer, index
+ end
+ end
+ end
+end
diff --git a/activerecord/test/cases/sanitize_test.rb b/activerecord/test/cases/sanitize_test.rb
index 817897ceac..766b2ff2ef 100644
--- a/activerecord/test/cases/sanitize_test.rb
+++ b/activerecord/test/cases/sanitize_test.rb
@@ -1,10 +1,21 @@
require "cases/helper"
require 'models/binary'
+require 'models/author'
+require 'models/post'
class SanitizeTest < ActiveRecord::TestCase
def setup
end
+ def test_sanitize_sql_hash_handles_associations
+ quoted_bambi = ActiveRecord::Base.connection.quote("Bambi")
+ quoted_column_name = ActiveRecord::Base.connection.quote_column_name("name")
+ quoted_table_name = ActiveRecord::Base.connection.quote_table_name("adorable_animals")
+ expected_value = "#{quoted_table_name}.#{quoted_column_name} = #{quoted_bambi}"
+
+ assert_equal expected_value, Binary.send(:sanitize_sql_hash, {adorable_animals: {name: 'Bambi'}})
+ end
+
def test_sanitize_sql_array_handles_string_interpolation
quoted_bambi = ActiveRecord::Base.connection.quote_string("Bambi")
assert_equal "name=#{quoted_bambi}", Binary.send(:sanitize_sql_array, ["name=%s", "Bambi"])
@@ -22,4 +33,17 @@ class SanitizeTest < ActiveRecord::TestCase
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper"])
assert_equal "name=#{quoted_bambi_and_thumper}", Binary.send(:sanitize_sql_array, ["name=?", "Bambi\nand\nThumper".mb_chars])
end
+
+ def test_sanitize_sql_array_handles_relations
+ david = Author.create!(name: 'David')
+ david_posts = david.posts.select(:id)
+
+ sub_query_pattern = /\(\bselect\b.*?\bwhere\b.*?\)/i
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (?)', david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for bind variables')
+
+ select_author_sql = Post.send(:sanitize_sql_array, ['id in (:post_ids)', post_ids: david_posts])
+ assert_match(sub_query_pattern, select_author_sql, 'should sanitize `Relation` as subquery for named bind variables')
+ end
end
diff --git a/activerecord/test/cases/schema_dumper_test.rb b/activerecord/test/cases/schema_dumper_test.rb
index 80f46c6b08..1ee8e60924 100644
--- a/activerecord/test/cases/schema_dumper_test.rb
+++ b/activerecord/test/cases/schema_dumper_test.rb
@@ -1,14 +1,9 @@
require "cases/helper"
-
class SchemaDumperTest < ActiveRecord::TestCase
- def initialize(*)
- super
- ActiveRecord::SchemaMigration.create_table
- end
-
def setup
super
+ ActiveRecord::SchemaMigration.create_table
@stream = StringIO.new
end
@@ -116,7 +111,7 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{c_int_4.*}, output
assert_no_match %r{c_int_4.*limit:}, output
- elsif current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ elsif current_adapter?(:MysqlAdapter, :Mysql2Adapter)
assert_match %r{c_int_1.*limit: 1}, output
assert_match %r{c_int_2.*limit: 2}, output
assert_match %r{c_int_3.*limit: 3}, output
@@ -182,13 +177,19 @@ 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
- assert_equal 'add_index "companies", ["firm_id", "type", "rating"], name: "company_index"', index_definition
+ if current_adapter?(:MysqlAdapter) || current_adapter?(:Mysql2Adapter) || current_adapter?(: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
+ end
end
def test_schema_dumps_partial_indices
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)"', index_definition
+ 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)
+ assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index", using: :btree', index_definition
else
assert_equal 'add_index "companies", ["firm_id", "type"], name: "company_partial_index"', index_definition
end
@@ -201,12 +202,23 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r(primary_key: "movieid"), match[1], "non-standard primary key not preserved"
end
- if current_adapter?(:MysqlAdapter) or current_adapter?(:Mysql2Adapter)
+ def test_schema_dump_should_use_false_as_default
+ output = standard_dump
+ assert_match %r{t\.boolean\s+"has_fun",.+default: false}, output
+ end
+
+ if current_adapter?(:MysqlAdapter, :Mysql2Adapter)
def test_schema_dump_should_not_add_default_value_for_mysql_text_field
output = standard_dump
assert_match %r{t.text\s+"body",\s+null: false$}, output
end
+ def test_schema_dump_includes_length_for_mysql_binary_fields
+ output = standard_dump
+ assert_match %r{t.binary\s+"var_binary",\s+limit: 255$}, output
+ assert_match %r{t.binary\s+"var_binary_large",\s+limit: 4095$}, output
+ end
+
def test_schema_dump_includes_length_for_mysql_blob_and_text_fields
output = standard_dump
assert_match %r{t.binary\s+"tiny_blob",\s+limit: 255$}, output
@@ -218,6 +230,12 @@ class SchemaDumperTest < ActiveRecord::TestCase
assert_match %r{t.text\s+"medium_text",\s+limit: 16777215$}, output
assert_match %r{t.text\s+"long_text",\s+limit: 2147483647$}, output
end
+
+ def test_schema_dumps_index_type
+ output = standard_dump
+ assert_match %r{add_index "key_tests", \["awesome"\], name: "index_key_tests_on_awesome", type: :fulltext}, output
+ assert_match %r{add_index "key_tests", \["pizza"\], name: "index_key_tests_on_pizza", using: :btree}, output
+ end
end
def test_schema_dump_includes_decimal_options
@@ -229,6 +247,26 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
if current_adapter?(:PostgreSQLAdapter)
+ def test_schema_dump_includes_bigint_default
+ output = standard_dump
+ assert_match %r{t.integer\s+"bigint_default",\s+limit: 8,\s+default: 0}, output
+ end
+
+ def test_schema_dump_includes_extensions
+ connection = ActiveRecord::Base.connection
+ skip unless connection.supports_extensions?
+
+ connection.stubs(:extensions).returns(['hstore'])
+ output = standard_dump
+ assert_match "# These are extensions that must be enabled", output
+ assert_match %r{enable_extension "hstore"}, output
+
+ connection.stubs(:extensions).returns([])
+ output = standard_dump
+ assert_no_match "# These are extensions that must be enabled", output
+ assert_no_match %r{enable_extension}, output
+ end
+
def test_schema_dump_includes_xml_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_xml_data_type"} =~ output
@@ -245,28 +283,28 @@ class SchemaDumperTest < ActiveRecord::TestCase
def test_schema_dump_includes_inet_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.inet "inet_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.inet\s+"inet_address",\s+default: "192.168.1.1"}, output
end
end
def test_schema_dump_includes_cidr_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.cidr "cidr_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.cidr\s+"cidr_address",\s+default: "192.168.1.0/24"}, output
end
end
def test_schema_dump_includes_macaddr_shorthand_definition
output = standard_dump
- if %r{create_table "postgresql_network_address"} =~ output
- assert_match %r{t.macaddr "macaddr_address"}, output
+ if %r{create_table "postgresql_network_addresses"} =~ output
+ assert_match %r{t.macaddr\s+"mac_address",\s+default: "ff:ff:ff:ff:ff:ff"}, output
end
end
def test_schema_dump_includes_uuid_shorthand_definition
output = standard_dump
- if %r{create_table "poistgresql_uuids"} =~ output
+ if %r{create_table "postgresql_uuids"} =~ output
assert_match %r{t.uuid "guid"}, output
end
end
@@ -278,6 +316,13 @@ class SchemaDumperTest < ActiveRecord::TestCase
end
end
+ def test_schema_dump_includes_ltrees_shorthand_definition
+ output = standard_dump
+ if %r{create_table "postgresql_ltrees"} =~ output
+ assert_match %r[t.ltree "path"], output
+ end
+ end
+
def test_schema_dump_includes_arrays_shorthand_definition
output = standard_dump
if %r{create_table "postgresql_arrays"} =~ output
diff --git a/activerecord/test/cases/scoping/default_scoping_test.rb b/activerecord/test/cases/scoping/default_scoping_test.rb
new file mode 100644
index 0000000000..76f395ba83
--- /dev/null
+++ b/activerecord/test/cases/scoping/default_scoping_test.rb
@@ -0,0 +1,384 @@
+require 'cases/helper'
+require 'models/post'
+require 'models/developer'
+
+class DefaultScopingTest < ActiveRecord::TestCase
+ fixtures :developers, :posts
+
+ def test_default_scope
+ expected = Developer.all.merge!(:order => 'salary DESC').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.collect { |dev| dev.salary }
+ assert_equal expected, received
+ end
+
+ def test_default_scope_as_class_method
+ assert_equal [developers(:david).becomes(ClassMethodDeveloperCalledDavid)], ClassMethodDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_as_class_method_referencing_scope
+ assert_equal [developers(:david).becomes(ClassMethodReferencingScopeDeveloperCalledDavid)], ClassMethodReferencingScopeDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_as_block_referencing_scope
+ assert_equal [developers(:david).becomes(LazyBlockReferencingScopeDeveloperCalledDavid)], LazyBlockReferencingScopeDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_lambda
+ assert_equal [developers(:david).becomes(LazyLambdaDeveloperCalledDavid)], LazyLambdaDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_block
+ assert_equal [developers(:david).becomes(LazyBlockDeveloperCalledDavid)], LazyBlockDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_with_callable
+ assert_equal [developers(:david).becomes(CallableDeveloperCalledDavid)], CallableDeveloperCalledDavid.all
+ end
+
+ def test_default_scope_is_unscoped_on_find
+ assert_equal 1, DeveloperCalledDavid.count
+ assert_equal 11, DeveloperCalledDavid.unscoped.count
+ end
+
+ def test_default_scope_is_unscoped_on_create
+ assert_nil DeveloperCalledJamis.unscoped.create!.name
+ end
+
+ def test_default_scope_with_conditions_string
+ assert_equal Developer.where(name: 'David').map(&:id).sort, DeveloperCalledDavid.all.map(&:id).sort
+ assert_equal nil, DeveloperCalledDavid.create!.name
+ end
+
+ def test_default_scope_with_conditions_hash
+ assert_equal Developer.where(name: 'Jamis').map(&:id).sort, DeveloperCalledJamis.all.map(&:id).sort
+ assert_equal 'Jamis', DeveloperCalledJamis.create!.name
+ end
+
+ def test_default_scoping_with_threads
+ skip "in-memory database mustn't disconnect" if in_memory_db?
+
+ 2.times do
+ Thread.new {
+ assert DeveloperOrderedBySalary.all.to_sql.include?('salary DESC')
+ DeveloperOrderedBySalary.connection.close
+ }.join
+ end
+ end
+
+ def test_default_scope_with_inheritance
+ wheres = InheritedPoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_default_scope_with_module_includes
+ wheres = ModuleIncludedPoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_default_scope_with_multiple_calls
+ wheres = MultiplePoorDeveloperCalledJamis.all.where_values_hash
+ assert_equal "Jamis", wheres['name']
+ assert_equal 50000, wheres['salary']
+ end
+
+ def test_scope_overwrites_default
+ expected = Developer.all.merge!(order: 'salary DESC, name DESC').to_a.collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.by_name.to_a.collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_reorder_overrides_default_scope_order
+ expected = Developer.order('name DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.reorder('name DESC').collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_order_after_reorder_combines_orders
+ expected = Developer.order('name DESC, id DESC').collect { |dev| [dev.name, dev.id] }
+ received = Developer.order('name ASC').reorder('name DESC').order('id DESC').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+ end
+
+ def test_unscope_overrides_default_scope
+ expected = Developer.all.collect { |dev| [dev.name, dev.id] }
+ received = Developer.order('name ASC, id DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+ end
+
+ def test_unscope_after_reordering_and_combining
+ expected = Developer.order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ received = DeveloperOrderedBySalary.reorder('name DESC').unscope(:order).order('id DESC, name DESC').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+
+ expected_2 = Developer.all.collect { |dev| [dev.name, dev.id] }
+ received_2 = Developer.order('id DESC, name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ assert_equal expected_2, received_2
+
+ expected_3 = Developer.all.collect { |dev| [dev.name, dev.id] }
+ received_3 = Developer.reorder('name DESC').unscope(:order).collect { |dev| [dev.name, dev.id] }
+ assert_equal expected_3, received_3
+ end
+
+ def test_unscope_with_where_attributes
+ expected = Developer.order('salary DESC').collect(&:name)
+ received = DeveloperOrderedBySalary.where(name: 'David').unscope(where: :name).collect(&:name)
+ assert_equal expected, received
+
+ expected_2 = Developer.order('salary DESC').collect(&:name)
+ received_2 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope({:where => :name}, :select).collect(&:name)
+ assert_equal expected_2, received_2
+
+ expected_3 = Developer.order('salary DESC').collect(&:name)
+ received_3 = DeveloperOrderedBySalary.select("id").where("name" => "Jamis").unscope(:select, :where).collect(&:name)
+ assert_equal expected_3, received_3
+
+ expected_4 = Developer.order('salary DESC').collect(&:name)
+ received_4 = DeveloperOrderedBySalary.where.not("name" => "Jamis").unscope(where: :name).collect(&:name)
+ assert_equal expected_4, received_4
+
+ expected_5 = Developer.order('salary DESC').collect(&:name)
+ received_5 = DeveloperOrderedBySalary.where.not("name" => ["Jamis", "David"]).unscope(where: :name).collect(&:name)
+ assert_equal expected_5, received_5
+ end
+
+ def test_unscope_multiple_where_clauses
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.where(name: 'Jamis').where(id: 1).unscope(where: [:name, :id]).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_with_grouping_attributes
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.group(:name).unscope(:group).collect { |dev| dev.name }
+ assert_equal expected, received
+
+ expected_2 = Developer.order('salary DESC').collect { |dev| dev.name }
+ received_2 = DeveloperOrderedBySalary.group("name").unscope(:group).collect { |dev| dev.name }
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_with_limit_in_query
+ expected = Developer.order('salary DESC').collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.limit(1).unscope(:limit).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_order_to_unscope_reordering
+ scope = DeveloperOrderedBySalary.order('salary DESC, name ASC').reverse_order.unscope(:order)
+ assert !(scope.to_sql =~ /order/i)
+ end
+
+ def test_unscope_reverse_order
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.unscope(:order).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_select
+ expected = Developer.order('salary ASC').collect { |dev| dev.name }
+ received = Developer.order('salary DESC').reverse_order.select(:name => "Jamis").unscope(:select).collect { |dev| dev.name }
+ assert_equal expected, received
+
+ expected_2 = Developer.all.collect { |dev| dev.id }
+ received_2 = Developer.select(:name).unscope(:select).collect { |dev| dev.id }
+ assert_equal expected_2, received_2
+ end
+
+ def test_unscope_offset
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.offset(5).unscope(:offset).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_joins_and_select_on_developers_projects
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.joins('JOIN developers_projects ON id = developer_id').select(:id).unscope(:joins, :select).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_includes
+ expected = Developer.all.collect { |dev| dev.name }
+ received = Developer.includes(:projects).select(:id).unscope(:includes, :select).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_having
+ expected = DeveloperOrderedBySalary.all.collect { |dev| dev.name }
+ received = DeveloperOrderedBySalary.having("name IN ('Jamis', 'David')").unscope(:having).collect { |dev| dev.name }
+ assert_equal expected, received
+ end
+
+ def test_unscope_and_scope
+ developer_klass = Class.new(Developer) do
+ scope :by_name, -> name { unscope(where: :name).where(name: name) }
+ end
+
+ expected = developer_klass.where(name: 'Jamis').collect { |dev| [dev.name, dev.id] }
+ received = developer_klass.where(name: 'David').by_name('Jamis').collect { |dev| [dev.name, dev.id] }
+ assert_equal expected, received
+ end
+
+ def test_unscope_errors_with_invalid_value
+ assert_raises(ArgumentError) do
+ Developer.includes(:projects).where(name: "Jamis").unscope(:stupidly_incorrect_value)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.all.unscope(:includes, :select, :some_broken_value)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.order('name DESC').reverse_order.unscope(:reverse_order)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.order('name DESC').where(name: "Jamis").unscope()
+ end
+ end
+
+ def test_unscope_errors_with_non_where_hash_keys
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").limit(4).unscope(limit: 4)
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").unscope("where" => :name)
+ end
+ end
+
+ def test_unscope_errors_with_non_symbol_or_hash_arguments
+ assert_raises(ArgumentError) do
+ Developer.where(name: "Jamis").limit(3).unscope("limit")
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.select("id").unscope("select")
+ end
+
+ assert_raises(ArgumentError) do
+ Developer.select("id").unscope(5)
+ end
+ end
+
+ def test_order_in_default_scope_should_not_prevail
+ expected = Developer.all.merge!(order: 'salary desc').to_a.collect { |dev| dev.salary }
+ received = DeveloperOrderedBySalary.all.merge!(order: 'salary').to_a.collect { |dev| dev.salary }
+ assert_equal expected, received
+ end
+
+ def test_create_attribute_overwrites_default_scoping
+ assert_equal 'David', PoorDeveloperCalledJamis.create!(:name => 'David').name
+ assert_equal 200000, PoorDeveloperCalledJamis.create!(:name => 'David', :salary => 200000).salary
+ end
+
+ def test_create_attribute_overwrites_default_values
+ assert_equal nil, PoorDeveloperCalledJamis.create!(:salary => nil).salary
+ assert_equal 50000, PoorDeveloperCalledJamis.create!(:name => 'David').salary
+ end
+
+ def test_default_scope_attribute
+ jamis = PoorDeveloperCalledJamis.new(:name => 'David')
+ assert_equal 50000, jamis.salary
+ end
+
+ def test_where_attribute
+ aaron = PoorDeveloperCalledJamis.where(:salary => 20).new(:name => 'Aaron')
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_where_attribute_merge
+ aaron = PoorDeveloperCalledJamis.where(:name => 'foo').new(:name => 'Aaron')
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_scope_composed_by_limit_and_then_offset_is_equal_to_scope_composed_by_offset_and_then_limit
+ posts_limit_offset = Post.limit(3).offset(2)
+ posts_offset_limit = Post.offset(2).limit(3)
+ assert_equal posts_limit_offset, posts_offset_limit
+ end
+
+ def test_create_with_merge
+ aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).merge(
+ PoorDeveloperCalledJamis.create_with(:name => 'Aaron')).new
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+
+ aaron = PoorDeveloperCalledJamis.create_with(:name => 'foo', :salary => 20).
+ create_with(:name => 'Aaron').new
+ assert_equal 20, aaron.salary
+ assert_equal 'Aaron', aaron.name
+ end
+
+ def test_create_with_reset
+ jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with(nil).new
+ assert_equal 'Jamis', jamis.name
+ end
+
+ # FIXME: I don't know if this is *desired* behavior, but it is *today's*
+ # behavior.
+ def test_create_with_empty_hash_will_not_reset
+ jamis = PoorDeveloperCalledJamis.create_with(:name => 'Aaron').create_with({}).new
+ assert_equal 'Aaron', jamis.name
+ end
+
+ def test_unscoped_with_named_scope_should_not_have_default_scope
+ assert_equal [DeveloperCalledJamis.find(developers(:poor_jamis).id)], DeveloperCalledJamis.poor
+
+ assert DeveloperCalledJamis.unscoped.poor.include?(developers(:david).becomes(DeveloperCalledJamis))
+
+ assert_equal 11, DeveloperCalledJamis.unscoped.length
+ assert_equal 1, DeveloperCalledJamis.poor.length
+ assert_equal 10, DeveloperCalledJamis.unscoped.poor.length
+ assert_equal 10, DeveloperCalledJamis.unscoped { DeveloperCalledJamis.poor }.length
+ end
+
+ def test_default_scope_select_ignored_by_aggregations
+ assert_equal DeveloperWithSelect.all.to_a.count, DeveloperWithSelect.count
+ end
+
+ def test_default_scope_select_ignored_by_grouped_aggregations
+ assert_equal Hash[Developer.all.group_by(&:salary).map { |s, d| [s, d.count] }],
+ DeveloperWithSelect.group(:salary).count
+ end
+
+ def test_default_scope_order_ignored_by_aggregations
+ assert_equal DeveloperOrderedBySalary.all.count, DeveloperOrderedBySalary.count
+ end
+
+ def test_default_scope_find_last
+ assert DeveloperOrderedBySalary.count > 1, "need more than one row for test"
+
+ lowest_salary_dev = DeveloperOrderedBySalary.find(developers(:poor_jamis).id)
+ assert_equal lowest_salary_dev, DeveloperOrderedBySalary.last
+ end
+
+ def test_default_scope_include_with_count
+ d = DeveloperWithIncludes.create!
+ d.audit_logs.create! :message => 'foo'
+
+ assert_equal 1, DeveloperWithIncludes.where(:audit_logs => { :message => 'foo' }).count
+ end
+
+ def test_default_scope_is_threadsafe
+ if in_memory_db?
+ skip "in memory db can't share a db between threads"
+ end
+
+ threads = []
+ assert_not_equal 1, ThreadsafeDeveloper.unscoped.count
+
+ threads << Thread.new do
+ Thread.current[:long_default_scope] = true
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads << Thread.new do
+ assert_equal 1, ThreadsafeDeveloper.all.to_a.count
+ ThreadsafeDeveloper.connection.close
+ end
+ threads.each(&:join)
+ end
+end
diff --git a/activerecord/test/cases/scoping/named_scoping_test.rb b/activerecord/test/cases/scoping/named_scoping_test.rb
new file mode 100644
index 0000000000..72c9787b84
--- /dev/null
+++ b/activerecord/test/cases/scoping/named_scoping_test.rb
@@ -0,0 +1,451 @@
+require "cases/helper"
+require 'models/post'
+require 'models/topic'
+require 'models/comment'
+require 'models/reply'
+require 'models/author'
+require 'models/developer'
+
+class NamedScopingTest < ActiveRecord::TestCase
+ fixtures :posts, :authors, :topics, :comments, :author_addresses
+
+ def test_implements_enumerable
+ assert !Topic.all.empty?
+
+ assert_equal Topic.all.to_a, Topic.base
+ assert_equal Topic.all.to_a, Topic.base.to_a
+ assert_equal Topic.first, Topic.base.first
+ assert_equal Topic.all.to_a, Topic.base.map { |i| i }
+ end
+
+ def test_found_items_are_cached
+ all_posts = Topic.base
+
+ assert_queries(1) do
+ all_posts.collect
+ all_posts.collect
+ end
+ end
+
+ def test_reload_expires_cache_of_found_items
+ all_posts = Topic.base
+ all_posts.to_a
+
+ new_post = Topic.create!
+ assert !all_posts.include?(new_post)
+ assert all_posts.reload.include?(new_post)
+ end
+
+ def test_delegates_finds_and_calculations_to_the_base_class
+ assert !Topic.all.empty?
+
+ assert_equal Topic.all.to_a, Topic.base.to_a
+ assert_equal Topic.first, Topic.base.first
+ assert_equal Topic.count, Topic.base.count
+ assert_equal Topic.average(:replies_count), Topic.base.average(:replies_count)
+ end
+
+ def test_method_missing_priority_when_delegating
+ klazz = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ scope :since, Proc.new { where('written_on >= ?', Time.now - 1.day) }
+ scope :to, Proc.new { where('written_on <= ?', Time.now) }
+ end
+ assert_equal klazz.to.since.to_a, klazz.since.to.to_a
+ end
+
+ def test_scope_should_respond_to_own_methods_and_methods_of_the_proxy
+ assert Topic.approved.respond_to?(:limit)
+ assert Topic.approved.respond_to?(:count)
+ assert Topic.approved.respond_to?(:length)
+ end
+
+ def test_scopes_with_options_limit_finds_to_those_matching_the_criteria_specified
+ assert !Topic.all.merge!(:where => {:approved => true}).to_a.empty?
+
+ assert_equal Topic.all.merge!(:where => {:approved => true}).to_a, Topic.approved
+ assert_equal Topic.where(:approved => true).count, Topic.approved.count
+ end
+
+ def test_scopes_with_string_name_can_be_composed
+ # NOTE that scopes defined with a string as a name worked on their own
+ # but when called on another scope the other scope was completely replaced
+ assert_equal Topic.replied.approved, Topic.replied.approved_as_string
+ end
+
+ def test_scopes_are_composable
+ assert_equal((approved = Topic.all.merge!(:where => {:approved => true}).to_a), Topic.approved)
+ assert_equal((replied = Topic.all.merge!(:where => 'replies_count > 0').to_a), Topic.replied)
+ assert !(approved == replied)
+ assert !(approved & replied).empty?
+
+ assert_equal approved & replied, Topic.approved.replied
+ end
+
+ def test_procedural_scopes
+ topics_written_before_the_third = Topic.where('written_on < ?', topics(:third).written_on)
+ topics_written_before_the_second = Topic.where('written_on < ?', topics(:second).written_on)
+ assert_not_equal topics_written_before_the_second, topics_written_before_the_third
+
+ assert_equal topics_written_before_the_third, Topic.written_before(topics(:third).written_on)
+ assert_equal topics_written_before_the_second, Topic.written_before(topics(:second).written_on)
+ end
+
+ def test_procedural_scopes_returning_nil
+ all_topics = Topic.all
+
+ assert_equal all_topics, Topic.written_before(nil)
+ end
+
+ def test_scope_with_object
+ objects = Topic.with_object
+ assert_operator objects.length, :>, 0
+ assert objects.all?(&:approved?), 'all objects should be approved'
+ end
+
+ def test_has_many_associations_have_access_to_scopes
+ assert_not_equal Post.containing_the_letter_a, authors(:david).posts
+ assert !Post.containing_the_letter_a.empty?
+
+ assert_equal authors(:david).posts & Post.containing_the_letter_a, authors(:david).posts.containing_the_letter_a
+ end
+
+ def test_scope_with_STI
+ assert_equal 3,Post.containing_the_letter_a.count
+ assert_equal 1,SpecialPost.containing_the_letter_a.count
+ end
+
+ def test_has_many_through_associations_have_access_to_scopes
+ assert_not_equal Comment.containing_the_letter_e, authors(:david).comments
+ assert !Comment.containing_the_letter_e.empty?
+
+ assert_equal authors(:david).comments & Comment.containing_the_letter_e, authors(:david).comments.containing_the_letter_e
+ end
+
+ def test_scopes_honor_current_scopes_from_when_defined
+ assert !Post.ranked_by_comments.limit_by(5).empty?
+ assert !authors(:david).posts.ranked_by_comments.limit_by(5).empty?
+ assert_not_equal Post.ranked_by_comments.limit_by(5), authors(:david).posts.ranked_by_comments.limit_by(5)
+ assert_not_equal Post.top(5), authors(:david).posts.top(5)
+ # Oracle sometimes sorts differently if WHERE condition is changed
+ assert_equal authors(:david).posts.ranked_by_comments.limit_by(5).to_a.sort_by(&:id), authors(:david).posts.top(5).to_a.sort_by(&:id)
+ assert_equal Post.ranked_by_comments.limit_by(5), Post.top(5)
+ end
+
+ def test_active_records_have_scope_named__all__
+ assert !Topic.all.empty?
+
+ assert_equal Topic.all.to_a, Topic.base
+ end
+
+ def test_active_records_have_scope_named__scoped__
+ scope = Topic.where("content LIKE '%Have%'")
+ assert !scope.empty?
+
+ assert_equal scope, Topic.all.merge!(where: "content LIKE '%Have%'")
+ end
+
+ def test_first_and_last_should_allow_integers_for_limit
+ assert_equal Topic.base.first(2), Topic.base.to_a.first(2)
+ assert_equal Topic.base.last(2), Topic.base.order("id").to_a.last(2)
+ end
+
+ def test_first_and_last_should_not_use_query_when_results_are_loaded
+ topics = Topic.base
+ topics.reload # force load
+ assert_no_queries do
+ topics.first
+ topics.last
+ end
+ end
+
+ def test_empty_should_not_load_results
+ topics = Topic.base
+ assert_queries(2) do
+ topics.empty? # use count query
+ topics.collect # force load
+ topics.empty? # use loaded (no query)
+ end
+ end
+
+ def test_any_should_not_load_results
+ topics = Topic.base
+ assert_queries(2) do
+ topics.any? # use count query
+ topics.collect # force load
+ topics.any? # use loaded (no query)
+ end
+ end
+
+ def test_any_should_call_proxy_found_if_using_a_block
+ topics = Topic.base
+ assert_queries(1) do
+ topics.expects(:empty?).never
+ topics.any? { true }
+ end
+ end
+
+ def test_any_should_not_fire_query_if_scope_loaded
+ topics = Topic.base
+ topics.collect # force load
+ assert_no_queries { assert topics.any? }
+ end
+
+ def test_model_class_should_respond_to_any
+ assert Topic.any?
+ Topic.delete_all
+ assert !Topic.any?
+ end
+
+ def test_many_should_not_load_results
+ topics = Topic.base
+ assert_queries(2) do
+ topics.many? # use count query
+ topics.collect # force load
+ topics.many? # use loaded (no query)
+ end
+ end
+
+ def test_many_should_call_proxy_found_if_using_a_block
+ topics = Topic.base
+ assert_queries(1) do
+ topics.expects(:size).never
+ topics.many? { true }
+ end
+ end
+
+ def test_many_should_not_fire_query_if_scope_loaded
+ topics = Topic.base
+ topics.collect # force load
+ assert_no_queries { assert topics.many? }
+ end
+
+ def test_many_should_return_false_if_none_or_one
+ topics = Topic.base.where(:id => 0)
+ assert !topics.many?
+ topics = Topic.base.where(:id => 1)
+ assert !topics.many?
+ end
+
+ def test_many_should_return_true_if_more_than_one
+ assert Topic.base.many?
+ end
+
+ def test_model_class_should_respond_to_many
+ Topic.delete_all
+ assert !Topic.many?
+ Topic.create!
+ assert !Topic.many?
+ Topic.create!
+ assert Topic.many?
+ end
+
+ def test_should_build_on_top_of_scope
+ topic = Topic.approved.build({})
+ assert topic.approved
+ end
+
+ def test_should_build_new_on_top_of_scope
+ topic = Topic.approved.new
+ assert topic.approved
+ end
+
+ def test_should_create_on_top_of_scope
+ topic = Topic.approved.create({})
+ assert topic.approved
+ end
+
+ def test_should_create_with_bang_on_top_of_scope
+ topic = Topic.approved.create!({})
+ assert topic.approved
+ end
+
+ def test_should_build_on_top_of_chained_scopes
+ topic = Topic.approved.by_lifo.build({})
+ assert topic.approved
+ assert_equal 'lifo', topic.author_name
+ end
+
+ # Method delegation for scope names which look like /\A[a-zA-Z_]\w*[!?]?\z/
+ # has been done by evaluating a string with a plain def statement. For scope
+ # names which contain spaces this approach doesn't work.
+ def test_spaces_in_scope_names
+ klass = Class.new(ActiveRecord::Base) do
+ self.table_name = "topics"
+ scope :"title containing space", -> { where("title LIKE '% %'") }
+ scope :approved, -> { where(:approved => true) }
+ end
+ assert_equal klass.send(:"title containing space"), klass.where("title LIKE '% %'")
+ assert_equal klass.approved.send(:"title containing space"), klass.approved.where("title LIKE '% %'")
+ end
+
+ def test_find_all_should_behave_like_select
+ assert_equal Topic.base.to_a.select(&:approved), Topic.base.to_a.find_all(&:approved)
+ end
+
+ def test_rand_should_select_a_random_object_from_proxy
+ assert_kind_of Topic, Topic.approved.sample
+ end
+
+ def test_should_use_where_in_query_for_scope
+ assert_equal Developer.where(name: 'Jamis').to_set, Developer.where(id: Developer.jamises).to_set
+ end
+
+ def test_size_should_use_count_when_results_are_not_loaded
+ topics = Topic.base
+ assert_queries(1) do
+ assert_sql(/COUNT/i) { topics.size }
+ end
+ end
+
+ def test_size_should_use_length_when_results_are_loaded
+ topics = Topic.base
+ topics.reload # force load
+ assert_no_queries do
+ topics.size # use loaded (no query)
+ end
+ end
+
+ def test_should_not_duplicates_where_values
+ where_values = Topic.where("1=1").scope_with_lambda.where_values
+ assert_equal ["1=1"], where_values
+ end
+
+ def test_chaining_with_duplicate_joins
+ join = "INNER JOIN comments ON comments.post_id = posts.id"
+ post = Post.find(1)
+ assert_equal post.comments.size, Post.joins(join).joins(join).where("posts.id = #{post.id}").size
+ end
+
+ def test_chaining_applies_last_conditions_when_creating
+ post = Topic.rejected.new
+ assert !post.approved?
+
+ post = Topic.rejected.approved.new
+ assert post.approved?
+
+ post = Topic.approved.rejected.new
+ assert !post.approved?
+
+ post = Topic.approved.rejected.approved.new
+ assert post.approved?
+ end
+
+ def test_chaining_combines_conditions_when_searching
+ # Normal hash conditions
+ assert_equal Topic.where(approved: false).where(approved: true).to_a, Topic.rejected.approved.to_a
+ assert_equal Topic.where(approved: true).where(approved: false).to_a, Topic.approved.rejected.to_a
+
+ # Nested hash conditions with same keys
+ assert_equal [], Post.with_special_comments.with_very_special_comments.to_a
+
+ # Nested hash conditions with different keys
+ assert_equal [posts(:sti_comments)], Post.with_special_comments.with_post(4).to_a.uniq
+ end
+
+ def test_scopes_batch_finders
+ assert_equal 3, Topic.approved.count
+
+ assert_queries(4) do
+ Topic.approved.find_each(:batch_size => 1) {|t| assert t.approved? }
+ end
+
+ assert_queries(2) do
+ Topic.approved.find_in_batches(:batch_size => 2) do |group|
+ group.each {|t| assert t.approved? }
+ end
+ end
+ end
+
+ def test_table_names_for_chaining_scopes_with_and_without_table_name_included
+ assert_nothing_raised do
+ Comment.for_first_post.for_first_author.to_a
+ end
+ end
+
+ def test_scopes_on_relations
+ # Topic.replied
+ approved_topics = Topic.all.approved.order('id DESC')
+ assert_equal topics(:fourth), approved_topics.first
+
+ replied_approved_topics = approved_topics.replied
+ assert_equal topics(:third), replied_approved_topics.first
+ end
+
+ def test_index_on_scope
+ approved = Topic.approved.order('id ASC')
+ assert_equal topics(:second), approved[0]
+ assert approved.loaded?
+ end
+
+ def test_nested_scopes_queries_size
+ assert_queries(1) do
+ Topic.approved.by_lifo.replied.written_before(Time.now).to_a
+ end
+ end
+
+ # Note: these next two are kinda odd because they are essentially just testing that the
+ # query cache works as it should, but they are here for legacy reasons as they was previously
+ # a separate cache on association proxies, and these show that that is not necessary.
+ def test_scopes_are_cached_on_associations
+ post = posts(:welcome)
+
+ Post.cache do
+ assert_queries(1) { post.comments.containing_the_letter_e.to_a }
+ assert_no_queries { post.comments.containing_the_letter_e.to_a }
+ end
+ end
+
+ def test_scopes_with_arguments_are_cached_on_associations
+ post = posts(:welcome)
+
+ Post.cache do
+ one = assert_queries(1) { post.comments.limit_by(1).to_a }
+ assert_equal 1, one.size
+
+ two = assert_queries(1) { post.comments.limit_by(2).to_a }
+ assert_equal 2, two.size
+
+ assert_no_queries { post.comments.limit_by(1).to_a }
+ assert_no_queries { post.comments.limit_by(2).to_a }
+ end
+ end
+
+ def test_scopes_to_get_newest
+ post = posts(:welcome)
+ old_last_comment = post.comments.newest
+ new_comment = post.comments.create(:body => "My new comment")
+ assert_equal new_comment, post.comments.newest
+ assert_not_equal old_last_comment, post.comments.newest
+ end
+
+ def test_scopes_are_reset_on_association_reload
+ post = posts(:welcome)
+
+ [:destroy_all, :reset, :delete_all].each do |method|
+ before = post.comments.containing_the_letter_e
+ post.association(:comments).send(method)
+ assert before.object_id != post.comments.containing_the_letter_e.object_id, "CollectionAssociation##{method} should reset the named scopes cache"
+ end
+ end
+
+ def test_scoped_are_lazy_loaded_if_table_still_does_not_exist
+ assert_nothing_raised do
+ require "models/without_table"
+ end
+ end
+
+ def test_eager_default_scope_relations_are_remove
+ klass = Class.new(ActiveRecord::Base)
+ klass.table_name = 'posts'
+
+ assert_raises(ArgumentError) do
+ klass.send(:default_scope, klass.where(:id => posts(:welcome).id))
+ end
+ end
+
+ def test_subclass_merges_scopes_properly
+ assert_equal 1, SpecialComment.where(body: 'go crazy').created.count
+ end
+
+end
diff --git a/activerecord/test/cases/scoping/relation_scoping_test.rb b/activerecord/test/cases/scoping/relation_scoping_test.rb
new file mode 100644
index 0000000000..0018fc06f2
--- /dev/null
+++ b/activerecord/test/cases/scoping/relation_scoping_test.rb
@@ -0,0 +1,331 @@
+require "cases/helper"
+require 'models/post'
+require 'models/author'
+require 'models/developer'
+require 'models/project'
+require 'models/comment'
+require 'models/category'
+require 'models/person'
+require 'models/reference'
+
+class RelationScopingTest < ActiveRecord::TestCase
+ fixtures :authors, :developers, :projects, :comments, :posts, :developers_projects
+
+ def test_reverse_order
+ assert_equal Developer.order("id DESC").to_a.reverse, Developer.order("id DESC").reverse_order
+ end
+
+ def test_reverse_order_with_arel_node
+ assert_equal Developer.order("id DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).reverse_order
+ end
+
+ def test_reverse_order_with_multiple_arel_nodes
+ assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order(Developer.arel_table[:id].desc).order(Developer.arel_table[:name].desc).reverse_order
+ end
+
+ def test_reverse_order_with_arel_nodes_and_strings
+ assert_equal Developer.order("id DESC").order("name DESC").to_a.reverse, Developer.order("id DESC").order(Developer.arel_table[:name].desc).reverse_order
+ end
+
+ def test_double_reverse_order_produces_original_order
+ assert_equal Developer.order("name DESC"), Developer.order("name DESC").reverse_order.reverse_order
+ end
+
+ def test_scoped_find
+ Developer.where("name = 'David'").scoping do
+ assert_nothing_raised { Developer.find(1) }
+ end
+ end
+
+ def test_scoped_find_first
+ developer = Developer.find(10)
+ Developer.where("salary = 100000").scoping do
+ assert_equal developer, Developer.order("name").first
+ end
+ end
+
+ def test_scoped_find_last
+ highest_salary = Developer.order("salary DESC").first
+
+ Developer.order("salary").scoping do
+ assert_equal highest_salary, Developer.last
+ end
+ end
+
+ def test_scoped_find_last_preserves_scope
+ lowest_salary = Developer.order("salary ASC").first
+ highest_salary = Developer.order("salary DESC").first
+
+ Developer.order("salary").scoping do
+ assert_equal highest_salary, Developer.last
+ assert_equal lowest_salary, Developer.first
+ end
+ end
+
+ def test_scoped_find_combines_and_sanitizes_conditions
+ Developer.where("salary = 9000").scoping do
+ assert_equal developers(:poor_jamis), Developer.where("name = 'Jamis'").first
+ end
+ end
+
+ def test_scoped_find_all
+ Developer.where("name = 'David'").scoping do
+ assert_equal [developers(:david)], Developer.all
+ end
+ end
+
+ def test_scoped_find_select
+ Developer.select("id, name").scoping do
+ developer = Developer.where("name = 'David'").first
+ assert_equal "David", developer.name
+ assert !developer.has_attribute?(:salary)
+ end
+ end
+
+ def test_scope_select_concatenates
+ Developer.select("id, name").scoping do
+ developer = Developer.select('salary').where("name = 'David'").first
+ assert_equal 80000, developer.salary
+ assert developer.has_attribute?(:id)
+ assert developer.has_attribute?(:name)
+ assert developer.has_attribute?(:salary)
+ end
+ end
+
+ def test_scoped_count
+ Developer.where("name = 'David'").scoping do
+ assert_equal 1, Developer.count
+ end
+
+ Developer.where('salary = 100000').scoping do
+ assert_equal 8, Developer.count
+ assert_equal 1, Developer.where("name LIKE 'fixture_1%'").count
+ end
+ end
+
+ def test_scoped_find_include
+ # with the include, will retrieve only developers for the given project
+ scoped_developers = Developer.includes(:projects).scoping do
+ Developer.where('projects.id' => 2).to_a
+ end
+ assert scoped_developers.include?(developers(:david))
+ assert !scoped_developers.include?(developers(:jamis))
+ assert_equal 1, scoped_developers.size
+ end
+
+ def test_scoped_find_joins
+ scoped_developers = Developer.joins('JOIN developers_projects ON id = developer_id').scoping do
+ Developer.where('developers_projects.project_id = 2').to_a
+ end
+
+ assert scoped_developers.include?(developers(:david))
+ assert !scoped_developers.include?(developers(:jamis))
+ assert_equal 1, scoped_developers.size
+ assert_equal developers(:david).attributes, scoped_developers.first.attributes
+ end
+
+ def test_scoped_create_with_where
+ new_comment = VerySpecialComment.where(:post_id => 1).scoping do
+ VerySpecialComment.create :body => "Wonderful world"
+ end
+
+ assert_equal 1, new_comment.post_id
+ assert Post.find(1).comments.include?(new_comment)
+ end
+
+ def test_scoped_create_with_create_with
+ new_comment = VerySpecialComment.create_with(:post_id => 1).scoping do
+ VerySpecialComment.create :body => "Wonderful world"
+ end
+
+ assert_equal 1, new_comment.post_id
+ assert Post.find(1).comments.include?(new_comment)
+ end
+
+ def test_scoped_create_with_create_with_has_higher_priority
+ new_comment = VerySpecialComment.where(:post_id => 2).create_with(:post_id => 1).scoping do
+ VerySpecialComment.create :body => "Wonderful world"
+ end
+
+ assert_equal 1, new_comment.post_id
+ assert Post.find(1).comments.include?(new_comment)
+ end
+
+ def test_ensure_that_method_scoping_is_correctly_restored
+ begin
+ Developer.where("name = 'Jamis'").scoping do
+ raise "an exception"
+ end
+ rescue
+ end
+
+ assert !Developer.all.where_values.include?("name = 'Jamis'")
+ end
+
+ def test_default_scope_filters_on_joins
+ assert_equal 1, DeveloperFilteredOnJoins.all.count
+ assert_equal DeveloperFilteredOnJoins.all.first, developers(:david).becomes(DeveloperFilteredOnJoins)
+ end
+
+ def test_update_all_default_scope_filters_on_joins
+ DeveloperFilteredOnJoins.update_all(:salary => 65000)
+ assert_equal 65000, Developer.find(developers(:david).id).salary
+
+ # has not changed jamis
+ assert_not_equal 65000, Developer.find(developers(:jamis).id).salary
+ end
+
+ def test_delete_all_default_scope_filters_on_joins
+ assert_not_equal [], DeveloperFilteredOnJoins.all
+
+ DeveloperFilteredOnJoins.delete_all()
+
+ assert_equal [], DeveloperFilteredOnJoins.all
+ assert_not_equal [], Developer.all
+ end
+end
+
+class NestedRelationScopingTest < ActiveRecord::TestCase
+ fixtures :authors, :developers, :projects, :comments, :posts
+
+ def test_merge_options
+ 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
+ end
+ end
+ end
+
+ def test_merge_inner_scope_has_priority
+ Developer.limit(5).scoping do
+ Developer.limit(10).scoping do
+ assert_equal 10, Developer.all.size
+ end
+ end
+ end
+
+ def test_replace_options
+ Developer.where(:name => 'David').scoping do
+ Developer.unscoped do
+ assert_equal 'Jamis', Developer.where(:name => 'Jamis').first[:name]
+ end
+
+ assert_equal 'David', Developer.first[:name]
+ end
+ end
+
+ def test_three_level_nested_exclusive_scoped_find
+ Developer.where("name = 'Jamis'").scoping do
+ assert_equal 'Jamis', Developer.first.name
+
+ Developer.unscoped.where("name = 'David'") do
+ assert_equal 'David', Developer.first.name
+
+ Developer.unscoped.where("name = 'Maiha'") do
+ assert_equal nil, Developer.first
+ end
+
+ # ensure that scoping is restored
+ assert_equal 'David', Developer.first.name
+ end
+
+ # ensure that scoping is restored
+ assert_equal 'Jamis', Developer.first.name
+ end
+ end
+
+ def test_nested_scoped_create
+ comment = Comment.create_with(:post_id => 1).scoping do
+ Comment.create_with(:post_id => 2).scoping do
+ Comment.create :body => "Hey guys, nested scopes are broken. Please fix!"
+ end
+ end
+
+ assert_equal 2, comment.post_id
+ end
+
+ def test_nested_exclusive_scope_for_create
+ comment = Comment.create_with(:body => "Hey guys, nested scopes are broken. Please fix!").scoping do
+ Comment.unscoped.create_with(:post_id => 1).scoping do
+ assert Comment.new.body.blank?
+ Comment.create :body => "Hey guys"
+ end
+ end
+
+ assert_equal 1, comment.post_id
+ assert_equal 'Hey guys', comment.body
+ end
+end
+
+class HasManyScopingTest< ActiveRecord::TestCase
+ fixtures :comments, :posts, :people, :references
+
+ def setup
+ @welcome = Post.find(1)
+ end
+
+ def test_forwarding_of_static_methods
+ assert_equal 'a comment...', Comment.what_are_you
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+
+ def test_forwarding_to_scoped
+ assert_equal 4, Comment.search_by_type('Comment').size
+ assert_equal 2, @welcome.comments.search_by_type('Comment').size
+ end
+
+ def test_nested_scope_finder
+ Comment.where('1=0').scoping do
+ assert_equal 0, @welcome.comments.count
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+
+ Comment.where('1=1').scoping do
+ assert_equal 2, @welcome.comments.count
+ assert_equal 'a comment...', @welcome.comments.what_are_you
+ end
+ end
+
+ def test_should_maintain_default_scope_on_associations
+ magician = BadReference.find(1)
+ assert_equal [magician], people(:michael).bad_references
+ end
+
+ def test_should_default_scope_on_associations_is_overridden_by_association_conditions
+ reference = references(:michael_unicyclist).becomes(BadReference)
+ assert_equal [reference], people(:michael).fixed_bad_references
+ end
+
+ def test_should_maintain_default_scope_on_eager_loaded_associations
+ michael = Person.where(:id => people(:michael).id).includes(:bad_references).first
+ magician = BadReference.find(1)
+ assert_equal [magician], michael.bad_references
+ end
+end
+
+class HasAndBelongsToManyScopingTest< ActiveRecord::TestCase
+ fixtures :posts, :categories, :categories_posts
+
+ def setup
+ @welcome = Post.find(1)
+ end
+
+ def test_forwarding_of_static_methods
+ assert_equal 'a category...', Category.what_are_you
+ assert_equal 'a category...', @welcome.categories.what_are_you
+ end
+
+ def test_nested_scope_finder
+ Category.where('1=0').scoping do
+ assert_equal 0, @welcome.categories.count
+ assert_equal 'a category...', @welcome.categories.what_are_you
+ end
+
+ Category.where('1=1').scoping do
+ assert_equal 2, @welcome.categories.count
+ assert_equal 'a category...', @welcome.categories.what_are_you
+ end
+ end
+end
diff --git a/activerecord/test/cases/serialization_test.rb b/activerecord/test/cases/serialization_test.rb
index 25b860878a..c46060a646 100644
--- a/activerecord/test/cases/serialization_test.rb
+++ b/activerecord/test/cases/serialization_test.rb
@@ -18,6 +18,10 @@ class SerializationTest < ActiveRecord::TestCase
}
end
+ def test_include_root_in_json_is_false_by_default
+ assert_equal false, ActiveRecord::Base.include_root_in_json, "include_root_in_json should be false by default but was not"
+ end
+
def test_serialize_should_be_reversible
FORMATS.each do |format|
@serialized = Contact.new.send("to_#{format}")
@@ -45,4 +49,20 @@ class SerializationTest < ActiveRecord::TestCase
assert_equal @contact_attributes[:awesome], contact.awesome, "For #{format}"
end
end
+
+ def test_include_root_in_json_allows_inheritance
+ original_root_in_json = ActiveRecord::Base.include_root_in_json
+ ActiveRecord::Base.include_root_in_json = true
+
+ klazz = Class.new(ActiveRecord::Base)
+ klazz.table_name = 'topics'
+ assert klazz.include_root_in_json
+
+ klazz.include_root_in_json = false
+ assert ActiveRecord::Base.include_root_in_json
+ assert !klazz.include_root_in_json
+ assert !klazz.new.include_root_in_json
+ ensure
+ ActiveRecord::Base.include_root_in_json = original_root_in_json
+ end
end
diff --git a/activerecord/test/cases/serialized_attribute_test.rb b/activerecord/test/cases/serialized_attribute_test.rb
index f24ee54cd2..bc67da8d27 100644
--- a/activerecord/test/cases/serialized_attribute_test.rb
+++ b/activerecord/test/cases/serialized_attribute_test.rb
@@ -1,5 +1,8 @@
-require "cases/helper"
+require 'cases/helper'
require 'models/topic'
+require 'models/reply'
+require 'models/person'
+require 'models/traffic_light'
require 'bcrypt'
class SerializedAttributeTest < ActiveRecord::TestCase
@@ -16,12 +19,6 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_equal %w(content), Topic.serialized_attributes.keys
end
- def test_serialized_attributes_are_class_level_settings
- topic = Topic.new
- assert_raise(NoMethodError) { topic.serialized_attributes = [] }
- assert_deprecated { topic.serialized_attributes }
- end
-
def test_serialized_attribute
Topic.serialize("content", MyObject)
@@ -56,11 +53,21 @@ class SerializedAttributeTest < ActiveRecord::TestCase
def test_serialized_attribute_before_type_cast_returns_unserialized_value
Topic.serialize :content, Hash
- t = Topic.new(:content => { :foo => :bar })
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ t = Topic.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.content_before_type_cast)
t.save!
t.reload
- assert_equal({ :foo => :bar }, t.content_before_type_cast)
+ assert_equal({ foo: :bar }, t.content_before_type_cast)
+ end
+
+ def test_serialized_attributes_before_type_cast_returns_unserialized_value
+ Topic.serialize :content, Hash
+
+ t = Topic.new(content: { foo: :bar })
+ assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
+ t.save!
+ t.reload
+ assert_equal({ foo: :bar }, t.attributes_before_type_cast["content"])
end
def test_serialized_attribute_calling_dup_method
@@ -202,4 +209,40 @@ class SerializedAttributeTest < ActiveRecord::TestCase
assert_kind_of BCrypt::Password, topic.content
assert_equal(true, topic.content == password, 'password should equal')
end
+
+ def test_serialize_attribute_via_select_method_when_time_zone_available
+ with_timezone_config aware_attributes: true do
+ Topic.serialize(:content, MyObject)
+
+ myobj = MyObject.new('value1', 'value2')
+ topic = Topic.create(content: myobj)
+
+ assert_equal(myobj, Topic.select(:content).find(topic.id).content)
+ assert_raise(ActiveModel::MissingAttributeError) { Topic.select(:id).find(topic.id).content }
+ end
+ end
+
+ def test_serialize_attribute_can_be_serialized_in_an_integer_column
+ insures = ['life']
+ person = SerializedPerson.new(first_name: 'David', insures: insures)
+ assert person.save
+ person = person.reload
+ assert_equal(insures, person.insures)
+ end
+
+ def test_regression_serialized_default_on_text_column_with_null_false
+ light = TrafficLight.new
+ assert_equal [], light.state
+ assert_equal [], light.long_state
+ end
+
+ def test_serialized_column_should_not_be_wrapped_twice
+ Topic.serialize(:content, MyObject)
+
+ 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)
+ end
end
diff --git a/activerecord/test/cases/statement_cache_test.rb b/activerecord/test/cases/statement_cache_test.rb
new file mode 100644
index 0000000000..76da49707f
--- /dev/null
+++ b/activerecord/test/cases/statement_cache_test.rb
@@ -0,0 +1,64 @@
+require 'cases/helper'
+require 'models/book'
+require 'models/liquid'
+require 'models/molecule'
+require 'models/electron'
+
+module ActiveRecord
+ class StatementCacheTest < ActiveRecord::TestCase
+ def setup
+ @connection = ActiveRecord::Base.connection
+ end
+
+ def test_statement_cache_with_simple_statement
+ cache = ActiveRecord::StatementCache.new do
+ Book.where(name: "my book").where("author_id > 3")
+ end
+
+ Book.create(name: "my book", author_id: 4)
+
+ books = cache.execute
+ 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
+ Liquid.joins(:molecules => :electrons).where('molecules.name' => 'dioxane', 'electrons.name' => 'lepton')
+ end
+
+ salty = Liquid.create(name: 'salty')
+ molecule = salty.molecules.create(name: 'dioxane')
+ molecule.electrons.create(name: 'lepton')
+
+ liquids = cache.execute
+ assert_equal "salty", liquids[0].name
+ end
+
+ def test_statement_cache_values_differ
+ cache = ActiveRecord::StatementCache.new do
+ Book.where(name: "my book")
+ end
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ first_books = cache.execute
+
+ 3.times do
+ Book.create(name: "my book")
+ end
+
+ additional_books = cache.execute
+ assert first_books != additional_books
+ end
+ end
+end
diff --git a/activerecord/test/cases/store_test.rb b/activerecord/test/cases/store_test.rb
index 562ca8d9ff..0c9f7ccd55 100644
--- a/activerecord/test/cases/store_test.rb
+++ b/activerecord/test/cases/store_test.rb
@@ -35,6 +35,12 @@ class StoreTest < ActiveRecord::TestCase
assert_equal '(123) 456-7890', @john.phone_number
end
+ test "overriding a read accessor using super" do
+ @john.settings[:color] = nil
+
+ assert_equal 'red', @john.color
+ end
+
test "updating the store will mark it as changed" do
@john.color = 'red'
assert @john.settings_changed?
@@ -66,10 +72,16 @@ class StoreTest < ActiveRecord::TestCase
assert_equal '1234567890', @john.settings[:phone_number]
end
+ test "overriding a write accessor using super" do
+ @john.color = 'yellow'
+
+ assert_equal 'blue', @john.color
+ end
+
test "preserve store attributes data in HashWithIndifferentAccess format without any conversion" do
- @john.json_data = HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
+ @john.json_data = ActiveSupport::HashWithIndifferentAccess.new(:height => 'tall', 'weight' => 'heavy')
@john.height = 'low'
- assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
+ assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal 'heavy', @john.json_data[:weight]
@@ -95,7 +107,7 @@ class StoreTest < ActiveRecord::TestCase
test "convert store attributes from any format other than Hash or HashWithIndifferent access losing the data" do
@john.json_data = "somedata"
@john.height = 'low'
- assert_equal true, @john.json_data.instance_of?(HashWithIndifferentAccess)
+ assert_equal true, @john.json_data.instance_of?(ActiveSupport::HashWithIndifferentAccess)
assert_equal 'low', @john.json_data[:height]
assert_equal 'low', @john.json_data['height']
assert_equal false, @john.json_data.delete_if { |k, v| k == 'height' }.any?
@@ -139,9 +151,15 @@ class StoreTest < ActiveRecord::TestCase
assert_equal [:color, :homepage, :favorite_food], Admin::User.stored_attributes[:settings]
end
- test "stores_attributes are class level settings" do
- assert_raise(NoMethodError) { @john.stored_attributes = Hash.new }
- assert_raise(NoMethodError) { @john.stored_attributes }
- end
+ test "stored_attributes are tracked per class" do
+ first_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :color
+ end
+ second_model = Class.new(ActiveRecord::Base) do
+ store_accessor :data, :width, :height
+ end
+ assert_equal [:color], first_model.stored_attributes[:data]
+ assert_equal [:width, :height], second_model.stored_attributes[:data]
+ end
end
diff --git a/activerecord/test/cases/tasks/database_tasks_test.rb b/activerecord/test/cases/tasks/database_tasks_test.rb
index 4f3489b7a5..e9000fef25 100644
--- a/activerecord/test/cases/tasks/database_tasks_test.rb
+++ b/activerecord/test/cases/tasks/database_tasks_test.rb
@@ -11,10 +11,10 @@ module ActiveRecord
end
ADAPTERS_TASKS = {
- :mysql => :mysql_tasks,
- :mysql2 => :mysql_tasks,
- :postgresql => :postgresql_tasks,
- :sqlite3 => :sqlite_tasks
+ mysql: :mysql_tasks,
+ mysql2: :mysql_tasks,
+ postgresql: :postgresql_tasks,
+ sqlite3: :sqlite_tasks
}
class DatabaseTasksRegisterTask < ActiveRecord::TestCase
@@ -31,8 +31,14 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.register_task(/foo/, klazz)
ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :foo}, "awesome-file.sql")
end
+
+ def test_unregistered_task
+ assert_raise(ActiveRecord::Tasks::DatabaseNotSupported) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump({'adapter' => :bar}, "awesome-file.sql")
+ end
+ end
end
-
+
class DatabaseTasksCreateTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
@@ -258,7 +264,7 @@ module ActiveRecord
class DatabaseTasksCharsetTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
-
+
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_charset") do
eval("@#{v}").expects(:charset)
@@ -269,7 +275,7 @@ module ActiveRecord
class DatabaseTasksCollationTest < ActiveRecord::TestCase
include DatabaseTasksSetupper
-
+
ADAPTERS_TASKS.each do |k, v|
define_method("test_#{k}_collation") do
eval("@#{v}").expects(:collation)
@@ -299,4 +305,11 @@ module ActiveRecord
end
end
end
+
+ class DatabaseTasksCheckSchemaFileTest < ActiveRecord::TestCase
+ def test_check_schema_file
+ Kernel.expects(:abort).with(regexp_matches(/awesome-file.sql/))
+ ActiveRecord::Tasks::DatabaseTasks.check_schema_file("awesome-file.sql")
+ end
+ end
end
diff --git a/activerecord/test/cases/tasks/mysql_rake_test.rb b/activerecord/test/cases/tasks/mysql_rake_test.rb
index 69a049fcfa..bdcf31043a 100644
--- a/activerecord/test/cases/tasks/mysql_rake_test.rb
+++ b/activerecord/test/cases/tasks/mysql_rake_test.rb
@@ -53,6 +53,16 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
+ $stderr.expects(:puts).with("my-app-db already exists").once
+
+ ActiveRecord::Base.connection.stubs(:create_database).raises(
+ ActiveRecord::StatementInvalid.new("Can't create database 'dev'; database exists:")
+ )
+
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
class MysqlDBCreateAsRootTest < ActiveRecord::TestCase
@@ -61,7 +71,7 @@ module ActiveRecord
return skip("only tested on mysql")
end
- @connection = stub(:create_database => true, :execute => true)
+ @connection = stub("Connection", create_database: true)
@error = Mysql::Error.new "Invalid permissions"
@configuration = {
'adapter' => 'mysql',
@@ -80,6 +90,7 @@ module ActiveRecord
end
def test_root_password_is_requested
+ assert_permissions_granted_for "pat"
skip "only if mysql is available" unless defined?(::Mysql)
$stdin.expects(:gets).returns("secret\n")
@@ -87,6 +98,7 @@ module ActiveRecord
end
def test_connection_established_as_root
+ assert_permissions_granted_for "pat"
ActiveRecord::Base.expects(:establish_connection).with(
'adapter' => 'mysql',
'database' => nil,
@@ -98,6 +110,7 @@ module ActiveRecord
end
def test_database_created_by_root
+ assert_permissions_granted_for "pat"
@connection.expects(:create_database).
with('my-app-db', :charset => 'utf8', :collation => 'utf8_unicode_ci')
@@ -105,12 +118,18 @@ module ActiveRecord
end
def test_grant_privileges_for_normal_user
- @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON my-app-db.* TO 'pat'@'localhost' IDENTIFIED BY 'wossname' WITH GRANT OPTION;")
+ assert_permissions_granted_for "pat"
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
+ def test_do_not_grant_privileges_for_root_user
+ @configuration['username'] = 'root'
+ @configuration['password'] = ''
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
def test_connection_established_as_normal_user
+ assert_permissions_granted_for "pat"
ActiveRecord::Base.expects(:establish_connection).returns do
ActiveRecord::Base.expects(:establish_connection).with(
'adapter' => 'mysql',
@@ -132,6 +151,13 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ private
+ def assert_permissions_granted_for(db_user)
+ db_name = @configuration['database']
+ db_password = @configuration['password']
+ @connection.expects(:execute).with("GRANT ALL PRIVILEGES ON #{db_name}.* TO '#{db_user}'@'localhost' IDENTIFIED BY '#{db_password}' WITH GRANT OPTION;")
+ end
end
class MySQLDBDropTest < ActiveRecord::TestCase
@@ -239,10 +265,30 @@ module ActiveRecord
def test_structure_dump
filename = "awesome-file.sql"
- Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db")
+ Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(true)
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
end
+
+ def test_warn_when_external_structure_dump_fails
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--result-file", filename, "--no-data", "test-db").returns(false)
+
+ warnings = capture(:stderr) do
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
+ end
+
+ assert_match(/Could not dump the database structure/, warnings)
+ end
+
+ def test_structure_dump_with_port_number
+ filename = "awesome-file.sql"
+ Kernel.expects(:system).with("mysqldump", "--port", "10000", "--result-file", filename, "--no-data", "test-db").returns(true)
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_dump(
+ @configuration.merge('port' => 10000),
+ filename)
+ end
end
class MySQLStructureLoadTest < ActiveRecord::TestCase
diff --git a/activerecord/test/cases/tasks/postgresql_rake_test.rb b/activerecord/test/cases/tasks/postgresql_rake_test.rb
index 62acd53003..6ea225178f 100644
--- a/activerecord/test/cases/tasks/postgresql_rake_test.rb
+++ b/activerecord/test/cases/tasks/postgresql_rake_test.rb
@@ -61,6 +61,16 @@ module ActiveRecord
ActiveRecord::Tasks::DatabaseTasks.create @configuration
end
+
+ def test_create_when_database_exists_outputs_info_to_stderr
+ $stderr.expects(:puts).with("my-app-db already exists").once
+
+ ActiveRecord::Base.connection.stubs(:create_database).raises(
+ ActiveRecord::StatementInvalid.new('database "my-app-db" already exists')
+ )
+
+ ActiveRecord::Tasks::DatabaseTasks.create @configuration
+ end
end
class PostgreSQLDBDropTest < ActiveRecord::TestCase
@@ -196,7 +206,7 @@ module ActiveRecord
@connection.expects(:schema_search_path).returns("foo")
ActiveRecord::Tasks::DatabaseTasks.structure_dump(@configuration, filename)
- assert File.exists?(filename)
+ assert File.exist?(filename)
ensure
FileUtils.rm(filename)
end
@@ -215,9 +225,16 @@ module ActiveRecord
Kernel.stubs(:system)
end
- def test_structure_dump
+ def test_structure_load
filename = "awesome-file.sql"
- Kernel.expects(:system).with("psql -f #{filename} my-app-db")
+ Kernel.expects(:system).with("psql -q -f #{filename} my-app-db")
+
+ ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
+ end
+
+ def test_structure_load_accepts_path_with_spaces
+ filename = "awesome file.sql"
+ Kernel.expects(:system).with("psql -q -f awesome\\ file.sql my-app-db")
ActiveRecord::Tasks::DatabaseTasks.structure_load(@configuration, filename)
end
diff --git a/activerecord/test/cases/tasks/sqlite_rake_test.rb b/activerecord/test/cases/tasks/sqlite_rake_test.rb
index 7209c0f14d..da3471adf9 100644
--- a/activerecord/test/cases/tasks/sqlite_rake_test.rb
+++ b/activerecord/test/cases/tasks/sqlite_rake_test.rb
@@ -159,8 +159,8 @@ module ActiveRecord
filename = "awesome-file.sql"
ActiveRecord::Tasks::DatabaseTasks.structure_dump @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
- assert File.exists?(filename)
+ assert File.exist?(dbfile)
+ assert File.exist?(filename)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
@@ -182,7 +182,7 @@ module ActiveRecord
open(filename, 'w') { |f| f.puts("select datetime('now', 'localtime');") }
ActiveRecord::Tasks::DatabaseTasks.structure_load @configuration, filename, '/rails/root'
- assert File.exists?(dbfile)
+ assert File.exist?(dbfile)
ensure
FileUtils.rm_f(filename)
FileUtils.rm_f(dbfile)
diff --git a/activerecord/test/cases/test_case.rb b/activerecord/test/cases/test_case.rb
index f3f7054794..8c6d189b0c 100644
--- a/activerecord/test/cases/test_case.rb
+++ b/activerecord/test/cases/test_case.rb
@@ -1,9 +1,108 @@
-ActiveSupport::Deprecation.silence do
- require 'active_record/test_case'
-end
+require 'active_support/test_case'
+
+module ActiveRecord
+ # = Active Record Test Case
+ #
+ # Defines some test assertions to test against SQL queries.
+ class TestCase < ActiveSupport::TestCase #:nodoc:
+ def teardown
+ SQLCounter.clear_log
+ end
+
+ def assert_date_from_db(expected, actual, message = nil)
+ # SybaseAdapter doesn't have a separate column type just for dates,
+ # so the time is in the string and incorrectly formatted
+ if current_adapter?(:SybaseAdapter)
+ assert_equal expected.to_s, actual.to_date.to_s, message
+ else
+ assert_equal expected.to_s, actual.to_s, message
+ end
+ end
+
+ def assert_sql(*patterns_to_match)
+ SQLCounter.clear_log
+ yield
+ SQLCounter.log_all
+ ensure
+ failed_patterns = []
+ patterns_to_match.each do |pattern|
+ failed_patterns << pattern unless SQLCounter.log_all.any?{ |sql| pattern === sql }
+ end
+ assert failed_patterns.empty?, "Query pattern(s) #{failed_patterns.map{ |p| p.inspect }.join(', ')} not found.#{SQLCounter.log.size == 0 ? '' : "\nQueries:\n#{SQLCounter.log.join("\n")}"}"
+ end
+
+ def assert_queries(num = 1, options = {})
+ ignore_none = options.fetch(:ignore_none) { num == :any }
+ SQLCounter.clear_log
+ x = yield
+ the_log = ignore_none ? SQLCounter.log_all : SQLCounter.log
+ if num == :any
+ assert_operator the_log.size, :>=, 1, "1 or more queries expected, but none were executed."
+ else
+ mesg = "#{the_log.size} instead of #{num} queries were executed.#{the_log.size == 0 ? '' : "\nQueries:\n#{the_log.join("\n")}"}"
+ assert_equal num, the_log.size, mesg
+ end
+ x
+ end
+
+ def assert_no_queries(options = {}, &block)
+ options.reverse_merge! ignore_none: true
+ assert_queries(0, options, &block)
+ end
+
+ def assert_column(model, column_name, msg=nil)
+ assert has_column?(model, column_name), msg
+ end
+
+ def assert_no_column(model, column_name, msg=nil)
+ assert_not has_column?(model, column_name), msg
+ end
+
+ def has_column?(model, column_name)
+ model.reset_column_information
+ model.column_names.include?(column_name.to_s)
+ end
+ end
-ActiveRecord::TestCase.class_eval do
- def sqlite3? connection
- connection.class.name.split('::').last == "SQLite3Adapter"
+ class SQLCounter
+ class << self
+ attr_accessor :ignored_sql, :log, :log_all
+ def clear_log; self.log = []; self.log_all = []; end
+ end
+
+ self.clear_log
+
+ self.ignored_sql = [/^PRAGMA/, /^SELECT currval/, /^SELECT CAST/, /^SELECT @@IDENTITY/, /^SELECT @@ROWCOUNT/, /^SAVEPOINT/, /^ROLLBACK TO SAVEPOINT/, /^RELEASE SAVEPOINT/, /^SHOW max_identifier_length/, /^BEGIN/, /^COMMIT/]
+
+ # FIXME: this needs to be refactored so specific database can add their own
+ # ignored SQL, or better yet, use a different notification for the queries
+ # instead examining the SQL content.
+ oracle_ignored = [/^select .*nextval/i, /^SAVEPOINT/, /^ROLLBACK TO/, /^\s*select .* from all_triggers/im]
+ mysql_ignored = [/^SHOW TABLES/i, /^SHOW FULL FIELDS/]
+ postgresql_ignored = [/^\s*select\b.*\bfrom\b.*pg_namespace\b/im, /^\s*select\b.*\battname\b.*\bfrom\b.*\bpg_attribute\b/im, /^SHOW search_path/i]
+ sqlite3_ignored = [/^\s*SELECT name\b.*\bFROM sqlite_master/im]
+
+ [oracle_ignored, mysql_ignored, postgresql_ignored, sqlite3_ignored].each do |db_ignored_sql|
+ ignored_sql.concat db_ignored_sql
+ end
+
+ attr_reader :ignore
+
+ def initialize(ignore = Regexp.union(self.class.ignored_sql))
+ @ignore = ignore
+ end
+
+ def call(name, start, finish, message_id, values)
+ sql = values[:sql]
+
+ # FIXME: this seems bad. we should probably have a better way to indicate
+ # the query was cached
+ return if 'CACHE' == values[:name]
+
+ self.class.log_all << sql
+ self.class.log << sql unless ignore =~ sql
+ end
end
+
+ ActiveSupport::Notifications.subscribe('sql.active_record', SQLCounter.new)
end
diff --git a/activerecord/test/cases/timestamp_test.rb b/activerecord/test/cases/timestamp_test.rb
index bb034848e1..ff1b01556d 100644
--- a/activerecord/test/cases/timestamp_test.rb
+++ b/activerecord/test/cases/timestamp_test.rb
@@ -113,6 +113,18 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal previously_owner_updated_at, pet.owner.updated_at
end
+ def test_saving_a_new_record_belonging_to_invalid_parent_with_touch_should_not_raise_exception
+ klass = Class.new(Owner) do
+ def self.name; 'Owner'; end
+ validate { errors.add(:base, :invalid) }
+ end
+
+ pet = Pet.new(owner: klass.new)
+ pet.save!
+
+ assert pet.owner.new_record?
+ end
+
def test_saving_a_record_with_a_belongs_to_that_specifies_touching_a_specific_attribute_the_parent_should_update_that_attribute
klass = Class.new(ActiveRecord::Base) do
def self.name; 'Pet'; end
@@ -164,6 +176,106 @@ class TimestampTest < ActiveRecord::TestCase
assert_not_equal time, owner.updated_at
end
+ def test_touching_a_record_touches_polymorphic_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ toy = klass.first
+ time = 3.days.ago
+ toy.update_columns(updated_at: time)
+
+ wheel = wheel_klass.new
+ wheel.wheelable = toy
+ wheel.save
+ wheel.touch
+
+ assert_not_equal time, toy.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_parent_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ belongs_to :pet, touch: true
+ end
+
+ toy1 = klass.find(1)
+ old_pet = toy1.pet
+
+ toy2 = klass.find(2)
+ new_pet = toy2.pet
+ time = 3.days.ago.at_beginning_of_hour
+
+ old_pet.update_columns(updated_at: time)
+ new_pet.update_columns(updated_at: time)
+
+ toy1.pet = new_pet
+ toy1.save!
+
+ old_pet.reload
+ new_pet.reload
+
+ assert_not_equal time, new_pet.updated_at
+ assert_not_equal time, old_pet.updated_at
+ end
+
+ def test_changing_parent_of_a_record_touches_both_new_and_old_polymorphic_parent_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ end
+
+ wheel_klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Wheel'; end
+ belongs_to :wheelable, :polymorphic => true, :touch => true
+ end
+
+ toy1 = klass.find(1)
+ toy2 = klass.find(2)
+
+ wheel = wheel_klass.new
+ wheel.wheelable = toy1
+ wheel.save!
+
+ time = 3.days.ago.at_beginning_of_hour
+
+ toy1.update_columns(updated_at: time)
+ toy2.update_columns(updated_at: time)
+
+ wheel.wheelable = toy2
+ wheel.save!
+
+ toy1.reload
+ toy2.reload
+
+ assert_not_equal time, toy1.updated_at
+ assert_not_equal time, toy2.updated_at
+ end
+
+ def test_clearing_association_touches_the_old_record
+ klass = Class.new(ActiveRecord::Base) do
+ def self.name; 'Toy'; end
+ belongs_to :pet, touch: true
+ end
+
+ toy = klass.find(1)
+ pet = toy.pet
+ time = 3.days.ago.at_beginning_of_hour
+
+ pet.update_columns(updated_at: time)
+
+ toy.pet = nil
+ toy.save!
+
+ pet.reload
+
+ assert_not_equal time, pet.updated_at
+ end
+
def test_timestamp_attributes_for_create
toy = Toy.first
assert_equal toy.send(:timestamp_attributes_for_create), [:created_at, :created_on]
diff --git a/activerecord/test/cases/transaction_callbacks_test.rb b/activerecord/test/cases/transaction_callbacks_test.rb
index 961ba8d9ba..5644a35385 100644
--- a/activerecord/test/cases/transaction_callbacks_test.rb
+++ b/activerecord/test/cases/transaction_callbacks_test.rb
@@ -5,9 +5,29 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
self.use_transactional_fixtures = false
fixtures :topics
+ class ReplyWithCallbacks < ActiveRecord::Base
+ self.table_name = :topics
+
+ belongs_to :topic, foreign_key: "parent_id"
+
+ validates_presence_of :content
+
+ after_commit :do_after_commit, on: :create
+
+ def history
+ @history ||= []
+ end
+
+ def do_after_commit
+ history << :commit_on_create
+ end
+ end
+
class TopicWithCallbacks < ActiveRecord::Base
self.table_name = :topics
+ has_many :replies, class_name: "ReplyWithCallbacks", foreign_key: "parent_id"
+
after_commit{|record| record.send(:do_after_commit, nil)}
after_commit(:on => :create){|record| record.send(:do_after_commit, :create)}
after_commit(:on => :update){|record| record.send(:do_after_commit, :update)}
@@ -93,6 +113,13 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal [:commit_on_create], @new_record.history
end
+ def test_only_call_after_commit_on_create_after_transaction_commits_for_new_record_if_create_succeeds_creating_through_association
+ topic = TopicWithCallbacks.create!(:title => "New topic", :written_on => Date.today)
+ reply = topic.replies.create
+
+ assert_equal [], reply.history
+ end
+
def test_call_after_rollback_after_transaction_rollsback
@first.after_commit_block{|r| r.history << :after_commit}
@first.after_rollback_block{|r| r.history << :after_rollback}
@@ -155,9 +182,9 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
end
def test_call_after_rollback_when_commit_fails
- @first.connection.class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :real_method_commit_db_transaction, :commit_db_transaction)
begin
- @first.connection.class.class_eval do
+ @first.class.connection.singleton_class.class_eval do
def commit_db_transaction; raise "boom!"; end
end
@@ -167,8 +194,8 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert !@first.save rescue nil
assert_equal [:after_rollback], @first.history
ensure
- @first.connection.class.send(:remove_method, :commit_db_transaction)
- @first.connection.class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
+ @first.class.connection.singleton_class.send(:remove_method, :commit_db_transaction)
+ @first.class.connection.singleton_class.send(:alias_method, :commit_db_transaction, :real_method_commit_db_transaction)
end
end
@@ -244,117 +271,47 @@ class TransactionCallbacksTest < ActiveRecord::TestCase
assert_equal :rollback, @first.last_after_transaction_error
assert_equal [:after_rollback], @second.history
end
-end
-
-
-class TransactionObserverCallbacksTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
- fixtures :topics
- class TopicWithObserverAttached < ActiveRecord::Base
- self.table_name = :topics
- def history
- @history ||= []
- end
+ def test_after_rollback_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.send(:after_rollback, :on => :save) }
end
- class TopicWithObserverAttachedObserver < ActiveRecord::Observer
- def after_commit(record)
- record.history.push "after_commit"
- end
-
- def after_rollback(record)
- record.history.push "after_rollback"
- end
+ def test_after_commit_callbacks_should_validate_on_condition
+ assert_raise(ArgumentError) { Topic.send(:after_commit, :on => :save) }
end
+end
- def test_after_commit_called
- assert TopicWithObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithObserverAttached.new
- topic.save!
-
- assert_equal %w{ after_commit }, topic.history
- end
+class CallbacksOnMultipleActionsTest < ActiveRecord::TestCase
+ self.use_transactional_fixtures = false
- def test_after_rollback_called
- assert TopicWithObserverAttachedObserver.instance, 'should have observer'
+ class TopicWithCallbacksOnMultipleActions < ActiveRecord::Base
+ self.table_name = :topics
- topic = TopicWithObserverAttached.new
+ after_commit(on: [:create, :destroy]) { |record| record.history << :create_and_destroy }
+ after_commit(on: [:create, :update]) { |record| record.history << :create_and_update }
+ after_commit(on: [:update, :destroy]) { |record| record.history << :update_and_destroy }
- Topic.transaction do
- topic.save!
- raise ActiveRecord::Rollback
+ def clear_history
+ @history = []
end
- assert topic.id.nil?
- assert !topic.persisted?
- assert_equal %w{ after_rollback }, topic.history
- end
-
- class TopicWithManualRollbackObserverAttached < ActiveRecord::Base
- self.table_name = :topics
def history
@history ||= []
end
end
- class TopicWithManualRollbackObserverAttachedObserver < ActiveRecord::Observer
- def after_save(record)
- record.history.push "after_save"
- raise ActiveRecord::Rollback
- end
- end
-
- def test_after_save_called_with_manual_rollback
- assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithManualRollbackObserverAttached.new
-
- assert !topic.save
- assert_equal nil, topic.id
- assert !topic.persisted?
- assert_equal %w{ after_save }, topic.history
- end
- def test_after_save_called_with_manual_rollback_bang
- assert TopicWithManualRollbackObserverAttachedObserver.instance, 'should have observer'
-
- topic = TopicWithManualRollbackObserverAttached.new
-
- topic.save!
- assert_equal nil, topic.id
- assert !topic.persisted?
- assert_equal %w{ after_save }, topic.history
- end
-end
-
-class SaveFromAfterCommitBlockTest < ActiveRecord::TestCase
- self.use_transactional_fixtures = false
-
- class TopicWithSaveInCallback < ActiveRecord::Base
- self.table_name = :topics
- after_commit :cache_topic, :on => :create
- after_commit :call_update, :on => :update
- attr_accessor :cached, :record_updated
-
- def call_update
- self.record_updated = true
- end
-
- def cache_topic
- unless cached
- self.cached = true
- self.save
- else
- self.cached = false
- end
- end
- end
+ def test_after_commit_on_multiple_actions
+ topic = TopicWithCallbacksOnMultipleActions.new
+ topic.save
+ assert_equal [:create_and_update, :create_and_destroy], topic.history
- def test_after_commit_in_save
- topic = TopicWithSaveInCallback.new()
+ topic.clear_history
+ topic.approved = true
topic.save
- assert_equal true, topic.cached
- assert_equal true, topic.record_updated
+ assert_equal [:update_and_destroy, :create_and_update], topic.history
+
+ topic.clear_history
+ topic.destroy
+ assert_equal [:update_and_destroy, :create_and_destroy], topic.history
end
end
diff --git a/activerecord/test/cases/transaction_isolation_test.rb b/activerecord/test/cases/transaction_isolation_test.rb
index a396da6645..4f1cb99b68 100644
--- a/activerecord/test/cases/transaction_isolation_test.rb
+++ b/activerecord/test/cases/transaction_isolation_test.rb
@@ -77,7 +77,7 @@ class TransactionIsolationTest < ActiveRecord::TestCase
Tag.transaction(isolation: :repeatable_read) do
tag.reload
- Tag2.find(tag.id).update_attributes(name: 'emily')
+ Tag2.find(tag.id).update(name: 'emily')
tag.reload
assert_equal 'jon', tag.name
diff --git a/activerecord/test/cases/transactions_test.rb b/activerecord/test/cases/transactions_test.rb
index bb4f2c8064..980981903a 100644
--- a/activerecord/test/cases/transactions_test.rb
+++ b/activerecord/test/cases/transactions_test.rb
@@ -14,6 +14,21 @@ class TransactionTest < ActiveRecord::TestCase
@first, @second = Topic.find(1, 2).sort_by { |t| t.id }
end
+ def test_raise_after_destroy
+ assert_not @first.frozen?
+
+ assert_raises(RuntimeError) {
+ Topic.transaction do
+ @first.destroy
+ assert @first.frozen?
+ raise
+ end
+ }
+
+ assert @first.reload
+ assert_not @first.frozen?
+ end
+
def test_successful
Topic.transaction do
@first.approved = true
@@ -102,21 +117,35 @@ class TransactionTest < ActiveRecord::TestCase
assert !Topic.find(1).approved?
end
- def test_update_attributes_should_rollback_on_failure
+ def test_raising_exception_in_nested_transaction_restore_state_in_save
+ topic = Topic.new
+
+ def topic.after_save_for_transaction
+ raise 'Make the transaction rollback'
+ end
+
+ assert_raises(RuntimeError) do
+ Topic.transaction { topic.save }
+ end
+
+ assert topic.new_record?, "#{topic.inspect} should be new record"
+ end
+
+ def test_update_should_rollback_on_failure
author = Author.find(1)
posts_count = author.posts.size
assert posts_count > 0
- status = author.update_attributes(:name => nil, :post_ids => [])
+ status = author.update(name: nil, post_ids: [])
assert !status
assert_equal posts_count, author.posts(true).size
end
- def test_update_attributes_should_rollback_on_failure!
+ def test_update_should_rollback_on_failure!
author = Author.find(1)
posts_count = author.posts.size
assert posts_count > 0
assert_raise(ActiveRecord::RecordInvalid) do
- author.update_attributes!(:name => nil, :post_ids => [])
+ author.update!(name: nil, post_ids: [])
end
assert_equal posts_count, author.posts(true).size
end
@@ -346,6 +375,36 @@ class TransactionTest < ActiveRecord::TestCase
assert_equal "Three", @three
end if Topic.connection.supports_savepoints?
+ def test_using_named_savepoints
+ Topic.transaction do
+ @first.approved = true
+ @first.save!
+ Topic.connection.create_savepoint("first")
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.rollback_to_savepoint("first")
+ assert @first.reload.approved?
+
+ @first.approved = false
+ @first.save!
+ Topic.connection.release_savepoint("first")
+ assert_not @first.reload.approved?
+ end
+ end if Topic.connection.supports_savepoints?
+
+ def test_releasing_named_savepoints
+ Topic.transaction do
+ Topic.connection.create_savepoint("another")
+ Topic.connection.release_savepoint("another")
+
+ # The savepoint is now gone and we can't remove it again.
+ assert_raises(ActiveRecord::StatementInvalid) do
+ Topic.connection.release_savepoint("another")
+ end
+ end
+ end
+
def test_rollback_when_commit_raises
Topic.connection.expects(:begin_db_transaction)
Topic.connection.expects(:commit_db_transaction).raises('OH NOES')
@@ -362,7 +421,9 @@ class TransactionTest < ActiveRecord::TestCase
topic = Topic.new(:title => 'test')
topic.freeze
e = assert_raise(RuntimeError) { topic.save }
- assert_equal "can't modify frozen Hash", e.message
+ assert_match(/frozen/i, e.message) # Not good enough, but we can't do much
+ # about it since there is no specific error
+ # for frozen objects.
assert !topic.persisted?, 'not persisted'
assert_nil topic.id
assert topic.frozen?, 'not frozen'
@@ -395,16 +456,6 @@ class TransactionTest < ActiveRecord::TestCase
assert !@second.destroyed?, 'not destroyed'
end
- if current_adapter?(:PostgreSQLAdapter) && defined?(PGconn::PQTRANS_IDLE)
- def test_outside_transaction_works
- assert assert_deprecated { Topic.connection.outside_transaction? }
- Topic.connection.begin_db_transaction
- assert assert_deprecated { !Topic.connection.outside_transaction? }
- Topic.connection.rollback_db_transaction
- assert assert_deprecated { Topic.connection.outside_transaction? }
- end
- end
-
def test_sqlite_add_column_in_transaction
return true unless current_adapter?(:SQLite3Adapter)
@@ -436,6 +487,34 @@ class TransactionTest < ActiveRecord::TestCase
end
end
+ def test_transactions_state_from_rollback
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.perform_rollback
+
+ assert transaction.state.rolledback?
+ assert !transaction.state.committed?
+ end
+
+ def test_transactions_state_from_commit
+ connection = Topic.connection
+ transaction = ActiveRecord::ConnectionAdapters::ClosedTransaction.new(connection).begin
+
+ assert transaction.open?
+ assert !transaction.state.rolledback?
+ assert !transaction.state.committed?
+
+ transaction.perform_commit
+
+ assert !transaction.state.rolledback?
+ assert transaction.state.committed?
+ end
+
private
%w(validation save destroy).each do |filter|
@@ -493,22 +572,22 @@ if current_adapter?(:PostgreSQLAdapter)
# This will cause transactions to overlap and fail unless they are performed on
# separate database connections.
def test_transaction_per_thread
- assert_nothing_raised do
- threads = (1..3).map do
- Thread.new do
- Topic.transaction do
- topic = Topic.find(1)
- topic.approved = !topic.approved?
- topic.save!
- topic.approved = !topic.approved?
- topic.save!
- end
- Topic.connection.close
+ skip "in memory db can't share a db between threads" if in_memory_db?
+
+ threads = 3.times.map do
+ Thread.new do
+ Topic.transaction do
+ topic = Topic.find(1)
+ topic.approved = !topic.approved?
+ assert topic.save!
+ topic.approved = !topic.approved?
+ assert topic.save!
end
+ Topic.connection.close
end
-
- threads.each { |t| t.join }
end
+
+ threads.each { |t| t.join }
end
# Test for dirty reads among simultaneous transactions.
@@ -560,14 +639,5 @@ if current_adapter?(:PostgreSQLAdapter)
assert_equal original_salary, Developer.find(1).salary
end
-
- test "#transaction_joinable= is deprecated" do
- Developer.transaction do
- conn = Developer.connection
- assert conn.current_transaction.joinable?
- assert_deprecated { conn.transaction_joinable = false }
- assert !conn.current_transaction.joinable?
- end
- end
end
end
diff --git a/activerecord/test/cases/unconnected_test.rb b/activerecord/test/cases/unconnected_test.rb
index 5a69054445..e82ca3f93d 100644
--- a/activerecord/test/cases/unconnected_test.rb
+++ b/activerecord/test/cases/unconnected_test.rb
@@ -7,13 +7,13 @@ class TestUnconnectedAdapter < ActiveRecord::TestCase
self.use_transactional_fixtures = false
def setup
- @underlying = ActiveRecord::Model.connection
- @specification = ActiveRecord::Model.remove_connection
+ @underlying = ActiveRecord::Base.connection
+ @specification = ActiveRecord::Base.remove_connection
end
def teardown
@underlying = nil
- ActiveRecord::Model.establish_connection(@specification)
+ ActiveRecord::Base.establish_connection(@specification)
load_schema if in_memory_db?
end
diff --git a/activerecord/test/cases/validations/association_validation_test.rb b/activerecord/test/cases/validations/association_validation_test.rb
index 7ac34bc71e..602f633c45 100644
--- a/activerecord/test/cases/validations/association_validation_test.rb
+++ b/activerecord/test/cases/validations/association_validation_test.rb
@@ -10,29 +10,33 @@ require 'models/interest'
class AssociationValidationTest < ActiveRecord::TestCase
fixtures :topics, :owners
- repair_validations(Topic, Reply, Owner)
+ repair_validations(Topic, Reply)
def test_validates_size_of_association
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'nopets')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'apet')
- assert o.valid?
+ 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
- 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?
+ 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
@@ -91,12 +95,14 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
def test_validates_size_of_association_utf8
- assert_nothing_raised { Owner.validates_size_of :pets, :minimum => 1 }
- o = Owner.new('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
- assert !o.save
- assert o.errors[:pets].any?
- o.pets.build('name' => 'ã‚ã„ã†ãˆãŠã‹ããã‘ã“')
- assert o.valid?
+ 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
@@ -118,21 +124,4 @@ class AssociationValidationTest < ActiveRecord::TestCase
end
end
- def test_validates_associated_models_in_the_same_context
- Topic.validates_presence_of :title, :on => :custom_context
- Topic.validates_associated :replies
- Reply.validates_presence_of :title, :on => :custom_context
-
- t = Topic.new('title' => '')
- r = t.replies.new('title' => '')
-
- assert t.valid?
- assert !t.valid?(:custom_context)
-
- t.title = "Longer"
- assert !t.valid?(:custom_context), "Should NOT be valid if the associated object is not valid in the same context."
-
- r.title = "Longer"
- assert t.valid?(:custom_context), "Should be valid if the associated object is not valid in the same context."
- end
end
diff --git a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
index 2f5ee32538..32d2bf746f 100644
--- a/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
+++ b/activerecord/test/cases/validations/i18n_generate_message_validation_test.rb
@@ -54,4 +54,23 @@ class I18nGenerateMessageValidationTest < ActiveRecord::TestCase
end
end
+ test "translation for 'taken' can be overridden" do
+ I18n.backend.store_translations "en", {errors: {attributes: {title: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
+
+ test "translation for 'taken' can be overridden in activerecord scope" do
+ I18n.backend.store_translations "en", {activerecord: {errors: {messages: {taken: "Custom taken message" }}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
+
+ test "translation for 'taken' can be overridden in activerecord model scope" do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {taken: "Custom taken message" }}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
+
+ test "translation for 'taken' can be overridden in activerecord attributes scope" do
+ I18n.backend.store_translations "en", {activerecord: {errors: {models: {topic: {attributes: {title: {taken: "Custom taken message" }}}}}}}
+ assert_equal "Custom taken message", @topic.errors.generate_message(:title, :taken, :value => 'title')
+ end
end
diff --git a/activerecord/test/cases/validations/presence_validation_test.rb b/activerecord/test/cases/validations/presence_validation_test.rb
index cd9175f454..1de8934406 100644
--- a/activerecord/test/cases/validations/presence_validation_test.rb
+++ b/activerecord/test/cases/validations/presence_validation_test.rb
@@ -18,6 +18,13 @@ class PresenceValidationTest < ActiveRecord::TestCase
assert b.valid?
end
+ def test_validates_presence_of_has_one
+ Boy.validates_presence_of(:face)
+ b = Boy.new
+ assert b.invalid?, "should not be valid if has_one association missing"
+ assert_equal 1, b.errors[:face].size, "validates_presence_of should only add one error"
+ end
+
def test_validates_presence_of_has_one_marked_for_destruction
Boy.validates_presence_of(:face)
b = Boy.new
diff --git a/activerecord/test/cases/validations/uniqueness_validation_test.rb b/activerecord/test/cases/validations/uniqueness_validation_test.rb
index 46212e49b6..2b33f01783 100644
--- a/activerecord/test/cases/validations/uniqueness_validation_test.rb
+++ b/activerecord/test/cases/validations/uniqueness_validation_test.rb
@@ -30,6 +30,11 @@ class ReplyWithTitleObject < Reply
def title; ReplyTitle.new; end
end
+class Employee < ActiveRecord::Base
+ self.table_name = 'postgresql_arrays'
+ validates_uniqueness_of :nicknames
+end
+
class UniquenessValidationTest < ActiveRecord::TestCase
fixtures :topics, 'warehouse-things', :developers
@@ -49,7 +54,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert !t2.save, "Shouldn't save t2 as unique"
assert_equal ["has already been taken"], t2.errors[:title]
- t2.title = "Now Im really also unique"
+ t2.title = "Now I am really also unique"
assert t2.save, "Should now save t2 as unique"
end
@@ -263,7 +268,7 @@ class UniquenessValidationTest < ActiveRecord::TestCase
end
def test_validate_case_sensitive_uniqueness_with_attribute_passed_as_integer
- Topic.validates_uniqueness_of(:title, :case_sensitve => true)
+ Topic.validates_uniqueness_of(:title, :case_sensitive => true)
Topic.create!('title' => 101)
t2 = Topic.new('title' => 101)
@@ -341,16 +346,34 @@ class UniquenessValidationTest < ActiveRecord::TestCase
assert w6.errors[:city].any?, "Should have errors for city"
assert_equal ["has already been taken"], w6.errors[:city], "Should have uniqueness message for city"
end
-
+
def test_validate_uniqueness_with_conditions
- Topic.validates_uniqueness_of(:title, :conditions => Topic.where('approved = ?', true))
+ Topic.validates_uniqueness_of :title, conditions: -> { where(approved: true) }
Topic.create("title" => "I'm a topic", "approved" => true)
Topic.create("title" => "I'm an unapproved topic", "approved" => false)
-
+
t3 = Topic.new("title" => "I'm a topic", "approved" => true)
assert !t3.valid?, "t3 shouldn't be valid"
-
+
t4 = Topic.new("title" => "I'm an unapproved topic", "approved" => false)
assert t4.valid?, "t4 should be valid"
end
+
+ def test_validate_uniqueness_with_non_callable_conditions_is_not_supported
+ assert_raises(ArgumentError) {
+ Topic.validates_uniqueness_of :title, conditions: Topic.where(approved: true)
+ }
+ end
+
+ def test_validate_uniqueness_with_array_column
+ return skip "Uniqueness on arrays has only been tested in PostgreSQL so far." if !current_adapter? :PostgreSQLAdapter
+
+ e1 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [1000, 1200])
+ assert e1.persisted?, "Saving e1"
+
+ e2 = Employee.create("nicknames" => ["john", "johnny"], "commission_by_quarter" => [2200])
+ assert !e2.persisted?, "e2 shouldn't be valid"
+ assert e2.errors[:nicknames].any?, "Should have errors for nicknames"
+ assert_equal ["has already been taken"], e2.errors[:nicknames], "Should have uniqueness message for nicknames"
+ end
end
diff --git a/activerecord/test/cases/validations_repair_helper.rb b/activerecord/test/cases/validations_repair_helper.rb
index 11912ca1cc..c02b3241cd 100644
--- a/activerecord/test/cases/validations_repair_helper.rb
+++ b/activerecord/test/cases/validations_repair_helper.rb
@@ -6,7 +6,7 @@ module ActiveRecord
def repair_validations(*model_classes)
teardown do
model_classes.each do |k|
- k.reset_callbacks(:validate)
+ k.clear_validators!
end
end
end
@@ -16,7 +16,7 @@ module ActiveRecord
yield
ensure
model_classes.each do |k|
- k.reset_callbacks(:validate)
+ k.clear_validators!
end
end
end
diff --git a/activerecord/test/cases/xml_serialization_test.rb b/activerecord/test/cases/xml_serialization_test.rb
index 68fa15de50..78fa2f935a 100644
--- a/activerecord/test/cases/xml_serialization_test.rb
+++ b/activerecord/test/cases/xml_serialization_test.rb
@@ -161,21 +161,17 @@ end
class DefaultXmlSerializationTimezoneTest < ActiveRecord::TestCase
def test_should_serialize_datetime_with_timezone
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Mickey', :updated_at => Time.utc(2006, 8, 1))
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
def test_should_serialize_datetime_with_timezone_reloaded
- timezone, Time.zone = Time.zone, "Pacific Time (US & Canada)"
-
- toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
- assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
- ensure
- Time.zone = timezone
+ with_timezone_config zone: "Pacific Time (US & Canada)" do
+ toy = Toy.create(:name => 'Minnie', :updated_at => Time.utc(2006, 8, 1)).reload
+ assert_match %r{<updated-at type=\"dateTime\">2006-07-31T17:00:00-07:00</updated-at>}, toy.to_xml
+ end
end
end
diff --git a/activerecord/test/cases/yaml_serialization_test.rb b/activerecord/test/cases/yaml_serialization_test.rb
index 302913e095..83a710b1b7 100644
--- a/activerecord/test/cases/yaml_serialization_test.rb
+++ b/activerecord/test/cases/yaml_serialization_test.rb
@@ -5,16 +5,10 @@ class YamlSerializationTest < ActiveRecord::TestCase
fixtures :topics
def test_to_yaml_with_time_with_zone_should_not_raise_exception
- tz = Time.zone
- Time.zone = ActiveSupport::TimeZone["Pacific Time (US & Canada)"]
- ActiveRecord::Base.time_zone_aware_attributes = true
-
- topic = Topic.new(:written_on => DateTime.now)
- assert_nothing_raised { topic.to_yaml }
-
- ensure
- Time.zone = tz
- ActiveRecord::Base.time_zone_aware_attributes = false
+ with_timezone_config aware_attributes: true, zone: "Pacific Time (US & Canada)" do
+ topic = Topic.new(:written_on => DateTime.now)
+ assert_nothing_raised { topic.to_yaml }
+ end
end
def test_roundtrip
diff --git a/activerecord/test/fixtures/all/admin b/activerecord/test/fixtures/all/admin
new file mode 120000
index 0000000000..984d12a043
--- /dev/null
+++ b/activerecord/test/fixtures/all/admin
@@ -0,0 +1 @@
+../to_be_linked/ \ No newline at end of file
diff --git a/activerecord/test/fixtures/dog_lovers.yml b/activerecord/test/fixtures/dog_lovers.yml
index d3e5e4a1aa..3f4c6c9e4c 100644
--- a/activerecord/test/fixtures/dog_lovers.yml
+++ b/activerecord/test/fixtures/dog_lovers.yml
@@ -2,3 +2,6 @@ david:
id: 1
bred_dogs_count: 0
trained_dogs_count: 1
+joanna:
+ id: 2
+ dogs_count: 1
diff --git a/activerecord/test/fixtures/dogs.yml b/activerecord/test/fixtures/dogs.yml
index 16d19be2c5..b5eb2c7b74 100644
--- a/activerecord/test/fixtures/dogs.yml
+++ b/activerecord/test/fixtures/dogs.yml
@@ -1,3 +1,4 @@
sophie:
id: 1
trainer_id: 1
+ dog_lover_id: 2
diff --git a/activerecord/test/fixtures/friendships.yml b/activerecord/test/fixtures/friendships.yml
index 1ee09175bf..ae0abe0162 100644
--- a/activerecord/test/fixtures/friendships.yml
+++ b/activerecord/test/fixtures/friendships.yml
@@ -1,4 +1,4 @@
Connection 1:
id: 1
- person_id: 1
- friend_id: 2 \ No newline at end of file
+ friend_id: 1
+ follower_id: 2
diff --git a/activerecord/test/fixtures/people.yml b/activerecord/test/fixtures/people.yml
index e640a38f1f..0ec05e8d56 100644
--- a/activerecord/test/fixtures/people.yml
+++ b/activerecord/test/fixtures/people.yml
@@ -5,6 +5,7 @@ michael:
number1_fan_id: 3
gender: M
followers_count: 1
+ friends_too_count: 1
david:
id: 2
first_name: David
@@ -12,6 +13,7 @@ david:
number1_fan_id: 1
gender: M
followers_count: 1
+ friends_too_count: 1
susan:
id: 3
first_name: Susan
@@ -19,3 +21,4 @@ susan:
number1_fan_id: 1
gender: F
followers_count: 1
+ friends_too_count: 1
diff --git a/activerecord/test/fixtures/pets.yml b/activerecord/test/fixtures/pets.yml
index a1601a53f0..2ec4f53e6d 100644
--- a/activerecord/test/fixtures/pets.yml
+++ b/activerecord/test/fixtures/pets.yml
@@ -12,3 +12,8 @@ mochi:
pet_id: 3
name: mochi
owner_id: 2
+
+bulbul:
+ pet_id: 4
+ name: bulbul
+ owner_id: 1
diff --git a/activerecord/test/fixtures/readers.yml b/activerecord/test/fixtures/readers.yml
index 8a6076655b..14b883f041 100644
--- a/activerecord/test/fixtures/readers.yml
+++ b/activerecord/test/fixtures/readers.yml
@@ -2,8 +2,10 @@ michael_welcome:
id: 1
post_id: 1
person_id: 1
+ first_post_id: 2
michael_authorless:
id: 2
post_id: 3
- person_id: 1 \ No newline at end of file
+ person_id: 1
+ first_post_id: 3
diff --git a/activerecord/test/fixtures/sponsors.yml b/activerecord/test/fixtures/sponsors.yml
index bfc6b238b1..2da541c539 100644
--- a/activerecord/test/fixtures/sponsors.yml
+++ b/activerecord/test/fixtures/sponsors.yml
@@ -8,5 +8,5 @@ boring_club_sponsor_for_groucho:
sponsorable_type: Member
crazy_club_sponsor_for_groucho:
sponsor_club: crazy_club
- sponsorable_id: 2
+ sponsorable_id: 3
sponsorable_type: Member
diff --git a/activerecord/test/fixtures/tasks.yml b/activerecord/test/fixtures/tasks.yml
index 402ca85faf..c38b32b0e5 100644
--- a/activerecord/test/fixtures/tasks.yml
+++ b/activerecord/test/fixtures/tasks.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
first_task:
id: 1
starting: 2005-03-30t06:30:00.00+01:00
diff --git a/activerecord/test/fixtures/to_be_linked/accounts.yml b/activerecord/test/fixtures/to_be_linked/accounts.yml
new file mode 100644
index 0000000000..9e341a15af
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/accounts.yml
@@ -0,0 +1,2 @@
+signals37:
+ name: 37signals
diff --git a/activerecord/test/fixtures/to_be_linked/users.yml b/activerecord/test/fixtures/to_be_linked/users.yml
new file mode 100644
index 0000000000..e2884beda5
--- /dev/null
+++ b/activerecord/test/fixtures/to_be_linked/users.yml
@@ -0,0 +1,10 @@
+david:
+ name: David
+ account: signals37
+
+jamis:
+ name: Jamis
+ account: signals37
+ settings:
+ :symbol: symbol
+ string: string
diff --git a/activerecord/test/fixtures/toys.yml b/activerecord/test/fixtures/toys.yml
index 037e335e0a..ae9044ec62 100644
--- a/activerecord/test/fixtures/toys.yml
+++ b/activerecord/test/fixtures/toys.yml
@@ -2,3 +2,13 @@ bone:
toy_id: 1
name: Bone
pet_id: 1
+
+doll:
+ toy_id: 2
+ name: Doll
+ pet_id: 2
+
+bulbuli:
+ toy_id: 3
+ name: Bulbuli
+ pet_id: 4
diff --git a/activerecord/test/fixtures/traffic_lights.yml b/activerecord/test/fixtures/traffic_lights.yml
index 6dabd53474..81b4e47959 100644
--- a/activerecord/test/fixtures/traffic_lights.yml
+++ b/activerecord/test/fixtures/traffic_lights.yml
@@ -4,3 +4,7 @@ uk:
- Green
- Red
- Orange
+ long_state:
+ - "Green, go ahead"
+ - "Red, wait"
+ - "Orange, caution light is about to switch" \ No newline at end of file
diff --git a/activerecord/test/migrations/10_urban/9_add_expressions.rb b/activerecord/test/migrations/10_urban/9_add_expressions.rb
new file mode 100644
index 0000000000..79a342e574
--- /dev/null
+++ b/activerecord/test/migrations/10_urban/9_add_expressions.rb
@@ -0,0 +1,11 @@
+class AddExpressions < ActiveRecord::Migration
+ def self.up
+ create_table("expressions") do |t|
+ t.column :expression, :string
+ end
+ end
+
+ def self.down
+ drop_table "expressions"
+ end
+end
diff --git a/activerecord/test/migrations/magic/1_currencies_have_symbols.rb b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
new file mode 100644
index 0000000000..c066c068c2
--- /dev/null
+++ b/activerecord/test/migrations/magic/1_currencies_have_symbols.rb
@@ -0,0 +1,12 @@
+# coding: ISO-8859-15
+
+class CurrenciesHaveSymbols < ActiveRecord::Migration
+ def self.up
+ # We use ¤ for default currency symbol
+ add_column "currencies", "symbol", :string, :default => "¤"
+ end
+
+ def self.down
+ remove_column "currencies", "symbol"
+ end
+end
diff --git a/activerecord/test/models/admin/user.rb b/activerecord/test/models/admin/user.rb
index 467f3ccd39..4c3b71e8f9 100644
--- a/activerecord/test/models/admin/user.rb
+++ b/activerecord/test/models/admin/user.rb
@@ -1,10 +1,24 @@
class Admin::User < ActiveRecord::Base
+ class Coder
+ def initialize(default = {})
+ @default = default
+ end
+
+ def dump(o)
+ ActiveSupport::JSON.encode(o || @default)
+ end
+
+ def load(s)
+ s.present? ? ActiveSupport::JSON.decode(s) : @default.clone
+ end
+ end
+
belongs_to :account
store :settings, :accessors => [ :color, :homepage ]
store_accessor :settings, :favorite_food
store :preferences, :accessors => [ :remember_login ]
- store :json_data, :accessors => [ :height, :weight ], :coder => JSON
- store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => JSON
+ store :json_data, :accessors => [ :height, :weight ], :coder => Coder.new
+ store :json_data_empty, :accessors => [ :is_a_good_guy ], :coder => Coder.new
def phone_number
read_store_attribute(:settings, :phone_number).gsub(/(\d{3})(\d{3})(\d{4})/,'(\1) \2-\3')
@@ -13,4 +27,13 @@ class Admin::User < ActiveRecord::Base
def phone_number=(value)
write_store_attribute(:settings, :phone_number, value && value.gsub(/[^\d]/,''))
end
+
+ def color
+ super || 'red'
+ end
+
+ def color=(value)
+ value = 'blue' unless %w(black red green blue).include?(value)
+ super
+ end
end
diff --git a/activerecord/test/models/author.rb b/activerecord/test/models/author.rb
index 77f4a2ec87..794d1af43d 100644
--- a/activerecord/test/models/author.rb
+++ b/activerecord/test/models/author.rb
@@ -1,5 +1,6 @@
class Author < ActiveRecord::Base
has_many :posts
+ has_one :post
has_many :very_special_comments, :through => :posts
has_many :posts_with_comments, -> { includes(:comments) }, :class_name => "Post"
has_many :popular_grouped_posts, -> { includes(:comments).group("type").having("SUM(comments_count) > 1").select("type") }, :class_name => "Post"
@@ -7,21 +8,14 @@ class Author < ActiveRecord::Base
has_many :posts_sorted_by_id_limited, -> { order('posts.id').limit(1) }, :class_name => "Post"
has_many :posts_with_categories, -> { includes(:categories) }, :class_name => "Post"
has_many :posts_with_comments_and_categories, -> { includes(:comments, :categories).order("posts.id") }, :class_name => "Post"
- has_many :posts_containing_the_letter_a, :class_name => "Post"
- has_many :posts_with_extension, :class_name => "Post" do #, :extend => ProxyTestExtension
- def testing_proxy_owner
- proxy_owner
- end
- def testing_proxy_reflection
- proxy_reflection
- end
- def testing_proxy_target
- proxy_target
- end
- end
+ has_many :posts_with_special_categorizations, :class_name => 'PostWithSpecialCategorization'
has_one :post_about_thinking, -> { where("posts.title like '%thinking%'") }, :class_name => 'Post'
has_one :post_about_thinking_with_last_comment, -> { where("posts.title like '%thinking%'").includes(:last_comment) }, :class_name => 'Post'
- has_many :comments, :through => :posts
+ has_many :comments, through: :posts do
+ def ratings
+ Rating.joins(:comment).merge(self)
+ end
+ end
has_many :comments_containing_the_letter_e, :through => :posts, :source => :comments
has_many :comments_with_order_and_conditions, -> { order('comments.body').where("comments.body like 'Thank%'") }, :through => :posts, :source => :comments
has_many :comments_with_include, -> { includes(:post) }, :through => :posts, :source => :comments
@@ -35,11 +29,17 @@ class Author < ActiveRecord::Base
has_many :thinking_posts, -> { where(:title => 'So I was thinking') }, :dependent => :delete_all, :class_name => 'Post'
has_many :welcome_posts, -> { where(:title => 'Welcome to the weblog') }, :class_name => 'Post'
+ has_many :welcome_posts_with_comment,
+ -> { where(title: 'Welcome to the weblog').where('comments_count = ?', 1) },
+ class_name: 'Post'
+ has_many :welcome_posts_with_comments,
+ -> { where(title: 'Welcome to the weblog').where(Post.arel_table[:comments_count].gt(0)) },
+ class_name: 'Post'
+
has_many :comments_desc, -> { order('comments.id DESC') }, :through => :posts, :source => :comments
- has_many :limited_comments, -> { limit(1) }, :through => :posts, :source => :comments
has_many :funky_comments, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments, -> { uniq.order('comments.id') }, :through => :posts, :source => :comments
- has_many :ordered_uniq_comments_desc, -> { uniq.order('comments.id DESC') }, :through => :posts, :source => :comments
+ has_many :ordered_uniq_comments, -> { distinct.order('comments.id') }, :through => :posts, :source => :comments
+ has_many :ordered_uniq_comments_desc, -> { distinct.order('comments.id DESC') }, :through => :posts, :source => :comments
has_many :readonly_comments, -> { readonly }, :through => :posts, :source => :comments
has_many :special_posts
@@ -86,20 +86,20 @@ class Author < ActiveRecord::Base
has_many :categories_like_general, -> { where(:name => 'General') }, :through => :categorizations, :source => :category, :class_name => 'Category'
has_many :categorized_posts, :through => :categorizations, :source => :post
- has_many :unique_categorized_posts, -> { uniq }, :through => :categorizations, :source => :post
+ has_many :unique_categorized_posts, -> { distinct }, :through => :categorizations, :source => :post
has_many :nothings, :through => :kateggorisatons, :class_name => 'Category'
has_many :author_favorites
has_many :favorite_authors, -> { order('name') }, :through => :author_favorites
- has_many :taggings, :through => :posts
+ has_many :taggings, :through => :posts, :source => :taggings
has_many :taggings_2, :through => :posts, :source => :tagging
has_many :tags, :through => :posts
has_many :post_categories, :through => :posts, :source => :categories
has_many :tagging_tags, :through => :taggings, :source => :tag
- has_many :similar_posts, -> { uniq }, :through => :tags, :source => :tagged_posts
+ has_many :similar_posts, -> { distinct }, :through => :tags, :source => :tagged_posts
has_many :distinct_tags, -> { select("DISTINCT tags.*").order("tags.name") }, :through => :posts, :source => :tags
has_many :tags_with_primary_key, :through => :posts
diff --git a/activerecord/test/models/auto_id.rb b/activerecord/test/models/auto_id.rb
index d720e2be5e..82c6544bd5 100644
--- a/activerecord/test/models/auto_id.rb
+++ b/activerecord/test/models/auto_id.rb
@@ -1,4 +1,4 @@
class AutoId < ActiveRecord::Base
- def self.table_name () "auto_id_tests" end
- def self.primary_key () "auto_id" end
+ self.table_name = "auto_id_tests"
+ self.primary_key = "auto_id"
end
diff --git a/activerecord/test/models/autoloadable/extra_firm.rb b/activerecord/test/models/autoloadable/extra_firm.rb
new file mode 100644
index 0000000000..5578ba0d9b
--- /dev/null
+++ b/activerecord/test/models/autoloadable/extra_firm.rb
@@ -0,0 +1,2 @@
+class ExtraFirm < Company
+end
diff --git a/activerecord/test/models/book.rb b/activerecord/test/models/book.rb
index ce81a37966..5458a28cc9 100644
--- a/activerecord/test/models/book.rb
+++ b/activerecord/test/models/book.rb
@@ -2,7 +2,7 @@ class Book < ActiveRecord::Base
has_many :authors
has_many :citations, :foreign_key => 'book1_id'
- has_many :references, -> { uniq }, :through => :citations, :source => :reference_of
+ has_many :references, -> { distinct }, :through => :citations, :source => :reference_of
has_many :subscriptions
has_many :subscribers, :through => :subscriptions
diff --git a/activerecord/test/models/bulb.rb b/activerecord/test/models/bulb.rb
index e4c0278c0d..4361188e21 100644
--- a/activerecord/test/models/bulb.rb
+++ b/activerecord/test/models/bulb.rb
@@ -1,6 +1,6 @@
class Bulb < ActiveRecord::Base
default_scope { where(:name => 'defaulty') }
- belongs_to :car
+ belongs_to :car, :touch => true
attr_reader :scope_after_initialize, :attributes_after_initialize
@@ -37,3 +37,9 @@ class CustomBulb < Bulb
self.frickinawesome = true if name == 'Dude'
end
end
+
+class FunkyBulb < Bulb
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
diff --git a/activerecord/test/models/cake_designer.rb b/activerecord/test/models/cake_designer.rb
new file mode 100644
index 0000000000..9c57ef573a
--- /dev/null
+++ b/activerecord/test/models/cake_designer.rb
@@ -0,0 +1,3 @@
+class CakeDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/car.rb b/activerecord/test/models/car.rb
index ac42f444e1..6d257dbe7e 100644
--- a/activerecord/test/models/car.rb
+++ b/activerecord/test/models/car.rb
@@ -1,11 +1,9 @@
class Car < ActiveRecord::Base
-
has_many :bulbs
+ has_many :funky_bulbs, class_name: 'FunkyBulb', dependent: :destroy
has_many :foo_bulbs, -> { where(:name => 'foo') }, :class_name => "Bulb"
- has_many :frickinawesome_bulbs, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_one :bulb
- has_one :frickinawesome_bulb, -> { where :frickinawesome => true }, :class_name => "Bulb"
has_many :tyres
has_many :engines, :dependent => :destroy
diff --git a/activerecord/test/models/category.rb b/activerecord/test/models/category.rb
index f8c8ebb70c..7da39a8e33 100644
--- a/activerecord/test/models/category.rb
+++ b/activerecord/test/models/category.rb
@@ -31,9 +31,4 @@ class Category < ActiveRecord::Base
end
class SpecialCategory < Category
-
- def self.what_are_you
- 'a special category...'
- end
-
end
diff --git a/activerecord/test/models/chef.rb b/activerecord/test/models/chef.rb
new file mode 100644
index 0000000000..67a4e54f06
--- /dev/null
+++ b/activerecord/test/models/chef.rb
@@ -0,0 +1,3 @@
+class Chef < ActiveRecord::Base
+ belongs_to :employable, polymorphic: true
+end
diff --git a/activerecord/test/models/citation.rb b/activerecord/test/models/citation.rb
index 545aa8110d..3d87eb795c 100644
--- a/activerecord/test/models/citation.rb
+++ b/activerecord/test/models/citation.rb
@@ -1,6 +1,3 @@
class Citation < ActiveRecord::Base
belongs_to :reference_of, :class_name => "Book", :foreign_key => :book2_id
-
- belongs_to :book1, :class_name => "Book", :foreign_key => :book1_id
- belongs_to :book2, :class_name => "Book", :foreign_key => :book2_id
end
diff --git a/activerecord/test/models/club.rb b/activerecord/test/models/club.rb
index 24a65b0f2f..566e0873f1 100644
--- a/activerecord/test/models/club.rb
+++ b/activerecord/test/models/club.rb
@@ -1,8 +1,7 @@
class Club < ActiveRecord::Base
has_one :membership
- has_many :memberships
+ has_many :memberships, :inverse_of => false
has_many :members, :through => :memberships
- has_many :current_memberships
has_one :sponsor
has_one :sponsored_member, :through => :sponsor, :source => :sponsorable, :source_type => "Member"
belongs_to :category
diff --git a/activerecord/test/models/column_name.rb b/activerecord/test/models/column_name.rb
index ec07205a3a..460eb4fe20 100644
--- a/activerecord/test/models/column_name.rb
+++ b/activerecord/test/models/column_name.rb
@@ -1,3 +1,3 @@
class ColumnName < ActiveRecord::Base
- def self.table_name () "colnametests" end
-end \ No newline at end of file
+ self.table_name = "colnametests"
+end
diff --git a/activerecord/test/models/comment.rb b/activerecord/test/models/comment.rb
index 4b2015fe01..ede5fbd0c6 100644
--- a/activerecord/test/models/comment.rb
+++ b/activerecord/test/models/comment.rb
@@ -29,16 +29,10 @@ class Comment < ActiveRecord::Base
end
class SpecialComment < Comment
- def self.what_are_you
- 'a special comment...'
- end
end
class SubSpecialComment < SpecialComment
end
class VerySpecialComment < Comment
- def self.what_are_you
- 'a very special comment...'
- end
end
diff --git a/activerecord/test/models/company.rb b/activerecord/test/models/company.rb
index 17b17724e8..0b0b304121 100644
--- a/activerecord/test/models/company.rb
+++ b/activerecord/test/models/company.rb
@@ -11,6 +11,11 @@ class Company < AbstractCompany
has_many :contracts
has_many :developers, :through => :contracts
+ scope :of_first_firm, lambda {
+ joins(:account => :firm).
+ where('firms.id' => 1)
+ }
+
def arbitrary_method
"I am Jack's profound disappointment"
end
@@ -35,17 +40,11 @@ module Namespaced
end
class Firm < Company
- ActiveSupport::Deprecation.silence do
- has_many :clients, -> { order "id" }, :dependent => :destroy, :counter_sql =>
- "SELECT COUNT(*) FROM companies WHERE firm_id = 1 " +
- "AND (#{QUOTED_TYPE} = 'Client' OR #{QUOTED_TYPE} = 'SpecialClient' OR #{QUOTED_TYPE} = 'VerySpecialClient' )",
- :before_remove => :log_before_remove,
- :after_remove => :log_after_remove
- end
+ has_many :clients, -> { order "id" }, :dependent => :destroy, :before_remove => :log_before_remove, :after_remove => :log_after_remove
has_many :unsorted_clients, :class_name => "Client"
has_many :unsorted_clients_with_symbol, :class_name => :Client
has_many :clients_sorted_desc, -> { order "id DESC" }, :class_name => "Client"
- has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
+ has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :inverse_of => :firm
has_many :clients_ordered_by_name, -> { order "name" }, :class_name => "Client"
has_many :unvalidated_clients_of_firm, :foreign_key => "client_of", :class_name => "Client", :validate => false
has_many :dependent_clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client", :dependent => :destroy
@@ -54,21 +53,7 @@ class Firm < Company
has_many :clients_with_interpolated_conditions, ->(firm) { where "rating > #{firm.rating}" }, :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
has_many :clients_like_ms_with_hash_conditions, -> { where(:name => 'Microsoft').order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id} " },
- :counter_sql => proc { "SELECT COUNT(*) FROM companies WHERE client_of = #{id}" }
- has_many :clients_using_zero_counter_sql, :class_name => "Client",
- :finder_sql => proc { "SELECT * FROM companies WHERE client_of = #{id}" },
- :counter_sql => proc { "SELECT 0 FROM companies WHERE client_of = #{id}" }
- has_many :no_clients_using_counter_sql, :class_name => "Client",
- :finder_sql => 'SELECT * FROM companies WHERE client_of = 1000',
- :counter_sql => 'SELECT COUNT(*) FROM companies WHERE client_of = 1000'
- has_many :clients_using_finder_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE 1=1'
- end
has_many :plain_clients, :class_name => 'Client'
- has_many :readonly_clients, -> { readonly }, :class_name => 'Client'
has_many :clients_using_primary_key, :class_name => 'Client',
:primary_key => 'name', :foreign_key => 'firm_name'
has_many :clients_using_primary_key_with_delete_all, :class_name => 'Client',
@@ -111,13 +96,7 @@ end
class DependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :nullify
has_many :companies, :foreign_key => 'client_of', :dependent => :nullify
-end
-
-class RestrictedFirm < Company
- ActiveSupport::Deprecation.silence do
- has_one :account, -> { order("id") }, :foreign_key => "firm_id", :dependent => :restrict
- has_many :companies, -> { order("id") }, :foreign_key => 'client_of', :dependent => :restrict
- end
+ has_one :company, :foreign_key => 'client_of', :dependent => :nullify
end
class RestrictedWithExceptionFirm < Company
@@ -140,9 +119,13 @@ class Client < Company
belongs_to :firm_with_primary_key_symbols, :class_name => "Firm", :primary_key => :name, :foreign_key => :firm_name
belongs_to :readonly_firm, -> { readonly }, :class_name => "Firm", :foreign_key => "firm_id"
belongs_to :bob_firm, -> { where :name => "Bob" }, :class_name => "Firm", :foreign_key => "client_of"
- has_many :accounts, :through => :firm
+ has_many :accounts, :through => :firm, :source => :accounts
belongs_to :account
+ validate do
+ firm
+ end
+
class RaisedOnSave < RuntimeError; end
attr_accessor :raise_on_save
before_save do
@@ -192,7 +175,6 @@ class ExclusivelyDependentFirm < Company
has_one :account, :foreign_key => "firm_id", :dependent => :delete
has_many :dependent_sanitized_conditional_clients_of_firm, -> { order("id").where("name = 'BigShot Inc.'") }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
has_many :dependent_conditional_clients_of_firm, -> { order("id").where("name = ?", 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
- has_many :dependent_hash_conditional_clients_of_firm, -> { order("id").where(:name => 'BigShot Inc.') }, :foreign_key => "client_of", :class_name => "Client", :dependent => :delete_all
end
class SpecialClient < Client
@@ -205,6 +187,8 @@ class Account < ActiveRecord::Base
belongs_to :firm, :class_name => 'Company'
belongs_to :unautosaved_firm, :foreign_key => "firm_id", :class_name => "Firm", :autosave => false
+ alias_attribute :available_credit, :credit_limit
+
def self.destroyed_account_ids
@destroyed_account_ids ||= Hash.new { |h,k| h[k] = [] }
end
diff --git a/activerecord/test/models/company_in_module.rb b/activerecord/test/models/company_in_module.rb
index 461bb0de09..38b0b6aafa 100644
--- a/activerecord/test/models/company_in_module.rb
+++ b/activerecord/test/models/company_in_module.rb
@@ -10,10 +10,6 @@ module MyApplication
has_many :clients_sorted_desc, -> { order("id DESC") }, :class_name => "Client"
has_many :clients_of_firm, -> { order "id" }, :foreign_key => "client_of", :class_name => "Client"
has_many :clients_like_ms, -> { where("name = 'Microsoft'").order("id") }, :class_name => "Client"
- ActiveSupport::Deprecation.silence do
- has_many :clients_using_sql, :class_name => "Client", :finder_sql => 'SELECT * FROM companies WHERE client_of = #{id}'
- end
-
has_one :account, :class_name => 'MyApplication::Billing::Account', :dependent => :destroy
end
diff --git a/activerecord/test/models/contract.rb b/activerecord/test/models/contract.rb
index 2cf5aa7a85..cdf7b267b5 100644
--- a/activerecord/test/models/contract.rb
+++ b/activerecord/test/models/contract.rb
@@ -1,6 +1,7 @@
class Contract < ActiveRecord::Base
belongs_to :company
belongs_to :developer
+ belongs_to :firm, :foreign_key => 'company_id'
before_save :hi
after_save :bye
diff --git a/activerecord/test/models/department.rb b/activerecord/test/models/department.rb
new file mode 100644
index 0000000000..08004a0ed3
--- /dev/null
+++ b/activerecord/test/models/department.rb
@@ -0,0 +1,4 @@
+class Department < ActiveRecord::Base
+ has_many :chefs
+ belongs_to :hotel
+end
diff --git a/activerecord/test/models/developer.rb b/activerecord/test/models/developer.rb
index 622dd75aeb..a26de55758 100644
--- a/activerecord/test/models/developer.rb
+++ b/activerecord/test/models/developer.rb
@@ -1,11 +1,5 @@
require 'ostruct'
-module DeveloperProjectsAssociationExtension
- def find_most_recent
- order("id DESC").first
- end
-end
-
module DeveloperProjectsAssociationExtension2
def find_least_recent
order("id ASC").first
@@ -44,6 +38,8 @@ class Developer < ActiveRecord::Base
has_and_belongs_to_many :special_projects, :join_table => 'developers_projects', :association_foreign_key => 'project_id'
has_many :audit_logs
+ has_many :contracts
+ has_many :firms, :through => :contracts, :source => :firm
scope :jamises, -> { where(:name => 'Jamis') }
@@ -57,6 +53,16 @@ class Developer < ActiveRecord::Base
def log=(message)
audit_logs.build :message => message
end
+
+ after_find :track_instance_count
+ cattr_accessor :instance_count
+
+ def track_instance_count
+ self.class.instance_count ||= 0
+ self.class.instance_count += 1
+ end
+ private :track_instance_count
+
end
class AuditLog < ActiveRecord::Base
@@ -91,6 +97,15 @@ class DeveloperWithIncludes < ActiveRecord::Base
default_scope { includes(:audit_logs) }
end
+class DeveloperFilteredOnJoins < ActiveRecord::Base
+ self.table_name = 'developers'
+ has_and_belongs_to_many :projects, -> { order('projects.id') }, :foreign_key => 'developer_id', :join_table => 'developers_projects'
+
+ def self.default_scope
+ joins(:projects).where(:projects => { :name => 'Active Controller' })
+ end
+end
+
class DeveloperOrderedBySalary < ActiveRecord::Base
self.table_name = 'developers'
default_scope { order('salary DESC') }
@@ -224,3 +239,8 @@ class ThreadsafeDeveloper < ActiveRecord::Base
limit(1)
end
end
+
+class CachedDeveloper < ActiveRecord::Base
+ self.table_name = "developers"
+ self.cache_timestamp_format = :number
+end
diff --git a/activerecord/test/models/dog.rb b/activerecord/test/models/dog.rb
index 72b7d33a86..b02b8447b8 100644
--- a/activerecord/test/models/dog.rb
+++ b/activerecord/test/models/dog.rb
@@ -1,4 +1,5 @@
class Dog < ActiveRecord::Base
- belongs_to :breeder, :class_name => "DogLover", :counter_cache => :bred_dogs_count
- belongs_to :trainer, :class_name => "DogLover", :counter_cache => :trained_dogs_count
+ belongs_to :breeder, class_name: "DogLover", counter_cache: :bred_dogs_count
+ belongs_to :trainer, class_name: "DogLover", counter_cache: :trained_dogs_count
+ belongs_to :doglover, foreign_key: :dog_lover_id, class_name: "DogLover", counter_cache: true
end
diff --git a/activerecord/test/models/dog_lover.rb b/activerecord/test/models/dog_lover.rb
index a33dc575c5..2c5be94aea 100644
--- a/activerecord/test/models/dog_lover.rb
+++ b/activerecord/test/models/dog_lover.rb
@@ -1,4 +1,5 @@
class DogLover < ActiveRecord::Base
- has_many :trained_dogs, :class_name => "Dog", :foreign_key => :trainer_id
- has_many :bred_dogs, :class_name => "Dog", :foreign_key => :breeder_id
+ has_many :trained_dogs, class_name: "Dog", foreign_key: :trainer_id, dependent: :destroy
+ has_many :bred_dogs, class_name: "Dog", foreign_key: :breeder_id
+ has_many :dogs
end
diff --git a/activerecord/test/models/drink_designer.rb b/activerecord/test/models/drink_designer.rb
new file mode 100644
index 0000000000..2db968ef11
--- /dev/null
+++ b/activerecord/test/models/drink_designer.rb
@@ -0,0 +1,3 @@
+class DrinkDesigner < ActiveRecord::Base
+ has_one :chef, as: :employable
+end
diff --git a/activerecord/test/models/friendship.rb b/activerecord/test/models/friendship.rb
index 6b4f7acc38..4b411ca8e0 100644
--- a/activerecord/test/models/friendship.rb
+++ b/activerecord/test/models/friendship.rb
@@ -1,4 +1,6 @@
class Friendship < ActiveRecord::Base
belongs_to :friend, class_name: 'Person'
- belongs_to :follower, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :followers_count
+ # friend_too exists to test a bug, and probably shouldn't be used elsewhere
+ belongs_to :friend_too, foreign_key: 'friend_id', class_name: 'Person', counter_cache: :friends_too_count
+ belongs_to :follower, class_name: 'Person'
end
diff --git a/activerecord/test/models/hotel.rb b/activerecord/test/models/hotel.rb
new file mode 100644
index 0000000000..b352cd22f3
--- /dev/null
+++ b/activerecord/test/models/hotel.rb
@@ -0,0 +1,6 @@
+class Hotel < ActiveRecord::Base
+ has_many :departments
+ has_many :chefs, through: :departments
+ has_many :cake_designers, source_type: 'CakeDesigner', source: :employable, through: :chefs
+ has_many :drink_designers, source_type: 'DrinkDesigner', source: :employable, through: :chefs
+end
diff --git a/activerecord/test/models/liquid.rb b/activerecord/test/models/liquid.rb
index 6cfd443e75..69d4d7df1a 100644
--- a/activerecord/test/models/liquid.rb
+++ b/activerecord/test/models/liquid.rb
@@ -1,5 +1,4 @@
class Liquid < ActiveRecord::Base
self.table_name = :liquid
- has_many :molecules, -> { uniq }
+ has_many :molecules, -> { distinct }
end
-
diff --git a/activerecord/test/models/man.rb b/activerecord/test/models/man.rb
index 4bff92dc98..f4d127730c 100644
--- a/activerecord/test/models/man.rb
+++ b/activerecord/test/models/man.rb
@@ -6,4 +6,5 @@ class Man < ActiveRecord::Base
# These are "broken" inverse_of associations for the purposes of testing
has_one :dirty_face, :class_name => 'Face', :inverse_of => :dirty_man
has_many :secret_interests, :class_name => 'Interest', :inverse_of => :secret_man
+ has_one :mixed_case_monkey
end
diff --git a/activerecord/test/models/member.rb b/activerecord/test/models/member.rb
index 1134b09d8b..72095f9236 100644
--- a/activerecord/test/models/member.rb
+++ b/activerecord/test/models/member.rb
@@ -2,14 +2,13 @@ class Member < ActiveRecord::Base
has_one :current_membership
has_one :selected_membership
has_one :membership
- has_many :fellow_members, :through => :club, :source => :members
has_one :club, :through => :current_membership
has_one :selected_club, :through => :selected_membership, :source => :club
has_one :favourite_club, -> { where "memberships.favourite = ?", true }, :through => :membership, :source => :club
has_one :hairy_club, -> { where :clubs => {:name => "Moustache and Eyebrow Fancier Club"} }, :through => :membership, :source => :club
has_one :sponsor, :as => :sponsorable
has_one :sponsor_club, :through => :sponsor
- has_one :member_detail
+ has_one :member_detail, :inverse_of => false
has_one :organization, :through => :member_detail
belongs_to :member_type
diff --git a/activerecord/test/models/member_detail.rb b/activerecord/test/models/member_detail.rb
index fe619f8732..9d253aa126 100644
--- a/activerecord/test/models/member_detail.rb
+++ b/activerecord/test/models/member_detail.rb
@@ -1,5 +1,5 @@
class MemberDetail < ActiveRecord::Base
- belongs_to :member
+ belongs_to :member, :inverse_of => false
belongs_to :organization
has_one :member_type, :through => :member
diff --git a/activerecord/test/models/membership.rb b/activerecord/test/models/membership.rb
index bcbb7e42c5..df7167ee93 100644
--- a/activerecord/test/models/membership.rb
+++ b/activerecord/test/models/membership.rb
@@ -8,6 +8,11 @@ class CurrentMembership < Membership
belongs_to :club
end
+class SuperMembership < Membership
+ belongs_to :member, -> { order('members.id DESC') }
+ belongs_to :club
+end
+
class SelectedMembership < Membership
def self.default_scope
select("'1' as foo")
diff --git a/activerecord/test/models/mixed_case_monkey.rb b/activerecord/test/models/mixed_case_monkey.rb
index 763baefd91..4d37371777 100644
--- a/activerecord/test/models/mixed_case_monkey.rb
+++ b/activerecord/test/models/mixed_case_monkey.rb
@@ -1,3 +1,5 @@
class MixedCaseMonkey < ActiveRecord::Base
self.primary_key = 'monkeyID'
+
+ belongs_to :man
end
diff --git a/activerecord/test/models/movie.rb b/activerecord/test/models/movie.rb
index 6384b4c801..c441be2bef 100644
--- a/activerecord/test/models/movie.rb
+++ b/activerecord/test/models/movie.rb
@@ -1,5 +1,3 @@
class Movie < ActiveRecord::Base
- def self.primary_key
- "movieid"
- end
+ self.primary_key = "movieid"
end
diff --git a/activerecord/test/models/owner.rb b/activerecord/test/models/owner.rb
index fea55f4535..1c7ed4aa3e 100644
--- a/activerecord/test/models/owner.rb
+++ b/activerecord/test/models/owner.rb
@@ -1,5 +1,5 @@
class Owner < ActiveRecord::Base
self.primary_key = :owner_id
- has_many :pets
+ has_many :pets, -> { order 'pets.name desc' }
has_many :toys, :through => :pets
end
diff --git a/activerecord/test/models/parrot.rb b/activerecord/test/models/parrot.rb
index c4ee2bd19d..e76e83f314 100644
--- a/activerecord/test/models/parrot.rb
+++ b/activerecord/test/models/parrot.rb
@@ -21,3 +21,9 @@ end
class DeadParrot < Parrot
belongs_to :killer, :class_name => 'Pirate'
end
+
+class FunkyParrot < Parrot
+ before_destroy do
+ raise "before_destroy was called"
+ end
+end
diff --git a/activerecord/test/models/person.rb b/activerecord/test/models/person.rb
index 6ad0cf6987..1a282dbce4 100644
--- a/activerecord/test/models/person.rb
+++ b/activerecord/test/models/person.rb
@@ -8,13 +8,17 @@ class Person < ActiveRecord::Base
has_many :posts_with_no_comments, -> { includes(:comments).where('comments.id is null').references(:comments) },
:through => :readers, :source => :post
- has_many :followers, foreign_key: 'friend_id', class_name: 'Friendship'
+ has_many :friendships, foreign_key: 'friend_id'
+ # friends_too exists to test a bug, and probably shouldn't be used elsewhere
+ has_many :friends_too, foreign_key: 'friend_id', class_name: 'Friendship'
+ has_many :followers, through: :friendships
has_many :references
has_many :bad_references
has_many :fixed_bad_references, -> { where :favourite => true }, :class_name => 'BadReference'
has_one :favourite_reference, -> { where 'favourite=?', true }, :class_name => 'Reference'
has_many :posts_with_comments_sorted_by_comment_id, -> { includes(:comments).order('comments.id') }, :through => :readers, :source => :post
+ has_many :first_posts, -> { where(id: [1, 2]) }, through: :readers
has_many :jobs, :through => :references
has_many :jobs_with_dependent_destroy, :source => :job, :through => :references, :dependent => :destroy
@@ -28,6 +32,7 @@ class Person < ActiveRecord::Base
has_many :agents_posts, :through => :agents, :source => :posts
has_many :agents_posts_authors, :through => :agents_posts, :source => :author
+ has_many :essays, primary_key: "first_name", foreign_key: "writer_id"
scope :males, -> { where(:gender => 'M') }
scope :females, -> { where(:gender => 'F') }
@@ -100,3 +105,24 @@ class NestedPerson < ActiveRecord::Base
assign_attributes({ :best_friend_attributes => { :first_name => new_name } })
end
end
+
+class Insure
+ INSURES = %W{life annuality}
+
+ def self.load mask
+ INSURES.select do |insure|
+ (1 << INSURES.index(insure)) & mask.to_i > 0
+ end
+ end
+
+ def self.dump insures
+ numbers = insures.map { |insure| INSURES.index(insure) }
+ numbers.inject(0) { |sum, n| sum + (1 << n) }
+ end
+end
+
+class SerializedPerson < ActiveRecord::Base
+ self.table_name = 'people'
+
+ serialize :insures, Insure
+end
diff --git a/activerecord/test/models/pet.rb b/activerecord/test/models/pet.rb
index 3cd5bceed5..f7970d7aab 100644
--- a/activerecord/test/models/pet.rb
+++ b/activerecord/test/models/pet.rb
@@ -1,5 +1,4 @@
class Pet < ActiveRecord::Base
-
attr_accessor :current_user
self.primary_key = :pet_id
@@ -13,5 +12,4 @@ class Pet < ActiveRecord::Base
after_destroy do |record|
Pet.after_destroy_output = record.current_user
end
-
end
diff --git a/activerecord/test/models/post.rb b/activerecord/test/models/post.rb
index c995f59a15..faf539a562 100644
--- a/activerecord/test/models/post.rb
+++ b/activerecord/test/models/post.rb
@@ -1,20 +1,28 @@
class Post < ActiveRecord::Base
+ class CategoryPost < ActiveRecord::Base
+ self.table_name = "categories_posts"
+ belongs_to :category
+ belongs_to :post
+ end
+
module NamedExtension
def author
'lifo'
end
end
+ module NamedExtension2
+ def greeting
+ "hello"
+ end
+ end
+
scope :containing_the_letter_a, -> { where("body LIKE '%a%'") }
scope :ranked_by_comments, -> { order("comments_count DESC") }
scope :limit_by, lambda {|l| limit(l) }
- belongs_to :author do
- def greeting
- "hello"
- end
- end
+ belongs_to :author
belongs_to :author_with_posts, -> { includes(:posts) }, :class_name => "Author", :foreign_key => :author_id
belongs_to :author_with_address, -> { includes(:author_address) }, :class_name => "Author", :foreign_key => :author_id
@@ -29,6 +37,9 @@ class Post < ActiveRecord::Base
scope :with_very_special_comments, -> { joins(:comments).where(:comments => {:type => 'VerySpecialComment'}) }
scope :with_post, ->(post_id) { joins(:comments).where(:comments => { :post_id => post_id }) }
+ scope :with_comments, -> { preload(:comments) }
+ scope :with_tags, -> { preload(:taggings) }
+
has_many :comments do
def find_most_recent
order("id DESC").first
@@ -43,9 +54,20 @@ class Post < ActiveRecord::Base
end
end
+ has_many :comments_with_extend, extend: NamedExtension, class_name: "Comment", foreign_key: "post_id" do
+ def greeting
+ "hello"
+ end
+ end
+
+ has_many :comments_with_extend_2, extend: [NamedExtension, NamedExtension2], class_name: "Comment", foreign_key: "post_id"
+
has_many :author_favorites, :through => :author
has_many :author_categorizations, :through => :author, :source => :categorizations
has_many :author_addresses, :through => :author
+ has_many :author_address_extra_with_address,
+ through: :author_with_address,
+ source: :author_address_extra
has_many :comments_with_interpolated_conditions,
->(p) { where "#{"#{p.aliased_table_name}." rescue ""}body = ?", 'Thank you for the welcome' },
@@ -59,6 +81,8 @@ class Post < ActiveRecord::Base
has_many :special_comments_ratings, :through => :special_comments, :source => :ratings
has_many :special_comments_ratings_taggings, :through => :special_comments_ratings, :source => :taggings
+ has_many :category_posts, :class_name => 'CategoryPost'
+ has_many :scategories, through: :category_posts, source: :category
has_and_belongs_to_many :categories
has_and_belongs_to_many :special_categories, :join_table => "categories_posts", :association_foreign_key => 'category_id'
@@ -109,7 +133,6 @@ class Post < ActiveRecord::Base
has_many :secure_readers
has_many :readers_with_person, -> { includes(:person) }, :class_name => "Reader"
has_many :people, :through => :readers
- has_many :secure_people, :through => :secure_readers
has_many :single_people, :through => :readers
has_many :people_with_callbacks, :source=>:person, :through => :readers,
:before_add => lambda {|owner, reader| log(:added, :before, reader.first_name) },
@@ -151,18 +174,6 @@ class SubStiPost < StiPost
self.table_name = Post.table_name
end
-ActiveSupport::Deprecation.silence do
- class DeprecatedPostWithComment < ActiveRecord::Base
- self.table_name = 'posts'
- default_scope where("posts.comments_count > 0").order("posts.comments_count ASC")
- end
-end
-
-class PostForAuthor < ActiveRecord::Base
- self.table_name = 'posts'
- cattr_accessor :selected_author
-end
-
class FirstPost < ActiveRecord::Base
self.table_name = 'posts'
default_scope { where(:id => 1) }
@@ -177,6 +188,11 @@ class PostWithDefaultInclude < ActiveRecord::Base
has_many :comments, :foreign_key => :post_id
end
+class PostWithSpecialCategorization < Post
+ has_many :categorizations, :foreign_key => :post_id
+ default_scope { where(:type => 'PostWithSpecialCategorization').joins(:categorizations).where(:categorizations => { :special => true }) }
+end
+
class PostWithDefaultScope < ActiveRecord::Base
self.table_name = 'posts'
default_scope { order(:title) }
diff --git a/activerecord/test/models/project.rb b/activerecord/test/models/project.rb
index af3ec4be83..7f42a4b1f8 100644
--- a/activerecord/test/models/project.rb
+++ b/activerecord/test/models/project.rb
@@ -1,25 +1,11 @@
class Project < ActiveRecord::Base
- has_and_belongs_to_many :developers, -> { uniq.order 'developers.name desc, developers.id desc' }
+ has_and_belongs_to_many :developers, -> { distinct.order 'developers.name desc, developers.id desc' }
has_and_belongs_to_many :readonly_developers, -> { readonly }, :class_name => "Developer"
- has_and_belongs_to_many :selected_developers, -> { uniq.select "developers.*" }, :class_name => "Developer"
has_and_belongs_to_many :non_unique_developers, -> { order 'developers.name desc, developers.id desc' }, :class_name => 'Developer'
has_and_belongs_to_many :limited_developers, -> { limit 1 }, :class_name => "Developer"
- has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").uniq }, :class_name => "Developer"
- has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').uniq }, :class_name => "Developer"
+ has_and_belongs_to_many :developers_named_david, -> { where("name = 'David'").distinct }, :class_name => "Developer"
+ has_and_belongs_to_many :developers_named_david_with_hash_conditions, -> { where(:name => 'David').distinct }, :class_name => "Developer"
has_and_belongs_to_many :salaried_developers, -> { where "salary > 0" }, :class_name => "Developer"
-
- ActiveSupport::Deprecation.silence do
- has_and_belongs_to_many :developers_with_finder_sql, :class_name => "Developer", :finder_sql => proc { "SELECT t.*, j.* FROM developers_projects j, developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id" }
- has_and_belongs_to_many :developers_with_multiline_finder_sql, :class_name => "Developer", :finder_sql => proc {
- "SELECT
- t.*, j.*
- FROM
- developers_projects j,
- developers t WHERE t.id = j.developer_id AND j.project_id = #{id} ORDER BY t.id"
- }
- has_and_belongs_to_many :developers_by_sql, :class_name => "Developer", :delete_sql => proc { |record| "DELETE FROM developers_projects WHERE project_id = #{id} AND developer_id = #{record.id}" }
- end
-
has_and_belongs_to_many :developers_with_callbacks, :class_name => "Developer", :before_add => Proc.new {|o, r| o.developers_log << "before_adding#{r.id || '<new>'}"},
:after_add => Proc.new {|o, r| o.developers_log << "after_adding#{r.id || '<new>'}"},
:before_remove => Proc.new {|o, r| o.developers_log << "before_removing#{r.id}"},
@@ -40,7 +26,4 @@ class Project < ActiveRecord::Base
end
class SpecialProject < Project
- def hello_world
- "hello there!"
- end
end
diff --git a/activerecord/test/models/reader.rb b/activerecord/test/models/reader.rb
index f8fb9c573e..3a6b7fad34 100644
--- a/activerecord/test/models/reader.rb
+++ b/activerecord/test/models/reader.rb
@@ -2,6 +2,7 @@ class Reader < ActiveRecord::Base
belongs_to :post
belongs_to :person, :inverse_of => :readers
belongs_to :single_person, :class_name => 'Person', :foreign_key => :person_id, :inverse_of => :reader
+ belongs_to :first_post, -> { where(id: [2, 3]) }
end
class SecureReader < ActiveRecord::Base
diff --git a/activerecord/test/models/reference.rb b/activerecord/test/models/reference.rb
index 561b431766..c2f9068f57 100644
--- a/activerecord/test/models/reference.rb
+++ b/activerecord/test/models/reference.rb
@@ -4,15 +4,14 @@ class Reference < ActiveRecord::Base
has_many :agents_posts_authors, :through => :person
- class << self
- attr_accessor :make_comments
- end
+ class << self; attr_accessor :make_comments; end
+ self.make_comments = false
before_destroy :make_comments
def make_comments
if self.class.make_comments
- person.update_attributes :comments => "Reference destroyed"
+ person.update comments: "Reference destroyed"
end
end
end
diff --git a/activerecord/test/models/reply.rb b/activerecord/test/models/reply.rb
index 079e325aad..3e82e55d89 100644
--- a/activerecord/test/models/reply.rb
+++ b/activerecord/test/models/reply.rb
@@ -1,14 +1,13 @@
require 'models/topic'
class Reply < Topic
- scope :base, -> { scoped }
-
belongs_to :topic, :foreign_key => "parent_id", :counter_cache => true
belongs_to :topic_with_primary_key, :class_name => "Topic", :primary_key => "title", :foreign_key => "parent_title", :counter_cache => "replies_count"
has_many :replies, :class_name => "SillyReply", :dependent => :destroy, :foreign_key => "parent_id"
end
class UniqueReply < Reply
+ belongs_to :topic, :foreign_key => 'parent_id', :counter_cache => true
validates_uniqueness_of :content, :scope => 'parent_id'
end
diff --git a/activerecord/test/models/speedometer.rb b/activerecord/test/models/speedometer.rb
index 0a7d38d8ec..497c3aba9a 100644
--- a/activerecord/test/models/speedometer.rb
+++ b/activerecord/test/models/speedometer.rb
@@ -1,4 +1,6 @@
class Speedometer < ActiveRecord::Base
self.primary_key = :speedometer_id
belongs_to :dashboard
+
+ has_many :minivans
end
diff --git a/activerecord/test/models/teapot.rb b/activerecord/test/models/teapot.rb
deleted file mode 100644
index b035b18c1b..0000000000
--- a/activerecord/test/models/teapot.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-class Teapot
- # I'm a little teapot,
- # Short and stout,
- # Here is my handle
- # Here is my spout
- # When I get all steamed up,
- # Hear me shout,
- # Tip me over and pour me out!
- #
- # HELL YEAH TEAPOT SONG
-
- include ActiveRecord::Model
-end
-
-class OtherTeapot < Teapot
-end
-
-class OMFGIMATEAPOT
- def aaahhh
- "mmm"
- end
-end
-
-class CoolTeapot < OMFGIMATEAPOT
- include ActiveRecord::Model
- self.table_name = "teapots"
-end
-
-class Ceiling
- include ActiveRecord::Model
-
- class Teapot
- include ActiveRecord::Model
- end
-end
diff --git a/activerecord/test/models/topic.rb b/activerecord/test/models/topic.rb
index 4b27c16681..40c8e97fc2 100644
--- a/activerecord/test/models/topic.rb
+++ b/activerecord/test/models/topic.rb
@@ -33,7 +33,7 @@ class Topic < ActiveRecord::Base
end
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id"
- has_many :replies_with_primary_key, :class_name => "Reply", :dependent => :destroy, :primary_key => "title", :foreign_key => "parent_title"
+ has_many :approved_replies, -> { approved }, class_name: 'Reply', foreign_key: "parent_id", counter_cache: 'replies_count'
has_many :unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
has_many :silly_unique_replies, :dependent => :destroy, :foreign_key => "parent_id"
@@ -106,6 +106,13 @@ class ImportantTopic < Topic
serialize :important, Hash
end
+class BlankTopic < Topic
+ # declared here to make sure that dynamic finder with a bang can find a model that responds to `blank?`
+ def blank?
+ true
+ end
+end
+
module Web
class Topic < ActiveRecord::Base
has_many :replies, :dependent => :destroy, :foreign_key => "parent_id", :class_name => 'Web::Reply'
diff --git a/activerecord/test/models/traffic_light.rb b/activerecord/test/models/traffic_light.rb
index 228f3f7bd4..a6b7edb882 100644
--- a/activerecord/test/models/traffic_light.rb
+++ b/activerecord/test/models/traffic_light.rb
@@ -1,3 +1,4 @@
class TrafficLight < ActiveRecord::Base
serialize :state, Array
+ serialize :long_state, Array
end
diff --git a/activerecord/test/schema/mysql2_specific_schema.rb b/activerecord/test/schema/mysql2_specific_schema.rb
index 24a43d7ece..a9a6514c9d 100644
--- a/activerecord/test/schema/mysql2_specific_schema.rb
+++ b/activerecord/test/schema/mysql2_specific_schema.rb
@@ -1,15 +1,29 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
- t.binary :tiny_blob, :limit => 255
- t.binary :normal_blob, :limit => 65535
- t.binary :medium_blob, :limit => 16777215
- t.binary :long_blob, :limit => 2147483647
- t.text :tiny_text, :limit => 255
- t.text :normal_text, :limit => 65535
- t.text :medium_text, :limit => 16777215
- t.text :long_text, :limit => 2147483647
+ create_table :binary_fields, force: true do |t|
+ t.binary :var_binary, limit: 255
+ t.binary :var_binary_large, limit: 4095
+ t.column :tiny_blob, 'tinyblob', limit: 255
+ t.binary :normal_blob, limit: 65535
+ t.binary :medium_blob, limit: 16777215
+ t.binary :long_blob, limit: 2147483647
+ t.text :tiny_text, limit: 255
+ t.text :normal_text, limit: 65535
+ t.text :medium_text, limit: 16777215
+ t.text :long_text, limit: 2147483647
end
+ add_index :binary_fields, :var_binary
+
+ create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
+ t.string :awesome
+ t.string :pizza
+ t.string :snacks
+ end
+
+ add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
+ add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
+ add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -38,7 +52,7 @@ SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
- enum_column ENUM('true','false')
+ enum_column ENUM('text','blob','tiny','medium','long')
)
SQL
end
diff --git a/activerecord/test/schema/mysql_specific_schema.rb b/activerecord/test/schema/mysql_specific_schema.rb
index 802c08b819..f2cffca52c 100644
--- a/activerecord/test/schema/mysql_specific_schema.rb
+++ b/activerecord/test/schema/mysql_specific_schema.rb
@@ -1,15 +1,29 @@
ActiveRecord::Schema.define do
- create_table :binary_fields, :force => true do |t|
- t.binary :tiny_blob, :limit => 255
- t.binary :normal_blob, :limit => 65535
- t.binary :medium_blob, :limit => 16777215
- t.binary :long_blob, :limit => 2147483647
- t.text :tiny_text, :limit => 255
- t.text :normal_text, :limit => 65535
- t.text :medium_text, :limit => 16777215
- t.text :long_text, :limit => 2147483647
+ create_table :binary_fields, force: true do |t|
+ t.binary :var_binary, limit: 255
+ t.binary :var_binary_large, limit: 4095
+ t.column :tiny_blob, 'tinyblob', limit: 255
+ t.binary :normal_blob, limit: 65535
+ t.binary :medium_blob, limit: 16777215
+ t.binary :long_blob, limit: 2147483647
+ t.text :tiny_text, limit: 255
+ t.text :normal_text, limit: 65535
+ t.text :medium_text, limit: 16777215
+ t.text :long_text, limit: 2147483647
end
+ add_index :binary_fields, :var_binary
+
+ create_table :key_tests, force: true, :options => 'ENGINE=MyISAM' do |t|
+ t.string :awesome
+ t.string :pizza
+ t.string :snacks
+ end
+
+ add_index :key_tests, :awesome, :type => :fulltext, :name => 'index_key_tests_on_awesome'
+ add_index :key_tests, :pizza, :using => :btree, :name => 'index_key_tests_on_pizza'
+ add_index :key_tests, :snacks, :name => 'index_key_tests_on_snack'
+
ActiveRecord::Base.connection.execute <<-SQL
DROP PROCEDURE IF EXISTS ten;
SQL
@@ -49,7 +63,7 @@ SQL
ActiveRecord::Base.connection.execute <<-SQL
CREATE TABLE enum_tests (
- enum_column ENUM('true','false')
+ enum_column ENUM('text','blob','tiny','medium','long')
)
SQL
diff --git a/activerecord/test/schema/postgresql_specific_schema.rb b/activerecord/test/schema/postgresql_specific_schema.rb
index d0e7338f15..6b7012a172 100644
--- a/activerecord/test/schema/postgresql_specific_schema.rb
+++ b/activerecord/test/schema/postgresql_specific_schema.rb
@@ -1,6 +1,6 @@
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
+ %w(postgresql_ranges postgresql_tsvectors postgresql_hstores postgresql_arrays postgresql_moneys postgresql_numbers postgresql_times postgresql_network_addresses postgresql_bit_strings postgresql_uuids postgresql_ltrees
postgresql_oids postgresql_xml_data_type defaults geometrics postgresql_timestamp_with_zones postgresql_partitioned_table postgresql_partitioned_table_parent postgresql_json_data_type).each do |table_name|
execute "DROP TABLE IF EXISTS #{quote_table_name table_name}"
end
@@ -32,6 +32,7 @@ ActiveRecord::Schema.define do
char3 text default 'a text field',
positive_integer integer default 1,
negative_integer integer default -1,
+ bigint_default bigint default 0::bigint,
decimal_number decimal(3,2) default 2.78,
multiline_default text DEFAULT '--- []
@@ -73,6 +74,18 @@ _SQL
);
_SQL
+ execute <<_SQL if supports_ranges?
+ CREATE TABLE postgresql_ranges (
+ id SERIAL PRIMARY KEY,
+ date_range daterange,
+ num_range numrange,
+ ts_range tsrange,
+ tstz_range tstzrange,
+ int4_range int4range,
+ int8_range int8range
+ );
+_SQL
+
execute <<_SQL
CREATE TABLE postgresql_tsvectors (
id SERIAL PRIMARY KEY,
@@ -89,6 +102,15 @@ _SQL
_SQL
end
+ if 't' == select_value("select 'ltree'=ANY(select typname from pg_type)")
+ execute <<_SQL
+ CREATE TABLE postgresql_ltrees (
+ id SERIAL PRIMARY KEY,
+ path ltree
+ );
+_SQL
+ end
+
if 't' == select_value("select 'json'=ANY(select typname from pg_type)")
execute <<_SQL
CREATE TABLE postgresql_json_data_type (
@@ -124,9 +146,9 @@ _SQL
execute <<_SQL
CREATE TABLE postgresql_network_addresses (
id SERIAL PRIMARY KEY,
- cidr_address CIDR,
- inet_address INET,
- mac_address MACADDR
+ cidr_address CIDR default '192.168.1.0/24',
+ inet_address INET default '192.168.1.1',
+ mac_address MACADDR default 'ff:ff:ff:ff:ff:ff'
);
_SQL
@@ -152,7 +174,7 @@ _SQL
);
_SQL
-begin
+ begin
execute <<_SQL
CREATE TABLE postgresql_partitioned_table_parent (
id SERIAL PRIMARY KEY,
@@ -174,14 +196,14 @@ begin
BEFORE INSERT ON postgresql_partitioned_table_parent
FOR EACH ROW EXECUTE PROCEDURE partitioned_insert_trigger();
_SQL
-rescue ActiveRecord::StatementInvalid => e
- if e.message =~ /language "plpgsql" does not exist/
- execute "CREATE LANGUAGE 'plpgsql';"
- retry
- else
- raise e
+ rescue ActiveRecord::StatementInvalid => e
+ if e.message =~ /language "plpgsql" does not exist/
+ execute "CREATE LANGUAGE 'plpgsql';"
+ retry
+ else
+ raise e
+ end
end
-end
begin
execute <<_SQL
@@ -190,7 +212,13 @@ end
data xml
);
_SQL
-rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
+ rescue #This version of PostgreSQL either has no XML support or is was not compiled with XML support: skipping table
+ end
+
+ # This table is to verify if the :limit option is being ignored for text and binary columns
+ create_table :limitless_fields, force: true do |t|
+ t.binary :binary, limit: 100_000
+ t.text :text, limit: 100_000
end
end
diff --git a/activerecord/test/schema/schema.rb b/activerecord/test/schema/schema.rb
index 2e4ec96933..88a686d436 100644
--- a/activerecord/test/schema/schema.rb
+++ b/activerecord/test/schema/schema.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
ActiveRecord::Schema.define do
def except(adapter_names_to_exclude)
unless [adapter_names_to_exclude].flatten.include?(adapter_name)
@@ -115,6 +117,7 @@ ActiveRecord::Schema.define do
t.integer :engines_count
t.integer :wheels_count
t.column :lock_version, :integer, :null => false, :default => 0
+ t.timestamps
end
create_table :categories, :force => true do |t|
@@ -182,6 +185,7 @@ ActiveRecord::Schema.define do
add_index :companies, [:firm_id, :type, :rating], :name => "company_index"
add_index :companies, [:firm_id, :type], :name => "company_partial_index", :where => "rating > 10"
+ add_index :companies, :name, :name => 'company_name_index', :using => :btree
create_table :vegetables, :force => true do |t|
t.string :name
@@ -218,6 +222,8 @@ ActiveRecord::Schema.define do
t.integer :salary, :default => 70000
t.datetime :created_at
t.datetime :updated_at
+ t.datetime :created_on
+ t.datetime :updated_on
end
create_table :developers_projects, :force => true, :id => false do |t|
@@ -227,14 +233,16 @@ ActiveRecord::Schema.define do
t.integer :access_level, :default => 1
end
- create_table :dog_lovers, :force => true do |t|
- t.integer :trained_dogs_count, :default => 0
- t.integer :bred_dogs_count, :default => 0
+ create_table :dog_lovers, force: true do |t|
+ t.integer :trained_dogs_count, default: 0
+ t.integer :bred_dogs_count, default: 0
+ t.integer :dogs_count, default: 0
end
create_table :dogs, :force => true do |t|
t.integer :trainer_id
t.integer :breeder_id
+ t.integer :dog_lover_id
end
create_table :edges, :force => true, :id => false do |t|
@@ -277,7 +285,7 @@ ActiveRecord::Schema.define do
create_table :friendships, :force => true do |t|
t.integer :friend_id
- t.integer :person_id
+ t.integer :follower_id
end
create_table :goofy_string_id, :force => true, :id => false do |t|
@@ -491,8 +499,10 @@ ActiveRecord::Schema.define do
t.integer :lock_version, :null => false, :default => 0
t.string :comments
t.integer :followers_count, :default => 0
+ t.integer :friends_too_count, :default => 0
t.references :best_friend
t.references :best_friend_of
+ t.integer :insures, null: false, default: 0
t.timestamps
end
@@ -565,6 +575,7 @@ ActiveRecord::Schema.define do
t.integer :post_id, :null => false
t.integer :person_id, :null => false
t.boolean :skimmer, :default => false
+ t.integer :first_post_id
end
create_table :references, :force => true do |t|
@@ -648,12 +659,6 @@ ActiveRecord::Schema.define do
t.datetime :ending
end
- create_table :teapots, :force => true do |t|
- t.string :name
- t.string :type
- t.timestamps
- end
-
create_table :topics, :force => true do |t|
t.string :title
t.string :author_name
@@ -672,6 +677,7 @@ ActiveRecord::Schema.define do
end
t.boolean :approved, :default => true
t.integer :replies_count, :default => 0
+ t.integer :unique_replies_count, :default => 0
t.integer :parent_id
t.string :parent_title
t.string :type
@@ -688,6 +694,7 @@ ActiveRecord::Schema.define do
create_table :traffic_lights, :force => true do |t|
t.string :location
t.string :state
+ t.text :long_state, :null => false
t.datetime :created_at
t.datetime :updated_at
end
@@ -776,7 +783,25 @@ ActiveRecord::Schema.define do
end
create_table :weirds, :force => true do |t|
t.string 'a$b'
+ t.string 'ãªã¾ãˆ'
+ t.string 'from'
+ end
+
+ create_table :hotels, force: true do |t|
+ end
+ create_table :departments, force: true do |t|
+ t.integer :hotel_id
end
+ create_table :cake_designers, force: true do |t|
+ end
+ create_table :drink_designers, force: true do |t|
+ end
+ create_table :chefs, force: true do |t|
+ t.integer :employable_id
+ t.string :employable_type
+ t.integer :department_id
+ end
+
except 'SQLite' do
# fk_test_has_fk should be before fk_test_has_pk
diff --git a/activerecord/test/schema/sqlite_specific_schema.rb b/activerecord/test/schema/sqlite_specific_schema.rb
index e9ddeb32cf..b7aff4f47d 100644
--- a/activerecord/test/schema/sqlite_specific_schema.rb
+++ b/activerecord/test/schema/sqlite_specific_schema.rb
@@ -1,9 +1,6 @@
ActiveRecord::Schema.define do
- # For sqlite 3.1.0+, make a table with an autoincrement column
- if supports_autoincrement?
- create_table :table_with_autoincrement, :force => true do |t|
- t.column :name, :string
- end
+ create_table :table_with_autoincrement, :force => true do |t|
+ t.column :name, :string
end
execute "DROP TABLE fk_test_has_fk" rescue nil
diff --git a/activerecord/test/support/connection.rb b/activerecord/test/support/connection.rb
index 92736e0ca9..196b3a9493 100644
--- a/activerecord/test/support/connection.rb
+++ b/activerecord/test/support/connection.rb
@@ -13,9 +13,9 @@ module ARTest
def self.connect
puts "Using #{connection_name}"
- ActiveRecord::Model.logger = ActiveSupport::Logger.new("debug.log")
- ActiveRecord::Model.configurations = connection_config
- ActiveRecord::Model.establish_connection 'arunit'
+ ActiveRecord::Base.logger = ActiveSupport::Logger.new("debug.log", 0, 100 * 1024 * 1024)
+ ActiveRecord::Base.configurations = connection_config
+ ActiveRecord::Base.establish_connection 'arunit'
ARUnit2Model.establish_connection 'arunit2'
end
end
diff --git a/activerecord/test/support/mysql.rb b/activerecord/test/support/mysql.rb
deleted file mode 100644
index 7a66415e64..0000000000
--- a/activerecord/test/support/mysql.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-if defined?(Mysql)
- class Mysql
- class Error
- # This monkey patch fixes annoy warning with mysql-2.8.1.gem when executing testcases.
- def errno_with_fix_warnings
- silence_warnings { errno_without_fix_warnings }
- end
- alias_method_chain :errno, :fix_warnings
- end
- end
-end
diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md
index 23e2ce0b03..2d934f8bc4 100644
--- a/activesupport/CHANGELOG.md
+++ b/activesupport/CHANGELOG.md
@@ -1,255 +1,251 @@
-## Rails 4.0.0 (unreleased) ##
+* Calling ActiveSupport::JSON.decode with unsupported options now raises an error.
-* Hash#extract! returns only those keys that present in the receiver.
+ *Godfrey Chan*
- {:a => 1, :b => 2}.extract!(:a, :x) # => {:a => 1}
+* Support :unless_exist in FileStore
- *Mikhail Dieterle*
+ *Michael Grosser*
-* Hash#extract! returns the same subclass, that the receiver is. I.e.
- HashWithIndifferentAccess#extract! returns HashWithIndifferentAccess instance.
+* Fix `slice!` deleting the default value of the hash.
- *Mikhail Dieterle*
+ *Antonio Santos*
-* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead. *Brian Durand*
+* `require_dependency` accepts objects that respond to `to_path`, in
+ particular `Pathname` instances.
-* Tests tag the Rails log with the current test class and test case:
+ *Benjamin Fleischer*
- [SessionsControllerTest] [test_0002_sign in] Processing by SessionsController#create as HTML
- [SessionsControllerTest] [test_0002_sign in] ...
+* Disable the ability to iterate over Range of AS::TimeWithZone
+ due to significant performance issues.
- *Jeremy Kemper*
+ *Bogdan Gusiev*
-* Add logger.push_tags and .pop_tags to complement logger.tagged:
+* Allow attaching event subscribers to ActiveSupport::Notifications namespaces
+ before they're defined. Essentially, this means instead of this:
- class Job
- def before
- Rails.logger.push_tags :jobs, self.class.name
+ class JokeSubscriber < ActiveSupport::Subscriber
+ def sql(event)
+ puts "A rabbi and a priest walk into a bar..."
end
- def after
- Rails.logger.pop_tags 2
- end
+ # This call needs to happen *after* defining the methods.
+ attach_to "active_record"
end
- *Jeremy Kemper*
+ You can do this:
-* Allow delegation to the class using the `:class` keyword, replacing
- `self.class` usage:
+ class JokeSubscriber < ActiveSupport::Subscriber
+ # This is much easier to read!
+ attach_to "active_record"
- class User
- def self.hello
- "world"
+ def sql(event)
+ puts "A rabbi and a priest walk into a bar..."
end
-
- delegate :hello, to: :class
end
- *Marc-Andre Lafortune*
+ This should make it easier to read and understand these subscribers.
-* `Date.beginning_of_week` thread local and `beginning_of_week` application
- config option added (default is Monday).
+ *Daniel Schierbeck*
- *Innokenty Mikhailov*
+* Add `Date#middle_of_day`, `DateTime#middle_of_day` and `Time#middle_of_day` methods.
-* An optional block can be passed to `config_accessor` to set its default value
+ Also added `midday`, `noon`, `at_midday`, `at_noon` and `at_middle_of_day` as aliases.
- class User
- include ActiveSupport::Configurable
- config_accessor :hair_colors do
- [:brown, :black, :blonde, :red]
- end
- end
+ *Anatoli Makarevich*
- User.hair_colors # => [:brown, :black, :blonde, :red]
+* Fix ActiveSupport::Cache::FileStore#cleanup to no longer rely on missing each_key method.
- *Larry Lv*
+ *Murray Steele*
-* ActiveSupport::Benchmarkable#silence has been deprecated due to its lack of
- thread safety. It will be removed without replacement in Rails 4.1.
+* Ensure that autoloaded constants in all-caps nestings are marked as
+ autoloaded.
- *Steve Klabnik*
+ *Simon Coffey*
-* An optional block can be passed to `Hash#deep_merge`. The block will be invoked
- for each duplicated key and used to resolve the conflict.
+* Add String#remove(pattern) as a short-hand for the common pattern of String#gsub(pattern, '')
- *Pranas Kiziela*
+ *DHH*
-* ActiveSupport::Deprecation is now a class. It is possible to create an instance
- of deprecator. Backwards compatibility has been preserved.
+* Adds a new deprecation behaviour that raises an exception. Throwing this
+ line into +config/environments/development.rb+
- You can choose which instance of the deprecator will be used.
+ ActiveSupport::Deprecation.behavior = :raise
- deprecate :method_name, :deprecator => deprecator_instance
+ will cause the application to raise an +ActiveSupport::DeprecationException+
+ on deprecations.
- You can use ActiveSupport::Deprecation in your gem.
+ Use this for aggressive deprecation cleanups.
- require 'active_support/deprecation'
- require 'active_support/core_ext/module/deprecation'
+ *Xavier Noria*
- class MyGem
- def self.deprecator
- ActiveSupport::Deprecation.new('2.0', 'MyGem')
- end
+* Remove 'cow' => 'kine' irregular inflection from default inflections.
- def old_method
- end
+ *Andrew White*
- def new_method
- end
+* Add `DateTime#to_s(:iso8601)` and `Date#to_s(:iso8601)` for consistency.
+
+ *Andrew White*
- deprecate :old_method => :new_method, :deprecator => deprecator
+* Add `Time#to_s(:iso8601)` for easy conversion of times to the iso8601 format for easy Javascript date parsing.
+
+ *DHH*
+
+* Improve `ActiveSupport::Cache::MemoryStore` cache size calculation.
+ The memory used by a key/entry pair is calculated via `#cached_size`:
+
+ def cached_size(key, entry)
+ key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
end
- MyGem.new.old_method
- # => DEPRECATION WARNING: old_method is deprecated and will be removed from MyGem 2.0 (use new_method instead). (called from <main> at file.rb:18)
+ 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. GH#11512
- *Piotr Niełacny & Robert Pankowecki*
+ *Simeon Simeonov*
-* `ERB::Util.html_escape` encodes single quote as `#39`. Decimal form has better support in old browsers. *Kalys Osmonov*
+* Only raise `Module::DelegationError` if it's the source of the exception.
-* `ActiveSupport::Callbacks`: deprecate monkey patch of object callbacks.
- Using the #filter method like this:
+ Fixes #10559
- before_filter MyFilter.new
+ *Andrew White*
- class MyFilter
- def filter(controller)
- end
- end
+* Make `Time.at_with_coercion` retain the second fraction and return local time.
- Is now deprecated with recommendation to use the corresponding filter type
- (`#before`, `#after` or `#around`):
+ Fixes #11350
- before_filter MyFilter.new
+ *Neer Friedman*, *Andrew White*
- class MyFilter
- def before(controller)
- end
- end
+* Make `HashWithIndifferentAccess#select` always return the hash, even when
+ `Hash#select!` returns `nil`, to allow further chaining.
- *Bogdan Gusiev*
+ *Marc Schütz*
-* An optional block can be passed to `HashWithIndifferentAccess#update` and `#merge`.
- The block will be invoked for each duplicated key, and used to resolve the conflict,
- thus replicating the behaviour of the corresponding methods on the `Hash` class.
+* Remove deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`).
- *Leo Cassarani*
+ *Arun Agrawal*
-* Remove `j` alias for `ERB::Util#json_escape`.
- The `j` alias is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`
- and both modules are included in the view context that would confuse the developers.
+* Remove deprecated `Module#local_constant_names` in favor of `Module#local_constants`.
- *Akira Matsuda*
+ *Arun Agrawal*
-* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore
+* Remove deprecated `DateTime.local_offset` in favor of `DateTime.civil_from_fromat`.
- *Guillermo Iguaran*
+ *Arun Agrawal*
-* Add default values to all `ActiveSupport::NumberHelper` methods, to avoid
- errors with empty locales or missing values.
+* Remove deprecated `Logger` core extensions (`core_ext/logger.rb`).
*Carlos Antonio da Silva*
-* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and
- `#encode_json` methods for custom JSON string literals.
+* Remove deprecated `Time#time_with_datetime_fallback`, `Time#utc_time`
+ and `Time#local_time` in favor of `Time#utc` and `Time#local`.
- *Erich Menge*
+ *Vipul A M*
-* Add String#indent. *fxn & Ace Suares*
+* Remove deprecated `Hash#diff` with no replacement.
-* Inflections can now be defined per locale. `singularize` and `pluralize`
- accept locale as an extra argument.
+ If you're using it to compare hashes for the purpose of testing, please use
+ MiniTest's `assert_equal` instead.
- *David Celis*
+ *Carlos Antonio da Silva*
-* `Object#try` will now return nil instead of raise a NoMethodError if the
- receiving object does not implement the method, but you can still get the
- old behavior by using the new `Object#try!`.
+* Remove deprecated `Date#to_time_in_current_zone` in favor of `Date#in_time_zone`.
- *DHH*
+ *Vipul A M*
+
+* Remove deprecated `Proc#bind` with no replacement.
-* `ERB::Util.html_escape` now escapes single quotes. *Santiago Pastorino*
+ *Carlos Antonio da Silva*
-* `Time#change` now works with time values with offsets other than UTC or the local time zone. *Andrew White*
+* Remove deprecated `Array#uniq_by` and `Array#uniq_by!`, use native
+ `Array#uniq` and `Array#uniq!` instead.
-* `ActiveSupport::Callbacks`: deprecate usage of filter object with `#before` and `#after` methods as `around` callback. *Bogdan Gusiev*
+ *Carlos Antonio da Silva*
-* Add `Time#prev_quarter` and `Time#next_quarter` short-hands for `months_ago(3)` and `months_since(3)`. *SungHee Kang*
+* Remove deprecated `ActiveSupport::BasicObject`, use `ActiveSupport::ProxyObject` instead.
-* Remove obsolete and unused `require_association` method from dependencies. *fxn*
+ *Carlos Antonio da Silva*
-* Add `:instance_accessor` option for `config_accessor`.
+* Remove deprecated `BufferedLogger`.
- class User
- include ActiveSupport::Configurable
- config_accessor :allowed_access, instance_accessor: false
- end
+ *Yves Senn*
- User.new.allowed_access = true # => NoMethodError
- User.new.allowed_access # => NoMethodError
+* Remove deprecated `assert_present` and `assert_blank` methods.
- *Francesco Rodriguez*
+ *Yves Senn*
-* ActionView::Helpers::NumberHelper methods have been moved to ActiveSupport::NumberHelper and are now available via
- Numeric#to_s. Numeric#to_s now accepts the formatting options :phone, :currency, :percentage, :delimited,
- :rounded, :human, and :human_size. *Andrew Mutz*
+* Fix return value from `BacktraceCleaner#noise` when the cleaner is configured
+ with multiple silencers.
-* Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys`, and `Hash#deep_transform_keys!`. *Mark McSpadden*
+ Fixes #11030
-* Changed xml type `datetime` to `dateTime` (with upper case letter `T`). *Angelo Capilleri*
+ *Mark J. Titorenko*
-* Add `:instance_accessor` option for `class_attribute`. *Alexey Vakhov*
+* `HashWithIndifferentAccess#select` now returns a `HashWithIndifferentAccess`
+ instance instead of a `Hash` instance.
-* `constantize` now looks in the ancestor chain. *Marc-Andre Lafortune & Andrew White*
+ Fixes #10723
-* Adds `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a +Hash+ instance into strings *Lucas Húngaro*
+ *Albert Llop*
-* Adds `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a +Hash+ instance into symbols *Lucas Húngaro*
+* Add `DateTime#usec` and `DateTime#nsec` so that `ActiveSupport::TimeWithZone` keeps
+ sub-second resolution when wrapping a `DateTime` value.
-* `Object#try` can't call private methods. *Vasiliy Ermolovich*
+ Fixes #10855
-* `AS::Callbacks#run_callbacks` remove `key` argument. *Francesco Rodriguez*
+ *Andrew White*
-* `deep_dup` works more expectedly now and duplicates also values in +Hash+ instances and elements in +Array+ instances. *Alexey Gaziev*
+* Fix `ActiveSupport::Dependencies::Loadable#load_dependency` calling
+ `#blame_file!` on Exceptions that do not have the Blamable mixin
-* Inflector no longer applies ice -> ouse to words like slice, police, ets *Wes Morgan*
+ *Andrew Kreiling*
-* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations *twinturbo*
+* Override `Time.at` to support the passing of Time-like values when called with a single argument.
-* Make Module#delegate stop using `send` - can no longer delegate to private methods. *dasch*
+ *Andrew White*
-* AS::Callbacks: deprecate `:rescuable` option. *Bogdan Gusiev*
+* Prevent side effects to hashes inside arrays when
+ `Hash#with_indifferent_access` is called.
-* Adds Integer#ordinal to get the ordinal suffix string of an integer. *Tim Gildea*
+ Fixes #10526
-* AS::Callbacks: `:per_key` option is no longer supported
+ *Yves Senn*
-* `AS::Callbacks#define_callbacks`: add `:skip_after_callbacks_if_terminated` option.
+* 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.
-* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it. *Carlos Antonio da Silva*
+ *Mike Dillon*
-* Remove ActiveSupport::TestCase#pending method, use `skip` instead. *Carlos Antonio da Silva*
+* Replace `multi_json` with `json`.
-* Deprecates the compatibility method Module#local_constant_names,
- use Module#local_constants instead (which returns symbols). *fxn*
+ Since Rails requires Ruby 1.9 and since Ruby 1.9 includes `json` in the standard library,
+ `multi_json` is no longer necessary.
-* Deletes the compatibility method Module#method_names,
- use Module#methods from now on (which returns symbols). *fxn*
+ *Erik Michaels-Ober*
-* Deletes the compatibility method Module#instance_method_names,
- use Module#instance_methods from now on (which returns symbols). *fxn*
+* 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.
-* BufferedLogger is deprecated. Use ActiveSupport::Logger, or the logger
- from Ruby stdlib.
+ *Mario Caropreso + Viktor Kelemen + zackham*
-* Unicode database updated to 6.1.0.
+* Fix skipping object callbacks using metadata fetched via callback chain
+ inspection methods (`_*_callbacks`)
-* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead
- of wrapping them in strings for safety.
+ *Sean Walbran*
-* Remove deprecated ActiveSupport::JSON::Variable. *Erich Menge*
+* 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)
+ end
-* Optimize log subscribers to check log level before doing any processing. *Brian Durand*
+ *Daniel Schierbeck*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/activesupport/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for previous changes.
diff --git a/activesupport/MIT-LICENSE b/activesupport/MIT-LICENSE
index c2bcf1d3e7..6aeeb7132d 100644
--- a/activesupport/MIT-LICENSE
+++ b/activesupport/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2005-2012 David Heinemeier Hansson
+Copyright (c) 2005-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/activesupport/README.rdoc b/activesupport/README.rdoc
index ed688ecc59..f3582767c0 100644
--- a/activesupport/README.rdoc
+++ b/activesupport/README.rdoc
@@ -12,7 +12,7 @@ The latest version of Active Support can be installed with RubyGems:
% [sudo] gem install activesupport
-Source code can be downloaded as part of the Rails project on GitHub
+Source code can be downloaded as part of the Rails project on GitHub:
* https://github.com/rails/rails/tree/master/activesupport
@@ -26,7 +26,7 @@ Active Support is released under the MIT license:
== Support
-API documentation is at
+API documentation is at:
* http://api.rubyonrails.org
diff --git a/activesupport/Rakefile b/activesupport/Rakefile
index 822c9d98ae..5ba153662a 100644
--- a/activesupport/Rakefile
+++ b/activesupport/Rakefile
@@ -9,22 +9,22 @@ Rake::TestTask.new do |t|
t.verbose = true
end
+
namespace :test do
- Rake::TestTask.new(:isolated) do |t|
- t.pattern = 'test/ts_isolated.rb'
+ task :isolated do
+ Dir.glob("test/**/*_test.rb").all? do |file|
+ sh(Gem.ruby, '-w', '-Ilib:test', file)
+ end or raise "Failures"
end
end
-# Create compressed packages
-dist_dirs = [ "lib", "test"]
-
spec = eval(File.read('activesupport.gemspec'))
Gem::PackageTask.new(spec) do |p|
p.gem_spec = spec
end
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/activesupport/activesupport.gemspec b/activesupport/activesupport.gemspec
index 30f4ded005..ffc2c2074e 100644
--- a/activesupport/activesupport.gemspec
+++ b/activesupport/activesupport.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -8,19 +8,21 @@ Gem::Specification.new do |s|
s.description = 'A toolkit of support libraries and Ruby core extensions extracted from the Rails framework. Rich support for multibyte strings, internationalization, time zones, and testing.'
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
s.files = Dir['CHANGELOG.md', 'MIT-LICENSE', 'README.rdoc', 'lib/**/*']
s.require_path = 'lib'
s.rdoc_options.concat ['--encoding', 'UTF-8']
- s.add_dependency('i18n', '~> 0.6')
- s.add_dependency('multi_json', '~> 1.3')
- s.add_dependency('tzinfo', '~> 0.3.33')
- s.add_dependency('minitest', '~> 4.1')
+ s.add_dependency('i18n', '~> 0.6', '>= 0.6.4')
+ s.add_dependency 'json', '~> 1.7'
+ s.add_dependency 'tzinfo', '~> 0.3.37'
+ s.add_dependency 'minitest', '~> 5.0'
+ s.add_dependency 'thread_safe','~> 0.1'
end
diff --git a/activesupport/bin/generate_tables b/activesupport/bin/generate_tables
index 5fefa429df..f39e89b7d0 100755
--- a/activesupport/bin/generate_tables
+++ b/activesupport/bin/generate_tables
@@ -28,12 +28,6 @@ module ActiveSupport
def initialize
@ucd = Unicode::UnicodeDatabase.new
-
- default = Codepoint.new
- default.combining_class = 0
- default.uppercase_mapping = 0
- default.lowercase_mapping = 0
- @ucd.codepoints = Hash.new(default)
end
def parse_codepoints(line)
diff --git a/activesupport/lib/active_support.rb b/activesupport/lib/active_support.rb
index 4e397ea110..5e1fe9e556 100644
--- a/activesupport/lib/active_support.rb
+++ b/activesupport/lib/active_support.rb
@@ -1,5 +1,5 @@
#--
-# Copyright (c) 2005-2012 David Heinemeier Hansson
+# Copyright (c) 2005-2013 David Heinemeier Hansson
#
# Permission is hereby granted, free of charge, to any person obtaining
# a copy of this software and associated documentation files (the
@@ -39,7 +39,7 @@ module ActiveSupport
eager_autoload do
autoload :BacktraceCleaner
- autoload :BasicObject
+ autoload :ProxyObject
autoload :Benchmarkable
autoload :Cache
autoload :Callbacks
diff --git a/activesupport/lib/active_support/all.rb b/activesupport/lib/active_support/all.rb
index f537818300..151008bbaa 100644
--- a/activesupport/lib/active_support/all.rb
+++ b/activesupport/lib/active_support/all.rb
@@ -1,3 +1,4 @@
require 'active_support'
+require 'active_support/deprecation'
require 'active_support/time'
require 'active_support/core_ext'
diff --git a/activesupport/lib/active_support/backtrace_cleaner.rb b/activesupport/lib/active_support/backtrace_cleaner.rb
index f1aff8a8e3..c88ae3e661 100644
--- a/activesupport/lib/active_support/backtrace_cleaner.rb
+++ b/activesupport/lib/active_support/backtrace_cleaner.rb
@@ -13,17 +13,17 @@ module ActiveSupport
# can focus on the rest.
#
# bc = BacktraceCleaner.new
- # bc.add_filter { |line| line.gsub(Rails.root, '') }
- # bc.add_silencer { |line| line =~ /mongrel|rubygems/ }
- # bc.clean(exception.backtrace) # will strip the Rails.root prefix and skip any lines from mongrel or rubygems
+ # bc.add_filter { |line| line.gsub(Rails.root, '') } # 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
#
# To reconfigure an existing BacktraceCleaner (like the default one in Rails)
# and show as much data as possible, you can always call
# <tt>BacktraceCleaner#remove_silencers!</tt>, which will restore the
# backtrace to a pristine state. If you need to reconfigure an existing
# BacktraceCleaner so that it does not filter or modify the paths of any lines
- # of the backtrace, you can call BacktraceCleaner#remove_filters! These two
- # methods will give you a completely untouched backtrace.
+ # of the backtrace, you can call <tt>BacktraceCleaner#remove_filters!<tt>
+ # These two methods will give you a completely untouched backtrace.
#
# Inspired by the Quiet Backtrace gem by Thoughtbot.
class BacktraceCleaner
@@ -72,6 +72,9 @@ module ActiveSupport
@silencers = []
end
+ # Removes all filters, but leaves in silencers. Useful if you suddenly
+ # need to see entire filepaths in the backtrace that you had already
+ # filtered out.
def remove_filters!
@filters = []
end
@@ -94,11 +97,7 @@ module ActiveSupport
end
def noise(backtrace)
- @silencers.each do |s|
- backtrace = backtrace.select { |line| s.call(line) }
- end
-
- backtrace
+ backtrace - silence(backtrace)
end
end
end
diff --git a/activesupport/lib/active_support/basic_object.rb b/activesupport/lib/active_support/basic_object.rb
deleted file mode 100644
index 6ccb0cd525..0000000000
--- a/activesupport/lib/active_support/basic_object.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-module ActiveSupport
- # A class with no predefined methods that behaves similarly to Builder's
- # BlankSlate. Used for proxy classes.
- class BasicObject < ::BasicObject
- undef_method :==
- undef_method :equal?
-
- # Let ActiveSupport::BasicObject at least raise exceptions.
- def raise(*args)
- ::Object.send(:raise, *args)
- end
- end
-end
diff --git a/activesupport/lib/active_support/benchmarkable.rb b/activesupport/lib/active_support/benchmarkable.rb
index 3d8bb13c49..805b7a714f 100644
--- a/activesupport/lib/active_support/benchmarkable.rb
+++ b/activesupport/lib/active_support/benchmarkable.rb
@@ -45,14 +45,5 @@ module ActiveSupport
yield
end
end
-
- # Silence the logger during the execution of the block.
- def silence
- ActiveSupport::Deprecation.warn "ActiveSupport::Benchmarkable#silence is deprecated. It will be removed from Rails 4.1."
- old_logger_level, logger.level = logger.level, ::Logger::ERROR if logger
- yield
- ensure
- logger.level = old_logger_level if logger
- end
end
end
diff --git a/activesupport/lib/active_support/buffered_logger.rb b/activesupport/lib/active_support/buffered_logger.rb
deleted file mode 100644
index 0595446189..0000000000
--- a/activesupport/lib/active_support/buffered_logger.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'active_support/deprecation'
-require 'active_support/logger'
-
-module ActiveSupport
- BufferedLogger = ActiveSupport::Deprecation::DeprecatedConstantProxy.new(
- 'BufferedLogger', '::ActiveSupport::Logger')
-end
diff --git a/activesupport/lib/active_support/cache.rb b/activesupport/lib/active_support/cache.rb
index 732ab4b7df..c5633d902f 100644
--- a/activesupport/lib/active_support/cache.rb
+++ b/activesupport/lib/active_support/cache.rb
@@ -3,7 +3,6 @@ require 'zlib'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/benchmark'
-require 'active_support/core_ext/exception'
require 'active_support/core_ext/class/attribute_accessors'
require 'active_support/core_ext/numeric/bytes'
require 'active_support/core_ext/numeric/time'
@@ -57,16 +56,7 @@ module ActiveSupport
case store
when Symbol
- store_class_name = store.to_s.camelize
- store_class =
- begin
- require "active_support/cache/#{store}"
- rescue LoadError => e
- raise "Could not find cache store adapter for #{store} (#{e})"
- else
- ActiveSupport::Cache.const_get(store_class_name)
- end
- store_class.new(*parameters)
+ retrieve_store_class(store).new(*parameters)
when nil
ActiveSupport::Cache::MemoryStore.new
else
@@ -74,6 +64,18 @@ module ActiveSupport
end
end
+ # Expands out the +key+ argument into a key that can be used for the
+ # cache store. Optionally accepts a namespace, and all keys will be
+ # scoped within that namespace.
+ #
+ # If the +key+ argument provided is an array, or responds to +to_a+, then
+ # each of elements in the array will be turned into parameters/keys and
+ # concatenated into a single key. For example:
+ #
+ # expand_cache_key([:foo, :bar]) # => "foo/bar"
+ # expand_cache_key([:foo, :bar], "namespace") # => "namespace/foo/bar"
+ #
+ # The +key+ argument can also respond to +cache_key+ or +to_param+.
def expand_cache_key(key, namespace = nil)
expanded_cache_key = namespace ? "#{namespace}/" : ""
@@ -95,6 +97,16 @@ module ActiveSupport
else key.to_param
end.to_s
end
+
+ # Obtains the specified cache store class, given the name of the +store+.
+ # Raises an error when the store class cannot be found.
+ def retrieve_store_class(store)
+ require "active_support/cache/#{store}"
+ rescue LoadError => e
+ raise "Could not find cache store adapter for #{store} (#{e})"
+ else
+ ActiveSupport::Cache.const_get(store.to_s.camelize)
+ end
end
# An abstract cache store class. There are multiple cache store
@@ -182,10 +194,10 @@ module ActiveSupport
# the cache with the given key, then that data is returned.
#
# If there is no such data in the cache (a cache miss), then +nil+ will be
- # returned. However, if a block has been passed, that block will be run
- # in the event of a cache miss. The return value of the block will be
- # written to the cache under the given cache key, and that return value
- # will be returned.
+ # returned. However, if a block has been passed, that block will be passed
+ # the key and executed in the event of a cache miss. The return value of the
+ # block will be written to the cache under the given cache key, and that
+ # return value will be returned.
#
# cache.write('today', 'Monday')
# cache.fetch('today') # => "Monday"
@@ -216,13 +228,13 @@ module ActiveSupport
#
# Setting <tt>:race_condition_ttl</tt> is very useful in situations where
# a cache entry is used very frequently and is under heavy load. If a
- # cache expires and due to heavy load seven different processes will try
+ # cache expires and due to heavy load several different processes will try
# to read data natively and then they all will try to write to cache. To
# avoid that case the first process to find an expired cache entry will
# bump the cache expiration time by the value set in <tt>:race_condition_ttl</tt>.
# Yes, this process is extending the time for a stale value by another few
# seconds. Because of extended life of the previous cache, other processes
- # will continue to use slightly stale data for a just a big longer. In the
+ # will continue to use slightly stale data for a just a bit longer. In the
# meantime that first process will go ahead and will write into cache the
# new value. After that all the processes will start getting new value.
# The key is to keep <tt>:race_condition_ttl</tt> small.
@@ -276,34 +288,14 @@ module ActiveSupport
if block_given?
options = merged_options(options)
key = namespaced_key(name, options)
- unless options[:force]
- entry = instrument(:read, name, options) do |payload|
- payload[:super_operation] = :fetch if payload
- read_entry(key, options)
- end
- end
- if entry && entry.expired?
- race_ttl = options[:race_condition_ttl].to_i
- if race_ttl && (Time.now - entry.expires_at <= race_ttl)
- # When an entry has :race_condition_ttl defined, put the stale entry back into the cache
- # for a brief period while the entry is begin recalculated.
- entry.expires_at = Time.now + race_ttl
- write_entry(key, entry, :expires_in => race_ttl * 2)
- else
- delete_entry(key, options)
- end
- entry = nil
- end
+
+ cached_entry = find_cached_entry(key, name, options) unless options[:force]
+ entry = handle_expired_entry(cached_entry, key, options)
if entry
- instrument(:fetch_hit, name, options) { |payload| }
- entry.value
+ get_entry_value(entry, name, options)
else
- result = instrument(:generate, name, options) do |payload|
- yield
- end
- write(name, result, options)
- result
+ save_block_result_to_cache(name, options) { |_name| yield _name }
end
else
read(name, options)
@@ -360,12 +352,40 @@ module ActiveSupport
results
end
+ # Fetches data from the cache, using the given keys. If there is data in
+ # the cache with the given keys, then that data is returned. Otherwise,
+ # the supplied block is called for each key for which there was no data,
+ # and the result will be written to the cache and returned.
+ #
+ # Options are passed to the underlying cache implementation.
+ #
+ # Returns an array with the data for each of the names. For example:
+ #
+ # cache.write("bim", "bam")
+ # cache.fetch_multi("bim", "boom") {|key| key * 2 }
+ # #=> ["bam", "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
+ value = yield name
+ write(name, value, options)
+ value
+ end
+ end
+ end
+
# Writes the value to the cache, with the key.
#
# Options are passed to the underlying cache implementation.
def write(name, value, options = nil)
options = merged_options(options)
- instrument(:write, name, options) do |payload|
+ instrument(:write, name, options) do
entry = Entry.new(value, options)
write_entry(namespaced_key(name, options), entry, options)
end
@@ -376,7 +396,7 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def delete(name, options = nil)
options = merged_options(options)
- instrument(:delete, name) do |payload|
+ instrument(:delete, name) do
delete_entry(namespaced_key(name, options), options)
end
end
@@ -386,9 +406,9 @@ module ActiveSupport
# Options are passed to the underlying cache implementation.
def exist?(name, options = nil)
options = merged_options(options)
- instrument(:exist?, name) do |payload|
+ instrument(:exist?, name) do
entry = read_entry(namespaced_key(name, options), options)
- entry && !entry.expired?
+ (entry && !entry.expired?) || false
end
end
@@ -532,6 +552,42 @@ module ActiveSupport
return unless logger && logger.debug? && !silence?
logger.debug("Cache #{operation}: #{key}#{options.blank? ? "" : " (#{options.inspect})"}")
end
+
+ def find_cached_entry(key, name, options)
+ instrument(:read, name, options) do |payload|
+ payload[:super_operation] = :fetch if payload
+ read_entry(key, options)
+ end
+ end
+
+ def handle_expired_entry(entry, key, options)
+ if entry && entry.expired?
+ race_ttl = options[:race_condition_ttl].to_i
+ if race_ttl && (Time.now.to_f - entry.expires_at <= race_ttl)
+ # When an entry has :race_condition_ttl defined, put the stale entry back into the cache
+ # for a brief period while the entry is begin recalculated.
+ entry.expires_at = Time.now + race_ttl
+ write_entry(key, entry, :expires_in => race_ttl * 2)
+ else
+ delete_entry(key, options)
+ end
+ entry = nil
+ end
+ entry
+ end
+
+ def get_entry_value(entry, name, options)
+ instrument(:fetch_hit, name, options) { |payload| }
+ entry.value
+ end
+
+ def save_block_result_to_cache(name, options)
+ result = instrument(:generate, name, options) do |payload|
+ yield(name)
+ end
+ write(name, result, options)
+ result
+ end
end
# This class is used to represent cache entries. Cache entries have a value and an optional
@@ -547,42 +603,42 @@ module ActiveSupport
# +:compress+, +:compress_threshold+, and +:expires_in+.
def initialize(value, options = {})
if should_compress?(value, options)
- @v = compress(value)
- @c = true
+ @value = compress(value)
+ @compressed = true
else
- @v = value
- end
- if expires_in = options[:expires_in]
- @x = (Time.now + expires_in).to_i
+ @value = value
end
+ @created_at = Time.now.to_f
+ @expires_in = options[:expires_in]
+ @expires_in = @expires_in.to_f if @expires_in
end
def value
- convert_version_3_entry! if defined?(@value)
- compressed? ? uncompress(@v) : @v
+ convert_version_4beta1_entry! if defined?(@v)
+ compressed? ? uncompress(@value) : @value
end
# Check if the entry is expired. The +expires_in+ parameter can override
# the value set when the entry was created.
def expired?
- convert_version_3_entry! if defined?(@value)
- if defined?(@x)
- @x && @x < Time.now.to_i
- else
- false
- end
+ convert_version_4beta1_entry! if defined?(@value)
+ @expires_in && @created_at + @expires_in <= Time.now.to_f
end
def expires_at
- Time.at(@x) if defined?(@x)
+ @expires_in ? @created_at + @expires_in : nil
end
def expires_at=(value)
- @x = value.to_i
+ if value
+ @expires_in = value.to_f - @created_at
+ else
+ @expires_in = nil
+ end
end
# Returns the size of the cached value. This could be less than
- # <tt>value.size</tt> if the data is compressed.
+ # <tt>value.size</tt> if the data is compressed.
def size
if defined?(@s)
@s
@@ -591,9 +647,9 @@ module ActiveSupport
when NilClass
0
when String
- @v.bytesize
+ @value.bytesize
else
- @s = Marshal.dump(@v).bytesize
+ @s = Marshal.dump(@value).bytesize
end
end
end
@@ -601,12 +657,12 @@ module ActiveSupport
# Duplicate the value in a class. This is used by cache implementations that don't natively
# serialize entries to protect against accidental cache modifications.
def dup_value!
- convert_version_3_entry! if defined?(@value)
- if @v && !compressed? && !(@v.is_a?(Numeric) || @v == true || @v == false)
- if @v.is_a?(String)
- @v = @v.dup
+ convert_version_4beta1_entry! if defined?(@v)
+ if @value && !compressed? && !(@value.is_a?(Numeric) || @value == true || @value == false)
+ if @value.is_a?(String)
+ @value = @value.dup
else
- @v = Marshal.load(Marshal.dump(@v))
+ @value = Marshal.load(Marshal.dump(@value))
end
end
end
@@ -622,7 +678,7 @@ module ActiveSupport
end
def compressed?
- defined?(@c) ? @c : false
+ defined?(@compressed) ? @compressed : false
end
def compress(value)
@@ -635,19 +691,19 @@ module ActiveSupport
# The internals of this method changed between Rails 3.x and 4.0. This method provides the glue
# to ensure that cache entries created under the old version still work with the new class definition.
- def convert_version_3_entry!
- if defined?(@value)
- @v = @value
- remove_instance_variable(:@value)
+ def convert_version_4beta1_entry!
+ if defined?(@v)
+ @value = @v
+ remove_instance_variable(:@v)
end
- if defined?(@compressed)
- @c = @compressed
- remove_instance_variable(:@compressed)
+ if defined?(@c)
+ @compressed = @c
+ remove_instance_variable(:@c)
end
- if defined?(@expires_in) && defined?(@created_at) && @expires_in && @created_at
- @x = (@created_at + @expires_in).to_i
- remove_instance_variable(:@created_at)
- remove_instance_variable(:@expires_in)
+ if defined?(@x) && @x
+ @created_at ||= Time.now.to_f
+ @expires_in = @x - @created_at
+ remove_instance_variable(:@x)
end
end
end
diff --git a/activesupport/lib/active_support/cache/file_store.rb b/activesupport/lib/active_support/cache/file_store.rb
index 2c1ad60d44..5cd6065077 100644
--- a/activesupport/lib/active_support/cache/file_store.rb
+++ b/activesupport/lib/active_support/cache/file_store.rb
@@ -1,3 +1,4 @@
+require 'active_support/core_ext/marshal'
require 'active_support/core_ext/file/atomic'
require 'active_support/core_ext/string/conversions'
require 'uri/common'
@@ -21,19 +22,26 @@ module ActiveSupport
extend Strategy::LocalCache
end
+ # Deletes all items from the cache. In this case it deletes all the entries in the specified
+ # file store directory except for .gitkeep. Be careful which directory is specified in your
+ # config file when using +FileStore+ because everything in that directory will be deleted.
def clear(options = nil)
root_dirs = Dir.entries(cache_path).reject {|f| (EXCLUDED_DIRS + [".gitkeep"]).include?(f)}
FileUtils.rm_r(root_dirs.collect{|f| File.join(cache_path, f)})
end
+ # Premptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
- each_key(options) do |key|
+ search_dir(cache_path) do |fname|
+ key = file_path_key(fname)
entry = read_entry(key, options)
delete_entry(key, options) if entry && entry.expired?
end
end
+ # Increments an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def increment(name, amount = 1, options = nil)
file_name = key_file_path(namespaced_key(name, options))
lock_file(file_name) do
@@ -48,6 +56,8 @@ module ActiveSupport
end
end
+ # Decrements an already existing integer value that is stored in the cache.
+ # If the key is not found nothing is done.
def decrement(name, amount = 1, options = nil)
file_name = key_file_path(namespaced_key(name, options))
lock_file(file_name) do
@@ -87,6 +97,7 @@ module ActiveSupport
def write_entry(key, entry, options)
file_name = key_file_path(key)
+ return false if options[:unless_exist] && File.exist?(file_name)
ensure_cache_path(File.dirname(file_name))
File.atomic_write(file_name, cache_path) {|f| Marshal.dump(entry, f)}
true
@@ -149,9 +160,9 @@ module ActiveSupport
# Delete empty directories in the cache.
def delete_empty_directories(dir)
- return if dir == cache_path
+ return if File.realpath(dir) == File.realpath(cache_path)
if Dir.entries(dir).reject {|f| EXCLUDED_DIRS.include?(f)}.empty?
- File.delete(dir) rescue nil
+ Dir.delete(dir) rescue nil
delete_empty_directories(File.dirname(dir))
end
end
diff --git a/activesupport/lib/active_support/cache/mem_cache_store.rb b/activesupport/lib/active_support/cache/mem_cache_store.rb
index 17450fe4d0..512296554f 100644
--- a/activesupport/lib/active_support/cache/mem_cache_store.rb
+++ b/activesupport/lib/active_support/cache/mem_cache_store.rb
@@ -6,6 +6,8 @@ rescue LoadError => e
end
require 'digest/md5'
+require 'active_support/core_ext/marshal'
+require 'active_support/core_ext/array/extract_options'
module ActiveSupport
module Cache
@@ -157,7 +159,7 @@ module ActiveSupport
# characters properly.
def escape_key(key)
key = key.to_s.dup
- key = key.force_encoding("BINARY")
+ key = key.force_encoding(Encoding::ASCII_8BIT)
key = key.gsub(ESCAPE_KEY_CHARS){ |match| "%#{match.getbyte(0).to_s(16).upcase}" }
key = "#{key[0, 213]}:md5:#{Digest::MD5.hexdigest(key)}" if key.size > 250
key
diff --git a/activesupport/lib/active_support/cache/memory_store.rb b/activesupport/lib/active_support/cache/memory_store.rb
index 4d26fb7e42..34ac91334a 100644
--- a/activesupport/lib/active_support/cache/memory_store.rb
+++ b/activesupport/lib/active_support/cache/memory_store.rb
@@ -36,6 +36,7 @@ module ActiveSupport
end
end
+ # Premptively iterates through all stored keys and removes the ones which have expired.
def cleanup(options = nil)
options = merged_options(options)
instrument(:cleanup, :size => @data.size) do
@@ -122,6 +123,13 @@ module ActiveSupport
end
protected
+
+ PER_ENTRY_OVERHEAD = 240
+
+ def cached_size(key, entry)
+ key.to_s.bytesize + entry.size + PER_ENTRY_OVERHEAD
+ end
+
def read_entry(key, options) # :nodoc:
entry = @data[key]
synchronize do
@@ -139,8 +147,11 @@ module ActiveSupport
synchronize do
old_entry = @data[key]
return false if @data.key?(key) && options[:unless_exist]
- @cache_size -= old_entry.size if old_entry
- @cache_size += entry.size
+ if old_entry
+ @cache_size -= (old_entry.size - entry.size)
+ else
+ @cache_size += cached_size(key, entry)
+ end
@key_access[key] = Time.now.to_f
@data[key] = entry
prune(@max_size * 0.75, @max_prune_time) if @cache_size > @max_size
@@ -152,7 +163,7 @@ module ActiveSupport
synchronize do
@key_access.delete(key)
entry = @data.delete(key)
- @cache_size -= entry.size if entry
+ @cache_size -= cached_size(key, entry) if entry
!!entry
end
end
diff --git a/activesupport/lib/active_support/cache/strategy/local_cache.rb b/activesupport/lib/active_support/cache/strategy/local_cache.rb
index db5f228a70..fb42c4a41e 100644
--- a/activesupport/lib/active_support/cache/strategy/local_cache.rb
+++ b/activesupport/lib/active_support/cache/strategy/local_cache.rb
@@ -8,6 +8,23 @@ 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
+ # Class for storing and registering the local caches.
+ class LocalCacheRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ def initialize
+ @registry = {}
+ end
+
+ def cache_for(local_cache_key)
+ @registry[local_cache_key]
+ end
+
+ def set_cache_for(local_cache_key, value)
+ @registry[local_cache_key] = value
+ end
+ end
+
# Simple memory backed cache. This cache is not thread safe and is intended only
# for serving as a temporary memory cache for a single thread.
class LocalStore < Store
@@ -41,24 +58,18 @@ module ActiveSupport
# Use a local cache for the duration of block.
def with_local_cache
- save_val = Thread.current[thread_local_key]
- begin
- Thread.current[thread_local_key] = LocalStore.new
- yield
- ensure
- Thread.current[thread_local_key] = save_val
- end
+ 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, :thread_local_key
+ attr_reader :name, :local_cache_key
- def initialize(name, thread_local_key)
+ def initialize(name, local_cache_key)
@name = name
- @thread_local_key = thread_local_key
+ @local_cache_key = local_cache_key
@app = nil
end
@@ -68,10 +79,10 @@ module ActiveSupport
end
def call(env)
- Thread.current[thread_local_key] = LocalStore.new
+ LocalCacheRegistry.set_cache_for(local_cache_key, LocalStore.new)
@app.call(env)
ensure
- Thread.current[thread_local_key] = nil
+ LocalCacheRegistry.set_cache_for(local_cache_key, nil)
end
end
@@ -80,7 +91,7 @@ module ActiveSupport
def middleware
@middleware ||= Middleware.new(
"ActiveSupport::Cache::Strategy::LocalCache",
- thread_local_key)
+ local_cache_key)
end
def clear(options = nil) # :nodoc:
@@ -95,29 +106,13 @@ module ActiveSupport
def increment(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- if local_cache
- local_cache.mute do
- if value
- local_cache.write(name, value, options)
- else
- local_cache.delete(name, options)
- end
- end
- end
+ increment_or_decrement(value, name, amount, options)
value
end
def decrement(name, amount = 1, options = nil) # :nodoc:
value = bypass_local_cache{super}
- if local_cache
- local_cache.mute do
- if value
- local_cache.write(name, value, options)
- else
- local_cache.delete(name, options)
- end
- end
- end
+ increment_or_decrement(value, name, amount, options)
value
end
@@ -146,21 +141,37 @@ module ActiveSupport
end
private
- def thread_local_key
- @thread_local_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
+ def increment_or_decrement(value, name, amount, options)
+ if local_cache
+ local_cache.mute do
+ if value
+ local_cache.write(name, value, options)
+ else
+ local_cache.delete(name, options)
+ end
+ end
+ end
+ end
+
+ def local_cache_key
+ @local_cache_key ||= "#{self.class.name.underscore}_local_cache_#{object_id}".gsub(/[\/-]/, '_').to_sym
end
def local_cache
- Thread.current[thread_local_key]
+ LocalCacheRegistry.cache_for(local_cache_key)
end
def bypass_local_cache
- save_cache = Thread.current[thread_local_key]
+ use_temporary_local_cache(nil) { yield }
+ end
+
+ def use_temporary_local_cache(temporary_cache)
+ save_cache = LocalCacheRegistry.cache_for(local_cache_key)
begin
- Thread.current[thread_local_key] = nil
+ LocalCacheRegistry.set_cache_for(local_cache_key, temporary_cache)
yield
ensure
- Thread.current[thread_local_key] = save_cache
+ LocalCacheRegistry.set_cache_for(local_cache_key, save_cache)
end
end
end
diff --git a/activesupport/lib/active_support/callbacks.rb b/activesupport/lib/active_support/callbacks.rb
index a02793bde9..c3aac31323 100644
--- a/activesupport/lib/active_support/callbacks.rb
+++ b/activesupport/lib/active_support/callbacks.rb
@@ -1,8 +1,10 @@
require 'active_support/concern'
require 'active_support/descendants_tracker'
+require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/kernel/singleton_class'
+require 'thread'
module ActiveSupport
# Callbacks are code hooks that are run at key points in an object's lifecycle.
@@ -14,7 +16,7 @@ module ActiveSupport
# Mixing in this module allows you to define the events in the object's
# lifecycle that will support callbacks (via +ClassMethods.define_callbacks+),
# set the instance methods, procs, or callback objects to be called (via
- # +ClassMethods.set_callback+), and run the installed callbacks at the
+ # +ClassMethods.set_callback+), and run the installed callbacks at the
# appropriate times (via +run_callbacks+).
#
# Three kinds of callbacks are supported: before callbacks, run before a
@@ -60,6 +62,8 @@ module ActiveSupport
extend ActiveSupport::DescendantsTracker
end
+ CALLBACK_FILTER_TYPES = [:before, :after, :around]
+
# Runs the callbacks for the given event.
#
# Calls the before and around callbacks in the order they were set, yields
@@ -73,8 +77,14 @@ module ActiveSupport
# save
# end
def run_callbacks(kind, &block)
- runner_name = self.class.__define_callbacks(kind, self)
- send(runner_name, &block)
+ cbs = send("_#{kind}_callbacks")
+ if cbs.empty?
+ yield if block_given?
+ else
+ runner = cbs.compile
+ e = Filters::Environment.new(self, false, nil, block)
+ runner.call(e).value
+ end
end
private
@@ -85,155 +95,315 @@ module ActiveSupport
def halted_callback_hook(filter)
end
- class Callback #:nodoc:#
- @@_callback_sequence = 0
-
- attr_accessor :chain, :filter, :kind, :options, :klass, :raw_filter
+ module Conditionals # :nodoc:
+ class Value
+ def initialize(&block)
+ @block = block
+ end
+ def call(target, value); @block.call(value); end
+ end
+ end
- def initialize(chain, filter, kind, options, klass)
- @chain, @kind, @klass = chain, kind, klass
- deprecate_per_key_option(options)
- normalize_options!(options)
+ module Filters
+ Environment = Struct.new(:target, :halted, :value, :run_block)
- @raw_filter, @options = filter, options
- @filter = _compile_filter(filter)
- recompile_options!
+ class End
+ def call(env)
+ block = env.run_block
+ env.value = !env.halted && (!block || block.call)
+ env
+ end
end
+ ENDING = End.new
+
+ class Before
+ def self.build(next_callback, user_callback, user_conditions, chain_config, filter)
+ halted_lambda = chain_config[:terminator]
+
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ elsif chain_config.key? :terminator
+ halting(next_callback, user_callback, halted_lambda, filter)
+ elsif user_conditions.any?
+ conditional(next_callback, user_callback, user_conditions)
+ else
+ simple next_callback, user_callback
+ end
+ end
+
+ private
- def deprecate_per_key_option(options)
- if options[:per_key]
- raise NotImplementedError, ":per_key option is no longer supported. Use generic :if and :unless options instead."
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions, halted_lambda, filter)
+ lambda { |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ result = user_callback.call target, value
+ env.halted = halted_lambda.call(target, result)
+ if env.halted
+ target.send :halted_callback_hook, filter
+ end
+ end
+ next_callback.call env
+ }
end
- end
- def clone(chain, klass)
- obj = super()
- obj.chain = chain
- obj.klass = klass
- obj.options = @options.dup
- obj.options[:if] = @options[:if].dup
- obj.options[:unless] = @options[:unless].dup
- obj
+ def self.halting(next_callback, user_callback, halted_lambda, filter)
+ lambda { |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ unless halted
+ result = user_callback.call target, value
+ env.halted = halted_lambda.call(target, result)
+ if env.halted
+ target.send :halted_callback_hook, filter
+ end
+ end
+ next_callback.call env
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+ next_callback.call env
+ }
+ end
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ user_callback.call env.target, env.value
+ next_callback.call env
+ }
+ end
end
- def normalize_options!(options)
- options[:if] = Array(options[:if])
- options[:unless] = Array(options[:unless])
+ class After
+ def self.build(next_callback, user_callback, user_conditions, chain_config)
+ if chain_config[:skip_after_callbacks_if_terminated]
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halting_and_conditional(next_callback, user_callback, user_conditions)
+ elsif chain_config.key?(:terminator)
+ halting(next_callback, user_callback)
+ elsif user_conditions.any?
+ conditional next_callback, user_callback, user_conditions
+ else
+ simple next_callback, user_callback
+ end
+ else
+ if user_conditions.any?
+ conditional next_callback, user_callback, user_conditions
+ else
+ simple next_callback, user_callback
+ end
+ end
+ end
+
+ private
+
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ env = next_callback.call env
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+ env
+ }
+ end
+
+ def self.halting(next_callback, user_callback)
+ lambda { |env|
+ env = next_callback.call env
+ unless env.halted
+ user_callback.call env.target, env.value
+ end
+ env
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ env = next_callback.call env
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call target, value
+ end
+ env
+ }
+ end
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ env = next_callback.call env
+ user_callback.call env.target, env.value
+ env
+ }
+ end
end
- def name
- chain.name
+ class Around
+ def self.build(next_callback, user_callback, user_conditions, chain_config)
+ if chain_config.key?(:terminator) && user_conditions.any?
+ halting_and_conditional(next_callback, user_callback, user_conditions)
+ elsif chain_config.key? :terminator
+ halting(next_callback, user_callback)
+ elsif user_conditions.any?
+ conditional(next_callback, user_callback, user_conditions)
+ else
+ simple(next_callback, user_callback)
+ end
+ end
+
+ private
+
+ def self.halting_and_conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ target = env.target
+ value = env.value
+ halted = env.halted
+
+ if !halted && user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call(target, value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ else
+ next_callback.call env
+ end
+ }
+ end
+
+ def self.halting(next_callback, user_callback)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ unless env.halted
+ user_callback.call(target, value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ else
+ next_callback.call env
+ end
+ }
+ end
+
+ def self.conditional(next_callback, user_callback, user_conditions)
+ lambda { |env|
+ target = env.target
+ value = env.value
+
+ if user_conditions.all? { |c| c.call(target, value) }
+ user_callback.call(target, value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ else
+ next_callback.call env
+ end
+ }
+ end
+
+ def self.simple(next_callback, user_callback)
+ lambda { |env|
+ user_callback.call(env.target, env.value) {
+ env = next_callback.call env
+ env.value
+ }
+ env
+ }
+ end
end
+ end
- def next_id
- @@_callback_sequence += 1
+ class Callback #:nodoc:#
+ def self.build(chain, filter, kind, options)
+ new chain.name, filter, kind, options, chain.config
end
- def matches?(_kind, _filter)
- @kind == _kind && @filter == _filter
+ attr_accessor :kind, :name
+ attr_reader :chain_config
+
+ def initialize(name, filter, kind, options, chain_config)
+ @chain_config = chain_config
+ @name = name
+ @kind = kind
+ @filter = filter
+ @key = compute_identifier filter
+ @if = Array(options[:if])
+ @unless = Array(options[:unless])
end
- def _update_filter(filter_options, new_options)
- filter_options[:if].concat(Array(new_options[:unless])) if new_options.key?(:unless)
- filter_options[:unless].concat(Array(new_options[:if])) if new_options.key?(:if)
+ def filter; @key; end
+ def raw_filter; @filter; end
+
+ def merge(chain, new_options)
+ options = {
+ :if => @if.dup,
+ :unless => @unless.dup
+ }
+
+ options[:if].concat Array(new_options.fetch(:unless, []))
+ options[:unless].concat Array(new_options.fetch(:if, []))
+
+ self.class.build chain, @filter, @kind, options
end
- def recompile!(_options)
- deprecate_per_key_option(_options)
- _update_filter(self.options, _options)
+ def matches?(_kind, _filter)
+ @kind == _kind && filter == _filter
+ end
- recompile_options!
+ def duplicates?(other)
+ case @filter
+ when Symbol, String
+ matches?(other.kind, other.filter)
+ else
+ false
+ end
end
# Wraps code with filter
- def apply(code)
- case @kind
+ def apply(next_callback)
+ user_conditions = conditions_lambdas
+ user_callback = make_lambda @filter
+
+ case kind
when :before
- <<-RUBY_EVAL
- if !halted && #{@compiled_options}
- # This double assignment is to prevent warnings in 1.9.3 as
- # the `result` variable is not always used except if the
- # terminator code refers to it.
- result = result = #{@filter}
- halted = (#{chain.config[:terminator]})
- if halted
- halted_callback_hook(#{@raw_filter.inspect.inspect})
- end
- end
- #{code}
- RUBY_EVAL
+ Filters::Before.build(next_callback, user_callback, user_conditions, chain_config, @filter)
when :after
- <<-RUBY_EVAL
- #{code}
- if #{!chain.config[:skip_after_callbacks_if_terminated] || "!halted"} && #{@compiled_options}
- #{@filter}
- end
- RUBY_EVAL
+ Filters::After.build(next_callback, user_callback, user_conditions, chain_config)
when :around
- name = define_conditional_callback
- <<-RUBY_EVAL
- #{name}(halted) do
- #{code}
- value
- end
- RUBY_EVAL
+ Filters::Around.build(next_callback, user_callback, user_conditions, chain_config)
end
end
private
- # Compile around filters with conditions into proxy methods
- # that contain the conditions.
- #
- # For `set_callback :save, :around, :filter_name, if: :condition':
- #
- # def _conditional_callback_save_17
- # if condition
- # filter_name do
- # yield self
- # end
- # else
- # yield self
- # end
- # end
- def define_conditional_callback
- name = "_conditional_callback_#{@kind}_#{next_id}"
- @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{name}(halted)
- if #{@compiled_options} && !halted
- #{@filter} do
- yield self
- end
- else
- yield self
- end
- end
- RUBY_EVAL
- name
- end
-
- # Options support the same options as filters themselves (and support
- # symbols, string, procs, and objects), so compile a conditional
- # expression based on the options.
- def recompile_options!
- conditions = ["true"]
-
- unless options[:if].empty?
- conditions << Array(_compile_filter(options[:if]))
- end
-
- unless options[:unless].empty?
- conditions << Array(_compile_filter(options[:unless])).map {|f| "!#{f}"}
- end
-
- @compiled_options = conditions.flatten.join(" && ")
+ def invert_lambda(l)
+ lambda { |*args, &blk| !l.call(*args, &blk) }
end
# Filters support:
#
- # Arrays:: Used in conditions. This is used to specify
- # multiple conditions. Used internally to
- # merge conditions from skip_* filters.
# Symbols:: A method to call.
# Strings:: Some content to evaluate.
# Procs:: A proc to call with the object.
@@ -242,127 +412,153 @@ module ActiveSupport
# All of these objects are compiled into methods and handled
# the same after this point:
#
- # Arrays:: Merged together into a single filter.
# Symbols:: Already methods.
- # Strings:: class_eval'ed into methods.
- # Procs:: define_method'ed into 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.
- def _compile_filter(filter)
- method_name = "_callback_#{@kind}_#{next_id}"
+ def make_lambda(filter)
case filter
- when Array
- filter.map {|f| _compile_filter(f)}
when Symbol
- filter
+ lambda { |target, _, &blk| target.send filter, &blk }
when String
- "(#{filter})"
- when Proc
- @klass.send(:define_method, method_name, &filter)
- return method_name if filter.arity <= 0
+ l = eval "lambda { |value| #{filter} }"
+ lambda { |target, value| target.instance_exec(value, &l) }
+ when Conditionals::Value then filter
+ when ::Proc
+ if filter.arity > 1
+ return lambda { |target, _, &block|
+ raise ArgumentError unless block
+ target.instance_exec(target, block, &filter)
+ }
+ end
- method_name << (filter.arity == 1 ? "(self)" : " self, Proc.new ")
+ if filter.arity <= 0
+ lambda { |target, _| target.instance_exec(&filter) }
+ else
+ lambda { |target, _| target.instance_exec(target, &filter) }
+ end
else
- @klass.send(:define_method, "#{method_name}_object") { filter }
+ scopes = Array(chain_config[:scope])
+ method_to_call = scopes.map{ |s| public_send(s) }.join("_")
- _normalize_legacy_filter(kind, filter)
- scopes = Array(chain.config[:scope])
- method_to_call = scopes.map{ |s| s.is_a?(Symbol) ? send(s) : s }.join("_")
-
- @klass.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{method_name}(&blk)
- #{method_name}_object.send(:#{method_to_call}, self, &blk)
- end
- RUBY_EVAL
-
- method_name
+ lambda { |target, _, &blk|
+ filter.public_send method_to_call, target, &blk
+ }
end
end
- def _normalize_legacy_filter(kind, filter)
- if !filter.respond_to?(kind) && filter.respond_to?(:filter)
- ActiveSupport::Deprecation.warn("Filter object with #filter method is deprecated. Define method corresponding to filter type (#before, #after or #around).")
- filter.singleton_class.class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{kind}(context, &block) filter(context, &block) end
- RUBY_EVAL
- elsif filter.respond_to?(:before) && filter.respond_to?(:after) && kind == :around && !filter.respond_to?(:around)
- ActiveSupport::Deprecation.warn("Filter object with #before and #after methods is deprecated. Define #around method instead.")
- def filter.around(context)
- should_continue = before(context)
- yield if should_continue
- after(context)
- end
+ def compute_identifier(filter)
+ case filter
+ when String, ::Proc
+ filter.object_id
+ else
+ filter
end
end
+
+ def conditions_lambdas
+ @if.map { |c| make_lambda c } +
+ @unless.map { |c| invert_lambda make_lambda c }
+ end
end
# An Array with a compile method.
- class CallbackChain < Array #:nodoc:#
+ class CallbackChain #:nodoc:#
+ include Enumerable
+
attr_reader :name, :config
def initialize(name, config)
@name = name
@config = {
- :terminator => "false",
:scope => [ :kind ]
- }.merge(config)
+ }.merge!(config)
+ @chain = []
+ @callbacks = nil
+ @mutex = Mutex.new
end
- def compile
- method = []
- method << "value = nil"
- method << "halted = false"
+ def each(&block); @chain.each(&block); end
+ def index(o); @chain.index(o); end
+ def empty?; @chain.empty?; end
- callbacks = "value = !halted && (!block_given? || yield)"
- reverse_each do |callback|
- callbacks = callback.apply(callbacks)
- end
- method << callbacks
+ def insert(index, o)
+ @callbacks = nil
+ @chain.insert(index, o)
+ end
- method << "value"
- method.join("\n")
+ def delete(o)
+ @callbacks = nil
+ @chain.delete(o)
end
- end
+ def clear
+ @callbacks = nil
+ @chain.clear
+ self
+ end
- module ClassMethods
+ def initialize_copy(other)
+ @callbacks = nil
+ @chain = other.chain.dup
+ @mutex = Mutex.new
+ end
- # This method defines callback chain method for the given kind
- # if it was not yet defined.
- # This generated method plays caching role.
- def __define_callbacks(kind, object) #:nodoc:
- name = __callback_runner_name(kind)
- unless object.respond_to?(name, true)
- str = object.send("_#{kind}_callbacks").compile
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def #{name}() #{str} end
- protected :#{name}
- RUBY_EVAL
+ def compile
+ @callbacks || @mutex.synchronize do
+ @callbacks ||= @chain.reverse.inject(Filters::ENDING) do |chain, callback|
+ callback.apply chain
+ end
end
- name
end
- def __reset_runner(symbol)
- name = __callback_runner_name(symbol)
- undef_method(name) if method_defined?(name)
+ def append(*callbacks)
+ callbacks.each { |c| append_one(c) }
end
- def __callback_runner_name(kind)
- "_run__#{self.name.hash.abs}__#{kind}__callbacks"
+ def prepend(*callbacks)
+ callbacks.each { |c| prepend_one(c) }
end
- # This is used internally to append, prepend and skip callbacks to the
- # CallbackChain.
- def __update_callbacks(name, filters = [], block = nil) #:nodoc:
- type = [:before, :after, :around].include?(filters.first) ? filters.shift : :before
- options = filters.last.is_a?(Hash) ? filters.pop : {}
+ protected
+ def chain; @chain; end
+
+ private
+
+ def append_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.push(callback)
+ end
+
+ def prepend_one(callback)
+ @callbacks = nil
+ remove_duplicates(callback)
+ @chain.unshift(callback)
+ end
+
+ def remove_duplicates(callback)
+ @callbacks = nil
+ @chain.delete_if { |c| callback.duplicates?(c) }
+ end
+ end
+
+ module ClassMethods
+ def normalize_callback_params(filters, block) # :nodoc:
+ type = CALLBACK_FILTER_TYPES.include?(filters.first) ? filters.shift : :before
+ options = filters.extract_options!
filters.unshift(block) if block
+ [type, filters, options.dup]
+ end
+ # This is used internally to append, prepend and skip callbacks to the
+ # CallbackChain.
+ def __update_callbacks(name) #:nodoc:
([self] + ActiveSupport::DescendantsTracker.descendants(self)).reverse.each do |target|
- chain = target.send("_#{name}_callbacks")
- yield target, chain.dup, type, filters, options
- target.__reset_runner(name)
+ chain = target.get_callbacks name
+ yield target, chain.dup
end
end
@@ -378,8 +574,8 @@ module ActiveSupport
#
# set_callback :save, :before_meth
#
- # The callback can specified as a symbol naming an instance method; as a
- # proc, lambda, or block; as a string to be instance evaluated; or as an
+ # The callback can be specified as a symbol naming an instance method; as a
+ # proc, lambda, or block; as a string to be instance evaluated; or as an
# object that responds to a certain method determined by the <tt>:scope</tt>
# argument to +define_callback+.
#
@@ -402,20 +598,15 @@ module ActiveSupport
# * <tt>:prepend</tt> - If +true+, the callback will be prepended to the
# existing chain rather than appended.
def set_callback(name, *filter_list, &block)
- mapped = nil
-
- __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
- mapped ||= filters.map do |filter|
- Callback.new(chain, filter, type, options.dup, self)
- end
-
- filters.each do |filter|
- chain.delete_if {|c| c.matches?(type, filter) }
- end
-
- options[:prepend] ? chain.unshift(*(mapped.reverse)) : chain.push(*mapped)
+ type, filters, options = normalize_callback_params(filter_list, block)
+ self_chain = get_callbacks name
+ mapped = filters.map do |filter|
+ Callback.build(self_chain, filter, type, options)
+ end
- target.send("_#{name}_callbacks=", chain)
+ __update_callbacks(name) do |target, chain|
+ options[:prepend] ? chain.prepend(*mapped) : chain.append(*mapped)
+ target.set_callbacks name, chain
end
end
@@ -427,36 +618,34 @@ module ActiveSupport
# skip_callback :validate, :before, :check_membership, if: -> { self.age > 18 }
# end
def skip_callback(name, *filter_list, &block)
- __update_callbacks(name, filter_list, block) do |target, chain, type, filters, options|
+ type, filters, options = normalize_callback_params(filter_list, block)
+
+ __update_callbacks(name) do |target, chain|
filters.each do |filter|
filter = chain.find {|c| c.matches?(type, filter) }
if filter && options.any?
- new_filter = filter.clone(chain, self)
+ new_filter = filter.merge(chain, options)
chain.insert(chain.index(filter), new_filter)
- new_filter.recompile!(options)
end
chain.delete(filter)
end
- target.send("_#{name}_callbacks=", chain)
+ target.set_callbacks name, chain
end
end
# Remove all set callbacks for the given event.
- def reset_callbacks(symbol)
- callbacks = send("_#{symbol}_callbacks")
+ def reset_callbacks(name)
+ callbacks = get_callbacks name
ActiveSupport::DescendantsTracker.descendants(self).each do |target|
- chain = target.send("_#{symbol}_callbacks").dup
+ chain = target.get_callbacks(name).dup
callbacks.each { |c| chain.delete(c) }
- target.send("_#{symbol}_callbacks=", chain)
- target.__reset_runner(symbol)
+ target.set_callbacks name, chain
end
- self.send("_#{symbol}_callbacks=", callbacks.dup.clear)
-
- __reset_runner(symbol)
+ self.set_callbacks name, callbacks.dup.clear
end
# Define sets of events in the object lifecycle that support callbacks.
@@ -468,10 +657,11 @@ module ActiveSupport
#
# * <tt>:terminator</tt> - Determines when a before filter will halt the
# callback chain, preventing following callbacks from being called and
- # the event from being triggered. This is a string to be eval'ed. The
- # result of the callback is available in the +result+ variable.
+ # the event from being triggered. This should be a lambda to be executed.
+ # The current object and the return result of the callback will be called
+ # with the lambda.
#
- # define_callbacks :validate, terminator: 'result == false'
+ # define_callbacks :validate, terminator: ->(target, result) { result == false }
#
# In this example, if any before validate callbacks returns +false+,
# other callbacks are not executed. Defaults to +false+, meaning no value
@@ -526,13 +716,30 @@ module ActiveSupport
# define_callbacks :save, scope: [:name]
#
# would call <tt>Audit#save</tt>.
- def define_callbacks(*callbacks)
- config = callbacks.last.is_a?(Hash) ? callbacks.pop : {}
- callbacks.each do |callback|
- class_attribute "_#{callback}_callbacks"
- send("_#{callback}_callbacks=", CallbackChain.new(callback, config))
+ 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"
+ set_callbacks name, CallbackChain.new(name, options)
end
end
+
+ protected
+
+ def get_callbacks(name)
+ send "_#{name}_callbacks"
+ end
+
+ def set_callbacks(name, callbacks)
+ send "_#{name}_callbacks=", callbacks
+ end
end
end
end
diff --git a/activesupport/lib/active_support/concern.rb b/activesupport/lib/active_support/concern.rb
index fb7065ef3b..b796d01dfd 100644
--- a/activesupport/lib/active_support/concern.rb
+++ b/activesupport/lib/active_support/concern.rb
@@ -4,7 +4,9 @@ module ActiveSupport
# module M
# def self.included(base)
# base.extend ClassMethods
- # scope :disabled, -> { where(disabled: true) }
+ # base.class_eval do
+ # scope :disabled, -> { where(disabled: true) }
+ # end
# end
#
# module ClassMethods
@@ -77,10 +79,8 @@ module ActiveSupport
# module Foo
# extend ActiveSupport::Concern
# included do
- # class_eval do
- # def self.method_injected_by_foo
- # ...
- # end
+ # def self.method_injected_by_foo
+ # ...
# end
# end
# end
@@ -98,25 +98,33 @@ module ActiveSupport
# include Bar # works, Bar takes care now of its dependencies
# end
module Concern
+ class MultipleIncludedBlocks < StandardError #:nodoc:
+ def initialize
+ super "Cannot define multiple 'included' blocks for a Concern"
+ end
+ end
+
def self.extended(base) #:nodoc:
- base.instance_variable_set("@_dependencies", [])
+ base.instance_variable_set(:@_dependencies, [])
end
def append_features(base)
- if base.instance_variable_defined?("@_dependencies")
- base.instance_variable_get("@_dependencies") << self
+ if base.instance_variable_defined?(:@_dependencies)
+ base.instance_variable_get(:@_dependencies) << self
return false
else
return false if base < self
@_dependencies.each { |dep| base.send(:include, dep) }
super
- base.extend const_get("ClassMethods") if const_defined?("ClassMethods")
- base.class_eval(&@_included_block) if instance_variable_defined?("@_included_block")
+ base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
+ base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
end
end
def included(base = nil, &block)
if base.nil?
+ raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
+
@_included_block = block
else
super
diff --git a/activesupport/lib/active_support/configurable.rb b/activesupport/lib/active_support/configurable.rb
index 16d2a6a290..e0d39d509f 100644
--- a/activesupport/lib/active_support/configurable.rb
+++ b/activesupport/lib/active_support/configurable.rb
@@ -38,7 +38,7 @@ module ActiveSupport
end
# Allows you to add shortcut so that you don't have to refer to attribute
- # through config. Also look at the example for config to contrast.
+ # through config. Also look at the example for config to contrast.
#
# Defines both class and instance config accessors.
#
@@ -75,7 +75,7 @@ module ActiveSupport
# end
#
# User.allowed_access = false
- #  User.allowed_access # => false
+ # User.allowed_access # => false
#
# User.new.allowed_access = true # => NoMethodError
# User.new.allowed_access # => NoMethodError
@@ -88,7 +88,7 @@ module ActiveSupport
# end
#
# User.allowed_access = false
- #  User.allowed_access # => false
+ # User.allowed_access # => false
#
# User.new.allowed_access = true # => NoMethodError
# User.new.allowed_access # => NoMethodError
diff --git a/activesupport/lib/active_support/core_ext.rb b/activesupport/lib/active_support/core_ext.rb
index b48bdf08e8..199aa91020 100644
--- a/activesupport/lib/active_support/core_ext.rb
+++ b/activesupport/lib/active_support/core_ext.rb
@@ -1,4 +1,3 @@
-Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].sort.each do |path|
- next if File.basename(path, '.rb') == 'logger'
- require "active_support/core_ext/#{File.basename(path, '.rb')}"
+Dir["#{File.dirname(__FILE__)}/core_ext/*.rb"].each do |path|
+ require path
end
diff --git a/activesupport/lib/active_support/core_ext/array.rb b/activesupport/lib/active_support/core_ext/array.rb
index 79ba79192a..7d0c1e4c8d 100644
--- a/activesupport/lib/active_support/core_ext/array.rb
+++ b/activesupport/lib/active_support/core_ext/array.rb
@@ -1,6 +1,5 @@
require 'active_support/core_ext/array/wrap'
require 'active_support/core_ext/array/access'
-require 'active_support/core_ext/array/uniq_by'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/array/extract_options'
require 'active_support/core_ext/array/grouping'
diff --git a/activesupport/lib/active_support/core_ext/array/access.rb b/activesupport/lib/active_support/core_ext/array/access.rb
index a8f9dddae5..67f58bc0fe 100644
--- a/activesupport/lib/active_support/core_ext/array/access.rb
+++ b/activesupport/lib/active_support/core_ext/array/access.rb
@@ -21,33 +21,35 @@ class Array
# Equal to <tt>self[1]</tt>.
#
- # %w( a b c d e).second # => "b"
+ # %w( a b c d e ).second # => "b"
def second
self[1]
end
# Equal to <tt>self[2]</tt>.
#
- # %w( a b c d e).third # => "c"
+ # %w( a b c d e ).third # => "c"
def third
self[2]
end
# Equal to <tt>self[3]</tt>.
#
- # %w( a b c d e).fourth # => "d"
+ # %w( a b c d e ).fourth # => "d"
def fourth
self[3]
end
# Equal to <tt>self[4]</tt>.
#
- # %w( a b c d e).fifth # => "e"
+ # %w( a b c d e ).fifth # => "e"
def fifth
self[4]
end
# Equal to <tt>self[41]</tt>. Also known as accessing "the reddit".
+ #
+ # (1..42).to_a.forty_two # => 42
def forty_two
self[41]
end
diff --git a/activesupport/lib/active_support/core_ext/array/conversions.rb b/activesupport/lib/active_support/core_ext/array/conversions.rb
index 6a0c4a015a..76ffd23ed1 100644
--- a/activesupport/lib/active_support/core_ext/array/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/array/conversions.rb
@@ -8,11 +8,11 @@ class Array
# Converts the array to a comma-separated sentence where the last element is
# joined by the connector word.
#
- # You can pass the following options to change the default behaviour. If you
+ # You can pass the following options to change the default behavior. If you
# pass an option key that doesn't exist in the list below, it will raise an
# <tt>ArgumentError</tt>.
#
- # Options:
+ # ==== Options
#
# * <tt>:words_connector</tt> - The sign or word used to join the elements
# in arrays with two or more elements (default: ", ").
@@ -24,9 +24,11 @@ class Array
# the connector options defined on the 'support.array' namespace in the
# corresponding dictionary file.
#
+ # ==== Examples
+ #
# [].to_sentence # => ""
# ['one'].to_sentence # => "one"
- # ['one', 'two'].to_sentence # => "one and two"
+ # ['one', 'two'].to_sentence # => "one and two"
# ['one', 'two', 'three'].to_sentence # => "one, two, and three"
#
# ['one', 'two'].to_sentence(passing: 'invalid option')
@@ -38,10 +40,10 @@ class Array
# ['one', 'two', 'three'].to_sentence(words_connector: ' or ', last_word_connector: ' or at least ')
# # => "one or two or at least three"
#
- # Examples using <tt>:locale</tt> option:
+ # Using <tt>:locale</tt> option:
#
# # Given this locale dictionary:
- # # 
+ # #
# # es:
# # support:
# # array:
@@ -53,7 +55,7 @@ class Array
# # => "uno y dos"
#
# ['uno', 'dos', 'tres'].to_sentence(locale: :es)
- # # => "uno o dos o al menos tres"
+ # # => "uno o dos o al menos tres"
def to_sentence(options = {})
options.assert_valid_keys(:words_connector, :two_words_connector, :last_word_connector, :locale)
@@ -80,23 +82,8 @@ class Array
end
end
- # Converts a collection of elements into a formatted string by calling
- # <tt>to_s</tt> on all elements and joining them. Having this model:
- #
- # class Blog < ActiveRecord::Base
- # def to_s
- # title
- # end
- # end
- #
- # Blog.all.map(&:title) #=> ["First Post", "Second Post", "Third post"]
- #
- # <tt>to_formatted_s</tt> shows us:
- #
- # Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
- #
- # Adding in the <tt>:db</tt> argument as the format yields a comma separated
- # id list:
+ # Extends <tt>Array#to_s</tt> to convert a collection of elements into a
+ # comma separated id list if <tt>:db</tt> argument is given as the format.
#
# Blog.all.to_formatted_s(:db) # => "1,2,3"
def to_formatted_s(format = :default)
@@ -194,7 +181,7 @@ class Array
options = options.dup
options[:indent] ||= 2
- options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
options[:root] ||= \
if first.class != Hash && all? { |e| e.is_a?(first.class) }
underscored = ActiveSupport::Inflector.underscore(first.class.name)
@@ -208,12 +195,12 @@ class Array
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
children = options.delete(:children) || root.singularize
- attributes = options[:skip_types] ? {} : {:type => 'array'}
+ attributes = options[:skip_types] ? {} : { type: 'array' }
if empty?
builder.tag!(root, attributes)
else
- builder.__send__(:method_missing, root, attributes) do
+ builder.tag!(root, attributes) do
each { |value| ActiveSupport::XmlMini.to_tag(children, value, options) }
yield builder if block_given?
end
diff --git a/activesupport/lib/active_support/core_ext/array/grouping.rb b/activesupport/lib/active_support/core_ext/array/grouping.rb
index f79b100b3b..37f007c751 100644
--- a/activesupport/lib/active_support/core_ext/array/grouping.rb
+++ b/activesupport/lib/active_support/core_ext/array/grouping.rb
@@ -25,15 +25,13 @@ class Array
# subtracting from number gives how many to add;
# modulo number ensures we don't add group of just fill.
padding = (number - size % number) % number
- collection = dup.concat([fill_with] * padding)
+ collection = dup.concat(Array.new(padding, fill_with))
end
if block_given?
collection.each_slice(number) { |slice| yield(slice) }
else
- groups = []
- collection.each_slice(number) { |group| groups << group }
- groups
+ collection.each_slice(number).to_a
end
end
@@ -58,7 +56,7 @@ class Array
# size / number gives minor group size;
# size % number gives how many objects need extra accommodation;
# each group hold either division or division + 1 items.
- division = size / number
+ division = size.div number
modulo = size % number
# create a new array avoiding dup
@@ -67,9 +65,9 @@ class Array
number.times do |index|
length = division + (modulo > 0 && modulo > index ? 1 : 0)
- padding = fill_with != false &&
- modulo > 0 && length == division ? 1 : 0
- groups << slice(start, length).concat([fill_with] * padding)
+ groups << last_group = slice(start, length)
+ last_group << fill_with if fill_with != false &&
+ modulo > 0 && length == division
start += length
end
@@ -86,13 +84,27 @@ class Array
# [1, 2, 3, 4, 5].split(3) # => [[1, 2], [4, 5]]
# (1..10).to_a.split { |i| i % 3 == 0 } # => [[1, 2], [4, 5], [7, 8], [10]]
def split(value = nil, &block)
- inject([[]]) do |results, element|
- if block && block.call(element) || value == element
- results << []
- else
- results.last << element
- end
+ if block
+ inject([[]]) do |results, element|
+ if block.call(element)
+ results << []
+ else
+ results.last << element
+ end
+ results
+ end
+ else
+ results, arr = [[]], self
+ until arr.empty?
+ if (idx = index(value))
+ results.last.concat(arr.shift(idx))
+ arr.shift
+ results << []
+ else
+ results.last.concat(arr.shift(arr.size))
+ end
+ end
results
end
end
diff --git a/activesupport/lib/active_support/core_ext/array/uniq_by.rb b/activesupport/lib/active_support/core_ext/array/uniq_by.rb
deleted file mode 100644
index c1d5a355a4..0000000000
--- a/activesupport/lib/active_support/core_ext/array/uniq_by.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class Array
- # *DEPRECATED*: Use +Array#uniq+ instead.
- #
- # Returns a unique array based on the criteria in the block.
- #
- # [1, 2, 3, 4].uniq_by { |i| i.odd? } # => [1, 2]
- def uniq_by(&block)
- ActiveSupport::Deprecation.warn 'uniq_by is deprecated. Use Array#uniq instead', caller
- uniq(&block)
- end
-
- # *DEPRECATED*: Use +Array#uniq!+ instead.
- #
- # Same as +uniq_by+, but modifies +self+.
- def uniq_by!(&block)
- ActiveSupport::Deprecation.warn 'uniq_by! is deprecated. Use Array#uniq! instead', caller
- uniq!(&block)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/array/wrap.rb b/activesupport/lib/active_support/core_ext/array/wrap.rb
index 7bf28b2f27..152eb02218 100644
--- a/activesupport/lib/active_support/core_ext/array/wrap.rb
+++ b/activesupport/lib/active_support/core_ext/array/wrap.rb
@@ -15,25 +15,24 @@ class Array
#
# * If the argument responds to +to_ary+ the method is invoked. <tt>Kernel#Array</tt>
# moves on to try +to_a+ if the returned value is +nil+, but <tt>Array.wrap</tt> returns
- # such a +nil+ right away.
+ # +nil+ right away.
# * If the returned value from +to_ary+ is neither +nil+ nor an +Array+ object, <tt>Kernel#Array</tt>
# raises an exception, while <tt>Array.wrap</tt> does not, it just returns the value.
- # * It does not call +to_a+ on the argument, though special-cases +nil+ to return an empty array.
+ # * It does not call +to_a+ on the argument, but returns an empty array if argument is +nil+.
#
- # The last point is particularly worth comparing for some enumerables:
+ # The second point is easily explained with some enumerables:
#
# Array(foo: :bar) # => [[:foo, :bar]]
- # Array.wrap(foo: :bar) # => [{:foo => :bar}]
+ # Array.wrap(foo: :bar) # => [{:foo=>:bar}]
#
# There's also a related idiom that uses the splat operator:
#
# [*object]
#
- # which for +nil+ returns <tt>[nil]</tt> (Ruby 1.8.7) or <tt>[]</tt> (Ruby
- # 1.9), and calls to <tt>Array(object)</tt> otherwise.
+ # which returns <tt>[]</tt> for +nil+, but calls to <tt>Array(object)</tt> otherwise.
#
- # Thus, in this case the behavior may be different for +nil+, and the differences with
- # <tt>Kernel#Array</tt> explained above apply to the rest of <tt>object</tt>s.
+ # The differences with <tt>Kernel#Array</tt> explained above
+ # apply to the rest of <tt>object</tt>s.
def self.wrap(object)
if object.nil?
[]
diff --git a/activesupport/lib/active_support/core_ext/benchmark.rb b/activesupport/lib/active_support/core_ext/benchmark.rb
index 2d110155a5..eb25b2bc44 100644
--- a/activesupport/lib/active_support/core_ext/benchmark.rb
+++ b/activesupport/lib/active_support/core_ext/benchmark.rb
@@ -1,6 +1,13 @@
require 'benchmark'
class << Benchmark
+ # Benchmark realtime in milliseconds.
+ #
+ # Benchmark.realtime { User.all }
+ # # => 8.0e-05
+ #
+ # Benchmark.ms { User.all }
+ # # => 0.074
def ms
1000 * realtime { yield }
end
diff --git a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
index 5dc5710c53..39b8cea807 100644
--- a/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/big_decimal/conversions.rb
@@ -1,4 +1,5 @@
require 'bigdecimal'
+require 'bigdecimal/util'
require 'yaml'
class BigDecimal
diff --git a/activesupport/lib/active_support/core_ext/class/attribute.rb b/activesupport/lib/active_support/core_ext/class/attribute.rb
index 1c3d26ead4..f2a221c396 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute.rb
@@ -44,7 +44,8 @@ class Class
# Base.setting # => []
# Subclass.setting # => [:foo]
#
- # For convenience, a query method is defined as well:
+ # For convenience, an instance predicate method is defined as well.
+ # To skip it, pass <tt>instance_predicate: false</tt>.
#
# Subclass.setting? # => false
#
@@ -71,47 +72,56 @@ class Class
options = attrs.extract_options!
instance_reader = options.fetch(:instance_accessor, true) && options.fetch(:instance_reader, true)
instance_writer = options.fetch(:instance_accessor, true) && options.fetch(:instance_writer, true)
+ instance_predicate = options.fetch(:instance_predicate, true)
attrs.each do |name|
- class_eval <<-RUBY, __FILE__, __LINE__ + 1
- def self.#{name}() nil end
- def self.#{name}?() !!#{name} end
+ define_singleton_method(name) { nil }
+ define_singleton_method("#{name}?") { !!public_send(name) } if instance_predicate
- def self.#{name}=(val)
- singleton_class.class_eval do
- remove_possible_method(:#{name})
- define_method(:#{name}) { val }
- end
+ ivar = "@#{name}"
+
+ define_singleton_method("#{name}=") do |val|
+ singleton_class.class_eval do
+ remove_possible_method(name)
+ define_method(name) { val }
+ end
- if singleton_class?
- class_eval do
- remove_possible_method(:#{name})
- def #{name}
- defined?(@#{name}) ? @#{name} : singleton_class.#{name}
+ if singleton_class?
+ class_eval do
+ remove_possible_method(name)
+ define_method(name) do
+ if instance_variable_defined? ivar
+ instance_variable_get ivar
+ else
+ singleton_class.send name
end
end
end
- val
end
+ val
+ end
- if instance_reader
- remove_possible_method :#{name}
- def #{name}
- defined?(@#{name}) ? @#{name} : self.class.#{name}
- end
-
- def #{name}?
- !!#{name}
+ if instance_reader
+ remove_possible_method name
+ define_method(name) do
+ if instance_variable_defined?(ivar)
+ instance_variable_get ivar
+ else
+ self.class.public_send name
end
end
- RUBY
+ define_method("#{name}?") { !!public_send(name) } if instance_predicate
+ end
attr_writer name if instance_writer
end
end
private
- def singleton_class?
- ancestors.first != self
+
+ unless respond_to?(:singleton_class?)
+ def singleton_class?
+ ancestors.first != self
+ end
end
end
diff --git a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
index fa1dbfdf06..34859617c9 100644
--- a/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
+++ b/activesupport/lib/active_support/core_ext/class/attribute_accessors.rb
@@ -32,7 +32,7 @@ class Class
def cattr_reader(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
unless defined? @@#{sym}
@@#{sym} = nil
@@ -93,7 +93,7 @@ class Class
def cattr_writer(*syms)
options = syms.extract_options!
syms.each do |sym|
- raise NameError.new('invalid attribute name') unless sym =~ /^[_A-Za-z]\w*$/
+ raise NameError.new("invalid class attribute name: #{sym}") unless sym =~ /^[_A-Za-z]\w*$/
class_eval(<<-EOS, __FILE__, __LINE__ + 1)
unless defined? @@#{sym}
@@#{sym} = nil
diff --git a/activesupport/lib/active_support/core_ext/class/subclasses.rb b/activesupport/lib/active_support/core_ext/class/subclasses.rb
index c2e0ebb3d4..3c4bfc5f1e 100644
--- a/activesupport/lib/active_support/core_ext/class/subclasses.rb
+++ b/activesupport/lib/active_support/core_ext/class/subclasses.rb
@@ -29,9 +29,9 @@ class Class
#
# class Foo; end
# class Bar < Foo; end
- # class Baz < Foo; end
+ # class Baz < Bar; end
#
- # Foo.subclasses # => [Baz, Bar]
+ # Foo.subclasses # => [Bar]
def subclasses
subclasses, chain = [], descendants
chain.each do |k|
diff --git a/activesupport/lib/active_support/core_ext/date/calculations.rb b/activesupport/lib/active_support/core_ext/date/calculations.rb
index 02ae57b4a6..c60e833441 100644
--- a/activesupport/lib/active_support/core_ext/date/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date/calculations.rb
@@ -51,28 +51,39 @@ class Date
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
# and then subtracts the specified number of seconds.
def ago(seconds)
- to_time_in_current_zone.since(-seconds)
+ in_time_zone.since(-seconds)
end
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
# and then adds the specified number of seconds
def since(seconds)
- to_time_in_current_zone.since(seconds)
+ in_time_zone.since(seconds)
end
alias :in :since
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the beginning of the day (0:00)
def beginning_of_day
- to_time_in_current_zone
+ in_time_zone
end
alias :midnight :beginning_of_day
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Converts Date to a Time (or DateTime if necessary) with the time portion set to the middle of the day (12:00)
+ def middle_of_day
+ in_time_zone.middle_of_day
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Converts Date to a Time (or DateTime if necessary) with the time portion set to the end of the day (23:59:59)
def end_of_day
- to_time_in_current_zone.end_of_day
+ in_time_zone.end_of_day
end
+ alias :at_end_of_day :end_of_day
def plus_with_duration(other) #:nodoc:
if ActiveSupport::Duration === other
@@ -118,4 +129,15 @@ class Date
options.fetch(:day, day)
)
end
+
+ # Allow Date to be compared with Time by converting to DateTime and relying on the <=> from there.
+ def compare_with_coercion(other)
+ if other.is_a?(Time)
+ self.to_datetime <=> other
+ else
+ compare_without_coercion(other)
+ end
+ end
+ alias_method :compare_without_coercion, :<=>
+ alias_method :<=>, :compare_with_coercion
end
diff --git a/activesupport/lib/active_support/core_ext/date/conversions.rb b/activesupport/lib/active_support/core_ext/date/conversions.rb
index 9120b0ba49..6bc8f12176 100644
--- a/activesupport/lib/active_support/core_ext/date/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date/conversions.rb
@@ -1,7 +1,6 @@
require 'date'
require 'active_support/inflector/methods'
require 'active_support/core_ext/date/zones'
-require 'active_support/core_ext/module/remove_method'
class Date
DATE_FORMATS = {
@@ -13,14 +12,15 @@ class Date
day_format = ActiveSupport::Inflector.ordinalize(date.day)
date.strftime("%B #{day_format}, %Y") # => "April 25th, 2007"
},
- :rfc822 => '%e %b %Y'
+ :rfc822 => '%e %b %Y',
+ :iso8601 => lambda { |date| date.iso8601 }
}
# Ruby 1.9 has Date#to_time which converts to localtime only.
- remove_possible_method :to_time
+ remove_method :to_time
# Ruby 1.9 has Date#xmlschema which converts to a string without the time component.
- remove_possible_method :xmlschema
+ remove_method :xmlschema
# Convert to a formatted string. See DATE_FORMATS for predefined formats.
#
@@ -35,6 +35,7 @@ class Date
# date.to_formatted_s(:long) # => "November 10, 2007"
# date.to_formatted_s(:long_ordinal) # => "November 10th, 2007"
# date.to_formatted_s(:rfc822) # => "10 Nov 2007"
+ # date.to_formatted_s(:iso8601) # => "2007-11-10"
#
# == Adding your own time formats to to_formatted_s
# You can add your own formats to the Date::DATE_FORMATS hash.
@@ -75,10 +76,10 @@ class Date
#
# date.to_time(:utc) # => Sat Nov 10 00:00:00 UTC 2007
def to_time(form = :local)
- ::Time.send("#{form}_time", year, month, day)
+ ::Time.send(form, year, month, day)
end
def xmlschema
- to_time_in_current_zone.xmlschema
+ in_time_zone.xmlschema
end
end
diff --git a/activesupport/lib/active_support/core_ext/date/zones.rb b/activesupport/lib/active_support/core_ext/date/zones.rb
index c1b3934722..d109b430db 100644
--- a/activesupport/lib/active_support/core_ext/date/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date/zones.rb
@@ -1,15 +1,6 @@
require 'date'
-require 'active_support/core_ext/time/zones'
+require 'active_support/core_ext/date_and_time/zones'
class Date
- # Converts Date to a TimeWithZone in the current zone if <tt>Time.zone</tt> or
- # <tt>Time.zone_default</tt> is set, otherwise converts Date to a Time via
- # Date#to_time.
- def to_time_in_current_zone
- if ::Time.zone
- ::Time.zone.local(year, month, day)
- else
- to_time
- end
- end
+ include DateAndTime::Zones
end
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
index 1f78b9eb5a..c869a0e210 100644
--- a/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_and_time/calculations.rb
@@ -78,7 +78,7 @@ module DateAndTime
# Returns a new date/time at the start of the month.
# DateTime objects will have a time set to 0:00.
def beginning_of_month
- first_hour{ change(:day => 1) }
+ first_hour(change(:day => 1))
end
alias :at_beginning_of_month :beginning_of_month
@@ -93,7 +93,7 @@ module DateAndTime
# Returns a new date/time at the end of the quarter.
# Example: 31st March, 30th June, 30th September.
- # DateTIme objects will have a time set to 23:59:59.
+ # DateTime objects will have a time set to 23:59:59.
def end_of_quarter
last_quarter_month = [3, 6, 9, 12].detect { |m| m >= month }
beginning_of_month.change(:month => last_quarter_month).end_of_month
@@ -109,11 +109,11 @@ module DateAndTime
alias :at_beginning_of_year :beginning_of_year
# Returns a new date/time representing the given day in the next week.
- # Week is assumed to start on +start_day+, default is
- # +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
- # DateTime objects have their time set to 0:00.
- def next_week(start_day = Date.beginning_of_week)
- first_hour{ weeks_since(1).beginning_of_week.days_since(days_span(start_day)) }
+ # The +given_day_in_next_week+ defaults to the beginning of the week
+ # which is determined by +Date.beginning_of_week+ or +config.beginning_of_week+
+ # when set. +DateTime+ objects have their time set to 0:00.
+ def next_week(given_day_in_next_week = Date.beginning_of_week)
+ first_hour(weeks_since(1).beginning_of_week.days_since(days_span(given_day_in_next_week)))
end
# Short-hand for months_since(1).
@@ -136,7 +136,7 @@ module DateAndTime
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 0:00.
def prev_week(start_day = Date.beginning_of_week)
- first_hour{ weeks_ago(1).beginning_of_week.days_since(days_span(start_day)) }
+ first_hour(weeks_ago(1).beginning_of_week.days_since(days_span(start_day)))
end
alias_method :last_week, :prev_week
@@ -188,7 +188,7 @@ module DateAndTime
# +Date.beginning_of_week+ or +config.beginning_of_week+ when set.
# DateTime objects have their time set to 23:59:59.
def end_of_week(start_day = Date.beginning_of_week)
- last_hour{ days_since(6 - days_to_week_start(start_day)) }
+ last_hour(days_since(6 - days_to_week_start(start_day)))
end
alias :at_end_of_week :end_of_week
@@ -202,7 +202,7 @@ module DateAndTime
# DateTime objects will have a time set to 23:59:59.
def end_of_month
last_day = ::Time.days_in_month(month, year)
- last_hour{ days_since(last_day - day) }
+ last_hour(days_since(last_day - day))
end
alias :at_end_of_month :end_of_month
@@ -215,14 +215,12 @@ module DateAndTime
private
- def first_hour
- result = yield
- acts_like?(:time) ? result.change(:hour => 0) : result
+ def first_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.beginning_of_day : date_or_time
end
- def last_hour
- result = yield
- acts_like?(:time) ? result.end_of_day : result
+ def last_hour(date_or_time)
+ date_or_time.acts_like?(:time) ? date_or_time.end_of_day : date_or_time
end
def days_span(day)
diff --git a/activesupport/lib/active_support/core_ext/date_and_time/zones.rb b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
new file mode 100644
index 0000000000..96c6df9407
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/date_and_time/zones.rb
@@ -0,0 +1,41 @@
+module DateAndTime
+ module Zones
+ # Returns the simultaneous time in <tt>Time.zone</tt> if a zone is given or
+ # if Time.zone_default is set. Otherwise, it returns the current time.
+ #
+ # Time.zone = 'Hawaii' # => 'Hawaii'
+ # DateTime.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
+ # Date.new(2000).in_time_zone # => Sat, 01 Jan 2000 00:00:00 HST -10:00
+ #
+ # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
+ # instead of the operating system's time zone.
+ #
+ # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
+ # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
+ #
+ # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # DateTime.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
+ # Date.new(2000).in_time_zone('Alaska') # => Sat, 01 Jan 2000 00:00:00 AKST -09:00
+ def in_time_zone(zone = ::Time.zone)
+ time_zone = ::Time.find_zone! zone
+ time = acts_like?(:time) ? self : nil
+
+ if time_zone
+ time_with_zone(time, time_zone)
+ else
+ time || self.to_time
+ end
+ end
+
+ private
+
+ def time_with_zone(time, zone)
+ if time
+ ActiveSupport::TimeWithZone.new(time.utc? ? time : time.getutc, zone)
+ else
+ ActiveSupport::TimeWithZone.new(nil, zone, to_time(:utc))
+ end
+ end
+ end
+end
+
diff --git a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
index c79745c5aa..8fbbe0d3e9 100644
--- a/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/acts_like.rb
@@ -1,3 +1,4 @@
+require 'date'
require 'active_support/core_ext/object/acts_like'
class DateTime
diff --git a/activesupport/lib/active_support/core_ext/date_time/calculations.rb b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
index 385aa586bb..8e5d723074 100644
--- a/activesupport/lib/active_support/core_ext/date_time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/calculations.rb
@@ -1,14 +1,7 @@
-require 'active_support/deprecation'
+require 'date'
class DateTime
class << self
- # *DEPRECATED*: Use +DateTime.civil_from_format+ directly.
- def local_offset
- ActiveSupport::Deprecation.warn 'DateTime.local_offset is deprecated. Use DateTime.civil_from_format directly.', caller
-
- ::Time.local(2012).utc_offset.to_r / 86400
- end
-
# Returns <tt>Time.zone.now.to_datetime</tt> when <tt>Time.zone</tt> or
# <tt>config.time_zone</tt> are set, otherwise returns
# <tt>Time.now.to_datetime</tt>.
@@ -17,24 +10,23 @@ class DateTime
end
end
- # Tells whether the DateTime object's datetime lies in the past.
- def past?
- self < ::DateTime.current
- end
-
- # Tells whether the DateTime object's datetime lies in the future.
- def future?
- self > ::DateTime.current
- end
-
# Seconds since midnight: DateTime.now.seconds_since_midnight.
def seconds_since_midnight
sec + (min * 60) + (hour * 3600)
end
+ # Returns the number of seconds until 23:59:59.
+ #
+ # DateTime.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
+ # DateTime.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
+ # DateTime.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
+ def seconds_until_end_of_day
+ end_of_day.to_i - to_i
+ end
+
# Returns a new DateTime where one or more of the elements have been changed
# according to the +options+ parameter. The time options (<tt>:hour</tt>,
- # <tt>:minute</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
+ # <tt>:min</tt>, <tt>:sec</tt>) reset cascadingly, so if only the hour is
# passed, then minute and sec is set to 0. If the hour and minute is passed,
# then sec is set to 0. The +options+ parameter takes a hash with any of these
# keys: <tt>:year</tt>, <tt>:month</tt>, <tt>:day</tt>, <tt>:hour</tt>,
@@ -50,7 +42,7 @@ class DateTime
options.fetch(:day, day),
options.fetch(:hour, hour),
options.fetch(:min, options[:hour] ? 0 : min),
- options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec),
+ options.fetch(:sec, (options[:hour] || options[:min]) ? 0 : sec + sec_fraction),
options.fetch(:offset, offset),
options.fetch(:start, start)
)
@@ -97,10 +89,21 @@ class DateTime
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Returns a new DateTime representing the middle of the day (12:00)
+ def middle_of_day
+ change(:hour => 12)
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Returns a new DateTime representing the end of the day (23:59:59).
def end_of_day
change(:hour => 23, :min => 59, :sec => 59)
end
+ alias :at_end_of_day :end_of_day
# Returns a new DateTime representing the start of the hour (hh:00:00).
def beginning_of_hour
@@ -112,6 +115,19 @@ class DateTime
def end_of_hour
change(:min => 59, :sec => 59)
end
+ alias :at_end_of_hour :end_of_hour
+
+ # Returns a new DateTime representing the start of the minute (hh:mm:00).
+ def beginning_of_minute
+ change(:sec => 0)
+ end
+ alias :at_beginning_of_minute :beginning_of_minute
+
+ # Returns a new DateTime representing the end of the minute (hh:mm:59).
+ def end_of_minute
+ change(:sec => 59)
+ end
+ alias :at_end_of_minute :end_of_minute
# Adjusts DateTime to UTC by adding its offset value; offset is set to 0.
#
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 b7d8414a9d..6ddfb72a0d 100644
--- a/activesupport/lib/active_support/core_ext/date_time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/conversions.rb
@@ -1,3 +1,4 @@
+require 'date'
require 'active_support/inflector/methods'
require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/date_time/calculations'
@@ -18,6 +19,7 @@ class DateTime
# datetime.to_formatted_s(:long) # => "December 04, 2007 00:00"
# datetime.to_formatted_s(:long_ordinal) # => "December 4th, 2007 00:00"
# datetime.to_formatted_s(:rfc822) # => "Tue, 04 Dec 2007 00:00:00 +0000"
+ # datetime.to_formatted_s(:iso8601) # => "2007-12-04T00:00:00+00:00"
#
# == Adding your own datetime formats to to_formatted_s
# DateTime formats are shared with Time. You can add your own to the
@@ -79,6 +81,16 @@ class DateTime
seconds_since_unix_epoch.to_i
end
+ # Returns the fraction of a second as microseconds
+ def usec
+ (sec_fraction * 1_000_000).to_i
+ end
+
+ # Returns the fraction of a second as nanoseconds
+ def nsec
+ (sec_fraction * 1_000_000_000).to_i
+ end
+
private
def offset_in_seconds
diff --git a/activesupport/lib/active_support/core_ext/date_time/zones.rb b/activesupport/lib/active_support/core_ext/date_time/zones.rb
index 6457ffbaf6..c39f358395 100644
--- a/activesupport/lib/active_support/core_ext/date_time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/date_time/zones.rb
@@ -1,24 +1,6 @@
-require 'active_support/core_ext/time/zones'
+require 'date'
+require 'active_support/core_ext/date_and_time/zones'
class DateTime
- # Returns the simultaneous time in <tt>Time.zone</tt>.
- #
- # Time.zone = 'Hawaii' # => 'Hawaii'
- # DateTime.new(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- #
- # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt>
- # as the local zone instead of the operating system's time zone.
- #
- # You can also pass in a TimeZone instance or string that identifies a TimeZone
- # as an argument, and the conversion will be based on that zone instead of
- # <tt>Time.zone</tt>.
- #
- # DateTime.new(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- def in_time_zone(zone = ::Time.zone)
- if zone
- ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone))
- else
- self
- end
- end
+ include DateAndTime::Zones
end
diff --git a/activesupport/lib/active_support/core_ext/exception.rb b/activesupport/lib/active_support/core_ext/exception.rb
deleted file mode 100644
index ba7757ea07..0000000000
--- a/activesupport/lib/active_support/core_ext/exception.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-module ActiveSupport
- FrozenObjectError = RuntimeError
-end
diff --git a/activesupport/lib/active_support/core_ext/file/atomic.rb b/activesupport/lib/active_support/core_ext/file/atomic.rb
index 81beb4e85d..0e7e3ba378 100644
--- a/activesupport/lib/active_support/core_ext/file/atomic.rb
+++ b/activesupport/lib/active_support/core_ext/file/atomic.rb
@@ -23,10 +23,10 @@ class File
yield temp_file
temp_file.close
- begin
+ if File.exist?(file_name)
# Get original file permissions
old_stat = stat(file_name)
- rescue Errno::ENOENT
+ else
# If not possible, probe which are the default permissions in the
# destination directory.
old_stat = probe_stat_in(dirname(file_name))
@@ -36,8 +36,13 @@ class File
FileUtils.mv(temp_file.path, file_name)
# Set correct permissions on new file
- chown(old_stat.uid, old_stat.gid, file_name)
- chmod(old_stat.mode, file_name)
+ begin
+ chown(old_stat.uid, old_stat.gid, file_name)
+ # This operation will affect filesystem ACL's
+ chmod(old_stat.mode, file_name)
+ rescue Errno::EPERM
+ # Changing file ownership failed, moving on.
+ end
end
# Private utility method.
diff --git a/activesupport/lib/active_support/core_ext/hash.rb b/activesupport/lib/active_support/core_ext/hash.rb
index 501483498d..686f12c6da 100644
--- a/activesupport/lib/active_support/core_ext/hash.rb
+++ b/activesupport/lib/active_support/core_ext/hash.rb
@@ -1,6 +1,5 @@
require 'active_support/core_ext/hash/conversions'
require 'active_support/core_ext/hash/deep_merge'
-require 'active_support/core_ext/hash/diff'
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/indifferent_access'
require 'active_support/core_ext/hash/keys'
diff --git a/activesupport/lib/active_support/core_ext/hash/conversions.rb b/activesupport/lib/active_support/core_ext/hash/conversions.rb
index 828f03ad62..8930376ac8 100644
--- a/activesupport/lib/active_support/core_ext/hash/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/hash/conversions.rb
@@ -74,86 +74,168 @@ class Hash
options = options.dup
options[:indent] ||= 2
options[:root] ||= 'hash'
- options[:builder] ||= Builder::XmlMarkup.new(:indent => options[:indent])
+ options[:builder] ||= Builder::XmlMarkup.new(indent: options[:indent])
builder = options[:builder]
builder.instruct! unless options.delete(:skip_instruct)
root = ActiveSupport::XmlMini.rename_key(options[:root].to_s, options)
- builder.__send__(:method_missing, root) do
+ builder.tag!(root) do
each { |key, value| ActiveSupport::XmlMini.to_tag(key, value, options) }
yield builder if block_given?
end
end
class << self
- def from_xml(xml)
- typecast_xml_value(unrename_keys(ActiveSupport::XmlMini.parse(xml)))
+ # Returns a Hash containing a collection of pairs when the key is the node name and the value is
+ # its content
+ #
+ # xml = <<-XML
+ # <?xml version="1.0" encoding="UTF-8"?>
+ # <hash>
+ # <foo type="integer">1</foo>
+ # <bar type="integer">2</bar>
+ # </hash>
+ # XML
+ #
+ # hash = Hash.from_xml(xml)
+ # # => {"hash"=>{"foo"=>1, "bar"=>2}}
+ #
+ # DisallowedType is raise 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
+ end
+
+ # Builds a Hash from XML just like <tt>Hash.from_xml</tt>, but also allows Symbol and YAML.
+ def from_trusted_xml(xml)
+ from_xml xml, []
+ end
+ end
+end
+
+module ActiveSupport
+ class XMLConverter # :nodoc:
+ class DisallowedType < StandardError
+ def initialize(type)
+ super "Disallowed type attribute: #{type.inspect}"
+ end
+ end
+
+ DISALLOWED_TYPES = %w(symbol yaml)
+
+ def initialize(xml, disallowed_types = nil)
+ @xml = normalize_keys(XmlMini.parse(xml))
+ @disallowed_types = disallowed_types || DISALLOWED_TYPES
+ end
+
+ def to_h
+ deep_to_h(@xml)
end
private
- def typecast_xml_value(value)
- case value.class.to_s
- when 'Hash'
- if value['type'] == 'array'
- _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
- if entries.nil? || (c = value['__content__'] && c.blank?)
- []
- else
- case entries.class.to_s # something weird with classes not matching here. maybe singleton methods breaking is_a?
- when 'Array'
- entries.collect { |v| typecast_xml_value(v) }
- when 'Hash'
- [typecast_xml_value(entries)]
- else
- raise "can't typecast #{entries.inspect}"
- end
- end
- elsif value['type'] == 'file' ||
- (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?))
- content = value['__content__']
- if parser = ActiveSupport::XmlMini::PARSING[value['type']]
- parser.arity == 1 ? parser.call(content) : parser.call(content, value)
- else
- content
- end
- elsif value['type'] == 'string' && value['nil'] != 'true'
- ''
- # blank or nil parsed values are represented by nil
- elsif value.blank? || value['nil'] == 'true'
- nil
- # 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)
- elsif value['type'] && value.size == 1 && !value['type'].is_a?(::Hash)
- nil
- else
- xml_value = Hash[value.map { |k,v| [k, typecast_xml_value(v)] }]
+ def normalize_keys(params)
+ case params
+ when Hash
+ Hash[params.map { |k,v| [k.to_s.tr('-', '_'), normalize_keys(v)] } ]
+ when Array
+ params.map { |v| normalize_keys(v) }
+ else
+ params
+ end
+ end
- # Turn { :files => { :file => #<StringIO> } } into { :files => #<StringIO> } so it is compatible with
- # how multipart uploaded files from HTML appear
- xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value
- end
- when 'Array'
- value.map! { |i| typecast_xml_value(i) }
- value.length > 1 ? value : value.first
- when 'String'
+ def deep_to_h(value)
+ case value
+ when Hash
+ process_hash(value)
+ when Array
+ process_array(value)
+ when String
value
else
raise "can't typecast #{value.class.name} - #{value.inspect}"
end
end
- def unrename_keys(params)
- case params.class.to_s
- when 'Hash'
- Hash[params.map { |k,v| [k.to_s.tr('-', '_'), unrename_keys(v)] } ]
- when 'Array'
- params.map { |v| unrename_keys(v) }
+ def process_hash(value)
+ if value.include?('type') && !value['type'].is_a?(Hash) && @disallowed_types.include?(value['type'])
+ raise DisallowedType, value['type']
+ end
+
+ if become_array?(value)
+ _, entries = Array.wrap(value.detect { |k,v| not v.is_a?(String) })
+ if entries.nil? || value['__content__'].try(:empty?)
+ []
else
- params
+ case entries
+ when Array
+ entries.collect { |v| deep_to_h(v) }
+ when Hash
+ [deep_to_h(entries)]
+ else
+ raise "can't typecast #{entries.inspect}"
+ end
+ end
+ elsif become_content?(value)
+ process_content(value)
+
+ elsif become_empty_string?(value)
+ ''
+ elsif become_hash?(value)
+ xml_value = Hash[value.map { |k,v| [k, deep_to_h(v)] }]
+
+ # Turn { files: { file: #<StringIO> } } into { files: #<StringIO> } so it is compatible with
+ # how multipart uploaded files from HTML appear
+ xml_value['file'].is_a?(StringIO) ? xml_value['file'] : xml_value
+ end
+ end
+
+ def become_content?(value)
+ value['type'] == 'file' || (value['__content__'] && (value.keys.size == 1 || value['__content__'].present?))
+ end
+
+ def become_array?(value)
+ value['type'] == 'array'
+ end
+
+ def become_empty_string?(value)
+ # {"string" => true}
+ # No tests fail when the second term is removed.
+ value['type'] == 'string' && value['nil'] != 'true'
+ end
+
+ def become_hash?(value)
+ !nothing?(value) && !garbage?(value)
+ end
+
+ def nothing?(value)
+ # blank or nil parsed values are represented by nil
+ value.blank? || value['nil'] == 'true'
+ end
+
+ 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)
+ value['type'] && !value['type'].is_a?(::Hash) && value.size == 1
+ end
+
+ def process_content(value)
+ content = value['__content__']
+ if parser = ActiveSupport::XmlMini::PARSING[value['type']]
+ parser.arity == 1 ? parser.call(content) : parser.call(content, value)
+ else
+ content
end
end
+
+ def process_array(value)
+ value.map! { |i| deep_to_h(i) }
+ value.length > 1 ? value : value.first
+ end
+
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 83f0c87b04..e07db50b77 100644
--- a/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
+++ b/activesupport/lib/active_support/core_ext/hash/deep_merge.rb
@@ -4,10 +4,10 @@ class Hash
# h1 = { x: { y: [4,5,6] }, z: [7,8,9] }
# h2 = { x: { y: [7,8,9] }, z: 'xyz' }
#
- # h1.deep_merge(h2) #=> {:x => {:y => [7, 8, 9]}, :z => "xyz"}
- # h2.deep_merge(h1) #=> {:x => {:y => [4, 5, 6]}, :z => [7, 8, 9]}
+ # h1.deep_merge(h2) #=> {x: {y: [7, 8, 9]}, z: "xyz"}
+ # h2.deep_merge(h1) #=> {x: {y: [4, 5, 6]}, z: [7, 8, 9]}
# h1.deep_merge(h2) { |key, old, new| Array.wrap(old) + Array.wrap(new) }
- # #=> {:x => {:y => [4, 5, 6, 7, 8, 9]}, :z => [7, 8, 9, "xyz"]}
+ # #=> {:x=>{:y=>[4, 5, 6, 7, 8, 9]}, :z=>[7, 8, 9, "xyz"]}
def deep_merge(other_hash, &block)
dup.deep_merge!(other_hash, &block)
end
diff --git a/activesupport/lib/active_support/core_ext/hash/diff.rb b/activesupport/lib/active_support/core_ext/hash/diff.rb
deleted file mode 100644
index 831dee8ecb..0000000000
--- a/activesupport/lib/active_support/core_ext/hash/diff.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-class Hash
- # Returns a hash that represents the difference between two hashes.
- #
- # {1 => 2}.diff(1 => 2) # => {}
- # {1 => 2}.diff(1 => 3) # => {1 => 2}
- # {}.diff(1 => 2) # => {1 => 2}
- # {1 => 2, 3 => 4}.diff(1 => 2) # => {3 => 4}
- def diff(other)
- dup.
- delete_if { |k, v| other[k] == v }.
- merge!(other.dup.delete_if { |k, v| has_key?(k) })
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/hash/except.rb b/activesupport/lib/active_support/core_ext/hash/except.rb
index 5cb00d0ebd..d90e996ad4 100644
--- a/activesupport/lib/active_support/core_ext/hash/except.rb
+++ b/activesupport/lib/active_support/core_ext/hash/except.rb
@@ -2,7 +2,7 @@ class Hash
# Return a hash that includes everything but the given keys. This is useful for
# limiting a set of parameters to everything but a few known toggles:
#
- # @person.update_attributes(params[:person].except(:admin))
+ # @person.update(params[:person].except(:admin))
def except(*keys)
dup.except!(*keys)
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 6c7e876fca..981e8436bf 100644
--- a/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
+++ b/activesupport/lib/active_support/core_ext/hash/indifferent_access.rb
@@ -16,7 +16,7 @@ class Hash
# converting to an <tt>ActiveSupport::HashWithIndifferentAccess</tt> would not be
# desirable.
#
- # b = { b: 1 }
+ # b = { b: 1 }
# { a: b }.with_indifferent_access['a'] # calls b.nested_under_indifferent_access
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 13081995b0..b4c451ace4 100644
--- a/activesupport/lib/active_support/core_ext/hash/keys.rb
+++ b/activesupport/lib/active_support/core_ext/hash/keys.rb
@@ -24,7 +24,7 @@ class Hash
# Return a new hash with all keys converted to strings.
#
- # hash = { name: 'Rob', age: '28' }
+ # hash = { name: 'Rob', age: '28' }
#
# hash.stringify_keys
# #=> { "name" => "Rob", "age" => "28" }
@@ -44,7 +44,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
@@ -102,7 +102,7 @@ class Hash
# This includes the keys from the root hash and from all
# nested hashes.
#
- # hash = { person: { name: 'Rob', age: '28' } }
+ # hash = { person: { name: 'Rob', age: '28' } }
#
# hash.deep_stringify_keys
# # => { "person" => { "name" => "Rob", "age" => "28" } }
@@ -121,10 +121,10 @@ class Hash
# they respond to +to_sym+. This includes the keys from the root hash
# and from all nested hashes.
#
- # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
+ # hash = { 'person' => { 'name' => 'Rob', 'age' => '28' } }
#
# hash.deep_symbolize_keys
- # # => { person: { name: "Rob", age: "28" } }
+ # # => { person: { name: "Rob", age: "28" } }
def deep_symbolize_keys
deep_transform_keys{ |key| key.to_sym rescue key }
end
diff --git a/activesupport/lib/active_support/core_ext/hash/slice.rb b/activesupport/lib/active_support/core_ext/hash/slice.rb
index de09f26f3d..8ad600b171 100644
--- a/activesupport/lib/active_support/core_ext/hash/slice.rb
+++ b/activesupport/lib/active_support/core_ext/hash/slice.rb
@@ -21,19 +21,21 @@ class Hash
# Returns a hash containing the removed key/value pairs.
#
# { a: 1, b: 2, c: 3, d: 4 }.slice!(:a, :b)
- # # => {:c => 3, :d => 4}
+ # # => {:c=>3, :d=>4}
def slice!(*keys)
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key, true)
omit = slice(*self.keys - keys)
hash = slice(*keys)
+ hash.default = default
+ hash.default_proc = default_proc if default_proc
replace(hash)
omit
end
# Removes and returns the key/value pairs matching the given keys.
#
- # { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => { a: 1, b: 2 }
- # { a: 1, b: 2 }.extract!(:a, :x) # => { a: 1 }
+ # { a: 1, b: 2, c: 3, d: 4 }.extract!(:a, :b) # => {:a=>1, :b=>2}
+ # { a: 1, b: 2 }.extract!(:a, :x) # => {:a=>1}
def extract!(*keys)
keys.each_with_object(self.class.new) { |key, result| result[key] = delete(key) if has_key?(key) }
end
diff --git a/activesupport/lib/active_support/core_ext/integer/time.rb b/activesupport/lib/active_support/core_ext/integer/time.rb
index 9fb4f6b73a..82080ffe51 100644
--- a/activesupport/lib/active_support/core_ext/integer/time.rb
+++ b/activesupport/lib/active_support/core_ext/integer/time.rb
@@ -1,3 +1,6 @@
+require 'active_support/duration'
+require 'active_support/core_ext/numeric/time'
+
class Integer
# Enables the use of time calculations and declarations, like <tt>45.minutes +
# 2.hours + 4.years</tt>.
diff --git a/activesupport/lib/active_support/core_ext/kernel/reporting.rb b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
index bc97da6ef2..36ab836457 100644
--- a/activesupport/lib/active_support/core_ext/kernel/reporting.rb
+++ b/activesupport/lib/active_support/core_ext/kernel/reporting.rb
@@ -1,4 +1,5 @@
require 'rbconfig'
+require 'tempfile'
module Kernel
# Sets $VERBOSE to nil for the duration of the block and back to its original
@@ -58,27 +59,41 @@ module Kernel
#
# puts 'This code gets executed and nothing related to ZeroDivisionError was seen'
def suppress(*exception_classes)
- begin yield
- rescue Exception => e
- raise unless exception_classes.any? { |cls| e.kind_of?(cls) }
- end
+ yield
+ rescue Exception => e
+ raise unless exception_classes.any? { |cls| e.kind_of?(cls) }
end
# Captures the given stream and returns it:
#
- # stream = capture(:stdout) { puts 'Cool' }
- # stream # => "Cool\n"
+ # stream = capture(:stdout) { puts 'notice' }
+ # stream # => "notice\n"
+ #
+ # stream = capture(:stderr) { warn 'error' }
+ # stream # => "error\n"
+ #
+ # even for subprocesses:
+ #
+ # stream = capture(:stdout) { system('echo notice') }
+ # stream # => "notice\n"
+ #
+ # stream = capture(:stderr) { system('echo error 1>&2') }
+ # stream # => "error\n"
def capture(stream)
- begin
- stream = stream.to_s
- eval "$#{stream} = StringIO.new"
- yield
- result = eval("$#{stream}").string
- ensure
- eval("$#{stream} = #{stream.upcase}")
- end
+ stream = stream.to_s
+ captured_stream = Tempfile.new(stream)
+ stream_io = eval("$#{stream}")
+ origin_stream = stream_io.dup
+ stream_io.reopen(captured_stream)
- result
+ yield
+
+ stream_io.rewind
+ return captured_stream.read
+ ensure
+ captured_stream.close
+ captured_stream.unlink
+ stream_io.reopen(origin_stream)
end
alias :silence :capture
diff --git a/activesupport/lib/active_support/core_ext/logger.rb b/activesupport/lib/active_support/core_ext/logger.rb
deleted file mode 100644
index 16fce81445..0000000000
--- a/activesupport/lib/active_support/core_ext/logger.rb
+++ /dev/null
@@ -1,84 +0,0 @@
-require 'active_support/core_ext/class/attribute_accessors'
-require 'active_support/deprecation'
-
-ActiveSupport::Deprecation.warn 'this file is deprecated and will be removed'
-
-# Adds the 'around_level' method to Logger.
-class Logger #:nodoc:
- def self.define_around_helper(level)
- module_eval <<-end_eval, __FILE__, __LINE__ + 1
- def around_#{level}(before_message, after_message) # def around_debug(before_message, after_message, &block)
- self.#{level}(before_message) # self.debug(before_message)
- return_value = yield(self) # return_value = yield(self)
- self.#{level}(after_message) # self.debug(after_message)
- return_value # return_value
- end # end
- end_eval
- end
- [:debug, :info, :error, :fatal].each {|level| define_around_helper(level) }
-end
-
-require 'logger'
-
-# Extensions to the built-in Ruby logger.
-#
-# If you want to use the default log formatter as defined in the Ruby core, then you
-# will need to set the formatter for the logger as in:
-#
-# logger.formatter = Formatter.new
-#
-# You can then specify the datetime format, for example:
-#
-# logger.datetime_format = "%Y-%m-%d"
-#
-# Note: This logger is deprecated in favor of ActiveSupport::BufferedLogger
-class Logger
- ##
- # :singleton-method:
- # Set to false to disable the silencer
- cattr_accessor :silencer
- self.silencer = true
-
- # Silences the logger for the duration of the block.
- def silence(temporary_level = Logger::ERROR)
- if silencer
- begin
- old_logger_level, self.level = level, temporary_level
- yield self
- ensure
- self.level = old_logger_level
- end
- else
- yield self
- end
- end
-
- alias :old_datetime_format= :datetime_format=
- # Logging date-time format (string passed to +strftime+). Ignored if the formatter
- # does not respond to datetime_format=.
- def datetime_format=(format)
- formatter.datetime_format = format if formatter.respond_to?(:datetime_format=)
- end
-
- alias :old_datetime_format :datetime_format
- # Get the logging datetime format. Returns nil if the formatter does not support
- # datetime formatting.
- def datetime_format
- formatter.datetime_format if formatter.respond_to?(:datetime_format)
- end
-
- alias :old_initialize :initialize
- # Overwrite initialize to set a default formatter.
- def initialize(*args)
- old_initialize(*args)
- self.formatter = SimpleFormatter.new
- end
-
- # Simple formatter which only displays the message.
- class SimpleFormatter < Logger::Formatter
- # This method is invoked when a log event occurs
- def call(severity, timestamp, progname, msg)
- "#{String === msg ? msg : msg.inspect}\n"
- end
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/marshal.rb b/activesupport/lib/active_support/core_ext/marshal.rb
new file mode 100644
index 0000000000..56c79c04bd
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/marshal.rb
@@ -0,0 +1,21 @@
+require 'active_support/core_ext/module/aliasing'
+
+module Marshal
+ class << self
+ def load_with_autoloading(source)
+ load_without_autoloading(source)
+ rescue ArgumentError, NameError => exc
+ if exc.message.match(%r|undefined class/module (.+)|)
+ # try loading the class/module
+ $1.constantize
+ # if it is a IO we need to go back to read the object
+ source.rewind if source.respond_to?(:rewind)
+ retry
+ else
+ raise exc
+ end
+ end
+
+ alias_method_chain :load, :autoloading
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/module/delegation.rb b/activesupport/lib/active_support/core_ext/module/delegation.rb
index e608eeaf42..182e74d9e1 100644
--- a/activesupport/lib/active_support/core_ext/module/delegation.rb
+++ b/activesupport/lib/active_support/core_ext/module/delegation.rb
@@ -1,8 +1,19 @@
class Module
- # Provides a delegate class method to easily expose contained objects' public methods
- # as your own. Pass one or more methods (specified as symbols or strings)
- # and the name of the target object via the <tt>:to</tt> option (also a symbol
- # or string). At least one method and the <tt>:to</tt> option are required.
+ # Error generated by +delegate+ when a method is called on +nil+ and +allow_nil+
+ # option is not used.
+ class DelegationError < NoMethodError; end
+
+ # Provides a +delegate+ class method to easily expose contained objects'
+ # public methods as your own.
+ #
+ # ==== Options
+ # * <tt>:to</tt> - Specifies the target object
+ # * <tt>:prefix</tt> - Prefixes the new method with the target name or a custom prefix
+ # * <tt>:allow_nil</tt> - if set to true, prevents a +NoMethodError+ to be raised
+ #
+ # The macro receives one or more method names (specified as symbols or
+ # strings) and the name of the target object via the <tt>:to</tt> option
+ # (also a symbol or string).
#
# Delegation is particularly useful with Active Record associations:
#
@@ -89,29 +100,44 @@ class Module
# invoice.customer_name # => 'John Doe'
# invoice.customer_address # => 'Vimmersvej 13'
#
- # If the delegate object is +nil+ an exception is raised, and that happens
- # no matter whether +nil+ responds to the delegated method. You can get a
- # +nil+ instead with the +:allow_nil+ option.
+ # If the target is +nil+ and does not respond to the delegated method a
+ # +NoMethodError+ is raised, as with any other value. Sometimes, however, it
+ # makes sense to be robust to that situation and that is the purpose of the
+ # <tt>:allow_nil</tt> option: If the target is not +nil+, or it is and
+ # responds to the method, everything works as usual. But if it is +nil+ and
+ # does not respond to the delegated method, +nil+ is returned.
#
- # class Foo
- # attr_accessor :bar
- # def initialize(bar = nil)
- # @bar = bar
- # end
- # delegate :zoo, to: :bar
+ # class User < ActiveRecord::Base
+ # has_one :profile
+ # delegate :age, to: :profile
+ # end
+ #
+ # User.new.age # raises NoMethodError: undefined method `age'
+ #
+ # But if not having a profile yet is fine and should not be an error
+ # condition:
+ #
+ # class User < ActiveRecord::Base
+ # has_one :profile
+ # delegate :age, to: :profile, allow_nil: true
# end
#
- # Foo.new.zoo # raises NoMethodError exception (you called nil.zoo)
+ # User.new.age # nil
+ #
+ # Note that if the target is not +nil+ then the call is attempted regardless of the
+ # <tt>:allow_nil</tt> option, and thus an exception is still raised if said object
+ # does not respond to the method:
#
# class Foo
- # attr_accessor :bar
- # def initialize(bar = nil)
+ # def initialize(bar)
# @bar = bar
# end
- # delegate :zoo, to: :bar, allow_nil: true
+ #
+ # delegate :name, to: :@bar, allow_nil: true
# end
#
- # Foo.new.zoo # returns nil
+ # Foo.new("Bar").name # raises NoMethodError: undefined method `name'
+ #
def delegate(*methods)
options = methods.pop
unless options.is_a?(Hash) && to = options[:to]
@@ -142,27 +168,36 @@ class Module
# methods still accept two arguments.
definition = (method =~ /[^\]]=$/) ? 'arg' : '*args, &block'
+ # The following generated methods call 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
- module_eval(<<-EOS, file, line - 2)
+ module_eval(<<-EOS, file, line - 3)
def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- if #{to} || #{to}.respond_to?(:#{method}) # if client || client.respond_to?(:name)
- #{to}.#{method}(#{definition}) # client.name(*args, &block)
+ _ = #{to} # _ = client
+ if !_.nil? || nil.respond_to?(:#{method}) # if !_.nil? || nil.respond_to?(:name)
+ _.#{method}(#{definition}) # _.name(*args, &block)
end # end
end # end
EOS
else
- exception = %(raise "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
+ exception = %(raise DelegationError, "#{self}##{method_prefix}#{method} delegated to #{to}.#{method}, but #{to} is nil: \#{self.inspect}")
- module_eval(<<-EOS, file, line - 1)
- def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
- #{to}.#{method}(#{definition}) # client.name(*args, &block)
- rescue NoMethodError # rescue NoMethodError
- if #{to}.nil? # if client.nil?
- #{exception} # # add helpful message to the exception
- else # else
- raise # raise
- end # end
- end # end
+ module_eval(<<-EOS, file, line - 2)
+ def #{method_prefix}#{method}(#{definition}) # def customer_name(*args, &block)
+ _ = #{to} # _ = client
+ _.#{method}(#{definition}) # _.name(*args, &block)
+ rescue NoMethodError => 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
EOS
end
end
diff --git a/activesupport/lib/active_support/core_ext/module/deprecation.rb b/activesupport/lib/active_support/core_ext/module/deprecation.rb
index 34ec6a3d8f..d873de197f 100644
--- a/activesupport/lib/active_support/core_ext/module/deprecation.rb
+++ b/activesupport/lib/active_support/core_ext/module/deprecation.rb
@@ -2,20 +2,20 @@ require 'active_support/deprecation/method_wrappers'
class Module
# deprecate :foo
- # deprecate :bar => 'message'
- # deprecate :foo, :bar, :baz => 'warning!', :qux => 'gone!'
+ # deprecate bar: 'message'
+ # deprecate :foo, :bar, baz: 'warning!', qux: 'gone!'
#
# You can also use custom deprecator instance:
#
- # deprecate :foo, :deprecator => MyLib::Deprecator.new
- # deprecate :foo, :bar => "warning!", :deprecator => MyLib::Deprecator.new
+ # deprecate :foo, deprecator: MyLib::Deprecator.new
+ # deprecate :foo, bar: "warning!", deprecator: MyLib::Deprecator.new
#
# \Custom deprecators must respond to <tt>deprecation_warning(deprecated_method_name, message, caller_backtrace)</tt>
# method where you can implement your custom warning behavior.
#
# class MyLib::Deprecator
- # def deprecation_warning(deprecated_method_name, message, caller_backtrace)
- # message = "#{method_name} is deprecated and will be removed from MyLibrary | #{message}"
+ # def deprecation_warning(deprecated_method_name, message, caller_backtrace = nil)
+ # message = "#{deprecated_method_name} is deprecated and will be removed from MyLibrary | #{message}"
# Kernel.warn message
# end
# end
diff --git a/activesupport/lib/active_support/core_ext/module/introspection.rb b/activesupport/lib/active_support/core_ext/module/introspection.rb
index 649a969149..f1d26ef28f 100644
--- a/activesupport/lib/active_support/core_ext/module/introspection.rb
+++ b/activesupport/lib/active_support/core_ext/module/introspection.rb
@@ -59,20 +59,4 @@ class Module
def local_constants #:nodoc:
constants(false)
end
-
- # *DEPRECATED*: Use +local_constants+ instead.
- #
- # Returns the names of the constants defined locally as strings.
- #
- # module M
- # X = 1
- # end
- # M.local_constant_names # => ["X"]
- #
- # This method is useful for forward compatibility, since Ruby 1.8 returns
- # constant names as strings, whereas 1.9 returns them as symbols.
- def local_constant_names
- ActiveSupport::Deprecation.warn 'Module#local_constant_names is deprecated, use Module#local_constants instead', caller
- local_constants.map { |c| c.to_s }
- end
end
diff --git a/activesupport/lib/active_support/core_ext/module/method_transplanting.rb b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
new file mode 100644
index 0000000000..b1097cc83b
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/module/method_transplanting.rb
@@ -0,0 +1,11 @@
+class Module
+ ###
+ # TODO: remove this after 1.9 support is dropped
+ def methods_transplantable? # :nodoc:
+ x = Module.new { def foo; end }
+ Module.new { define_method :bar, x.instance_method(:foo) }
+ true
+ rescue TypeError
+ false
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object.rb b/activesupport/lib/active_support/core_ext/object.rb
index ab01d7787a..f4f9152d6a 100644
--- a/activesupport/lib/active_support/core_ext/object.rb
+++ b/activesupport/lib/active_support/core_ext/object.rb
@@ -1,11 +1,14 @@
require 'active_support/core_ext/object/acts_like'
require 'active_support/core_ext/object/blank'
-require 'active_support/core_ext/object/deep_dup'
require 'active_support/core_ext/object/duplicable'
+require 'active_support/core_ext/object/deep_dup'
+require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/inclusion'
+
+require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/instance_variables'
-require 'active_support/core_ext/object/to_json'
+
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/object/to_param'
require 'active_support/core_ext/object/to_query'
-require 'active_support/core_ext/object/try'
require 'active_support/core_ext/object/with_options'
diff --git a/activesupport/lib/active_support/core_ext/object/acts_like.rb b/activesupport/lib/active_support/core_ext/object/acts_like.rb
index fcc8e50f06..3912cc5ace 100644
--- a/activesupport/lib/active_support/core_ext/object/acts_like.rb
+++ b/activesupport/lib/active_support/core_ext/object/acts_like.rb
@@ -1,9 +1,9 @@
class Object
# A duck-type assistant method. For example, Active Support extends Date
- # to define an acts_like_date? method, and extends Time to define
- # acts_like_time?. As a result, we can do "x.acts_like?(:time)" and
- # "x.acts_like?(:date)" to do duck-type-safe comparisons, since classes that
- # we want to act like Time simply need to define an acts_like_time? method.
+ # to define an <tt>acts_like_date?</tt> method, and extends Time to define
+ # <tt>acts_like_time?</tt>. As a result, we can do <tt>x.acts_like?(:time)</tt> and
+ # <tt>x.acts_like?(:date)</tt> to do duck-type-safe comparisons, since classes that
+ # we want to act like Time simply need to define an <tt>acts_like_time?</tt> method.
def acts_like?(duck)
respond_to? :"acts_like_#{duck}?"
end
diff --git a/activesupport/lib/active_support/core_ext/object/conversions.rb b/activesupport/lib/active_support/core_ext/object/conversions.rb
new file mode 100644
index 0000000000..540f7aadb0
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/conversions.rb
@@ -0,0 +1,4 @@
+require 'active_support/core_ext/object/to_param'
+require 'active_support/core_ext/object/to_query'
+require 'active_support/core_ext/array/conversions'
+require 'active_support/core_ext/hash/conversions'
diff --git a/activesupport/lib/active_support/core_ext/object/deep_dup.rb b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
index f55fbc282e..1d639f3af6 100644
--- a/activesupport/lib/active_support/core_ext/object/deep_dup.rb
+++ b/activesupport/lib/active_support/core_ext/object/deep_dup.rb
@@ -32,7 +32,7 @@ end
class Hash
# Returns a deep copy of hash.
#
- # hash = { a: { b: 'b' } }
+ # hash = { a: { b: 'b' } }
# dup = hash.deep_dup
# dup[:a][:c] = 'c'
#
diff --git a/activesupport/lib/active_support/core_ext/object/inclusion.rb b/activesupport/lib/active_support/core_ext/object/inclusion.rb
index 3fec465ec0..b5671f66d0 100644
--- a/activesupport/lib/active_support/core_ext/object/inclusion.rb
+++ b/activesupport/lib/active_support/core_ext/object/inclusion.rb
@@ -1,25 +1,15 @@
class Object
- # Returns true if this object is included in the argument(s). Argument must be
- # any object which responds to +#include?+ or optionally, multiple arguments can be passed in. Usage:
+ # Returns true if this object is included in the argument. Argument must be
+ # any object which responds to +#include?+. Usage:
#
- # characters = ['Konata', 'Kagami', 'Tsukasa']
- # 'Konata'.in?(characters) # => true
+ # characters = ["Konata", "Kagami", "Tsukasa"]
+ # "Konata".in?(characters) # => true
#
- # character = 'Konata'
- # character.in?('Konata', 'Kagami', 'Tsukasa') # => true
- #
- # This will throw an ArgumentError if a single argument is passed in and it doesn't respond
+ # This will throw an ArgumentError if the argument doesn't respond
# to +#include?+.
- def in?(*args)
- if args.length > 1
- args.include? self
- else
- another_object = args.first
- if another_object.respond_to? :include?
- another_object.include? self
- else
- raise ArgumentError.new 'The single parameter passed to #in? must respond to #include?'
- end
- end
+ def in?(another_object)
+ another_object.include?(self)
+ rescue NoMethodError
+ raise ArgumentError.new("The parameter passed to #in? must respond to #include?")
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/instance_variables.rb b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
index 40821fd619..755e1c6b16 100644
--- a/activesupport/lib/active_support/core_ext/object/instance_variables.rb
+++ b/activesupport/lib/active_support/core_ext/object/instance_variables.rb
@@ -13,7 +13,7 @@ class Object
Hash[instance_variables.map { |name| [name[1..-1], instance_variable_get(name)] }]
end
- # Returns an array of instance variable names including "@".
+ # Returns an array of instance variable names as strings including "@".
#
# class C
# def initialize(x, y)
diff --git a/activesupport/lib/active_support/core_ext/object/json.rb b/activesupport/lib/active_support/core_ext/object/json.rb
new file mode 100644
index 0000000000..554da1a2aa
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/object/json.rb
@@ -0,0 +1,218 @@
+# Hack to load json gem first so we can overwrite its to_json.
+require 'json'
+require 'bigdecimal'
+require 'time'
+
+# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
+# their default behavior. That said, we need to define the basic to_json method in all of them,
+# otherwise they will always use to_json gem implementation, which is backwards incompatible in
+# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
+# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
+[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
+ klass.class_eval do
+ # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
+ def to_json(options = nil)
+ ActiveSupport::JSON.encode(self, options)
+ end
+ end
+end
+
+class Object
+ def as_json(options = nil) #:nodoc:
+ if respond_to?(:to_hash)
+ to_hash
+ else
+ instance_values
+ end
+ end
+end
+
+class Struct #:nodoc:
+ def as_json(options = nil)
+ Hash[members.zip(values)]
+ end
+end
+
+class TrueClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+
+ def encode_json(encoder) #:nodoc:
+ to_s
+ end
+end
+
+class FalseClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+
+ def encode_json(encoder) #:nodoc:
+ to_s
+ end
+end
+
+class NilClass
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+
+ def encode_json(encoder) #:nodoc:
+ 'null'
+ end
+end
+
+class String
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+
+ def encode_json(encoder) #:nodoc:
+ encoder.escape(self)
+ end
+end
+
+class Symbol
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Numeric
+ def as_json(options = nil) #:nodoc:
+ self
+ end
+
+ def encode_json(encoder) #:nodoc:
+ to_s
+ end
+end
+
+class Float
+ # Encoding Infinity or NaN to JSON should return "null". The default returns
+ # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]').
+ def as_json(options = nil) #:nodoc:
+ finite? ? self : nil
+ end
+end
+
+class BigDecimal
+ # A BigDecimal would be naturally represented as a JSON number. Most libraries,
+ # however, parse non-integer JSON numbers directly as floats. Clients using
+ # those libraries would get in general a wrong number and no way to recover
+ # other than manually inspecting the string with the JSON code itself.
+ #
+ # That's why a JSON string is returned. The JSON literal is not numeric, but
+ # if the other end knows by contract that the data is supposed to be a
+ # BigDecimal, it still has the chance to post-process the string and get the
+ # real value.
+ #
+ # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to
+ # override this behavior.
+ def as_json(options = nil) #:nodoc:
+ if finite?
+ ActiveSupport.encode_big_decimal_as_string ? to_s : self
+ else
+ nil
+ end
+ end
+end
+
+class Regexp
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+module Enumerable
+ def as_json(options = nil) #:nodoc:
+ to_a.as_json(options)
+ end
+end
+
+class Range
+ def as_json(options = nil) #:nodoc:
+ to_s
+ end
+end
+
+class Array
+ def as_json(options = nil) #:nodoc:
+ # use encoder as a proxy to call as_json on all elements, to protect from circular references
+ encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
+ map { |v| encoder.as_json(v, options) }
+ end
+
+ def encode_json(encoder) #:nodoc:
+ # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly
+ "[#{map { |v| v.encode_json(encoder) } * ','}]"
+ end
+end
+
+class Hash
+ def as_json(options = nil) #:nodoc:
+ # create a subset of the hash by applying :only or :except
+ subset = if options
+ if attrs = options[:only]
+ slice(*Array(attrs))
+ elsif attrs = options[:except]
+ except(*Array(attrs))
+ else
+ self
+ end
+ else
+ self
+ end
+
+ # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
+ encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
+ Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
+ end
+
+ def encode_json(encoder) #:nodoc:
+ # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
+ # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
+
+ # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
+ # like Time/Date in their original (not jsonified) form, etc.
+
+ "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
+ end
+end
+
+class Time
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ xmlschema
+ else
+ %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
+ end
+ end
+end
+
+class Date
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ strftime("%Y-%m-%d")
+ else
+ strftime("%Y/%m/%d")
+ end
+ end
+end
+
+class DateTime
+ def as_json(options = nil) #:nodoc:
+ if ActiveSupport.use_standard_json_time_format
+ xmlschema
+ else
+ strftime('%Y/%m/%d %H:%M:%S %z')
+ end
+ end
+end
+
+class Process::Status
+ def as_json(options = nil)
+ { :exitstatus => exitstatus, :pid => pid }
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/object/to_json.rb b/activesupport/lib/active_support/core_ext/object/to_json.rb
index 83cc8066e7..3dcae6fc7f 100644
--- a/activesupport/lib/active_support/core_ext/object/to_json.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_json.rb
@@ -1,27 +1,5 @@
-# Hack to load json gem first so we can overwrite its to_json.
-begin
- require 'json'
-rescue LoadError
-end
+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.'
-# The JSON gem adds a few modules to Ruby core classes containing :to_json definition, overwriting
-# their default behavior. That said, we need to define the basic to_json method in all of them,
-# otherwise they will always use to_json gem implementation, which is backwards incompatible in
-# several cases (for instance, the JSON implementation for Hash does not work) with inheritance
-# and consequently classes as ActiveSupport::OrderedHash cannot be serialized to json.
-[Object, Array, FalseClass, Float, Hash, Integer, NilClass, String, TrueClass].each do |klass|
- klass.class_eval do
- # Dumps object in JSON (JavaScript Object Notation). See www.json.org for more info.
- def to_json(options = nil)
- ActiveSupport::JSON.encode(self, options)
- end
- end
-end
-
-module Process
- class Status
- def as_json(options = nil)
- { :exitstatus => exitstatus, :pid => pid }
- end
- end
-end
+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 0d5f3501e5..3b137ce6ae 100644
--- a/activesupport/lib/active_support/core_ext/object/to_param.rb
+++ b/activesupport/lib/active_support/core_ext/object/to_param.rb
@@ -53,6 +53,6 @@ class Hash
def to_param(namespace = nil)
collect do |key, value|
value.to_query(namespace ? "#{namespace}[#{key}]" : key)
- end.sort * '&'
+ end.sort! * '&'
end
end
diff --git a/activesupport/lib/active_support/core_ext/object/try.rb b/activesupport/lib/active_support/core_ext/object/try.rb
index 1079ddde98..48190e1e66 100644
--- a/activesupport/lib/active_support/core_ext/object/try.rb
+++ b/activesupport/lib/active_support/core_ext/object/try.rb
@@ -1,35 +1,43 @@
class Object
- # Invokes the public method identified by the symbol +method+, passing it any arguments
- # and/or the block specified, just like the regular Ruby <tt>Object#public_send</tt> does.
+ # Invokes the public method whose name goes as first argument just like
+ # +public_send+ does, except that if the receiver does not respond to it the
+ # call returns +nil+ rather than raising an exception.
#
- # *Unlike* that method however, a +NoMethodError+ exception will *not* be raised
- # and +nil+ will be returned instead, if the receiving object is a +nil+ object or NilClass.
+ # This method is defined to be able to write
#
- # This is also true if the receiving object does not implemented the tried method. It will
- # return +nil+ in that case as well.
- #
- # If try is called without a method to call, it will yield any given block with the object.
+ # @person.try(:name)
#
- # Please also note that +try+ is defined on +Object+, therefore it won't work with
- # subclasses of +BasicObject+. For example, using try with +SimpleDelegator+ will
- # delegate +try+ to target instead of calling it on delegator itself.
+ # instead of
#
- # Without +try+
- # @person && @person.name
- # or
# @person ? @person.name : nil
#
- # With +try+
- # @person.try(:name)
+ # +try+ returns +nil+ when called on +nil+ regardless of whether it responds
+ # to the method:
+ #
+ # nil.try(:to_i) # => nil, rather than 0
+ #
+ # Arguments and blocks are forwarded to the method if invoked:
+ #
+ # @posts.try(:each_slice, 2) do |a, b|
+ # ...
+ # end
+ #
+ # The number of arguments in the signature must match. If the object responds
+ # to the method the call is attempted and +ArgumentError+ is still raised
+ # otherwise.
#
- # +try+ also accepts arguments and/or a block, for the method it is trying
- # Person.try(:find, 1)
- # @people.try(:collect) {|p| p.name}
+ # If +try+ is called without arguments it yields the receiver to a given
+ # block unless it is +nil+:
#
- # Without a method argument try will yield to the block unless the receiver is nil.
- # @person.try { |p| "#{p.first_name} #{p.last_name}" }
+ # @person.try do |p|
+ # ...
+ # end
#
- # +try+ behaves like +Object#public_send+, unless called on +NilClass+.
+ # Please also note that +try+ is defined on +Object+, therefore it won't work
+ # with instances of classes that do not have +Object+ among their ancestors,
+ # like direct subclasses of +BasicObject+. For example, using +try+ with
+ # +SimpleDelegator+ will delegate +try+ to the target instead of calling it on
+ # delegator itself.
def try(*a, &b)
if a.empty? && block_given?
yield self
@@ -39,7 +47,7 @@ class Object
end
# Same as #try, but will raise a NoMethodError exception if the receiving is not nil and
- # does not implemented the tried method.
+ # does not implement the tried method.
def try!(*a, &b)
if a.empty? && block_given?
yield self
diff --git a/activesupport/lib/active_support/core_ext/proc.rb b/activesupport/lib/active_support/core_ext/proc.rb
deleted file mode 100644
index cd63740940..0000000000
--- a/activesupport/lib/active_support/core_ext/proc.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require "active_support/core_ext/kernel/singleton_class"
-require "active_support/deprecation"
-
-class Proc #:nodoc:
- def bind(object)
- ActiveSupport::Deprecation.warn 'Proc#bind is deprecated and will be removed in future versions', caller
-
- block, time = self, Time.now
- object.class_eval do
- method_name = "__bind_#{time.to_i}_#{time.usec}"
- define_method(method_name, &block)
- method = instance_method(method_name)
- remove_method(method_name)
- method
- end.bind(object)
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/range.rb b/activesupport/lib/active_support/core_ext/range.rb
index 1d8b1ede5a..9368e81235 100644
--- a/activesupport/lib/active_support/core_ext/range.rb
+++ b/activesupport/lib/active_support/core_ext/range.rb
@@ -1,3 +1,4 @@
require 'active_support/core_ext/range/conversions'
require 'active_support/core_ext/range/include_range'
require 'active_support/core_ext/range/overlaps'
+require 'active_support/core_ext/range/each'
diff --git a/activesupport/lib/active_support/core_ext/range/each.rb b/activesupport/lib/active_support/core_ext/range/each.rb
new file mode 100644
index 0000000000..d51ea2e944
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/range/each.rb
@@ -0,0 +1,24 @@
+require 'active_support/core_ext/module/aliasing'
+require 'active_support/core_ext/object/acts_like'
+
+class Range #:nodoc:
+
+ def each_with_time_with_zone(&block)
+ ensure_iteration_allowed
+ each_without_time_with_zone(&block)
+ end
+ alias_method_chain :each, :time_with_zone
+
+ def step_with_time_with_zone(n = 1, &block)
+ ensure_iteration_allowed
+ step_without_time_with_zone(n, &block)
+ end
+ alias_method_chain :step, :time_with_zone
+
+ private
+ def ensure_iteration_allowed
+ if first.acts_like?(:time)
+ raise TypeError, "can't iterate from #{first.class}"
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/range/include_range.rb b/activesupport/lib/active_support/core_ext/range/include_range.rb
index 3af66aaf2f..3a07401c8a 100644
--- a/activesupport/lib/active_support/core_ext/range/include_range.rb
+++ b/activesupport/lib/active_support/core_ext/range/include_range.rb
@@ -1,3 +1,5 @@
+require 'active_support/core_ext/module/aliasing'
+
class Range
# Extends the default Range#include? to support range comparisons.
# (1..5).include?(1..5) # => true
diff --git a/activesupport/lib/active_support/core_ext/string.rb b/activesupport/lib/active_support/core_ext/string.rb
index ad864765a3..c656db2c6c 100644
--- a/activesupport/lib/active_support/core_ext/string.rb
+++ b/activesupport/lib/active_support/core_ext/string.rb
@@ -4,10 +4,10 @@ require 'active_support/core_ext/string/multibyte'
require 'active_support/core_ext/string/starts_ends_with'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/string/access'
-require 'active_support/core_ext/string/xchar'
require 'active_support/core_ext/string/behavior'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/exclude'
require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/inquiry'
require 'active_support/core_ext/string/indent'
+require 'active_support/core_ext/string/zones'
diff --git a/activesupport/lib/active_support/core_ext/string/access.rb b/activesupport/lib/active_support/core_ext/string/access.rb
index 8fa8157d65..ee3b6d2b3f 100644
--- a/activesupport/lib/active_support/core_ext/string/access.rb
+++ b/activesupport/lib/active_support/core_ext/string/access.rb
@@ -59,7 +59,7 @@ class String
# str.from(0).to(-1) #=> "hello"
# str.from(1).to(-2) #=> "ell"
def to(position)
- self[0..position]
+ self[0, position + 1]
end
# Returns the first character. If a limit is supplied, returns a substring
diff --git a/activesupport/lib/active_support/core_ext/string/conversions.rb b/activesupport/lib/active_support/core_ext/string/conversions.rb
index 9b9d83932e..6691fc0995 100644
--- a/activesupport/lib/active_support/core_ext/string/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/string/conversions.rb
@@ -3,26 +3,35 @@ require 'active_support/core_ext/time/calculations'
class String
# Converts a string to a Time value.
- # The +form+ can be either :utc or :local (default :utc).
+ # The +form+ can be either :utc or :local (default :local).
#
- # The time is parsed using Date._parse method.
- # If +form+ is :local, then time is formatted using Time.zone
+ # The time is parsed using Time.parse method.
+ # If +form+ is :local, then the time is in the system timezone.
+ # If the date part is missing then the current date is used and if
+ # the time part is missing then it is assumed to be 00:00:00.
#
- # "3-2-2012".to_time # => 2012-02-03 00:00:00 UTC
- # "12:20".to_time # => ArgumentError: invalid date
- # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 UTC
- # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 UTC
- # "2012-12-13T06:12".to_time(:local) # => 2012-12-13 06:12:00 +0100
- def to_time(form = :utc)
- unless blank?
- date_values = ::Date._parse(self, false).
- values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset).
- map! { |arg| arg || 0 }
- date_values[6] *= 1000000
- offset = date_values.pop
+ # "13-12-2012".to_time # => 2012-12-13 00:00:00 +0100
+ # "06:12".to_time # => 2012-12-13 06:12:00 +0100
+ # "2012-12-13 06:12".to_time # => 2012-12-13 06:12:00 +0100
+ # "2012-12-13T06:12".to_time # => 2012-12-13 06:12:00 +0100
+ # "2012-12-13T06:12".to_time(:utc) # => 2012-12-13 05:12:00 UTC
+ # "12/13/2012".to_time # => ArgumentError: argument out of range
+ def to_time(form = :local)
+ parts = Date._parse(self, false)
+ return if parts.empty?
- ::Time.send("#{form}_time", *date_values) - offset
- end
+ now = Time.now
+ time = Time.new(
+ parts.fetch(:year, now.year),
+ parts.fetch(:mon, now.month),
+ parts.fetch(:mday, now.day),
+ parts.fetch(:hour, 0),
+ parts.fetch(:min, 0),
+ parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset, form == :utc ? 0 : nil)
+ )
+
+ form == :utc ? time.utc : time.getlocal
end
# Converts a string to a Date value.
@@ -32,11 +41,7 @@ class String
# "2012-12-13".to_date #=> Thu, 13 Dec 2012
# "12/13/2012".to_date #=> ArgumentError: invalid date
def to_date
- unless blank?
- date_values = ::Date._parse(self, false).values_at(:year, :mon, :mday)
-
- ::Date.new(*date_values)
- end
+ ::Date.parse(self, false) unless blank?
end
# Converts a string to a DateTime value.
@@ -46,13 +51,6 @@ class String
# "2012-12-13 12:50".to_datetime #=> Thu, 13 Dec 2012 12:50:00 +0000
# "12/13/2012".to_datetime #=> ArgumentError: invalid date
def to_datetime
- unless blank?
- date_values = ::Date._parse(self, false).
- values_at(:year, :mon, :mday, :hour, :min, :sec, :zone, :sec_fraction).
- map! { |arg| arg || 0 }
- date_values[5] += date_values.pop
-
- ::DateTime.civil(*date_values)
- end
+ ::DateTime.parse(self, false) unless blank?
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/encoding.rb b/activesupport/lib/active_support/core_ext/string/encoding.rb
deleted file mode 100644
index dc635ed6a5..0000000000
--- a/activesupport/lib/active_support/core_ext/string/encoding.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'active_support/deprecation'
-
-class String
- def encoding_aware?
- ActiveSupport::Deprecation.warn 'String#encoding_aware? is deprecated', caller
- true
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/string/filters.rb b/activesupport/lib/active_support/core_ext/string/filters.rb
index e05447439a..49c0df6026 100644
--- a/activesupport/lib/active_support/core_ext/string/filters.rb
+++ b/activesupport/lib/active_support/core_ext/string/filters.rb
@@ -3,6 +3,8 @@ class String
# the string, and then changing remaining consecutive whitespace
# groups into one space each.
#
+ # Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E).
+ #
# %{ Multi-line
# string }.squish # => "Multi-line string"
# " foo bar \n \t boo".squish # => "foo bar boo"
@@ -12,11 +14,22 @@ class String
# Performs a destructive squish. See String#squish.
def squish!
- strip!
- gsub!(/\s+/, ' ')
+ gsub!(/\A[[:space:]]+/, '')
+ gsub!(/[[:space:]]+\z/, '')
+ gsub!(/[[:space:]]+/, ' ')
self
end
+ # Returns a new string with all occurrences of the pattern removed. Short-hand for String#gsub(pattern, '').
+ def remove(pattern)
+ gsub pattern, ''
+ end
+
+ # Alters the string by removing all occurrences of the pattern. Short-hand for String#gsub!(pattern, '').
+ def remove!(pattern)
+ gsub! pattern, ''
+ end
+
# Truncates a given +text+ after a given <tt>length</tt> if +text+ is longer than <tt>length</tt>:
#
# 'Once upon a time in a world far far away'.truncate(27)
@@ -38,8 +51,8 @@ class String
def truncate(truncate_at, options = {})
return dup unless length > truncate_at
- options[:omission] ||= '...'
- length_with_room_for_omission = truncate_at - options[:omission].length
+ omission = options[:omission] || '...'
+ length_with_room_for_omission = truncate_at - omission.length
stop = \
if options[:separator]
rindex(options[:separator], length_with_room_for_omission) || length_with_room_for_omission
@@ -47,6 +60,6 @@ class String
length_with_room_for_omission
end
- self[0...stop] + options[:omission]
+ "#{self[0, stop]}#{omission}"
end
end
diff --git a/activesupport/lib/active_support/core_ext/string/indent.rb b/activesupport/lib/active_support/core_ext/string/indent.rb
index afc3032272..ce3a69cf5f 100644
--- a/activesupport/lib/active_support/core_ext/string/indent.rb
+++ b/activesupport/lib/active_support/core_ext/string/indent.rb
@@ -29,7 +29,7 @@ class String
# "foo\n\t\tbar".indent(2) # => "\t\tfoo\n\t\t\t\tbar"
# "foo".indent(2, "\t") # => "\t\tfoo"
#
- # While +indent_string+ is tipically one space or tab, it may be any string.
+ # While +indent_string+ is typically one space or tab, it may be any string.
#
# The third argument, +indent_empty_lines+, is a flag that says whether
# empty lines should be indented. Default is false.
diff --git a/activesupport/lib/active_support/core_ext/string/inflections.rb b/activesupport/lib/active_support/core_ext/string/inflections.rb
index 6edfcd7493..56e8a5f98d 100644
--- a/activesupport/lib/active_support/core_ext/string/inflections.rb
+++ b/activesupport/lib/active_support/core_ext/string/inflections.rb
@@ -38,10 +38,10 @@ class String
end
# The reverse of +pluralize+, returns the singular form of a word in a string.
- #
+ #
# If the optional parameter +locale+ is specified,
# the word will be singularized as a word of that language.
- # By default, this paramter is set to <tt>:en</tt>.
+ # By default, this parameter is set to <tt>:en</tt>.
# You must define your own inflection rules for languages other than English.
#
# 'posts'.singularize # => "post"
@@ -160,7 +160,7 @@ class String
# @person = Person.find(1)
# # => #<Person id: 1, name: "Donald E. Knuth">
#
- # <%= link_to(@person.name, person_path %>
+ # <%= link_to(@person.name, person_path) %>
# # => <a href="/person/1-donald-e-knuth">Donald E. Knuth</a>
def parameterize(sep = '-')
ActiveSupport::Inflector.parameterize(self, sep)
@@ -185,7 +185,7 @@ class String
#
# Singular names are not handled correctly.
#
- # 'business'.classify # => "Busines"
+ # 'business'.classify # => "Business"
def classify
ActiveSupport::Inflector.classify(self)
end
@@ -193,8 +193,8 @@ class String
# Capitalizes the first word, turns underscores into spaces, and strips '_id'.
# Like +titleize+, this is meant for creating pretty output.
#
- # 'employee_salary' # => "Employee salary"
- # 'author_id' # => "Author"
+ # 'employee_salary'.humanize # => "Employee salary"
+ # 'author_id'.humanize # => "Author"
def humanize
ActiveSupport::Inflector.humanize(self)
end
diff --git a/activesupport/lib/active_support/core_ext/string/multibyte.rb b/activesupport/lib/active_support/core_ext/string/multibyte.rb
index 4e7824ad74..a124202936 100644
--- a/activesupport/lib/active_support/core_ext/string/multibyte.rb
+++ b/activesupport/lib/active_support/core_ext/string/multibyte.rb
@@ -6,7 +6,7 @@ class String
#
# +mb_chars+ is a multibyte safe proxy for string methods.
#
- # In Ruby 1.8 and older it creates and returns an instance of the ActiveSupport::Multibyte::Chars class which
+ # It creates and returns an instance of the ActiveSupport::Multibyte::Chars class which
# encapsulates the original string. A Unicode safe version of all the String methods are defined on this proxy
# class. If the proxy class doesn't respond to a certain method, it's forwarded to the encapsulated string.
#
@@ -17,9 +17,6 @@ class String
# name.mb_chars.reverse.to_s # => "rellüM sualC"
# name.mb_chars.length # => 12
#
- # In Ruby 1.9 and newer +mb_chars+ returns +self+ because String is (mostly) encoding aware. This means that
- # it becomes easy to run one version of your code on multiple Ruby versions.
- #
# == Method chaining
#
# All the methods on the Chars proxy which normally return a string will return a Chars object. This allows
@@ -36,11 +33,7 @@ class String
# For more information about the methods defined on the Chars proxy see ActiveSupport::Multibyte::Chars. For
# information about how to change the default Multibyte behavior see ActiveSupport::Multibyte.
def mb_chars
- if ActiveSupport::Multibyte.proxy_class.consumes?(self)
- ActiveSupport::Multibyte.proxy_class.new(self)
- else
- self
- end
+ ActiveSupport::Multibyte.proxy_class.new(self)
end
def is_utf8?
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 5f85cedcf5..dc033ed11b 100644
--- a/activesupport/lib/active_support/core_ext/string/output_safety.rb
+++ b/activesupport/lib/active_support/core_ext/string/output_safety.rb
@@ -42,7 +42,7 @@ class ERB
# html_escape_once('&lt;&lt; Accept & Checkout')
# # => "&lt;&lt; Accept &amp; Checkout"
def html_escape_once(s)
- result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP) { |special| HTML_ESCAPE[special] }
+ result = s.to_s.gsub(HTML_ESCAPE_ONCE_REGEXP, HTML_ESCAPE)
s.html_safe? ? result.html_safe : result
end
@@ -60,7 +60,7 @@ class ERB
# json_escape('{"name":"john","created_at":"2010-04-28T01:39:31Z","id":1}')
# # => {name:john,created_at:2010-04-28T01:39:31Z,id:1}
def json_escape(s)
- result = s.to_s.gsub(JSON_ESCAPE_REGEXP) { |special| JSON_ESCAPE[special] }
+ result = s.to_s.gsub(JSON_ESCAPE_REGEXP, JSON_ESCAPE)
s.html_safe? ? result.html_safe : result
end
diff --git a/activesupport/lib/active_support/core_ext/string/xchar.rb b/activesupport/lib/active_support/core_ext/string/xchar.rb
deleted file mode 100644
index f9a5b4fb64..0000000000
--- a/activesupport/lib/active_support/core_ext/string/xchar.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-begin
- # See http://fast-xs.rubyforge.org/ by Eric Wong.
- # Also included with hpricot.
- require 'fast_xs'
-rescue LoadError
- # fast_xs extension unavailable
-else
- begin
- require 'builder'
- rescue LoadError
- # builder demands the first shot at defining String#to_xs
- end
-
- class String
- alias_method :original_xs, :to_xs if method_defined?(:to_xs)
- alias_method :to_xs, :fast_xs
- end
-end
diff --git a/activesupport/lib/active_support/core_ext/string/zones.rb b/activesupport/lib/active_support/core_ext/string/zones.rb
new file mode 100644
index 0000000000..510c884c18
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/string/zones.rb
@@ -0,0 +1,14 @@
+require 'active_support/core_ext/string/conversions'
+require 'active_support/core_ext/time/zones'
+
+class String
+ # Converts String to a TimeWithZone in the current zone if Time.zone or Time.zone_default
+ # is set, otherwise converts String to a Time via String#to_time
+ def in_time_zone(zone = ::Time.zone)
+ if zone
+ ::Time.find_zone!(zone).parse(self)
+ else
+ to_time
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/core_ext/struct.rb b/activesupport/lib/active_support/core_ext/struct.rb
new file mode 100644
index 0000000000..c2c30044f2
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/struct.rb
@@ -0,0 +1,6 @@
+# Backport of Struct#to_h from Ruby 2.0
+class Struct # :nodoc:
+ def to_h
+ Hash[members.zip(values)]
+ end
+end unless Struct.instance_methods.include?(:to_h)
diff --git a/activesupport/lib/active_support/core_ext/thread.rb b/activesupport/lib/active_support/core_ext/thread.rb
new file mode 100644
index 0000000000..e80f442973
--- /dev/null
+++ b/activesupport/lib/active_support/core_ext/thread.rb
@@ -0,0 +1,79 @@
+class Thread
+ LOCK = Mutex.new # :nodoc:
+
+ # Returns the value of a thread local variable that has been set. Note that
+ # these are different than fiber local values.
+ #
+ # Thread local values are carried along with threads, and do not respect
+ # fibers. For example:
+ #
+ # Thread.new {
+ # Thread.current.thread_variable_set("foo", "bar") # set a thread local
+ # Thread.current["foo"] = "bar" # set a fiber local
+ #
+ # Fiber.new {
+ # Fiber.yield [
+ # Thread.current.thread_variable_get("foo"), # get the thread local
+ # Thread.current["foo"], # get the fiber local
+ # ]
+ # }.resume
+ # }.join.value # => ['bar', nil]
+ #
+ # The value <tt>"bar"</tt> is returned for the thread local, where +nil+ is returned
+ # for the fiber local. The fiber is executed in the same thread, so the
+ # thread local values are available.
+ def thread_variable_get(key)
+ _locals[key.to_sym]
+ end
+
+ # Sets a thread local with +key+ to +value+. Note that these are local to
+ # threads, and not to fibers. Please see Thread#thread_variable_get for
+ # more information.
+ def thread_variable_set(key, value)
+ _locals[key.to_sym] = value
+ end
+
+ # Returns an an array of the names of the thread-local variables (as Symbols).
+ #
+ # thr = Thread.new do
+ # Thread.current.thread_variable_set(:cat, 'meow')
+ # Thread.current.thread_variable_set("dog", 'woof')
+ # end
+ # thr.join #=> #<Thread:0x401b3f10 dead>
+ # thr.thread_variables #=> [:dog, :cat]
+ #
+ # Note that these are not fiber local variables. Please see Thread#thread_variable_get
+ # for more details.
+ def thread_variables
+ _locals.keys
+ end
+
+ # Returns <tt>true</tt> if the given string (or symbol) exists as a
+ # thread-local variable.
+ #
+ # me = Thread.current
+ # me.thread_variable_set(:oliver, "a")
+ # me.thread_variable?(:oliver) #=> true
+ # me.thread_variable?(:stanley) #=> false
+ #
+ # Note that these are not fiber local variables. Please see Thread#thread_variable_get
+ # for more details.
+ def thread_variable?(key)
+ _locals.has_key?(key.to_sym)
+ end
+
+ def freeze
+ _locals.freeze
+ super
+ end
+
+ private
+
+ def _locals
+ if defined?(@_locals)
+ @_locals
+ else
+ LOCK.synchronize { @_locals ||= {} }
+ end
+ end
+end unless Thread.instance_methods.include?(:thread_variable_set)
diff --git a/activesupport/lib/active_support/core_ext/time/calculations.rb b/activesupport/lib/active_support/core_ext/time/calculations.rb
index e3665cd896..6e0af0db4d 100644
--- a/activesupport/lib/active_support/core_ext/time/calculations.rb
+++ b/activesupport/lib/active_support/core_ext/time/calculations.rb
@@ -25,36 +25,27 @@ class Time
end
end
- # Returns a new Time if requested year can be accommodated by Ruby's Time class
- # (i.e., if year is within either 1970..2038 or 1902..2038, depending on system architecture);
- # otherwise returns a DateTime.
- def time_with_datetime_fallback(utc_or_local, year, month=1, day=1, hour=0, min=0, sec=0, usec=0)
- time = ::Time.send(utc_or_local, year, month, day, hour, min, sec, usec)
-
- # This check is needed because Time.utc(y) returns a time object in the 2000s for 0 <= y <= 138.
- if time.year == year
- time
- else
- ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec)
- end
- rescue
- ::DateTime.civil_from_format(utc_or_local, year, month, day, hour, min, sec)
+ # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
+ def current
+ ::Time.zone ? ::Time.zone.now : ::Time.now
end
- # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:utc</tt>.
- def utc_time(*args)
- time_with_datetime_fallback(:utc, *args)
- end
+ # Layers additional behavior on Time.at so that ActiveSupport::TimeWithZone and DateTime
+ # instances can be used when called with a single argument
+ def at_with_coercion(*args)
+ return at_without_coercion(*args) if args.size != 1
- # Wraps class method +time_with_datetime_fallback+ with +utc_or_local+ set to <tt>:local</tt>.
- def local_time(*args)
- time_with_datetime_fallback(:local, *args)
- end
+ # Time.at can be called with a time or numerical value
+ time_or_number = args.first
- # Returns <tt>Time.zone.now</tt> when <tt>Time.zone</tt> or <tt>config.time_zone</tt> are set, otherwise just returns <tt>Time.now</tt>.
- def current
- ::Time.zone ? ::Time.zone.now : ::Time.now
+ if time_or_number.is_a?(ActiveSupport::TimeWithZone) || time_or_number.is_a?(DateTime)
+ at_without_coercion(time_or_number.to_f).getlocal
+ else
+ at_without_coercion(time_or_number)
+ end
end
+ alias_method :at_without_coercion, :at
+ alias_method :at, :at_with_coercion
end
# Seconds since midnight: Time.now.seconds_since_midnight
@@ -62,6 +53,15 @@ class Time
to_i - change(:hour => 0).to_i + (usec / 1.0e+6)
end
+ # Returns the number of seconds until 23:59:59.
+ #
+ # Time.new(2012, 8, 29, 0, 0, 0).seconds_until_end_of_day # => 86399
+ # Time.new(2012, 8, 29, 12, 34, 56).seconds_until_end_of_day # => 41103
+ # Time.new(2012, 8, 29, 23, 59, 59).seconds_until_end_of_day # => 0
+ def seconds_until_end_of_day
+ end_of_day.to_i - to_i
+ end
+
# Returns a new Time where one or more of the elements have been changed according
# to the +options+ parameter. The time options (<tt>:hour</tt>, <tt>:min</tt>,
# <tt>:sec</tt>, <tt>:usec</tt>) reset cascadingly, so if only the hour is passed,
@@ -135,13 +135,23 @@ class Time
# Returns a new Time representing the start of the day (0:00)
def beginning_of_day
- #(self - seconds_since_midnight).change(:usec => 0)
+ #(self - seconds_since_midnight).change(usec: 0)
change(:hour => 0)
end
alias :midnight :beginning_of_day
alias :at_midnight :beginning_of_day
alias :at_beginning_of_day :beginning_of_day
+ # Returns a new Time representing the middle of the day (12:00)
+ def middle_of_day
+ change(:hour => 12)
+ end
+ alias :midday :middle_of_day
+ alias :noon :middle_of_day
+ alias :at_midday :middle_of_day
+ alias :at_noon :middle_of_day
+ alias :at_middle_of_day :middle_of_day
+
# Returns a new Time representing the end of the day, 23:59:59.999999 (.999999999 in ruby1.9)
def end_of_day
change(
@@ -151,6 +161,7 @@ class Time
:usec => Rational(999999999, 1000)
)
end
+ alias :at_end_of_day :end_of_day
# Returns a new Time representing the start of the hour (x:00)
def beginning_of_hour
@@ -166,6 +177,22 @@ class Time
:usec => Rational(999999999, 1000)
)
end
+ alias :at_end_of_hour :end_of_hour
+
+ # Returns a new Time representing the start of the minute (x:xx:00)
+ def beginning_of_minute
+ change(:sec => 0)
+ end
+ alias :at_beginning_of_minute :beginning_of_minute
+
+ # Returns a new Time representing the end of the minute, x:xx:59.999999 (.999999999 in ruby1.9)
+ def end_of_minute
+ change(
+ :sec => 59,
+ :usec => Rational(999999999, 1000)
+ )
+ end
+ alias :at_end_of_minute :end_of_minute
# Returns a Range representing the whole day of the current time.
def all_day
diff --git a/activesupport/lib/active_support/core_ext/time/conversions.rb b/activesupport/lib/active_support/core_ext/time/conversions.rb
index 48654eb1cc..9fd26156c7 100644
--- a/activesupport/lib/active_support/core_ext/time/conversions.rb
+++ b/activesupport/lib/active_support/core_ext/time/conversions.rb
@@ -16,7 +16,8 @@ class Time
:rfc822 => lambda { |time|
offset_format = time.formatted_offset(false)
time.strftime("%a, %d %b %Y %H:%M:%S #{offset_format}")
- }
+ },
+ :iso8601 => lambda { |time| time.iso8601 }
}
# Converts to a formatted string. See DATE_FORMATS for builtin formats.
@@ -34,6 +35,7 @@ class Time
# time.to_formatted_s(:long) # => "January 18, 2007 06:10"
# time.to_formatted_s(:long_ordinal) # => "January 18th, 2007 06:10"
# time.to_formatted_s(:rfc822) # => "Thu, 18 Jan 2007 06:10:17 -0600"
+ # time.to_formatted_s(:iso8601) # => "2007-01-18T06:10:17-06:00"
#
# == Adding your own time formats to +to_formatted_s+
# You can add your own formats to the Time::DATE_FORMATS hash.
diff --git a/activesupport/lib/active_support/core_ext/time/marshal.rb b/activesupport/lib/active_support/core_ext/time/marshal.rb
index 1bf622d6a6..497c4c3fb8 100644
--- a/activesupport/lib/active_support/core_ext/time/marshal.rb
+++ b/activesupport/lib/active_support/core_ext/time/marshal.rb
@@ -24,7 +24,7 @@ if Time.local(2010).zone != Marshal.load(Marshal.dump(Time.local(2010))).zone
def _dump(*args)
obj = dup
obj.instance_variable_set('@_zone', zone)
- obj._dump_without_zone(*args)
+ obj.send :_dump_without_zone, *args
end
end
end
diff --git a/activesupport/lib/active_support/core_ext/time/zones.rb b/activesupport/lib/active_support/core_ext/time/zones.rb
index 139d48f59c..bbda04d60c 100644
--- a/activesupport/lib/active_support/core_ext/time/zones.rb
+++ b/activesupport/lib/active_support/core_ext/time/zones.rb
@@ -1,6 +1,8 @@
require 'active_support/time_with_zone'
+require 'active_support/core_ext/date_and_time/zones'
class Time
+ include DateAndTime::Zones
class << self
attr_accessor :zone_default
@@ -73,24 +75,4 @@ class Time
find_zone!(time_zone) rescue nil
end
end
-
- # Returns the simultaneous time in <tt>Time.zone</tt>.
- #
- # Time.zone = 'Hawaii' # => 'Hawaii'
- # Time.utc(2000).in_time_zone # => Fri, 31 Dec 1999 14:00:00 HST -10:00
- #
- # This method is similar to Time#localtime, except that it uses <tt>Time.zone</tt> as the local zone
- # instead of the operating system's time zone.
- #
- # You can also pass in a TimeZone instance or string that identifies a TimeZone as an argument,
- # and the conversion will be based on that zone instead of <tt>Time.zone</tt>.
- #
- # Time.utc(2000).in_time_zone('Alaska') # => Fri, 31 Dec 1999 15:00:00 AKST -09:00
- def in_time_zone(zone = ::Time.zone)
- if zone
- ActiveSupport::TimeWithZone.new(utc? ? self : getutc, ::Time.find_zone!(zone))
- else
- self
- end
- end
end
diff --git a/activesupport/lib/active_support/dependencies.rb b/activesupport/lib/active_support/dependencies.rb
index 42746582fa..19d4ff51d7 100644
--- a/activesupport/lib/active_support/dependencies.rb
+++ b/activesupport/lib/active_support/dependencies.rb
@@ -1,5 +1,6 @@
require 'set'
require 'thread'
+require 'thread_safe'
require 'pathname'
require 'active_support/core_ext/module/aliasing'
require 'active_support/core_ext/module/attribute_accessors'
@@ -7,6 +8,7 @@ require 'active_support/core_ext/module/introspection'
require 'active_support/core_ext/module/anonymous'
require 'active_support/core_ext/module/qualified_const'
require 'active_support/core_ext/object/blank'
+require 'active_support/core_ext/kernel/reporting'
require 'active_support/core_ext/load_error'
require 'active_support/core_ext/name_error'
require 'active_support/core_ext/string/starts_ends_with'
@@ -44,7 +46,7 @@ module ActiveSupport #:nodoc:
self.autoload_once_paths = []
# An array of qualified constant names that have been loaded. Adding a name
- # to this array will cause it to be unloaded the next time Dependencies are
+ # to this array will cause it to be unloaded the next time Dependencies are
# cleared.
mattr_accessor :autoloaded_constants
self.autoloaded_constants = []
@@ -197,9 +199,19 @@ module ActiveSupport #:nodoc:
Dependencies.require_or_load(file_name)
end
+ # Interprets a file using <tt>mechanism</tt> and marks its defined
+ # constants as autoloaded. <tt>file_name</tt> can be either a string or
+ # respond to <tt>to_path</tt>.
+ #
+ # Use this method in code that absolutely needs a certain constant to be
+ # defined at that point. A typical use case is to make constant name
+ # resolution deterministic for constants with the same relative name in
+ # different namespaces whose evaluation would depend on load order
+ # otherwise.
def require_dependency(file_name, message = "No such file to load -- %s")
+ file_name = file_name.to_path if file_name.respond_to?(:to_path)
unless file_name.is_a?(String)
- raise ArgumentError, "the file name must be a String -- you passed #{file_name.inspect}"
+ raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
end
Dependencies.depend_on(file_name, message)
@@ -212,7 +224,7 @@ module ActiveSupport #:nodoc:
yield
end
rescue Exception => exception # errors from loading file
- exception.blame_file! file
+ exception.blame_file! file if exception.respond_to? :blame_file!
raise
end
@@ -344,7 +356,7 @@ module ActiveSupport #:nodoc:
# Given +path+, a filesystem path to a ruby file, return an array of
# constant paths which would cause Dependencies to attempt to load this
- # file.
+ # file.
def loadable_constants_for_path(path, bases = autoload_paths)
path = $` if path =~ /\.rb\z/
expanded_path = File.expand_path(path)
@@ -394,7 +406,7 @@ module ActiveSupport #:nodoc:
# Attempt to autoload the provided module name by searching for a directory
# matching the expected path suffix. If found, the module is created and
# assigned to +into+'s constants with the name +const_name+. Provided that
- # the directory was loaded from a reloadable base path, it is added to the
+ # the directory was loaded from a reloadable base path, it is added to the
# set of constants that are to be unloaded.
def autoload_module!(into, const_name, qualified_name, path_suffix)
return nil unless base_path = autoloadable_module?(path_suffix)
@@ -415,7 +427,7 @@ module ActiveSupport #:nodoc:
def load_file(path, const_paths = loadable_constants_for_path(path))
log_call path, const_paths
const_paths = [const_paths].compact unless const_paths.is_a? Array
- parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || :Object }
+ parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }
result = nil
newly_defined_paths = new_constants_in(*parent_paths) do
@@ -458,7 +470,7 @@ module ActiveSupport #:nodoc:
if loaded.include?(expanded)
raise "Circular dependency detected while autoloading constant #{qualified_name}"
else
- require_or_load(expanded)
+ require_or_load(expanded, qualified_name)
raise LoadError, "Unable to autoload constant #{qualified_name}, expected #{file_path} to define it" unless from_mod.const_defined?(const_name, false)
return from_mod.const_get(const_name)
end
@@ -517,7 +529,7 @@ module ActiveSupport #:nodoc:
class ClassCache
def initialize
- @store = Hash.new
+ @store = ThreadSafe::Cache.new
end
def empty?
@@ -572,7 +584,6 @@ module ActiveSupport #:nodoc:
# Determine if the given constant has been automatically loaded.
def autoloaded?(desc)
- # No name => anonymous module.
return false if desc.is_a?(Module) && desc.anonymous?
name = to_constant_name desc
return false unless qualified_const_defined? name
@@ -634,26 +645,69 @@ module ActiveSupport #:nodoc:
when String then desc.sub(/^::/, '')
when Symbol then desc.to_s
when Module
- desc.name.presence ||
+ desc.name ||
raise(ArgumentError, "Anonymous modules have no name to be referenced by")
else raise TypeError, "Not a valid constant descriptor: #{desc.inspect}"
end
end
def remove_constant(const) #:nodoc:
- return false unless qualified_const_defined? const
+ # Normalize ::Foo, ::Object::Foo, Object::Foo, Object::Object::Foo, etc. as Foo.
+ normalized = const.to_s.sub(/\A::/, '')
+ normalized.sub!(/\A(Object::)+/, '')
+
+ constants = normalized.split('::')
+ to_remove = constants.pop
- # Normalize ::Foo, Foo, Object::Foo, and ::Object::Foo to Object::Foo
- names = const.to_s.sub(/^::(Object)?/, 'Object::').split("::")
- to_remove = names.pop
- parent = Inflector.constantize(names * '::')
+ if constants.empty?
+ parent = Object
+ else
+ # This method is robust to non-reachable constants.
+ #
+ # Non-reachable constants may be passed if some of the parents were
+ # autoloaded and already removed. It is easier to do a sanity check
+ # here than require the caller to be clever. We check the parent
+ # rather than the very const argument because we do not want to
+ # trigger Kernel#autoloads, see the comment below.
+ parent_name = constants.join('::')
+ return unless qualified_const_defined?(parent_name)
+ parent = constantize(parent_name)
+ end
log "removing constant #{const}"
- constantized = constantize(const)
- constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
- parent.instance_eval { remove_const to_remove }
- true
+ # In an autoloaded user.rb like this
+ #
+ # autoload :Foo, 'foo'
+ #
+ # class User < ActiveRecord::Base
+ # end
+ #
+ # we correctly register "Foo" as being autoloaded. But if the app does
+ # not use the "Foo" constant we need to be careful not to trigger
+ # loading "foo.rb" ourselves. While #const_defined? and #const_get? do
+ # require the file, #autoload? and #remove_const don't.
+ #
+ # We are going to remove the constant nonetheless ---which exists as
+ # far as Ruby is concerned--- because if the user removes the macro
+ # call from a class or module that were not autoloaded, as in the
+ # example above with Object, accessing to that constant must err.
+ unless parent.autoload?(to_remove)
+ begin
+ constantized = parent.const_get(to_remove, false)
+ rescue NameError
+ log "the constant #{const} is not reachable anymore, skipping"
+ return
+ else
+ constantized.before_remove_const if constantized.respond_to?(:before_remove_const)
+ end
+ end
+
+ begin
+ parent.instance_eval { remove_const to_remove }
+ rescue NameError
+ log "the constant #{const} is not reachable anymore, skipping"
+ end
end
protected
diff --git a/activesupport/lib/active_support/dependencies/autoload.rb b/activesupport/lib/active_support/dependencies/autoload.rb
index 9fc58a338f..c0dba5f7fd 100644
--- a/activesupport/lib/active_support/dependencies/autoload.rb
+++ b/activesupport/lib/active_support/dependencies/autoload.rb
@@ -34,7 +34,7 @@ module ActiveSupport
def autoload(const_name, path = @_at_path)
unless path
- full = [name, @_under_path, const_name.to_s, path].compact.join("::")
+ full = [name, @_under_path, const_name.to_s].compact.join("::")
path = Inflector.underscore(full)
end
diff --git a/activesupport/lib/active_support/deprecation.rb b/activesupport/lib/active_support/deprecation.rb
index 6c15fffc0f..ab16977bda 100644
--- a/activesupport/lib/active_support/deprecation.rb
+++ b/activesupport/lib/active_support/deprecation.rb
@@ -25,14 +25,14 @@ module ActiveSupport
include Reporting
include MethodWrapper
- # The version the deprecated behavior will be removed, by default.
+ # The version number in which the deprecated behavior will be removed, by default.
attr_accessor :deprecation_horizon
- # It accepts two parameters on initialization. The first is an version of library
- # and the second is an library name
+ # It accepts two parameters on initialization. The first is a version of library
+ # and the second is a library name
#
# ActiveSupport::Deprecation.new('2.0', 'MyLibrary')
- def initialize(deprecation_horizon = '4.1', gem_name = 'Rails')
+ def initialize(deprecation_horizon = '4.2', gem_name = 'Rails')
self.gem_name = gem_name
self.deprecation_horizon = deprecation_horizon
# By default, warnings are not silenced and debugging is off.
diff --git a/activesupport/lib/active_support/deprecation/behaviors.rb b/activesupport/lib/active_support/deprecation/behaviors.rb
index 90db180124..328b8c320a 100644
--- a/activesupport/lib/active_support/deprecation/behaviors.rb
+++ b/activesupport/lib/active_support/deprecation/behaviors.rb
@@ -1,14 +1,24 @@
require "active_support/notifications"
module ActiveSupport
+ class DeprecationException < StandardError
+ end
+
class Deprecation
# Default warning behaviors per Rails.env.
DEFAULT_BEHAVIORS = {
- :stderr => Proc.new { |message, callstack|
+ raise: ->(message, callstack) {
+ e = DeprecationException.new(message)
+ e.set_backtrace(callstack)
+ raise e
+ },
+
+ stderr: ->(message, callstack) {
$stderr.puts(message)
$stderr.puts callstack.join("\n ") if debug
},
- :log => Proc.new { |message, callstack|
+
+ log: ->(message, callstack) {
logger =
if defined?(Rails) && Rails.logger
Rails.logger
@@ -19,11 +29,13 @@ module ActiveSupport
logger.warn message
logger.debug callstack.join("\n ") if debug
},
- :notify => Proc.new { |message, callstack|
+
+ notify: ->(message, callstack) {
ActiveSupport::Notifications.instrument("deprecation.rails",
:message => message, :callstack => callstack)
},
- :silence => Proc.new { |message, callstack| }
+
+ silence: ->(message, callstack) {},
}
module Behavior
@@ -40,6 +52,7 @@ module ActiveSupport
#
# Available behaviors:
#
+ # [+raise+] Raise <tt>ActiveSupport::DeprecationException</tt>.
# [+stderr+] Log all deprecation warnings to +$stderr+.
# [+log+] Log all deprecation warnings to +Rails.logger+.
# [+notify+] Use +ActiveSupport::Notifications+ to notify +deprecation.rails+.
@@ -52,7 +65,7 @@ module ActiveSupport
# ActiveSupport::Deprecation.behavior = :stderr
# ActiveSupport::Deprecation.behavior = [:stderr, :log]
# ActiveSupport::Deprecation.behavior = MyCustomHandler
- # ActiveSupport::Deprecation.behavior = proc { |message, callstack|
+ # ActiveSupport::Deprecation.behavior = ->(message, callstack) {
# # custom stuff
# }
def behavior=(behavior)
diff --git a/activesupport/lib/active_support/deprecation/instance_delegator.rb b/activesupport/lib/active_support/deprecation/instance_delegator.rb
index ff240cb887..8472a58add 100644
--- a/activesupport/lib/active_support/deprecation/instance_delegator.rb
+++ b/activesupport/lib/active_support/deprecation/instance_delegator.rb
@@ -3,13 +3,13 @@ require 'active_support/core_ext/module/delegation'
module ActiveSupport
class Deprecation
- module InstanceDelegator
+ module InstanceDelegator # :nodoc:
def self.included(base)
base.extend(ClassMethods)
base.public_class_method :new
end
- module ClassMethods
+ module ClassMethods # :nodoc:
def include(included_module)
included_module.instance_methods.each { |m| method_added(m) }
super
@@ -21,4 +21,4 @@ module ActiveSupport
end
end
end
-end \ No newline at end of file
+end
diff --git a/activesupport/lib/active_support/deprecation/method_wrappers.rb b/activesupport/lib/active_support/deprecation/method_wrappers.rb
index d3907b03e5..cab8a1b14d 100644
--- a/activesupport/lib/active_support/deprecation/method_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/method_wrappers.rb
@@ -33,7 +33,7 @@ module ActiveSupport
method_names.each do |method_name|
target_module.alias_method_chain(method_name, :deprecation) do |target, punctuation|
target_module.send(:define_method, "#{target}_with_deprecation#{punctuation}") do |*args, &block|
- deprecator.deprecation_warning(method_name, options[method_name], caller)
+ deprecator.deprecation_warning(method_name, options[method_name])
send(:"#{target}_without_deprecation#{punctuation}", *args, &block)
end
end
diff --git a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
index 17e69c34a5..a03a66b96b 100644
--- a/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
+++ b/activesupport/lib/active_support/deprecation/proxy_wrappers.rb
@@ -25,12 +25,12 @@ module ActiveSupport
end
end
- # This DeprecatedObjectProxy transforms object to depracated object.
+ # This DeprecatedObjectProxy transforms object to deprecated object.
#
# @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!")
# @old_object = DeprecatedObjectProxy.new(Object.new, "Don't use this object anymore!", deprecator_instance)
#
- # When someone execute any method expect +inspect+ on proxy object this will
+ # When someone executes any method except +inspect+ on proxy object this will
# trigger +warn+ method on +deprecator_instance+.
#
# Default deprecator is <tt>ActiveSupport::Deprecation</tt>
@@ -52,7 +52,7 @@ module ActiveSupport
end
# This DeprecatedInstanceVariableProxy transforms instance variable to
- # depracated instance variable.
+ # deprecated instance variable.
#
# class Example
# def initialize(deprecator)
@@ -93,7 +93,7 @@ module ActiveSupport
end
end
- # This DeprecatedConstantProxy transforms constant to depracated constant.
+ # This DeprecatedConstantProxy transforms constant to deprecated constant.
#
# OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST')
# OLD_CONST = ActiveSupport::Deprecation::DeprecatedConstantProxy.new('OLD_CONST', 'NEW_CONST', deprecator_instance)
diff --git a/activesupport/lib/active_support/deprecation/reporting.rb b/activesupport/lib/active_support/deprecation/reporting.rb
index 1ce54d9381..a7d265d732 100644
--- a/activesupport/lib/active_support/deprecation/reporting.rb
+++ b/activesupport/lib/active_support/deprecation/reporting.rb
@@ -11,8 +11,10 @@ module ActiveSupport
#
# ActiveSupport::Deprecation.warn('something broke!')
# # => "DEPRECATION WARNING: something broke! (called from your_code.rb:1)"
- def warn(message = nil, callstack = caller)
+ def warn(message = nil, callstack = nil)
return if silenced
+
+ callstack ||= caller(2)
deprecation_message(callstack, message).tap do |m|
behavior.each { |b| b.call(m, callstack) }
end
@@ -34,7 +36,8 @@ module ActiveSupport
@silenced = old_silenced
end
- def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = caller)
+ def deprecation_warning(deprecated_method_name, message = nil, caller_backtrace = nil)
+ caller_backtrace ||= caller(2)
deprecated_method_warning(deprecated_method_name, message).tap do |msg|
warn(msg, caller_backtrace)
end
diff --git a/activesupport/lib/active_support/descendants_tracker.rb b/activesupport/lib/active_support/descendants_tracker.rb
index e2a8b4d4e3..27861e01d0 100644
--- a/activesupport/lib/active_support/descendants_tracker.rb
+++ b/activesupport/lib/active_support/descendants_tracker.rb
@@ -2,35 +2,50 @@ module ActiveSupport
# This module provides an internal implementation to track descendants
# which is faster than iterating through ObjectSpace.
module DescendantsTracker
- @@direct_descendants = Hash.new { |h, k| h[k] = [] }
+ @@direct_descendants = {}
- def self.direct_descendants(klass)
- @@direct_descendants[klass]
- end
+ class << self
+ def direct_descendants(klass)
+ @@direct_descendants[klass] || []
+ end
- def self.descendants(klass)
- @@direct_descendants[klass].inject([]) do |descendants, _klass|
- descendants << _klass
- descendants.concat _klass.descendants
+ def descendants(klass)
+ arr = []
+ accumulate_descendants(klass, arr)
+ arr
end
- end
- def self.clear
- if defined? ActiveSupport::Dependencies
- @@direct_descendants.each do |klass, descendants|
- if ActiveSupport::Dependencies.autoloaded?(klass)
- @@direct_descendants.delete(klass)
- else
- descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) }
+ def clear
+ if defined? ActiveSupport::Dependencies
+ @@direct_descendants.each do |klass, descendants|
+ if ActiveSupport::Dependencies.autoloaded?(klass)
+ @@direct_descendants.delete(klass)
+ else
+ descendants.reject! { |v| ActiveSupport::Dependencies.autoloaded?(v) }
+ end
end
+ else
+ @@direct_descendants.clear
+ end
+ end
+
+ # This is the only method that is not thread safe, but is only ever called
+ # during the eager loading phase.
+ def store_inherited(klass, descendant)
+ (@@direct_descendants[klass] ||= []) << descendant
+ end
+
+ private
+ def accumulate_descendants(klass, acc)
+ if direct_descendants = @@direct_descendants[klass]
+ acc.concat(direct_descendants)
+ direct_descendants.each { |direct_descendant| accumulate_descendants(direct_descendant, acc) }
end
- else
- @@direct_descendants.clear
end
end
def inherited(base)
- self.direct_descendants << base
+ DescendantsTracker.store_inherited(self, base)
super
end
diff --git a/activesupport/lib/active_support/duration.rb b/activesupport/lib/active_support/duration.rb
index 7e99646117..87b6407038 100644
--- a/activesupport/lib/active_support/duration.rb
+++ b/activesupport/lib/active_support/duration.rb
@@ -1,4 +1,4 @@
-require 'active_support/basic_object'
+require 'active_support/proxy_object'
require 'active_support/core_ext/array/conversions'
require 'active_support/core_ext/object/acts_like'
@@ -7,7 +7,7 @@ module ActiveSupport
# Time#advance, respectively. It mainly supports the methods on Numeric.
#
# 1.month.ago # equivalent to Time.now.advance(months: -1)
- class Duration < BasicObject
+ class Duration < ProxyObject
attr_accessor :value, :parts
def initialize(value, parts) #:nodoc:
@@ -70,13 +70,11 @@ module ActiveSupport
alias :until :ago
def inspect #:nodoc:
- consolidated = parts.inject(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }
- parts = [:years, :months, :days, :minutes, :seconds].map do |length|
- n = consolidated[length]
- "#{n} #{n == 1 ? length.to_s.singularize : length.to_s}" if n.nonzero?
- end.compact
- parts = ["0 seconds"] if parts.empty?
- parts.to_sentence(:locale => :en)
+ parts.
+ reduce(::Hash.new(0)) { |h,(l,r)| h[l] += r; h }.
+ sort_by {|unit, _ | [:years, :months, :days, :minutes, :seconds].index(unit)}.
+ map {|unit, val| "#{val} #{val == 1 ? unit.to_s.chop : unit.to_s}"}.
+ to_sentence(:locale => :en)
end
def as_json(options = nil) #:nodoc:
diff --git a/activesupport/lib/active_support/file_update_checker.rb b/activesupport/lib/active_support/file_update_checker.rb
index a6b9aa3503..78b627c286 100644
--- a/activesupport/lib/active_support/file_update_checker.rb
+++ b/activesupport/lib/active_support/file_update_checker.rb
@@ -68,7 +68,7 @@ module ActiveSupport
end
# Executes the given block and updates the latest watched files and
- # timestamp.
+ # timestamp.
def execute
@last_watched = watched
@last_update_at = updated_at(@last_watched)
@@ -92,7 +92,7 @@ module ActiveSupport
def watched
@watched || begin
- all = @files.select { |f| File.exists?(f) }
+ all = @files.select { |f| File.exist?(f) }
all.concat(Dir[@glob]) if @glob
all
end
@@ -115,7 +115,7 @@ module ActiveSupport
end
def compile_glob(hash)
- hash.freeze # Freeze so changes aren't accidently pushed
+ hash.freeze # Freeze so changes aren't accidentally pushed
return if hash.empty?
globs = hash.map do |key, value|
diff --git a/activesupport/lib/active_support/gzip.rb b/activesupport/lib/active_support/gzip.rb
index 6ef33ab683..b837c879bb 100644
--- a/activesupport/lib/active_support/gzip.rb
+++ b/activesupport/lib/active_support/gzip.rb
@@ -25,9 +25,9 @@ module ActiveSupport
end
# Compresses a string using gzip.
- def self.compress(source)
+ def self.compress(source, level=Zlib::DEFAULT_COMPRESSION, strategy=Zlib::DEFAULT_STRATEGY)
output = Stream.new
- gz = Zlib::GzipWriter.new(output)
+ gz = Zlib::GzipWriter.new(output, level, strategy)
gz.write(source)
gz.close
output.string
diff --git a/activesupport/lib/active_support/hash_with_indifferent_access.rb b/activesupport/lib/active_support/hash_with_indifferent_access.rb
index 0c78f1611f..3da99872c0 100644
--- a/activesupport/lib/active_support/hash_with_indifferent_access.rb
+++ b/activesupport/lib/active_support/hash_with_indifferent_access.rb
@@ -78,7 +78,7 @@ module ActiveSupport
end
def self.[](*args)
- new.merge(Hash[*args])
+ new.merge!(Hash[*args])
end
alias_method :regular_writer, :[]= unless method_defined?(:regular_writer)
@@ -91,7 +91,7 @@ module ActiveSupport
#
# This value can be later fetched using either +:key+ or +'key'+.
def []=(key, value)
- regular_writer(convert_key(key), convert_value(value))
+ regular_writer(convert_key(key), convert_value(value, for: :assignment))
end
alias_method :store, :[]=
@@ -204,6 +204,14 @@ module ActiveSupport
replace(reverse_merge( other_hash ))
end
+ # Replaces the contents of this hash with other_hash.
+ #
+ # h = { "a" => 100, "b" => 200 }
+ # h.replace({ "c" => 300, "d" => 400 }) #=> {"c"=>300, "d"=>400}
+ def replace(other_hash)
+ super(self.class.new_from_hash_copying_default(other_hash))
+ end
+
# Removes the specified key from the hash.
def delete(key)
super(convert_key(key))
@@ -215,13 +223,21 @@ module ActiveSupport
def deep_stringify_keys; dup end
undef :symbolize_keys!
undef :deep_symbolize_keys!
- def symbolize_keys; to_hash.symbolize_keys end
- def deep_symbolize_keys; to_hash.deep_symbolize_keys end
+ def symbolize_keys; to_hash.symbolize_keys! end
+ def deep_symbolize_keys; to_hash.deep_symbolize_keys! end
def to_options!; self end
+ def select(*args, &block)
+ dup.tap {|hash| hash.select!(*args, &block)}
+ end
+
# Convert to a regular hash with string keys.
def to_hash
- Hash.new(default).merge!(self)
+ _new_hash= {}
+ each do |key, value|
+ _new_hash[convert_key(key)] = convert_value(value, for: :to_hash)
+ end
+ Hash.new(default).merge!(_new_hash)
end
protected
@@ -229,12 +245,18 @@ module ActiveSupport
key.kind_of?(Symbol) ? key.to_s : key
end
- def convert_value(value)
+ def convert_value(value, options = {})
if value.is_a? Hash
- value.nested_under_indifferent_access
+ if options[:for] == :to_hash
+ value.to_hash
+ else
+ value.nested_under_indifferent_access
+ end
elsif value.is_a?(Array)
- value = value.dup if value.frozen?
- value.map! { |e| convert_value(e) }
+ unless options[:for] == :assignment
+ value = value.dup
+ end
+ value.map! { |e| convert_value(e, options) }
else
value
end
diff --git a/activesupport/lib/active_support/i18n.rb b/activesupport/lib/active_support/i18n.rb
index 188653bd9b..6cc98191d4 100644
--- a/activesupport/lib/active_support/i18n.rb
+++ b/activesupport/lib/active_support/i18n.rb
@@ -1,10 +1,13 @@
+require 'active_support/core_ext/hash/deep_merge'
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
begin
require 'i18n'
- require 'active_support/lazy_load_hooks'
rescue LoadError => e
$stderr.puts "The i18n gem is not available. Please add it to your Gemfile and run bundle install"
raise e
end
+require 'active_support/lazy_load_hooks'
ActiveSupport.run_load_hooks(:i18n)
I18n.load_path << "#{File.dirname(__FILE__)}/locale/en.yml"
diff --git a/activesupport/lib/active_support/inflections.rb b/activesupport/lib/active_support/inflections.rb
index ef882ebd09..4ea6abfa12 100644
--- a/activesupport/lib/active_support/inflections.rb
+++ b/activesupport/lib/active_support/inflections.rb
@@ -57,7 +57,6 @@ module ActiveSupport
inflect.irregular('child', 'children')
inflect.irregular('sex', 'sexes')
inflect.irregular('move', 'moves')
- inflect.irregular('cow', 'kine')
inflect.irregular('zombie', 'zombies')
inflect.uncountable(%w(equipment information rice money species series fish sheep jeans police))
diff --git a/activesupport/lib/active_support/inflector/inflections.rb b/activesupport/lib/active_support/inflector/inflections.rb
index af506d6f2e..c96debb93f 100644
--- a/activesupport/lib/active_support/inflector/inflections.rb
+++ b/activesupport/lib/active_support/inflector/inflections.rb
@@ -1,3 +1,4 @@
+require 'thread_safe'
require 'active_support/core_ext/array/prepend_and_append'
require 'active_support/i18n'
@@ -24,9 +25,10 @@ module ActiveSupport
# singularization rules that is runs. This guarantees that your rules run
# before any of the rules that may already have been loaded.
class Inflections
+ @__instance__ = ThreadSafe::Cache.new
+
def self.instance(locale = :en)
- @__instance__ ||= Hash.new { |h, k| h[k] = new }
- @__instance__[locale]
+ @__instance__[locale] ||= new
end
attr_reader :plurals, :singulars, :uncountables, :humans, :acronyms, :acronym_regex
@@ -36,7 +38,7 @@ module ActiveSupport
end
# Private, for the test suite.
- def initialize_dup(orig) # :nodoc:
+ def initialize_dup(orig) # :nodoc:
%w(plurals singulars uncountables humans acronyms acronym_regex).each do |scope|
instance_variable_set("@#{scope}", orig.send(scope).dup)
end
@@ -44,7 +46,7 @@ module ActiveSupport
# Specifies a new acronym. An acronym must be specified as it will appear
# in a camelized string. An underscore string that contains the acronym
- # will retain the acronym when passed to +camelize+, +humanize+, or
+ # will retain the acronym when passed to +camelize+, +humanize+, or
# +titleize+. A camelized string that contains the acronym will maintain
# the acronym when titleized or humanized, and will convert the acronym
# into a non-delimited single lowercase word when passed to +underscore+.
@@ -79,7 +81,7 @@ module ActiveSupport
#
# +acronym+ may be used to specify any word that contains an acronym or
# otherwise needs to maintain a non-standard capitalization. The only
- # restriction is that the word must begin with a capital letter.
+ # restriction is that the word must begin with a capital letter.
#
# acronym 'RESTful'
# underscore 'RESTful' #=> 'restful'
@@ -97,7 +99,7 @@ module ActiveSupport
end
# Specifies a new pluralization rule and its replacement. The rule can
- # either be a string or a regular expression. The replacement should
+ # either be a string or a regular expression. The replacement should
# always be a string that may include references to the matched data from
# the rule.
def plural(rule, replacement)
@@ -126,17 +128,29 @@ module ActiveSupport
def irregular(singular, plural)
@uncountables.delete(singular)
@uncountables.delete(plural)
- if singular[0,1].upcase == plural[0,1].upcase
- plural(Regexp.new("(#{singular[0,1]})#{singular[1..-1]}$", "i"), '\1' + plural[1..-1])
- plural(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + plural[1..-1])
- singular(Regexp.new("(#{plural[0,1]})#{plural[1..-1]}$", "i"), '\1' + singular[1..-1])
+
+ s0 = singular[0]
+ srest = singular[1..-1]
+
+ p0 = plural[0]
+ prest = plural[1..-1]
+
+ if s0.upcase == p0.upcase
+ plural(/(#{s0})#{srest}$/i, '\1' + prest)
+ plural(/(#{p0})#{prest}$/i, '\1' + prest)
+
+ singular(/(#{s0})#{srest}$/i, '\1' + srest)
+ singular(/(#{p0})#{prest}$/i, '\1' + srest)
else
- plural(Regexp.new("#{singular[0,1].upcase}(?i)#{singular[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
- plural(Regexp.new("#{singular[0,1].downcase}(?i)#{singular[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
- plural(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), plural[0,1].upcase + plural[1..-1])
- plural(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), plural[0,1].downcase + plural[1..-1])
- singular(Regexp.new("#{plural[0,1].upcase}(?i)#{plural[1..-1]}$"), singular[0,1].upcase + singular[1..-1])
- singular(Regexp.new("#{plural[0,1].downcase}(?i)#{plural[1..-1]}$"), singular[0,1].downcase + singular[1..-1])
+ plural(/#{s0.upcase}(?i)#{srest}$/, p0.upcase + prest)
+ plural(/#{s0.downcase}(?i)#{srest}$/, p0.downcase + prest)
+ plural(/#{p0.upcase}(?i)#{prest}$/, p0.upcase + prest)
+ plural(/#{p0.downcase}(?i)#{prest}$/, p0.downcase + prest)
+
+ singular(/#{s0.upcase}(?i)#{srest}$/, s0.upcase + srest)
+ singular(/#{s0.downcase}(?i)#{srest}$/, s0.downcase + srest)
+ singular(/#{p0.upcase}(?i)#{prest}$/, s0.upcase + srest)
+ singular(/#{p0.downcase}(?i)#{prest}$/, s0.downcase + srest)
end
end
diff --git a/activesupport/lib/active_support/inflector/methods.rb b/activesupport/lib/active_support/inflector/methods.rb
index 3910a2dc42..ffdb7b53c4 100644
--- a/activesupport/lib/active_support/inflector/methods.rb
+++ b/activesupport/lib/active_support/inflector/methods.rb
@@ -1,6 +1,5 @@
# encoding: utf-8
-require 'active_support/inflector/inflections'
require 'active_support/inflections'
module ActiveSupport
@@ -37,7 +36,7 @@ module ActiveSupport
# string.
#
# If passed an optional +locale+ parameter, the word will be
- # pluralized using rules defined for that language. By default,
+ # singularized using rules defined for that language. By default,
# this parameter is set to <tt>:en</tt>.
#
# 'posts'.singularize # => "post"
@@ -73,7 +72,9 @@ module ActiveSupport
else
string = string.sub(/^(?:#{inflections.acronym_regex}(?=\b|[A-Z_])|\w)/) { $&.downcase }
end
- string.gsub(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }.gsub('/', '::')
+ string.gsub!(/(?:_|(\/))([a-z\d]*)/i) { "#{$1}#{inflections.acronyms[$2] || $2.capitalize}" }
+ string.gsub!('/', '::')
+ string
end
# Makes an underscored, lowercase form from the expression in the string.
@@ -88,8 +89,7 @@ module ActiveSupport
#
# 'SSLError'.underscore.camelize # => "SslError"
def underscore(camel_cased_word)
- word = camel_cased_word.to_s.dup
- word.gsub!('::', '/')
+ word = camel_cased_word.to_s.gsub('::', '/')
word.gsub!(/(?:([A-Za-z\d])|^)(#{inflections.acronym_regex})(?=\b|[^a-z])/) { "#{$1}#{$1 && '_'}#{$2.downcase}" }
word.gsub!(/([A-Z\d]+)([A-Z][a-z])/,'\1_\2')
word.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
@@ -185,7 +185,7 @@ module ActiveSupport
#
# See also +demodulize+.
def deconstantize(path)
- path.to_s[0...(path.rindex('::') || 0)] # implementation based on the one in facets' Module#spacename
+ path.to_s[0, path.rindex('::') || 0] # implementation based on the one in facets' Module#spacename
end
# Creates a foreign key name from a class name.
@@ -219,7 +219,12 @@ module ActiveSupport
# unknown.
def constantize(camel_cased_word)
names = camel_cased_word.split('::')
- names.shift if names.empty? || names.first.empty?
+
+ # Trigger a builtin 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.
+ names.shift if names.size > 1 && names.first.empty?
names.inject(Object) do |constant, name|
if constant == Object
@@ -266,14 +271,12 @@ module ActiveSupport
# 'UnknownModule'.safe_constantize # => nil
# 'UnknownModule::Foo::Bar'.safe_constantize # => nil
def safe_constantize(camel_cased_word)
- begin
- constantize(camel_cased_word)
- rescue NameError => e
- raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ ||
- e.name.to_s == camel_cased_word.to_s
- rescue ArgumentError => e
- raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
- end
+ constantize(camel_cased_word)
+ rescue NameError => e
+ raise unless e.message =~ /(uninitialized constant|wrong constant name) #{const_regexp(camel_cased_word)}$/ ||
+ e.name.to_s == camel_cased_word.to_s
+ rescue ArgumentError => e
+ raise unless e.message =~ /not missing constant #{const_regexp(camel_cased_word)}\!$/
end
# Returns the suffix that should be added to a number to denote the position
@@ -286,10 +289,12 @@ module ActiveSupport
# ordinal(-11) # => "th"
# ordinal(-1021) # => "st"
def ordinal(number)
- if (11..13).include?(number.to_i.abs % 100)
+ abs_number = number.to_i.abs
+
+ if (11..13).include?(abs_number % 100)
"th"
else
- case number.to_i.abs % 10
+ case abs_number % 10
when 1; "st"
when 2; "nd"
when 3; "rd"
@@ -314,9 +319,14 @@ module ActiveSupport
private
# Mount a regular expression that will match part by part of the constant.
- # For instance, Foo::Bar::Baz will generate Foo(::Bar(::Baz)?)?
+ #
+ # const_regexp("Foo::Bar::Baz") # => /Foo(::Bar(::Baz)?)?/
+ # const_regexp("::") # => /::/
def const_regexp(camel_cased_word) #:nodoc:
parts = camel_cased_word.split("::")
+
+ return Regexp.escape(camel_cased_word) if parts.blank?
+
last = parts.pop
parts.reverse.inject(last) do |acc, part|
diff --git a/activesupport/lib/active_support/json/decoding.rb b/activesupport/lib/active_support/json/decoding.rb
index a4a32b2ad0..315c76199a 100644
--- a/activesupport/lib/active_support/json/decoding.rb
+++ b/activesupport/lib/active_support/json/decoding.rb
@@ -1,6 +1,6 @@
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/module/delegation'
-require 'multi_json'
+require 'json'
module ActiveSupport
# Look for and parse json strings that look like ISO 8601 times.
@@ -13,8 +13,15 @@ module ActiveSupport
#
# ActiveSupport::JSON.decode("{\"team\":\"rails\",\"players\":\"36\"}")
# => {"team" => "rails", "players" => "36"}
- def decode(json, options ={})
- data = MultiJson.load(json, options)
+ def decode(json, options = {})
+ if options.present?
+ raise ArgumentError, "In Rails 4.1, ActiveSupport::JSON.decode no longer " \
+ "accepts an options hash for MultiJSON. MultiJSON reached its end of life " \
+ "and has been removed."
+ end
+
+ data = ::JSON.parse(json, quirks_mode: true)
+
if ActiveSupport.parse_json_times
convert_dates_from(data)
else
@@ -22,23 +29,6 @@ module ActiveSupport
end
end
- def engine
- MultiJson.adapter
- end
- alias :backend :engine
-
- def engine=(name)
- MultiJson.use(name)
- end
- alias :backend= :engine=
-
- def with_backend(name)
- old_backend, self.backend = backend, name
- yield
- ensure
- self.backend = old_backend
- end
-
# Returns the class of the error that will be raised when there is an
# error in decoding JSON. Using this method means you won't directly
# depend on the ActiveSupport's JSON implementation, in case it changes
@@ -50,7 +40,7 @@ module ActiveSupport
# Rails.logger.warn("Attempted to decode invalid JSON: #{some_string}")
# end
def parse_error
- MultiJson::DecodeError
+ ::JSON::ParserError
end
private
diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb
index f65c831e04..4d73672047 100644
--- a/activesupport/lib/active_support/json/encoding.rb
+++ b/activesupport/lib/active_support/json/encoding.rb
@@ -1,13 +1,12 @@
-require 'active_support/core_ext/object/to_json'
+#encoding: us-ascii
+
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/module/delegation'
-require 'active_support/json/variable'
-require 'bigdecimal'
require 'active_support/core_ext/big_decimal/conversions' # for #to_s
require 'active_support/core_ext/hash/except'
require 'active_support/core_ext/hash/slice'
require 'active_support/core_ext/object/instance_variables'
-require 'time'
require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/date_time/conversions'
require 'active_support/core_ext/date/conversions'
@@ -65,7 +64,7 @@ module ActiveSupport
# they can detect circular references.
options.merge(:encoder => self)
else
- options
+ options.dup
end
end
@@ -98,13 +97,18 @@ module ActiveSupport
"\010" => '\b',
"\f" => '\f',
"\n" => '\n',
+ "\xe2\x80\xa8" => '\u2028',
+ "\xe2\x80\xa9" => '\u2029',
"\r" => '\r',
"\t" => '\t',
'"' => '\"',
'\\' => '\\\\',
'>' => '\u003E',
'<' => '\u003C',
- '&' => '\u0026' }
+ '&' => '\u0026',
+ "#{0xe2.chr}#{0x80.chr}#{0xa8.chr}" => '\u2028',
+ "#{0xe2.chr}#{0x80.chr}#{0xa9.chr}" => '\u2029',
+ }
class << self
# If true, use ISO 8601 format for dates and times. Otherwise, fall back
@@ -121,21 +125,15 @@ module ActiveSupport
def escape_html_entities_in_json=(value)
self.escape_regex = \
if @escape_html_entities_in_json = value
- /[\x00-\x1F"\\><&]/
+ /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\><&]/
else
- /[\x00-\x1F"\\]/
+ /\xe2\x80\xa8|\xe2\x80\xa9|[\x00-\x1F"\\]/
end
end
def escape(string)
string = string.encode(::Encoding::UTF_8, :undef => :replace).force_encoding(::Encoding::BINARY)
- json = string.
- gsub(escape_regex) { |s| ESCAPED_CHARS[s] }.
- gsub(/([\xC0-\xDF][\x80-\xBF]|
- [\xE0-\xEF][\x80-\xBF]{2}|
- [\xF0-\xF7][\x80-\xBF]{3})+/nx) { |s|
- s.unpack("U*").pack("n*").unpack("H*")[0].gsub(/.{4}/n, '\\\\u\&')
- }
+ json = string.gsub(escape_regex) { |s| ESCAPED_CHARS[s] }
json = %("#{json}")
json.force_encoding(::Encoding::UTF_8)
json
@@ -148,197 +146,3 @@ module ActiveSupport
end
end
end
-
-class Object
- def as_json(options = nil) #:nodoc:
- if respond_to?(:to_hash)
- to_hash
- else
- instance_values
- end
- end
-end
-
-class Struct #:nodoc:
- def as_json(options = nil)
- Hash[members.zip(values)]
- end
-end
-
-class TrueClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class FalseClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class NilClass
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- 'null'
- end
-end
-
-class String
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- encoder.escape(self)
- end
-end
-
-class Symbol
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-class Numeric
- def as_json(options = nil) #:nodoc:
- self
- end
-
- def encode_json(encoder) #:nodoc:
- to_s
- end
-end
-
-class Float
- # Encoding Infinity or NaN to JSON should return "null". The default returns
- # "Infinity" or "NaN" which breaks parsing the JSON. E.g. JSON.parse('[NaN]').
- def as_json(options = nil) #:nodoc:
- finite? ? self : nil
- end
-end
-
-class BigDecimal
- # A BigDecimal would be naturally represented as a JSON number. Most libraries,
- # however, parse non-integer JSON numbers directly as floats. Clients using
- # those libraries would get in general a wrong number and no way to recover
- # other than manually inspecting the string with the JSON code itself.
- #
- # That's why a JSON string is returned. The JSON literal is not numeric, but
- # if the other end knows by contract that the data is supposed to be a
- # BigDecimal, it still has the chance to post-process the string and get the
- # real value.
- #
- # Use <tt>ActiveSupport.use_standard_json_big_decimal_format = true</tt> to
- # override this behaviour.
- def as_json(options = nil) #:nodoc:
- if finite?
- ActiveSupport.encode_big_decimal_as_string ? to_s : self
- else
- nil
- end
- end
-end
-
-class Regexp
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-module Enumerable
- def as_json(options = nil) #:nodoc:
- to_a.as_json(options)
- end
-end
-
-class Range
- def as_json(options = nil) #:nodoc:
- to_s
- end
-end
-
-class Array
- def as_json(options = nil) #:nodoc:
- # use encoder as a proxy to call as_json on all elements, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- map { |v| encoder.as_json(v, options) }
- end
-
- def encode_json(encoder) #:nodoc:
- # we assume here that the encoder has already run as_json on self and the elements, so we run encode_json directly
- "[#{map { |v| v.encode_json(encoder) } * ','}]"
- end
-end
-
-class Hash
- def as_json(options = nil) #:nodoc:
- # create a subset of the hash by applying :only or :except
- subset = if options
- if attrs = options[:only]
- slice(*Array(attrs))
- elsif attrs = options[:except]
- except(*Array(attrs))
- else
- self
- end
- else
- self
- end
-
- # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
- encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::Encoder.new(options)
- Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
- end
-
- def encode_json(encoder) #:nodoc:
- # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
- # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
-
- # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
- # like Time/Date in their original (not jsonified) form, etc.
-
- "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
- end
-end
-
-class Time
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- %(#{strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
- end
- end
-end
-
-class Date
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- strftime("%Y-%m-%d")
- else
- strftime("%Y/%m/%d")
- end
- end
-end
-
-class DateTime
- def as_json(options = nil) #:nodoc:
- if ActiveSupport.use_standard_json_time_format
- xmlschema
- else
- strftime('%Y/%m/%d %H:%M:%S %z')
- end
- end
-end
diff --git a/activesupport/lib/active_support/json/variable.rb b/activesupport/lib/active_support/json/variable.rb
deleted file mode 100644
index 8af661a795..0000000000
--- a/activesupport/lib/active_support/json/variable.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require 'active_support/deprecation'
-
-module ActiveSupport
- module JSON
- # Deprecated: A string that returns itself as its JSON-encoded form.
- class Variable < String
- def initialize(*args)
- ActiveSupport::Deprecation.warn 'ActiveSupport::JSON::Variable is deprecated and will be removed in Rails 4.1. ' \
- 'For your own custom JSON literals, define #as_json and #encode_json yourself.'
- super
- end
-
- def as_json(options = nil) self end #:nodoc:
- def encode_json(encoder) self end #:nodoc:
- end
- end
-end
diff --git a/activesupport/lib/active_support/key_generator.rb b/activesupport/lib/active_support/key_generator.rb
index 04d170f801..598c46bce5 100644
--- a/activesupport/lib/active_support/key_generator.rb
+++ b/activesupport/lib/active_support/key_generator.rb
@@ -1,9 +1,10 @@
+require 'thread_safe'
require 'openssl'
module ActiveSupport
# KeyGenerator is a simple wrapper around OpenSSL's implementation of PBKDF2
# It can be used to derive a number of keys for various purposes from a given secret.
- # This lets rails applications have a single secure secret, but avoid reusing that
+ # This lets Rails applications have a single secure secret, but avoid reusing that
# key in multiple incompatible contexts.
class KeyGenerator
def initialize(secret, options = {})
@@ -20,4 +21,55 @@ module ActiveSupport
OpenSSL::PKCS5.pbkdf2_hmac_sha1(@secret, salt, @iterations, key_size)
end
end
+
+ # CachingKeyGenerator is a wrapper around KeyGenerator which allows users to avoid
+ # re-executing the key generation process when it's called using the same salt and
+ # key_size
+ class CachingKeyGenerator
+ def initialize(key_generator)
+ @key_generator = key_generator
+ @cache_keys = ThreadSafe::Cache.new
+ end
+
+ # Returns a derived key suitable for use. The default key_size is chosen
+ # to be compatible with the default settings of ActiveSupport::MessageVerifier.
+ # i.e. OpenSSL::Digest::SHA1#block_length
+ def generate_key(salt, key_size=64)
+ @cache_keys["#{salt}#{key_size}"] ||= @key_generator.generate_key(salt, key_size)
+ end
+ end
+
+ class LegacyKeyGenerator # :nodoc:
+ SECRET_MIN_LENGTH = 30 # Characters
+
+ def initialize(secret)
+ ensure_secret_secure(secret)
+ @secret = secret
+ end
+
+ def generate_key(salt)
+ @secret
+ end
+
+ private
+
+ # To prevent users from using something insecure like "Password" we make sure that the
+ # secret they've provided is at least 30 characters in length.
+ def ensure_secret_secure(secret)
+ if secret.blank?
+ raise ArgumentError, "A secret is required to generate an " +
+ "integrity hash for cookie session data. Use " +
+ "config.secret_key_base = \"some secret phrase of at " +
+ "least #{SECRET_MIN_LENGTH} characters\"" +
+ "in config/initializers/secret_token.rb"
+ end
+
+ if secret.length < SECRET_MIN_LENGTH
+ raise ArgumentError, "Secret should be something secure, " +
+ "like \"#{SecureRandom.hex(16)}\". The value you " +
+ "provided, \"#{secret}\", is shorter than the minimum length " +
+ "of #{SECRET_MIN_LENGTH} characters"
+ end
+ end
+ end
end
diff --git a/activesupport/lib/active_support/lazy_load_hooks.rb b/activesupport/lib/active_support/lazy_load_hooks.rb
index e489512531..e2b8f0f648 100644
--- a/activesupport/lib/active_support/lazy_load_hooks.rb
+++ b/activesupport/lib/active_support/lazy_load_hooks.rb
@@ -1,5 +1,5 @@
module ActiveSupport
- # lazy_load_hooks allows rails to lazily load a lot of components and thus
+ # lazy_load_hooks allows Rails to lazily load a lot of components and thus
# making the app boot faster. Because of this feature now there is no need to
# require <tt>ActiveRecord::Base</tt> at boot time purely to apply
# configuration. Instead a hook is registered that applies configuration once
diff --git a/activesupport/lib/active_support/locale/en.yml b/activesupport/lib/active_support/locale/en.yml
index f4900dc935..a4563ace8f 100644
--- a/activesupport/lib/active_support/locale/en.yml
+++ b/activesupport/lib/active_support/locale/en.yml
@@ -16,9 +16,9 @@ en:
abbr_month_names: [~, Jan, Feb, Mar, Apr, May, Jun, Jul, Aug, Sep, Oct, Nov, Dec]
# Used in date_select and datetime_select.
order:
- - :year
- - :month
- - :day
+ - year
+ - month
+ - day
time:
formats:
diff --git a/activesupport/lib/active_support/log_subscriber.rb b/activesupport/lib/active_support/log_subscriber.rb
index a58afc6b9d..e95dc5a866 100644
--- a/activesupport/lib/active_support/log_subscriber.rb
+++ b/activesupport/lib/active_support/log_subscriber.rb
@@ -1,5 +1,6 @@
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/class/attribute'
+require 'active_support/subscriber'
module ActiveSupport
# ActiveSupport::LogSubscriber is an object set to consume
@@ -33,7 +34,7 @@ module ActiveSupport
# Log subscriber also has some helpers to deal with logging and automatically
# flushes all logs when the request finishes (via action_dispatch.callback
# notification) in a Rails environment.
- class LogSubscriber
+ class LogSubscriber < Subscriber
# Embed in a String to clear all previous ANSI sequences.
CLEAR = "\e[0m"
BOLD = "\e[1m"
@@ -53,24 +54,15 @@ module ActiveSupport
class << self
def logger
- @logger ||= Rails.logger if defined?(Rails)
- @logger
+ @logger ||= if defined?(Rails) && Rails.respond_to?(:logger)
+ Rails.logger
+ end
end
attr_writer :logger
- def attach_to(namespace, log_subscriber=new, notifier=ActiveSupport::Notifications)
- log_subscribers << log_subscriber
-
- log_subscriber.public_methods(false).each do |event|
- next if %w{ start finish }.include?(event.to_s)
-
- notifier.subscribe("#{event}.#{namespace}", log_subscriber)
- end
- end
-
def log_subscribers
- @@log_subscribers ||= []
+ subscribers
end
# Flush all log_subscribers' logger.
@@ -79,39 +71,18 @@ module ActiveSupport
end
end
- def initialize
- @queue_key = [self.class.name, object_id].join "-"
- super
- end
-
def logger
LogSubscriber.logger
end
def start(name, id, payload)
- return unless logger
-
- e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload)
- parent = event_stack.last
- parent << e if parent
-
- event_stack.push e
+ super if logger
end
def finish(name, id, payload)
- return unless logger
-
- finished = Time.now
- event = event_stack.pop
- event.end = finished
- event.payload.merge!(payload)
-
- method = name.split('.').first
- begin
- send(method, event)
- rescue Exception => e
- logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
- end
+ super if logger
+ rescue Exception => e
+ logger.error "Could not log #{name.inspect} event. #{e.class}: #{e.message} #{e.backtrace}"
end
protected
@@ -134,11 +105,5 @@ module ActiveSupport
bold = bold ? BOLD : ""
"#{bold}#{color}#{text}#{CLEAR}"
end
-
- private
-
- def event_stack
- Thread.current[@queue_key] ||= []
- end
end
end
diff --git a/activesupport/lib/active_support/log_subscriber/test_helper.rb b/activesupport/lib/active_support/log_subscriber/test_helper.rb
index 63dad7e01a..75f353f62c 100644
--- a/activesupport/lib/active_support/log_subscriber/test_helper.rb
+++ b/activesupport/lib/active_support/log_subscriber/test_helper.rb
@@ -1,5 +1,5 @@
require 'active_support/log_subscriber'
-require 'active_support/buffered_logger'
+require 'active_support/logger'
require 'active_support/notifications'
module ActiveSupport
@@ -15,7 +15,7 @@ module ActiveSupport
# end
#
# def test_basic_query_logging
- # Developer.all
+ # Developer.all.to_a
# wait
# assert_equal 1, @logger.logged(:debug).size
# assert_match(/Developer Load/, @logger.logged(:debug).last)
diff --git a/activesupport/lib/active_support/logger.rb b/activesupport/lib/active_support/logger.rb
index 65202f99fc..4a55bbb350 100644
--- a/activesupport/lib/active_support/logger.rb
+++ b/activesupport/lib/active_support/logger.rb
@@ -1,7 +1,11 @@
+require 'active_support/core_ext/class/attribute_accessors'
+require 'active_support/logger_silence'
require 'logger'
module ActiveSupport
class Logger < ::Logger
+ include LoggerSilence
+
# Broadcasts logs to multiple loggers.
def self.broadcast(logger) # :nodoc:
Module.new do
diff --git a/activesupport/lib/active_support/logger_silence.rb b/activesupport/lib/active_support/logger_silence.rb
new file mode 100644
index 0000000000..a8efdef944
--- /dev/null
+++ b/activesupport/lib/active_support/logger_silence.rb
@@ -0,0 +1,24 @@
+require 'active_support/concern'
+
+module LoggerSilence
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_accessor :silencer
+ self.silencer = true
+ end
+
+ # Silences the logger for the duration of the block.
+ def silence(temporary_level = Logger::ERROR)
+ if silencer
+ begin
+ old_logger_level, self.level = level, temporary_level
+ yield self
+ ensure
+ self.level = old_logger_level
+ end
+ else
+ yield self
+ end
+ end
+end \ No newline at end of file
diff --git a/activesupport/lib/active_support/message_encryptor.rb b/activesupport/lib/active_support/message_encryptor.rb
index 580267708c..bffdfc6201 100644
--- a/activesupport/lib/active_support/message_encryptor.rb
+++ b/activesupport/lib/active_support/message_encryptor.rb
@@ -1,5 +1,6 @@
require 'openssl'
require 'base64'
+require 'active_support/core_ext/array/extract_options'
module ActiveSupport
# MessageEncryptor is a simple way to encrypt values which get stored
@@ -11,10 +12,11 @@ module ActiveSupport
# This can be used in situations similar to the <tt>MessageVerifier</tt>, but
# where you don't want users to be able to determine the value of the payload.
#
- # key = OpenSSL::Digest::SHA256.new('password').digest # => "\x89\xE0\x156\xAC..."
- # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
- # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
- # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
+ # salt = SecureRandom.random_bytes(64)
+ # key = ActiveSupport::KeyGenerator.new('password').generate_key(salt) # => "\x89\xE0\x156\xAC..."
+ # crypt = ActiveSupport::MessageEncryptor.new(key) # => #<ActiveSupport::MessageEncryptor ...>
+ # encrypted_data = crypt.encrypt_and_sign('my secret data') # => "NlFBTTMwOUV5UlA1QlNEN2xkY2d6eThYWWh..."
+ # crypt.decrypt_and_verify(encrypted_data) # => "my secret data"
class MessageEncryptor
module NullSerializer #:nodoc:
def self.load(value)
@@ -27,27 +29,30 @@ module ActiveSupport
end
class InvalidMessage < StandardError; end
- OpenSSLCipherError = OpenSSL::Cipher.const_defined?(:CipherError) ? OpenSSL::Cipher::CipherError : OpenSSL::CipherError
+ OpenSSLCipherError = OpenSSL::Cipher::CipherError
# Initialize a new MessageEncryptor. +secret+ must be at least as long as
# the cipher key size. For the default 'aes-256-cbc' cipher, this is 256
# bits. If you are using a user-entered secret, you can generate a suitable
# key with <tt>OpenSSL::Digest::SHA256.new(user_secret).digest</tt> or
- # similar.
+ # similar.
#
# Options:
# * <tt>:cipher</tt> - Cipher to use. Can be any cipher returned by
# <tt>OpenSSL::Cipher.ciphers</tt>. Default is 'aes-256-cbc'.
# * <tt>:serializer</tt> - Object serializer to use. Default is +Marshal+.
- def initialize(secret, options = {})
+ def initialize(secret, *signature_key_or_options)
+ options = signature_key_or_options.extract_options!
+ sign_secret = signature_key_or_options.first
@secret = secret
+ @sign_secret = sign_secret
@cipher = options[:cipher] || 'aes-256-cbc'
- @verifier = MessageVerifier.new(@secret, :serializer => NullSerializer)
+ @verifier = MessageVerifier.new(@sign_secret || @secret, :serializer => NullSerializer)
@serializer = options[:serializer] || Marshal
end
# Encrypt and sign a message. We need to sign the message in order to avoid
- # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
+ # padding attacks. Reference: http://www.limited-entropy.com/padding-oracle-attacks.
def encrypt_and_sign(value)
verifier.generate(_encrypt(value))
end
@@ -62,12 +67,11 @@ module ActiveSupport
def _encrypt(value)
cipher = new_cipher
- # Rely on OpenSSL for the initialization vector
- iv = cipher.random_iv
-
cipher.encrypt
cipher.key = @secret
- cipher.iv = iv
+
+ # Rely on OpenSSL for the initialization vector
+ iv = cipher.random_iv
encrypted_data = cipher.update(@serializer.dump(value))
encrypted_data << cipher.final
diff --git a/activesupport/lib/active_support/message_verifier.rb b/activesupport/lib/active_support/message_verifier.rb
index 140b6ca08d..e0cd92ae3c 100644
--- a/activesupport/lib/active_support/message_verifier.rb
+++ b/activesupport/lib/active_support/message_verifier.rb
@@ -6,7 +6,7 @@ module ActiveSupport
# signed to prevent tampering.
#
# This is useful for cases like remember-me tokens and auto-unsubscribe links
- # where the session store isn't suitable or available.
+ # where the session store isn't suitable or available.
#
# Remember Me:
# cookies[:remember_me] = @verifier.generate([@user.id, 2.weeks.from_now])
@@ -19,10 +19,10 @@ module ActiveSupport
# end
#
# By default it uses Marshal to serialize the message. If you want to use
- # another serialization method, you can set the serializer attribute to
- # something that responds to dump and load, e.g.:
+ # another serialization method, you can set the serializer in the options
+ # hash upon initialization:
#
- # @verifier.serializer = YAML
+ # @verifier = ActiveSupport::MessageVerifier.new('s3Krit', serializer: YAML)
class MessageVerifier
class InvalidSignature < StandardError; end
diff --git a/activesupport/lib/active_support/multibyte.rb b/activesupport/lib/active_support/multibyte.rb
index 1bf8e618ad..ffebd9a60b 100644
--- a/activesupport/lib/active_support/multibyte.rb
+++ b/activesupport/lib/active_support/multibyte.rb
@@ -5,7 +5,7 @@ module ActiveSupport #:nodoc:
# The proxy class returned when calling mb_chars. You can use this accessor
# to configure your own proxy class so you can support other encodings. See
- # the ActiveSupport::Multibyte::Chars implementation for an example how to
+ # the ActiveSupport::Multibyte::Chars implementation for an example how to
# do this.
#
# ActiveSupport::Multibyte.proxy_class = CharsForUTF32
diff --git a/activesupport/lib/active_support/multibyte/chars.rb b/activesupport/lib/active_support/multibyte/chars.rb
index a42e7f6542..3c0cf9f137 100644
--- a/activesupport/lib/active_support/multibyte/chars.rb
+++ b/activesupport/lib/active_support/multibyte/chars.rb
@@ -56,11 +56,10 @@ module ActiveSupport #:nodoc:
# Forward all undefined methods to the wrapped string.
def method_missing(method, *args, &block)
+ result = @wrapped_string.__send__(method, *args, &block)
if method.to_s =~ /!$/
- result = @wrapped_string.__send__(method, *args, &block)
self if result
else
- result = @wrapped_string.__send__(method, *args, &block)
result.kind_of?(String) ? chars(result) : result
end
end
diff --git a/activesupport/lib/active_support/multibyte/unicode.rb b/activesupport/lib/active_support/multibyte/unicode.rb
index f49ca47f14..1845c6ae38 100644
--- a/activesupport/lib/active_support/multibyte/unicode.rb
+++ b/activesupport/lib/active_support/multibyte/unicode.rb
@@ -11,7 +11,7 @@ module ActiveSupport
NORMALIZATION_FORMS = [:c, :kc, :d, :kd]
# The Unicode version that is supported by the implementation
- UNICODE_VERSION = '6.1.0'
+ UNICODE_VERSION = '6.2.0'
# The default normalization used for operations that require
# normalization. It can be set to any of the normalizations
@@ -145,7 +145,7 @@ module ActiveSupport
ncp << (HANGUL_TBASE + tindex) unless tindex == 0
decomposed.concat ncp
# if the codepoint is decomposable in with the current decomposition type
- elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatability)
+ elsif (ncp = database.codepoints[cp].decomp_mapping) and (!database.codepoints[cp].decomp_type || type == :compatibility)
decomposed.concat decompose(type, ncp.dup)
else
decomposed << cp
@@ -218,51 +218,31 @@ module ActiveSupport
# Passing +true+ will forcibly tidy all bytes, assuming that the string's
# encoding is entirely CP1252 or ISO-8859-1.
def tidy_bytes(string, force = false)
+ return string if string.empty?
+
if force
- return string.unpack("C*").map do |b|
- tidy_byte(b)
- end.flatten.compact.pack("C*").unpack("U*").pack("U*")
+ return string.encode(Encoding::UTF_8, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
- bytes = string.unpack("C*")
- conts_expected = 0
- last_lead = 0
-
- bytes.each_index do |i|
+ # We can't transcode to the same format, so we choose a nearly-identical encoding.
+ # We're going to 'transcode' bytes from UTF-8 when possible, then fall back to
+ # CP1252 when we get errors. The final string will be 'converted' back to UTF-8
+ # before returning.
+ reader = Encoding::Converter.new(Encoding::UTF_8, Encoding::UTF_8_MAC)
- byte = bytes[i]
- is_cont = byte > 127 && byte < 192
- is_lead = byte > 191 && byte < 245
- is_unused = byte > 240
- is_restricted = byte > 244
+ source = string.dup
+ out = ''.force_encoding(Encoding::UTF_8_MAC)
- # Impossible or highly unlikely byte? Clean it.
- if is_unused || is_restricted
- bytes[i] = tidy_byte(byte)
- elsif is_cont
- # Not expecting continuation byte? Clean up. Otherwise, now expect one less.
- conts_expected == 0 ? bytes[i] = tidy_byte(byte) : conts_expected -= 1
- else
- if conts_expected > 0
- # Expected continuation, but got ASCII or leading? Clean backwards up to
- # the leading byte.
- (1..(i - last_lead)).each {|j| bytes[i - j] = tidy_byte(bytes[i - j])}
- conts_expected = 0
- end
- if is_lead
- # Final byte is leading? Clean it.
- if i == bytes.length - 1
- bytes[i] = tidy_byte(bytes.last)
- else
- # Valid leading byte? Expect continuations determined by position of
- # first zero bit, with max of 3.
- conts_expected = byte < 224 ? 1 : byte < 240 ? 2 : 3
- last_lead = i
- end
- end
- end
+ loop do
+ reader.primitive_convert(source, out)
+ _, _, _, error_bytes, _ = reader.primitive_errinfo
+ break if error_bytes.nil?
+ out << error_bytes.encode(Encoding::UTF_8_MAC, Encoding::Windows_1252, invalid: :replace, undef: :replace)
end
- bytes.empty? ? "" : bytes.flatten.compact.pack("C*").unpack("U*").pack("U*")
+
+ reader.finish
+
+ out.encode!(Encoding::UTF_8)
end
# Returns the KC normalization of the string by default. NFKC is
@@ -283,9 +263,9 @@ module ActiveSupport
when :c
compose(reorder_characters(decompose(:canonical, codepoints)))
when :kd
- reorder_characters(decompose(:compatability, codepoints))
+ reorder_characters(decompose(:compatibility, codepoints))
when :kc
- compose(reorder_characters(decompose(:compatability, codepoints)))
+ compose(reorder_characters(decompose(:compatibility, codepoints)))
else
raise ArgumentError, "#{form} is not a valid normalization variant", caller
end.pack('U*')
@@ -307,6 +287,13 @@ module ActiveSupport
class Codepoint
attr_accessor :code, :combining_class, :decomp_type, :decomp_mapping, :uppercase_mapping, :lowercase_mapping
+ # Initializing Codepoint object with default values
+ def initialize
+ @combining_class = 0
+ @uppercase_mapping = 0
+ @lowercase_mapping = 0
+ end
+
def swapcase_mapping
uppercase_mapping > 0 ? uppercase_mapping : lowercase_mapping
end
diff --git a/activesupport/lib/active_support/notifications.rb b/activesupport/lib/active_support/notifications.rb
index 099117cebb..b32aa75e59 100644
--- a/activesupport/lib/active_support/notifications.rb
+++ b/activesupport/lib/active_support/notifications.rb
@@ -1,5 +1,6 @@
require 'active_support/notifications/instrumenter'
require 'active_support/notifications/fanout'
+require 'active_support/per_thread_registry'
module ActiveSupport
# = Notifications
@@ -84,15 +85,15 @@ module ActiveSupport
# resulting in the following output within the logs including a hash with the payload:
#
# notification: process_action.action_controller 2012-04-13 01:08:35 +0300 2012-04-13 01:08:35 +0300 af358ed7fab884532ec7 {
- # :controller=>"Devise::SessionsController",
- # :action=>"new",
- # :params=>{"action"=>"new", "controller"=>"devise/sessions"},
- # :format=>:html,
- # :method=>"GET",
- # :path=>"/login/sign_in",
- # :status=>200,
- # :view_runtime=>279.3080806732178,
- # :db_runtime=>40.053
+ # controller: "Devise::SessionsController",
+ # action: "new",
+ # params: {"action"=>"new", "controller"=>"devise/sessions"},
+ # format: :html,
+ # method: "GET",
+ # path: "/login/sign_in",
+ # status: 200,
+ # view_runtime: 279.3080806732178,
+ # db_runtime: 40.053
# }
#
# You can also subscribe to all events whose name matches a certain regexp:
@@ -142,8 +143,8 @@ module ActiveSupport
#
# == Default Queue
#
- # Notifications ships with a queue implementation that consumes and publish events
- # to log subscribers in a thread. You can use any queue implementation you want.
+ # Notifications ships with a queue implementation that consumes and publishes events
+ # to all log subscribers. You can use any queue implementation you want.
#
module Notifications
class << self
@@ -177,7 +178,27 @@ module ActiveSupport
end
def instrumenter
- Thread.current[:"instrumentation_#{notifier.object_id}"] ||= Instrumenter.new(notifier)
+ InstrumentationRegistry.instrumenter_for(notifier)
+ end
+ end
+
+ # This class is a registry which holds all of the +Instrumenter+ objects
+ # in a particular thread local. To access the +Instrumenter+ object for a
+ # particular +notifier+, you can call the following method:
+ #
+ # InstrumentationRegistry.instrumenter_for(notifier)
+ #
+ # The instrumenters for multiple notifiers are held in a single instance of
+ # this class.
+ class InstrumentationRegistry # :nodoc:
+ extend ActiveSupport::PerThreadRegistry
+
+ def initialize
+ @registry = {}
+ end
+
+ def instrumenter_for(notifier)
+ @registry[notifier] ||= Instrumenter.new(notifier)
end
end
diff --git a/activesupport/lib/active_support/notifications/fanout.rb b/activesupport/lib/active_support/notifications/fanout.rb
index 2e5bcf4639..8f5fa646e8 100644
--- a/activesupport/lib/active_support/notifications/fanout.rb
+++ b/activesupport/lib/active_support/notifications/fanout.rb
@@ -1,4 +1,5 @@
require 'mutex_m'
+require 'thread_safe'
module ActiveSupport
module Notifications
@@ -11,7 +12,7 @@ module ActiveSupport
def initialize
@subscribers = []
- @listeners_for = {}
+ @listeners_for = ThreadSafe::Cache.new
super
end
@@ -44,7 +45,9 @@ module ActiveSupport
end
def listeners_for(name)
- synchronize do
+ # this is correctly done double-checked locking (ThreadSafe::Cache's lookups have volatile semantics)
+ @listeners_for[name] || synchronize do
+ # use synchronisation when accessing @subscribers
@listeners_for[name] ||= @subscribers.select { |s| s.subscribed_to?(name) }
end
end
@@ -76,6 +79,13 @@ module ActiveSupport
def initialize(pattern, delegate)
@pattern = pattern
@delegate = delegate
+ @can_publish = delegate.respond_to?(:publish)
+ end
+
+ def publish(name, *args)
+ if @can_publish
+ @delegate.publish name, *args
+ end
end
def start(name, id, payload)
@@ -97,21 +107,18 @@ module ActiveSupport
end
class Timed < Evented
- def initialize(pattern, delegate)
- @timestack = []
- super
- end
-
def publish(name, *args)
@delegate.call name, *args
end
def start(name, id, payload)
- @timestack.push Time.now
+ timestack = Thread.current[:_timestack] ||= []
+ timestack.push Time.now
end
def finish(name, id, payload)
- started = @timestack.pop
+ timestack = Thread.current[:_timestack]
+ started = timestack.pop
@delegate.call(name, started, Time.now, id, payload)
end
end
diff --git a/activesupport/lib/active_support/notifications/instrumenter.rb b/activesupport/lib/active_support/notifications/instrumenter.rb
index ab0b162ee0..3a244b34b5 100644
--- a/activesupport/lib/active_support/notifications/instrumenter.rb
+++ b/activesupport/lib/active_support/notifications/instrumenter.rb
@@ -2,12 +2,12 @@ require 'securerandom'
module ActiveSupport
module Notifications
- # Instrumentors are stored in a thread local.
+ # Instrumenters are stored in a thread local.
class Instrumenter
attr_reader :id
def initialize(notifier)
- @id = unique_id
+ @id = unique_id
@notifier = notifier
end
@@ -15,21 +15,32 @@ module ActiveSupport
# and publish it. Notice that events get sent even if an error occurs
# in the passed-in block.
def instrument(name, payload={})
- @notifier.start(name, @id, payload)
+ start name, payload
begin
- yield
+ yield payload
rescue Exception => e
payload[:exception] = [e.class.name, e.message]
raise e
ensure
- @notifier.finish(name, @id, payload)
+ finish name, payload
end
end
+ # Send a start notification with +name+ and +payload+.
+ def start(name, payload)
+ @notifier.start name, @id, payload
+ end
+
+ # Send a finish notification with +name+ and +payload+.
+ def finish(name, payload)
+ @notifier.finish name, @id, payload
+ end
+
private
- def unique_id
- SecureRandom.hex(10)
- end
+
+ def unique_id
+ SecureRandom.hex(10)
+ end
end
class Event
@@ -43,10 +54,11 @@ module ActiveSupport
@transaction_id = transaction_id
@end = ending
@children = []
+ @duration = nil
end
def duration
- 1000.0 * (self.end - time)
+ @duration ||= 1000.0 * (self.end - time)
end
def <<(event)
diff --git a/activesupport/lib/active_support/number_helper.rb b/activesupport/lib/active_support/number_helper.rb
index 2191471daa..e0151baa36 100644
--- a/activesupport/lib/active_support/number_helper.rb
+++ b/activesupport/lib/active_support/number_helper.rb
@@ -108,7 +108,7 @@ module ActiveSupport
DECIMAL_UNITS = { 0 => :unit, 1 => :ten, 2 => :hundred, 3 => :thousand, 6 => :million, 9 => :billion, 12 => :trillion, 15 => :quadrillion,
-1 => :deci, -2 => :centi, -3 => :mili, -6 => :micro, -9 => :nano, -12 => :pico, -15 => :femto }
-
+ INVERTED_DECIMAL_UNITS = DECIMAL_UNITS.invert
STORAGE_UNITS = [:byte, :kb, :mb, :gb, :tb]
# Formats a +number+ into a US phone number (e.g., (555)
@@ -244,14 +244,14 @@ module ActiveSupport
#
# ==== Examples
#
- # number_to_percentage(100) # => 100.000%
- # number_to_percentage('98') # => 98.000%
- # number_to_percentage(100, precision: 0) # => 100%
- # number_to_percentage(1000, delimiter: '.', separator: ,') # => 1.000,000%
- # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
- # number_to_percentage(1000, locale: :fr) # => 1 000,000%
- # number_to_percentage('98a') # => 98a%
- # number_to_percentage(100, format: '%n %') # => 100 %
+ # number_to_percentage(100) # => 100.000%
+ # number_to_percentage('98') # => 98.000%
+ # number_to_percentage(100, precision: 0) # => 100%
+ # number_to_percentage(1000, delimiter: '.', separator: ',') # => 1.000,000%
+ # number_to_percentage(302.24398923423, precision: 5) # => 302.24399%
+ # number_to_percentage(1000, locale: :fr) # => 1 000,000%
+ # number_to_percentage('98a') # => 98a%
+ # number_to_percentage(100, format: '%n %') # => 100 %
def number_to_percentage(number, options = {})
return unless number
options = options.symbolize_keys
@@ -295,7 +295,7 @@ module ActiveSupport
options = format_options(options[:locale]).merge!(options)
- parts = number.to_s.to_str.split('.')
+ parts = number.to_s.split('.')
parts[0].gsub!(/(\d)(?=(\d\d\d)+(?!\d))/, "\\1#{options[:delimiter]}")
parts.join(options[:separator])
end
@@ -356,7 +356,8 @@ module ActiveSupport
digits, rounded_number = 1, 0
else
digits = (Math.log10(number.abs) + 1).floor
- rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new((10 ** (digits - precision)).to_f.to_s)).round.to_f * 10 ** (digits - precision)
+ multiplier = 10 ** (digits - precision)
+ rounded_number = (BigDecimal.new(number.to_s) / BigDecimal.new(multiplier.to_f.to_s)).round.to_f * multiplier
digits = (Math.log10(rounded_number.abs) + 1).floor # After rounding, the number of digits may have changed
end
precision -= digits
@@ -459,7 +460,7 @@ module ActiveSupport
# See <tt>number_to_human_size</tt> if you want to print a file
# size.
#
- # You can also define you own unit-quantifier names if you want
+ # You can also define your own unit-quantifier names if you want
# to use other decimal units (eg.: 1500 becomes "1.5
# kilometers", 0.150 becomes "150 milliliters", etc). You may
# define a wide range of unit quantifiers, even fractional ones
@@ -560,8 +561,6 @@ module ActiveSupport
#for backwards compatibility with those that didn't add strip_insignificant_zeros to their locale files
options[:strip_insignificant_zeros] = true if not options.key?(:strip_insignificant_zeros)
- inverted_du = DECIMAL_UNITS.invert
-
units = options.delete :units
unit_exponents = case units
when Hash
@@ -572,7 +571,7 @@ module ActiveSupport
translate_number_value_with_default("human.decimal_units.units", :locale => options[:locale], :raise => true)
else
raise ArgumentError, ":units must be a Hash or String translation scope."
- end.keys.map{|e_name| inverted_du[e_name] }.sort_by{|e| -e}
+ end.keys.map!{|e_name| INVERTED_DECIMAL_UNITS[e_name] }.sort_by!{|e| -e}
number_exponent = number != 0 ? Math.log10(number.abs).floor : 0
display_exponent = unit_exponents.find{ |e| number_exponent >= e } || 0
@@ -580,7 +579,7 @@ module ActiveSupport
unit = case units
when Hash
- units[DECIMAL_UNITS[display_exponent]]
+ units[DECIMAL_UNITS[display_exponent]] || ''
when String, Symbol
I18n.translate(:"#{units}.#{DECIMAL_UNITS[display_exponent]}", :locale => options[:locale], :count => number.to_i)
else
diff --git a/activesupport/lib/active_support/ordered_options.rb b/activesupport/lib/active_support/ordered_options.rb
index c9518bda79..a33e2c58a9 100644
--- a/activesupport/lib/active_support/ordered_options.rb
+++ b/activesupport/lib/active_support/ordered_options.rb
@@ -40,6 +40,14 @@ module ActiveSupport
end
end
+ # +InheritableOptions+ provides a constructor to build an +OrderedOptions+
+ # hash inherited from another hash.
+ #
+ # Use this if you already have some hash and you want to create a new one based on it.
+ #
+ # h = ActiveSupport::InheritableOptions.new({ girl: 'Mary', boy: 'John' })
+ # h.girl # => 'Mary'
+ # h.boy # => 'John'
class InheritableOptions < OrderedOptions
def initialize(parent = nil)
if parent.kind_of?(OrderedOptions)
diff --git a/activesupport/lib/active_support/per_thread_registry.rb b/activesupport/lib/active_support/per_thread_registry.rb
new file mode 100644
index 0000000000..aa682fb36c
--- /dev/null
+++ b/activesupport/lib/active_support/per_thread_registry.rb
@@ -0,0 +1,52 @@
+module ActiveSupport
+ # This module is used to encapsulate access to thread local variables.
+ #
+ # Instead of polluting the thread locals namespace:
+ #
+ # Thread.current[:connection_handler]
+ #
+ # you define a class that extends this module:
+ #
+ # module ActiveRecord
+ # class RuntimeRegistry
+ # extend ActiveSupport::PerThreadRegistry
+ #
+ # attr_accessor :connection_handler
+ # end
+ # end
+ #
+ # and invoke the declared instance accessors as class methods. So
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler = connection_handler
+ #
+ # sets a connection handler local to the current thread, and
+ #
+ # ActiveRecord::RuntimeRegistry.connection_handler
+ #
+ # returns a connection handler local to the current thread.
+ #
+ # This feature is accomplished by instantiating the class and storing the
+ # instance as a thread local keyed by the class name. In the example above
+ # a key "ActiveRecord::RuntimeRegistry" is stored in <tt>Thread.current</tt>.
+ # The class methods proxy to said thread local instance.
+ #
+ # If the class has an initializer, it must accept no arguments.
+ module PerThreadRegistry
+ protected
+
+ def method_missing(name, *args, &block) # :nodoc:
+ # Caches the method definition as a singleton method of the receiver.
+ define_singleton_method(name) do |*a, &b|
+ per_thread_registry_instance.public_send(name, *a, &b)
+ end
+
+ send(name, *args, &block)
+ end
+
+ private
+
+ def per_thread_registry_instance
+ Thread.current[name] ||= new
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/proxy_object.rb b/activesupport/lib/active_support/proxy_object.rb
new file mode 100644
index 0000000000..20a0fd8e62
--- /dev/null
+++ b/activesupport/lib/active_support/proxy_object.rb
@@ -0,0 +1,13 @@
+module ActiveSupport
+ # A class with no predefined methods that behaves similarly to Builder's
+ # BlankSlate. Used for proxy classes.
+ class ProxyObject < ::BasicObject
+ undef_method :==
+ undef_method :equal?
+
+ # Let ActiveSupport::ProxyObject at least raise exceptions.
+ def raise(*args)
+ ::Object.send(:raise, *args)
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/queueing.rb b/activesupport/lib/active_support/queueing.rb
deleted file mode 100644
index c8ba28021d..0000000000
--- a/activesupport/lib/active_support/queueing.rb
+++ /dev/null
@@ -1,103 +0,0 @@
-require 'delegate'
-require 'thread'
-
-module ActiveSupport
- # A Queue that simply inherits from STDLIB's Queue. When this
- # queue is used, Rails automatically starts a job runner in a
- # background thread.
- class Queue < ::Queue
- attr_writer :consumer
-
- def initialize(consumer_options = {})
- super()
- @consumer_options = consumer_options
- end
-
- def consumer
- @consumer ||= ThreadedQueueConsumer.new(self, @consumer_options)
- end
-
- # Drain the queue, running all jobs in a different thread. This method
- # may not be available on production queues.
- def drain
- # run the jobs in a separate thread so assumptions of synchronous
- # jobs are caught in test mode.
- consumer.drain
- end
- end
-
- class SynchronousQueue < Queue
- def push(job)
- super.tap { drain }
- end
- alias << push
- alias enq push
- end
-
- # In test mode, the Rails queue is backed by an Array so that assertions
- # can be made about its contents. The test queue provides a +jobs+
- # method to make assertions about the queue's contents and a +drain+
- # method to drain the queue and run the jobs.
- #
- # Jobs are run in a separate thread to catch mistakes where code
- # assumes that the job is run in the same thread.
- class TestQueue < Queue
- # Get a list of the jobs off this queue. This method may not be
- # available on production queues.
- def jobs
- @que.dup
- end
-
- # Marshal and unmarshal job before pushing it onto the queue. This will
- # raise an exception on any attempts in tests to push jobs that can't (or
- # shouldn't) be marshalled.
- def push(job)
- super Marshal.load(Marshal.dump(job))
- end
- end
-
- # The threaded consumer will run jobs in a background thread in
- # development mode or in a VM where running jobs on a thread in
- # production mode makes sense.
- #
- # When the process exits, the consumer pushes a nil onto the
- # queue and joins the thread, which will ensure that all jobs
- # are executed before the process finally dies.
- class ThreadedQueueConsumer
- def initialize(queue, options = {})
- @queue = queue
- @logger = options[:logger]
- end
-
- def start
- @thread = Thread.new { consume }
- self
- end
-
- def shutdown
- @queue.push nil
- @thread.join
- end
-
- def drain
- run(@queue.pop) until @queue.empty?
- end
-
- def consume
- while job = @queue.pop
- run job
- end
- end
-
- def run(job)
- job.run
- rescue Exception => exception
- handle_exception job, exception
- end
-
- def handle_exception(job, exception)
- raise exception unless @logger
- @logger.error "Job Error: #{exception.message}\n#{exception.backtrace.join("\n")}"
- end
- end
-end
diff --git a/activesupport/lib/active_support/rescuable.rb b/activesupport/lib/active_support/rescuable.rb
index 9a038dfbca..a7eba91ac5 100644
--- a/activesupport/lib/active_support/rescuable.rb
+++ b/activesupport/lib/active_support/rescuable.rb
@@ -1,6 +1,5 @@
require 'active_support/concern'
require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/proc'
require 'active_support/core_ext/string/inflections'
require 'active_support/core_ext/array/extract_options'
diff --git a/activesupport/lib/active_support/subscriber.rb b/activesupport/lib/active_support/subscriber.rb
new file mode 100644
index 0000000000..f2966f23fc
--- /dev/null
+++ b/activesupport/lib/active_support/subscriber.rb
@@ -0,0 +1,116 @@
+require 'active_support/per_thread_registry'
+
+module ActiveSupport
+ # ActiveSupport::Subscriber is an object set to consume
+ # ActiveSupport::Notifications. The subscriber dispatches notifications to
+ # a registered object based on its given namespace.
+ #
+ # An example would be Active Record subscriber responsible for collecting
+ # statistics about queries:
+ #
+ # module ActiveRecord
+ # class StatsSubscriber < ActiveSupport::Subscriber
+ # def sql(event)
+ # Statsd.timing("sql.#{event.payload[:name]}", event.duration)
+ # end
+ # end
+ # end
+ #
+ # And it's finally registered as:
+ #
+ # ActiveRecord::StatsSubscriber.attach_to :active_record
+ #
+ # Since we need to know all instance methods before attaching the log
+ # subscriber, the line above should be called after your subscriber definition.
+ #
+ # After configured, whenever a "sql.active_record" notification is published,
+ # it will properly dispatch the event (ActiveSupport::Notifications::Event) to
+ # the +sql+ method.
+ class Subscriber
+ class << self
+
+ # Attach the subscriber to a namespace.
+ def attach_to(namespace, subscriber=new, notifier=ActiveSupport::Notifications)
+ @namespace = namespace
+ @subscriber = subscriber
+ @notifier = notifier
+
+ subscribers << subscriber
+
+ # Add event subscribers for all existing methods on the class.
+ subscriber.public_methods(false).each do |event|
+ add_event_subscriber(event)
+ end
+ end
+
+ # Adds event subscribers for all new methods added to the class.
+ def method_added(event)
+ # Only public methods are added as subscribers, and only if a notifier
+ # has been set up. This means that subscribers will only be set up for
+ # classes that call #attach_to.
+ if public_method_defined?(event) && notifier
+ add_event_subscriber(event)
+ end
+ end
+
+ def subscribers
+ @@subscribers ||= []
+ end
+
+ protected
+
+ attr_reader :subscriber, :notifier, :namespace
+
+ def add_event_subscriber(event)
+ return if %w{ start finish }.include?(event.to_s)
+
+ notifier.subscribe("#{event}.#{namespace}", subscriber)
+ end
+ end
+
+ def initialize
+ @queue_key = [self.class.name, object_id].join "-"
+ super
+ end
+
+ def start(name, id, payload)
+ e = ActiveSupport::Notifications::Event.new(name, Time.now, nil, id, payload)
+ parent = event_stack.last
+ parent << e if parent
+
+ event_stack.push e
+ end
+
+ def finish(name, id, payload)
+ finished = Time.now
+ event = event_stack.pop
+ event.end = finished
+ event.payload.merge!(payload)
+
+ method = name.split('.').first
+ send(method, event)
+ end
+
+ private
+
+ def event_stack
+ SubscriberQueueRegistry.get_queue(@queue_key)
+ end
+ end
+
+ # This is a registry for all the event stacks kept for subscribers.
+ #
+ # See the documentation of <tt>ActiveSupport::PerThreadRegistry</tt>
+ # for further details.
+ class SubscriberQueueRegistry # :nodoc:
+ extend PerThreadRegistry
+
+ def initialize
+ @registry = {}
+ end
+
+ def get_queue(queue_key)
+ @registry[queue_key] ||= []
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/tagged_logging.rb b/activesupport/lib/active_support/tagged_logging.rb
index 33810442da..18bc919734 100644
--- a/activesupport/lib/active_support/tagged_logging.rb
+++ b/activesupport/lib/active_support/tagged_logging.rb
@@ -11,7 +11,7 @@ module ActiveSupport
# logger.tagged('BCX') { logger.tagged('Jason') { logger.info 'Stuff' } } # Logs "[BCX] [Jason] Stuff"
#
# This is used by the default Rails.logger as configured by Railties to make
- # it easy to stamp log lines with subdomains, request ids, and anything else
+ # it easy to stamp log lines with subdomains, request ids, and anything else
# to aid debugging of multi-user production applications.
module TaggedLogging
module Formatter # :nodoc:
diff --git a/activesupport/lib/active_support/test_case.rb b/activesupport/lib/active_support/test_case.rb
index 7b9378a7f6..5e24118d34 100644
--- a/activesupport/lib/active_support/test_case.rb
+++ b/activesupport/lib/active_support/test_case.rb
@@ -1,27 +1,42 @@
gem 'minitest' # make sure we get the gem, not stdlib
-require 'minitest/spec'
+require 'minitest'
require 'active_support/testing/tagged_logging'
require 'active_support/testing/setup_and_teardown'
require 'active_support/testing/assertions'
require 'active_support/testing/deprecation'
+require 'active_support/testing/declarative'
require 'active_support/testing/isolation'
-require 'active_support/testing/mocha_module'
require 'active_support/testing/constant_lookup'
require 'active_support/core_ext/kernel/reporting'
require 'active_support/deprecation'
-module ActiveSupport
- class TestCase < ::MiniTest::Spec
+begin
+ silence_warnings { require 'mocha/setup' }
+rescue LoadError
+end
- include ActiveSupport::Testing::MochaModule
+module Minitest # :nodoc:
+ class << self
+ remove_method :__run
+ end
- # Use AS::TestCase for the base class when describing a model
- register_spec_type(self) do |desc|
- Class === desc && desc < ActiveRecord::Model
- end
+ def self.__run reporter, options # :nodoc:
+ # FIXME: MT5's runnables is not ordered. This is needed because
+ # we have tests with cross-class order-dependent bugs.
+ suites = Runnable.runnables.sort_by { |ts| ts.name.to_s }
+
+ parallel, serial = suites.partition { |s| s.test_order == :parallel }
- Assertion = MiniTest::Assertion
- alias_method :method_name, :__name__
+ ParallelEach.new(parallel).map { |suite| suite.run reporter, options } +
+ serial.map { |suite| suite.run reporter, options }
+ end
+end
+
+module ActiveSupport
+ class TestCase < ::Minitest::Test
+ Assertion = Minitest::Assertion
+
+ alias_method :method_name, :name
$tags = {}
def self.for_tag(tag)
@@ -38,33 +53,25 @@ module ActiveSupport
include ActiveSupport::Testing::SetupAndTeardown
include ActiveSupport::Testing::Assertions
include ActiveSupport::Testing::Deprecation
-
- def self.describe(text)
- if block_given?
- super
- else
- ActiveSupport::Deprecation.warn("`describe` without a block is deprecated, please switch to: `def self.name; #{text.inspect}; end`\n")
-
- class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
- def self.name
- "#{text}"
- end
- RUBY_EVAL
- end
- end
-
- class << self
- alias :test :it
- end
+ extend ActiveSupport::Testing::Declarative
# test/unit backwards compatibility methods
alias :assert_raise :assert_raises
- alias :assert_not_nil :refute_nil
+ alias :assert_not_empty :refute_empty
alias :assert_not_equal :refute_equal
+ alias :assert_not_in_delta :refute_in_delta
+ alias :assert_not_in_epsilon :refute_in_epsilon
+ alias :assert_not_includes :refute_includes
+ alias :assert_not_instance_of :refute_instance_of
+ alias :assert_not_kind_of :refute_kind_of
alias :assert_no_match :refute_match
+ alias :assert_not_nil :refute_nil
+ alias :assert_not_operator :refute_operator
+ alias :assert_not_predicate :refute_predicate
+ alias :assert_not_respond_to :refute_respond_to
alias :assert_not_same :refute_same
- # Fails if the block raises an exception.
+ # Fails if the block raises an exception.
#
# assert_nothing_raised do
# ...
diff --git a/activesupport/lib/active_support/testing/assertions.rb b/activesupport/lib/active_support/testing/assertions.rb
index 8466049e20..76a591bc3b 100644
--- a/activesupport/lib/active_support/testing/assertions.rb
+++ b/activesupport/lib/active_support/testing/assertions.rb
@@ -3,6 +3,22 @@ require 'active_support/core_ext/object/blank'
module ActiveSupport
module Testing
module Assertions
+ # Assert that an expression is not truthy. Passes if <tt>object</tt> is
+ # +nil+ or +false+. "Truthy" means "considered true in a conditional"
+ # like <tt>if foo</tt>.
+ #
+ # assert_not nil # => true
+ # assert_not false # => true
+ # assert_not 'foo' # => 'foo' is not nil or false
+ #
+ # An error message can be specified.
+ #
+ # assert_not foo, 'foo should be false'
+ def assert_not(object, message = nil)
+ message ||= "Expected #{mu_pp(object)} to be nil or false"
+ assert !object, message
+ end
+
# Test numeric difference between the return value of an expression as a
# result of what is evaluated in the yielded block.
#
@@ -76,34 +92,6 @@ module ActiveSupport
def assert_no_difference(expression, message = nil, &block)
assert_difference expression, 0, message, &block
end
-
- # Test if an expression is blank. Passes if <tt>object.blank?</tt>
- # is +true+.
- #
- # assert_blank [] # => true
- # assert_blank [[]] # => [[]] is not blank
- #
- # An error message can be specified.
- #
- # assert_blank [], 'this should be blank'
- def assert_blank(object, message=nil)
- message ||= "#{object.inspect} is not blank"
- assert object.blank?, message
- end
-
- # Test if an expression is not blank. Passes if <tt>object.present?</tt>
- # is +true+.
- #
- # assert_present({ data: 'x' }) # => true
- # assert_present({}) # => {} is blank
- #
- # An error message can be specified.
- #
- # assert_present({ data: 'x' }, 'this should not be blank')
- def assert_present(object, message=nil)
- message ||= "#{object.inspect} is blank"
- assert object.present?, message
- end
end
end
end
diff --git a/activesupport/lib/active_support/testing/autorun.rb b/activesupport/lib/active_support/testing/autorun.rb
new file mode 100644
index 0000000000..5aa5f46310
--- /dev/null
+++ b/activesupport/lib/active_support/testing/autorun.rb
@@ -0,0 +1,5 @@
+gem 'minitest'
+
+require 'minitest'
+
+Minitest.autorun
diff --git a/activesupport/lib/active_support/testing/constant_lookup.rb b/activesupport/lib/active_support/testing/constant_lookup.rb
index 73e87befb6..1b2a75c35d 100644
--- a/activesupport/lib/active_support/testing/constant_lookup.rb
+++ b/activesupport/lib/active_support/testing/constant_lookup.rb
@@ -30,7 +30,7 @@ module ActiveSupport
module ConstantLookup
extend ::ActiveSupport::Concern
- module ClassMethods
+ module ClassMethods # :nodoc:
def determine_constant_from_test_name(test_name)
names = test_name.split "::"
while names.size > 0 do
@@ -38,6 +38,8 @@ module ActiveSupport
begin
constant = names.join("::").constantize
break(constant) if yield(constant)
+ rescue NoMethodError # subclass of NameError
+ raise
rescue NameError
# Constant wasn't found, move on
ensure
diff --git a/activesupport/lib/active_support/testing/declarative.rb b/activesupport/lib/active_support/testing/declarative.rb
new file mode 100644
index 0000000000..1fa73caefa
--- /dev/null
+++ b/activesupport/lib/active_support/testing/declarative.rb
@@ -0,0 +1,43 @@
+module ActiveSupport
+ module Testing
+ module Declarative
+
+ def self.extended(klass) #:nodoc:
+ klass.class_eval do
+
+ unless method_defined?(:describe)
+ def self.describe(text)
+ class_eval <<-RUBY_EVAL, __FILE__, __LINE__ + 1
+ def self.name
+ "#{text}"
+ end
+ RUBY_EVAL
+ 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.
+ #
+ # test "verify something" do
+ # ...
+ # end
+ def test(name, &block)
+ test_name = "test_#{name.gsub(/\s+/,'_')}".to_sym
+ defined = instance_method(test_name) rescue false
+ raise "#{test_name} is already defined in #{self}" if defined
+ if block_given?
+ define_method(test_name, &block)
+ else
+ define_method(test_name) do
+ flunk "No implementation provided for #{name}"
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/activesupport/lib/active_support/testing/isolation.rb b/activesupport/lib/active_support/testing/isolation.rb
index 27d444fd91..d5d31cecbe 100644
--- a/activesupport/lib/active_support/testing/isolation.rb
+++ b/activesupport/lib/active_support/testing/isolation.rb
@@ -1,69 +1,11 @@
require 'rbconfig'
+require 'minitest/parallel_each'
+
module ActiveSupport
module Testing
- class RemoteError < StandardError
-
- attr_reader :message, :backtrace
-
- def initialize(exception)
- @message = "caught #{exception.class.name}: #{exception.message}"
- @backtrace = exception.backtrace
- end
- end
-
- class ProxyTestResult
- def initialize
- @calls = []
- end
-
- def add_error(e)
- e = Test::Unit::Error.new(e.test_name, RemoteError.new(e.exception))
- @calls << [:add_error, e]
- end
-
- def __replay__(result)
- @calls.each do |name, args|
- result.send(name, *args)
- end
- end
-
- def method_missing(name, *args)
- @calls << [name, args]
- end
- end
-
module Isolation
require 'thread'
- class ParallelEach
- include Enumerable
-
- # default to 2 cores
- CORES = (ENV['TEST_CORES'] || 2).to_i
-
- def initialize list
- @list = list
- @queue = SizedQueue.new CORES
- end
-
- def grep pattern
- self.class.new super
- end
-
- def each
- threads = CORES.times.map {
- Thread.new {
- while job = @queue.pop
- yield job
- end
- }
- }
- @list.each { |i| @queue << i }
- CORES.times { @queue << nil }
- threads.each(&:join)
- end
- end
-
def self.included(klass) #:nodoc:
klass.extend(Module.new {
def test_methods
@@ -76,23 +18,23 @@ module ActiveSupport
!ENV["NO_FORK"] && ((RbConfig::CONFIG['host_os'] !~ /mswin|mingw/) && (RUBY_PLATFORM !~ /java/))
end
+ @@class_setup_mutex = Mutex.new
+
def _run_class_setup # class setup method should only happen in parent
- unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST']
- self.class.setup if self.class.respond_to?(:setup)
- @@ran_class_setup = true
+ @@class_setup_mutex.synchronize do
+ unless defined?(@@ran_class_setup) || ENV['ISOLATION_TEST']
+ self.class.setup if self.class.respond_to?(:setup)
+ @@ran_class_setup = true
+ end
end
end
- def run(runner)
- _run_class_setup
-
- serialized = run_in_isolation do |isolated_runner|
- super(isolated_runner)
+ def run
+ serialized = run_in_isolation do
+ super
end
- retval, proxy = Marshal.load(serialized)
- proxy.__replay__(runner)
- retval
+ Marshal.load(serialized)
end
module Forking
@@ -101,9 +43,8 @@ module ActiveSupport
pid = fork do
read.close
- proxy = ProxyTestResult.new
- retval = yield proxy
- write.puts [Marshal.dump([retval, proxy])].pack("m")
+ yield
+ write.puts [Marshal.dump(self.dup)].pack("m")
exit!
end
@@ -123,19 +64,18 @@ module ActiveSupport
require "tempfile"
if ENV["ISOLATION_TEST"]
- proxy = ProxyTestResult.new
- retval = yield proxy
+ yield
File.open(ENV["ISOLATION_OUTPUT"], "w") do |file|
- file.puts [Marshal.dump([retval, proxy])].pack("m")
+ file.puts [Marshal.dump(self.dup)].pack("m")
end
exit!
else
Tempfile.open("isolation") do |tmpfile|
- ENV["ISOLATION_TEST"] = @method_name
+ ENV["ISOLATION_TEST"] = self.class.name
ENV["ISOLATION_OUTPUT"] = tmpfile.path
load_paths = $-I.map {|p| "-I\"#{File.expand_path(p)}\"" }.join(" ")
- `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")} -t\"#{self.class}\"`
+ `#{Gem.ruby} #{load_paths} #{$0} #{ORIG_ARGV.join(" ")}`
ENV.delete("ISOLATION_TEST")
ENV.delete("ISOLATION_OUTPUT")
diff --git a/activesupport/lib/active_support/testing/mocha_module.rb b/activesupport/lib/active_support/testing/mocha_module.rb
deleted file mode 100644
index ed2942d23a..0000000000
--- a/activesupport/lib/active_support/testing/mocha_module.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-module ActiveSupport
- module Testing
- module MochaModule
- begin
- require 'mocha_standalone'
- include Mocha::API
-
- def before_setup
- mocha_setup
- super
- end
-
- def after_teardown
- super
- mocha_verify
- mocha_teardown
- end
- rescue LoadError
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/testing/performance.rb b/activesupport/lib/active_support/testing/performance.rb
deleted file mode 100644
index 7102ffe2ed..0000000000
--- a/activesupport/lib/active_support/testing/performance.rb
+++ /dev/null
@@ -1,271 +0,0 @@
-require 'fileutils'
-require 'active_support/concern'
-require 'active_support/core_ext/class/delegating_attributes'
-require 'active_support/core_ext/string/inflections'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/number_helper'
-
-module ActiveSupport
- module Testing
- module Performance
- extend ActiveSupport::Concern
-
- included do
- superclass_delegating_accessor :profile_options
- self.profile_options = {}
- end
-
- # each implementation should define metrics and freeze the defaults
- DEFAULTS =
- if ARGV.include?('--benchmark') # HAX for rake test
- { :runs => 4,
- :output => 'tmp/performance',
- :benchmark => true }
- else
- { :runs => 1,
- :output => 'tmp/performance',
- :benchmark => false }
- end
-
- def full_profile_options
- DEFAULTS.merge(profile_options)
- end
-
- def full_test_name
- "#{self.class.name}##{method_name}"
- end
-
- def run(runner)
- @runner = runner
-
- run_warmup
- if full_profile_options && metrics = full_profile_options[:metrics]
- metrics.each do |metric_name|
- if klass = Metrics[metric_name.to_sym]
- run_profile(klass.new)
- end
- end
- end
-
- return
- end
-
- def run_test(metric, mode)
- result = '.'
- begin
- run_callbacks :setup
- setup
- metric.send(mode) { __send__ method_name }
- rescue Exception => e
- result = @runner.puke(self.class, method_name, e)
- ensure
- begin
- teardown
- run_callbacks :teardown
- rescue Exception => e
- result = @runner.puke(self.class, method_name, e)
- end
- end
- result
- end
-
- protected
- # overridden by each implementation.
- def run_gc; end
-
- def run_warmup
- run_gc
-
- time = Metrics::Time.new
- run_test(time, :benchmark)
- puts "%s (%s warmup)" % [full_test_name, time.format(time.total)]
-
- run_gc
- end
-
- def run_profile(metric)
- klass = full_profile_options[:benchmark] ? Benchmarker : Profiler
- performer = klass.new(self, metric)
-
- performer.run
- puts performer.report
- performer.record
- end
-
- class Performer
- delegate :run_test, :full_profile_options, :full_test_name, :to => :@harness
-
- def initialize(harness, metric)
- @harness, @metric, @supported = harness, metric, false
- end
-
- def report
- if @supported
- rate = @total / full_profile_options[:runs]
- '%20s: %s' % [@metric.name, @metric.format(rate)]
- else
- '%20s: unsupported' % @metric.name
- end
- end
-
- protected
- def output_filename
- "#{full_profile_options[:output]}/#{full_test_name}_#{@metric.name}"
- end
- end
-
- # overridden by each implementation.
- class Profiler < Performer
- def time_with_block
- before = Time.now
- yield
- Time.now - before
- end
-
- def run; end
- def record; end
- end
-
- class Benchmarker < Performer
- def initialize(*args)
- super
- @supported = @metric.respond_to?('measure')
- end
-
- def run
- return unless @supported
-
- full_profile_options[:runs].to_i.times { run_test(@metric, :benchmark) }
- @total = @metric.total
- end
-
- def record
- avg = @metric.total / full_profile_options[:runs].to_i
- now = Time.now.utc.xmlschema
- with_output_file do |file|
- file.puts "#{avg},#{now},#{environment}"
- end
- end
-
- def environment
- @env ||= [].tap do |env|
- env << "#{$1}.#{$2}" if File.directory?('.git') && `git branch -v` =~ /^\* (\S+)\s+(\S+)/
- env << rails_version if defined?(Rails::VERSION::STRING)
- env << "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL}"
- env << RUBY_PLATFORM
- end.join(',')
- end
-
- protected
- if defined?(Rails::VERSION::STRING)
- HEADER = 'measurement,created_at,app,rails,ruby,platform'
- else
- HEADER = 'measurement,created_at,app,ruby,platform'
- end
-
- def with_output_file
- fname = output_filename
-
- if new = !File.exist?(fname)
- FileUtils.mkdir_p(File.dirname(fname))
- end
-
- File.open(fname, 'ab') do |file|
- file.puts(HEADER) if new
- yield file
- end
- end
-
- def output_filename
- "#{super}.csv"
- end
-
- def rails_version
- "rails-#{Rails::VERSION::STRING}#{rails_branch}"
- end
-
- def rails_branch
- if File.directory?('vendor/rails/.git')
- Dir.chdir('vendor/rails') do
- ".#{$1}.#{$2}" if `git branch -v` =~ /^\* (\S+)\s+(\S+)/
- end
- end
- end
- end
-
- module Metrics
- def self.[](name)
- const_get(name.to_s.camelize)
- rescue NameError
- nil
- end
-
- class Base
- include ActiveSupport::NumberHelper
-
- attr_reader :total
-
- def initialize
- @total = 0
- end
-
- def name
- @name ||= self.class.name.demodulize.underscore
- end
-
- def benchmark
- with_gc_stats do
- before = measure
- yield
- @total += (measure - before)
- end
- end
-
- # overridden by each implementation.
- def profile; end
-
- protected
- # overridden by each implementation.
- def with_gc_stats; end
- end
-
- class Time < Base
- def measure
- ::Time.now.to_f
- end
-
- def format(measurement)
- if measurement < 1
- '%d ms' % (measurement * 1000)
- else
- '%.2f sec' % measurement
- end
- end
- end
-
- class Amount < Base
- def format(measurement)
- number_to_delimited(measurement.floor)
- end
- end
-
- class DigitalInformationUnit < Base
- def format(measurement)
- number_to_human_size(measurement, :precision => 2)
- end
- end
-
- # each implementation provides its own metrics like ProcessTime, Memory or GcRuns
- end
- end
- end
-end
-
-case RUBY_ENGINE
- when 'ruby' then require 'active_support/testing/performance/ruby'
- when 'rbx' then require 'active_support/testing/performance/rubinius'
- when 'jruby' then require 'active_support/testing/performance/jruby'
- else
- $stderr.puts 'Your ruby interpreter is not supported for benchmarking.'
- exit
-end
diff --git a/activesupport/lib/active_support/testing/performance/jruby.rb b/activesupport/lib/active_support/testing/performance/jruby.rb
deleted file mode 100644
index 34e3f9f45f..0000000000
--- a/activesupport/lib/active_support/testing/performance/jruby.rb
+++ /dev/null
@@ -1,115 +0,0 @@
-require 'jruby/profiler'
-require 'java'
-java_import java.lang.management.ManagementFactory
-
-module ActiveSupport
- module Testing
- module Performance
- DEFAULTS.merge!(
- if ARGV.include?('--benchmark')
- {:metrics => [:wall_time, :user_time, :memory, :gc_runs, :gc_time]}
- else
- { :metrics => [:wall_time],
- :formats => [:flat, :graph] }
- end).freeze
-
- protected
- def run_gc
- ManagementFactory.memory_mx_bean.gc
- end
-
- class Profiler < Performer
- def initialize(*args)
- super
- @supported = @metric.is_a?(Metrics::WallTime)
- end
-
- def run
- return unless @supported
-
- @total = time_with_block do
- @data = JRuby::Profiler.profile do
- full_profile_options[:runs].to_i.times { run_test(@metric, :profile) }
- end
- end
- end
-
- def record
- return unless @supported
-
- klasses = full_profile_options[:formats].map { |f| JRuby::Profiler.const_get("#{f.to_s.camelize}ProfilePrinter") }.compact
-
- klasses.each do |klass|
- fname = output_filename(klass)
- FileUtils.mkdir_p(File.dirname(fname))
- File.open(fname, 'wb') do |file|
- klass.new(@data).printProfile(file)
- end
- end
- end
-
- protected
- def output_filename(printer_class)
- suffix =
- case printer_class.name.demodulize
- when 'FlatProfilePrinter'; 'flat.txt'
- when 'GraphProfilePrinter'; 'graph.txt'
- else printer_class.name.sub(/ProfilePrinter$/, '').underscore
- end
-
- "#{super()}_#{suffix}"
- end
- end
-
- module Metrics
- class Base
- def profile
- yield
- end
-
- protected
- def with_gc_stats
- ManagementFactory.memory_mx_bean.gc
- yield
- end
- end
-
- class WallTime < Time
- def measure
- super
- end
- end
-
- class CpuTime < Time
- def measure
- ManagementFactory.thread_mx_bean.get_current_thread_cpu_time / 1000 / 1000 / 1000.0 # seconds
- end
- end
-
- class UserTime < Time
- def measure
- ManagementFactory.thread_mx_bean.get_current_thread_user_time / 1000 / 1000 / 1000.0 # seconds
- end
- end
-
- class Memory < DigitalInformationUnit
- def measure
- ManagementFactory.memory_mx_bean.non_heap_memory_usage.used + ManagementFactory.memory_mx_bean.heap_memory_usage.used
- end
- end
-
- class GcRuns < Amount
- def measure
- ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_runs, current_gc| total_runs += current_gc.collection_count }
- end
- end
-
- class GcTime < Time
- def measure
- ManagementFactory.garbage_collector_mx_beans.inject(0) { |total_time, current_gc| total_time += current_gc.collection_time } / 1000.0 # seconds
- end
- end
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/testing/performance/rubinius.rb b/activesupport/lib/active_support/testing/performance/rubinius.rb
deleted file mode 100644
index d9ebfbe352..0000000000
--- a/activesupport/lib/active_support/testing/performance/rubinius.rb
+++ /dev/null
@@ -1,113 +0,0 @@
-require 'rubinius/agent'
-
-module ActiveSupport
- module Testing
- module Performance
- DEFAULTS.merge!(
- if ARGV.include?('--benchmark')
- {:metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time]}
- else
- { :metrics => [:wall_time],
- :formats => [:flat, :graph] }
- end).freeze
-
- protected
- def run_gc
- GC.run(true)
- end
-
- class Performer; end
-
- class Profiler < Performer
- def initialize(*args)
- super
- @supported = @metric.is_a?(Metrics::WallTime)
- end
-
- def run
- return unless @supported
-
- @profiler = Rubinius::Profiler::Instrumenter.new
-
- @total = time_with_block do
- @profiler.profile(false) do
- full_profile_options[:runs].to_i.times { run_test(@metric, :profile) }
- end
- end
- end
-
- def record
- return unless @supported
-
- if(full_profile_options[:formats].include?(:flat))
- create_path_and_open_file(:flat) do |file|
- @profiler.show(file)
- end
- end
-
- if(full_profile_options[:formats].include?(:graph))
- create_path_and_open_file(:graph) do |file|
- @profiler.show(file)
- end
- end
- end
-
- protected
- def create_path_and_open_file(printer_name)
- fname = "#{output_filename}_#{printer_name}.txt"
- FileUtils.mkdir_p(File.dirname(fname))
- File.open(fname, 'wb') do |file|
- yield(file)
- end
- end
- end
-
- module Metrics
- class Base
- attr_reader :loopback
-
- def profile
- yield
- end
-
- protected
- def with_gc_stats
- @loopback = Rubinius::Agent.loopback
- GC.run(true)
- yield
- end
- end
-
- class WallTime < Time
- def measure
- super
- end
- end
-
- class Memory < DigitalInformationUnit
- def measure
- loopback.get("system.memory.counter.bytes").last
- end
- end
-
- class Objects < Amount
- def measure
- loopback.get("system.memory.counter.objects").last
- end
- end
-
- class GcRuns < Amount
- def measure
- loopback.get("system.gc.full.count").last + loopback.get("system.gc.young.count").last
- end
- end
-
- class GcTime < Time
- def measure
- (loopback.get("system.gc.full.wallclock").last + loopback.get("system.gc.young.wallclock").last) / 1000.0
- end
- end
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/testing/performance/ruby.rb b/activesupport/lib/active_support/testing/performance/ruby.rb
deleted file mode 100644
index 12aef0d7fe..0000000000
--- a/activesupport/lib/active_support/testing/performance/ruby.rb
+++ /dev/null
@@ -1,173 +0,0 @@
-begin
- require 'ruby-prof'
-rescue LoadError
- $stderr.puts 'Specify ruby-prof as application\'s dependency in Gemfile to run benchmarks.'
- exit
-end
-
-module ActiveSupport
- module Testing
- module Performance
- DEFAULTS.merge!(
- if ARGV.include?('--benchmark')
- { :metrics => [:wall_time, :memory, :objects, :gc_runs, :gc_time] }
- else
- { :min_percent => 0.01,
- :metrics => [:process_time, :memory, :objects],
- :formats => [:flat, :graph_html, :call_tree, :call_stack] }
- end).freeze
-
- protected
- remove_method :run_gc
- def run_gc
- GC.start
- end
-
- class Profiler < Performer
- def initialize(*args)
- super
- @supported = @metric.measure_mode rescue false
- end
-
- remove_method :run
- def run
- return unless @supported
-
- RubyProf.measure_mode = @metric.measure_mode
- RubyProf.start
- RubyProf.pause
- full_profile_options[:runs].to_i.times { run_test(@metric, :profile) }
- @data = RubyProf.stop
- @total = @data.threads.sum(0) { |thread| thread.methods.max.total_time }
- end
-
- remove_method :record
- def record
- return unless @supported
-
- klasses = full_profile_options[:formats].map { |f| RubyProf.const_get("#{f.to_s.camelize}Printer") }.compact
-
- klasses.each do |klass|
- fname = output_filename(klass)
- FileUtils.mkdir_p(File.dirname(fname))
- File.open(fname, 'wb') do |file|
- klass.new(@data).print(file, full_profile_options.slice(:min_percent))
- end
- end
- end
-
- protected
- def output_filename(printer_class)
- suffix =
- case printer_class.name.demodulize
- when 'FlatPrinter'; 'flat.txt'
- when 'FlatPrinterWithLineNumbers'; 'flat_line_numbers.txt'
- when 'GraphPrinter'; 'graph.txt'
- when 'GraphHtmlPrinter'; 'graph.html'
- when 'GraphYamlPrinter'; 'graph.yml'
- when 'CallTreePrinter'; 'tree.txt'
- when 'CallStackPrinter'; 'stack.html'
- when 'DotPrinter'; 'graph.dot'
- else printer_class.name.sub(/Printer$/, '').underscore
- end
-
- "#{super()}_#{suffix}"
- end
- end
-
- module Metrics
- class Base
- def measure_mode
- self.class::Mode
- end
-
- remove_method :profile
- def profile
- RubyProf.resume
- yield
- ensure
- RubyProf.pause
- end
-
- protected
- remove_method :with_gc_stats
- def with_gc_stats
- GC::Profiler.enable
- GC.start
- yield
- ensure
- GC::Profiler.disable
- end
- end
-
- class ProcessTime < Time
- Mode = RubyProf::PROCESS_TIME if RubyProf.const_defined?(:PROCESS_TIME)
-
- def measure
- RubyProf.measure_process_time
- end
- end
-
- class WallTime < Time
- Mode = RubyProf::WALL_TIME if RubyProf.const_defined?(:WALL_TIME)
-
- def measure
- RubyProf.measure_wall_time
- end
- end
-
- class CpuTime < Time
- Mode = RubyProf::CPU_TIME if RubyProf.const_defined?(:CPU_TIME)
-
- def initialize(*args)
- # FIXME: yeah my CPU is 2.33 GHz
- RubyProf.cpu_frequency = 2.33e9 unless RubyProf.cpu_frequency > 0
- super
- end
-
- def measure
- RubyProf.measure_cpu_time
- end
- end
-
- class Memory < DigitalInformationUnit
- Mode = RubyProf::MEMORY if RubyProf.const_defined?(:MEMORY)
-
- # Ruby 1.9 + GCdata patch
- if GC.respond_to?(:malloc_allocated_size)
- def measure
- GC.malloc_allocated_size
- end
- end
- end
-
- class Objects < Amount
- Mode = RubyProf::ALLOCATIONS if RubyProf.const_defined?(:ALLOCATIONS)
-
- # Ruby 1.9 + GCdata patch
- if GC.respond_to?(:malloc_allocations)
- def measure
- GC.malloc_allocations
- end
- end
- end
-
- class GcRuns < Amount
- Mode = RubyProf::GC_RUNS if RubyProf.const_defined?(:GC_RUNS)
-
- def measure
- GC.count
- end
- end
-
- class GcTime < Time
- Mode = RubyProf::GC_TIME if RubyProf.const_defined?(:GC_TIME)
-
- def measure
- GC::Profiler.total_time
- end
- end
- end
- end
- end
-end
diff --git a/activesupport/lib/active_support/testing/setup_and_teardown.rb b/activesupport/lib/active_support/testing/setup_and_teardown.rb
index a65148cf1f..33f2b8dc9b 100644
--- a/activesupport/lib/active_support/testing/setup_and_teardown.rb
+++ b/activesupport/lib/active_support/testing/setup_and_teardown.rb
@@ -3,6 +3,19 @@ require 'active_support/callbacks'
module ActiveSupport
module Testing
+ # Adds support for +setup+ and +teardown+ callbacks.
+ # These callbacks serve as a replacement to overwriting the
+ # <tt>#setup</tt> and <tt>#teardown</tt> methods of your TestCase.
+ #
+ # class ExampleTest < ActiveSupport::TestCase
+ # setup do
+ # # ...
+ # end
+ #
+ # teardown do
+ # # ...
+ # end
+ # end
module SetupAndTeardown
extend ActiveSupport::Concern
@@ -12,21 +25,23 @@ module ActiveSupport
end
module ClassMethods
+ # Add a callback, which runs before <tt>TestCase#setup</tt>.
def setup(*args, &block)
set_callback(:setup, :before, *args, &block)
end
+ # Add a callback, which runs after <tt>TestCase#teardown</tt>.
def teardown(*args, &block)
set_callback(:teardown, :after, *args, &block)
end
end
- def before_setup
+ def before_setup # :nodoc:
super
run_callbacks :setup
end
- def after_teardown
+ def after_teardown # :nodoc:
run_callbacks :teardown
super
end
diff --git a/activesupport/lib/active_support/testing/tagged_logging.rb b/activesupport/lib/active_support/testing/tagged_logging.rb
index 899467c45f..f4cee64091 100644
--- a/activesupport/lib/active_support/testing/tagged_logging.rb
+++ b/activesupport/lib/active_support/testing/tagged_logging.rb
@@ -1,30 +1,25 @@
-require 'active_support/concern'
-
module ActiveSupport
module Testing
- module TaggedLogging
- extend ActiveSupport::Concern
-
+ # Logs a "PostsControllerTest: test name" heading before each test to
+ # make test.log easier to search and follow along with.
+ module TaggedLogging #:nodoc:
attr_writer :tagged_logger
def before_setup
- tagged_logger.push_tags(self.class.name, __name__) if tagged_logging?
- super
- end
-
- def after_teardown
+ if tagged_logger
+ heading = "#{self.class}: #{name}"
+ divider = '-' * heading.size
+ tagged_logger.info divider
+ tagged_logger.info heading
+ tagged_logger.info divider
+ end
super
- tagged_logger.pop_tags(2) if tagged_logging?
end
private
def tagged_logger
@tagged_logger ||= (defined?(Rails.logger) && Rails.logger)
end
-
- def tagged_logging?
- tagged_logger && tagged_logger.respond_to?(:push_tags)
- end
end
end
end
diff --git a/activesupport/lib/active_support/time.rb b/activesupport/lib/active_support/time.rb
index bcd5d78b54..92a593965e 100644
--- a/activesupport/lib/active_support/time.rb
+++ b/activesupport/lib/active_support/time.rb
@@ -9,21 +9,12 @@ end
require 'date'
require 'time'
-require 'active_support/core_ext/time/marshal'
-require 'active_support/core_ext/time/acts_like'
-require 'active_support/core_ext/time/calculations'
-require 'active_support/core_ext/time/conversions'
-require 'active_support/core_ext/time/zones'
-
-require 'active_support/core_ext/date/acts_like'
-require 'active_support/core_ext/date/calculations'
-require 'active_support/core_ext/date/conversions'
-require 'active_support/core_ext/date/zones'
-
-require 'active_support/core_ext/date_time/acts_like'
-require 'active_support/core_ext/date_time/calculations'
-require 'active_support/core_ext/date_time/conversions'
-require 'active_support/core_ext/date_time/zones'
+require 'active_support/core_ext/time'
+require 'active_support/core_ext/date'
+require 'active_support/core_ext/date_time'
require 'active_support/core_ext/integer/time'
require 'active_support/core_ext/numeric/time'
+
+require 'active_support/core_ext/string/conversions'
+require 'active_support/core_ext/string/zones'
diff --git a/activesupport/lib/active_support/time_with_zone.rb b/activesupport/lib/active_support/time_with_zone.rb
index b931de3fac..95b9b8e5ae 100644
--- a/activesupport/lib/active_support/time_with_zone.rb
+++ b/activesupport/lib/active_support/time_with_zone.rb
@@ -80,22 +80,43 @@ module ActiveSupport
end
alias_method :getlocal, :localtime
+ # Returns true if the current time is within Daylight Savings Time for the
+ # specified time zone.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # Time.zone.parse("2012-5-30").dst? # => true
+ # Time.zone.parse("2012-11-30").dst? # => false
def dst?
period.dst?
end
alias_method :isdst, :dst?
+ # Returns true if the current time zone is set to UTC.
+ #
+ # Time.zone = 'UTC' # => 'UTC'
+ # Time.zone.now.utc? # => true
+ # Time.zone = 'Eastern Time (US & Canada)' # => 'Eastern Time (US & Canada)'
+ # Time.zone.now.utc? # => false
def utc?
time_zone.name == 'UTC'
end
alias_method :gmt?, :utc?
+ # Returns the offset from current time to UTC time in seconds.
def utc_offset
period.utc_total_offset
end
alias_method :gmt_offset, :utc_offset
alias_method :gmtoff, :utc_offset
+ # Returns a formatted string of the offset from UTC, or an alternative
+ # string if the time zone is already UTC.
+ #
+ # Time.zone = 'Eastern Time (US & Canada)' # => "Eastern Time (US & Canada)"
+ # Time.zone.now.formatted_offset(true) # => "-05:00"
+ # Time.zone.now.formatted_offset(false) # => "-0500"
+ # Time.zone = 'UTC' # => "UTC"
+ # Time.zone.now.formatted_offset(true, "0") # => "0"
def formatted_offset(colon = true, alternate_utc_string = nil)
utc? && alternate_utc_string || TimeZone.seconds_to_utc_offset(utc_offset, colon)
end
@@ -133,7 +154,7 @@ module ActiveSupport
# # => "2005/02/01 15:15:10 +0000"
def as_json(options = nil)
if ActiveSupport::JSON::Encoding.use_standard_json_time_format
- xmlschema
+ xmlschema(3)
else
%(#{time.strftime("%Y/%m/%d %H:%M:%S")} #{formatted_offset(false)})
end
@@ -147,10 +168,18 @@ module ActiveSupport
end
end
+ # Returns a string of the object's date and time in the format used by
+ # HTTP requests.
+ #
+ # Time.zone.now.httpdate # => "Tue, 01 Jan 2013 04:39:43 GMT"
def httpdate
utc.httpdate
end
+ # Returns a string of the object's date and time in the RFC 2822 standard
+ # format.
+ #
+ # Time.zone.now.rfc2822 # => "Tue, 01 Jan 2013 04:51:39 +0000"
def rfc2822
to_s(:rfc822)
end
@@ -185,18 +214,24 @@ module ActiveSupport
utc <=> other
end
+ # Returns true if the current object's time is within the specified
+ # +min+ and +max+ time.
def between?(min, max)
utc.between?(min, max)
end
+ # Returns true if the current object's time is in the past.
def past?
utc.past?
end
+ # Returns true if the current object's time falls within
+ # the current day.
def today?
time.today?
end
+ # Returns true if the current object's time is in the future.
def future?
utc.future?
end
@@ -257,7 +292,7 @@ module ActiveSupport
end
end
- %w(year mon month day mday wday yday hour min sec to_date).each do |method_name|
+ %w(year mon month day mday wday yday hour min sec usec nsec to_date).each do |method_name|
class_eval <<-EOV, __FILE__, __LINE__ + 1
def #{method_name} # def month
time.#{method_name} # time.month
@@ -265,10 +300,6 @@ module ActiveSupport
EOV
end
- def usec
- time.respond_to?(:usec) ? time.usec : 0
- end
-
def to_a
[time.sec, time.min, time.hour, time.day, time.mon, time.year, time.wday, time.yday, dst?, zone]
end
@@ -282,9 +313,13 @@ module ActiveSupport
end
alias_method :tv_sec, :to_i
- # A TimeWithZone acts like a Time, so just return +self+.
+ def to_r
+ utc.to_r
+ end
+
+ # Return an instance of Time in the system timezone.
def to_time
- utc
+ utc.to_time
end
def to_datetime
@@ -327,6 +362,8 @@ module ActiveSupport
# TimeWithZone with the existing +time_zone+.
def method_missing(sym, *args, &block)
wrap_with_time_zone time.__send__(sym, *args, &block)
+ rescue NoMethodError => e
+ raise e, e.message.sub(time.inspect, self.inspect), e.backtrace
end
private
@@ -344,7 +381,7 @@ module ActiveSupport
end
def transfer_time_values_to_utc_constructor(time)
- ::Time.utc_time(time.year, time.month, time.day, time.hour, time.min, time.sec, time.respond_to?(:nsec) ? Rational(time.nsec, 1000) : 0)
+ ::Time.utc(time.year, time.month, time.day, time.hour, time.min, time.sec, Rational(time.nsec, 1000))
end
def duration_of_variable_length?(obj)
diff --git a/activesupport/lib/active_support/values/time_zone.rb b/activesupport/lib/active_support/values/time_zone.rb
index 231d61da96..3cf82a24b9 100644
--- a/activesupport/lib/active_support/values/time_zone.rb
+++ b/activesupport/lib/active_support/values/time_zone.rb
@@ -5,7 +5,7 @@ module ActiveSupport
# The TimeZone class serves as a wrapper around TZInfo::Timezone instances.
# It allows us to do the following:
#
- # * Limit the set of zones provided by TZInfo to a meaningful subset of 142
+ # * Limit the set of zones provided by TZInfo to a meaningful subset of 146
# zones.
# * Retrieve and display zones with a friendlier name
# (e.g., "Eastern Time (US & Canada)" instead of "America/New_York").
@@ -62,6 +62,7 @@ module ActiveSupport
"Newfoundland" => "America/St_Johns",
"Brasilia" => "America/Sao_Paulo",
"Buenos Aires" => "America/Argentina/Buenos_Aires",
+ "Montevideo" => "America/Montevideo",
"Georgetown" => "America/Guyana",
"Greenland" => "America/Godthab",
"Mid-Atlantic" => "Atlantic/South_Georgia",
@@ -150,7 +151,7 @@ module ActiveSupport
"Taipei" => "Asia/Taipei",
"Perth" => "Australia/Perth",
"Irkutsk" => "Asia/Irkutsk",
- "Ulaan Bataar" => "Asia/Ulaanbaatar",
+ "Ulaanbaatar" => "Asia/Ulaanbaatar",
"Seoul" => "Asia/Seoul",
"Osaka" => "Asia/Tokyo",
"Sapporo" => "Asia/Tokyo",
@@ -167,7 +168,7 @@ module ActiveSupport
"Guam" => "Pacific/Guam",
"Port Moresby" => "Pacific/Port_Moresby",
"Magadan" => "Asia/Magadan",
- "Solomon Is." => "Asia/Magadan",
+ "Solomon Is." => "Pacific/Guadalcanal",
"New Caledonia" => "Pacific/Noumea",
"Fiji" => "Pacific/Fiji",
"Kamchatka" => "Asia/Kamchatka",
@@ -176,6 +177,7 @@ module ActiveSupport
"Wellington" => "Pacific/Auckland",
"Nuku'alofa" => "Pacific/Tongatapu",
"Tokelau Is." => "Pacific/Fakaofo",
+ "Chatham Is." => "Pacific/Chatham",
"Samoa" => "Pacific/Apia"
}
@@ -238,7 +240,7 @@ module ActiveSupport
# Compare #name and TZInfo identifier to a supplied regexp, returning +true+
# if a match is found.
def =~(re)
- return true if name =~ re || MAPPING[name] =~ re
+ re === name || re === MAPPING[name]
end
# Returns a textual representation of this time zone.
@@ -252,7 +254,7 @@ module ActiveSupport
# Time.zone = 'Hawaii' # => "Hawaii"
# Time.zone.local(2007, 2, 1, 15, 30, 45) # => Thu, 01 Feb 2007 15:30:45 HST -10:00
def local(*args)
- time = Time.utc_time(*args)
+ time = Time.utc(*args)
ActiveSupport::TimeWithZone.new(nil, self, time)
end
@@ -278,18 +280,23 @@ 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)
- date_parts = Date._parse(str)
- return if date_parts.empty?
- time = Time.parse(str, now) rescue DateTime.parse(str)
-
- if date_parts[:offset].nil?
- if date_parts[:hour] && time.hour != date_parts[:hour]
- time = DateTime.parse(str)
- end
-
- ActiveSupport::TimeWithZone.new(nil, self, time)
+ 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(:hour, 0),
+ parts.fetch(:min, 0),
+ parts.fetch(:sec, 0) + parts.fetch(:sec_fraction, 0),
+ parts.fetch(:offset, 0)
+ )
+
+ if parts[:offset]
+ TimeWithZone.new(time.utc, self)
else
- time.in_time_zone(self)
+ TimeWithZone.new(nil, self, time)
end
end
@@ -321,7 +328,7 @@ module ActiveSupport
end
# Available so that TimeZone instances respond like TZInfo::Timezone
- # instances.
+ # instances.
def period_for_utc(time)
tzinfo.period_for_utc(time)
end
diff --git a/activesupport/lib/active_support/values/unicode_tables.dat b/activesupport/lib/active_support/values/unicode_tables.dat
index df17a8cccf..2571faa019 100644
--- a/activesupport/lib/active_support/values/unicode_tables.dat
+++ b/activesupport/lib/active_support/values/unicode_tables.dat
Binary files differ
diff --git a/activesupport/lib/active_support/version.rb b/activesupport/lib/active_support/version.rb
index 8a8f8f946d..8762330a6e 100644
--- a/activesupport/lib/active_support/version.rb
+++ b/activesupport/lib/active_support/version.rb
@@ -1,10 +1,11 @@
module ActiveSupport
- module VERSION #:nodoc:
- MAJOR = 4
- MINOR = 0
- TINY = 0
- PRE = "beta"
+ # Returns the version of the currently loaded ActiveSupport as a Gem::Version
+ def self.version
+ Gem::Version.new "4.1.0.beta"
+ end
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ module VERSION #:nodoc:
+ MAJOR, MINOR, TINY, PRE = ActiveSupport.version.segments
+ STRING = ActiveSupport.version.to_s
end
end
diff --git a/activesupport/lib/active_support/xml_mini.rb b/activesupport/lib/active_support/xml_mini.rb
index 88f9acb588..d082a0a499 100644
--- a/activesupport/lib/active_support/xml_mini.rb
+++ b/activesupport/lib/active_support/xml_mini.rb
@@ -76,23 +76,24 @@ module ActiveSupport
)
end
- attr_reader :backend
delegate :parse, :to => :backend
+ def backend
+ current_thread_backend || @backend
+ end
+
def backend=(name)
- if name.is_a?(Module)
- @backend = name
- else
- require "active_support/xml_mini/#{name.downcase}"
- @backend = ActiveSupport.const_get("XmlMini_#{name}")
- end
+ backend = name && cast_backend_name_to_module(name)
+ self.current_thread_backend = backend if current_thread_backend
+ @backend = backend
end
def with_backend(name)
- old_backend, self.backend = backend, name
+ old_backend = current_thread_backend
+ self.current_thread_backend = name && cast_backend_name_to_module(name)
yield
ensure
- self.backend = old_backend
+ self.current_thread_backend = old_backend
end
def to_tag(key, value, options)
@@ -163,6 +164,25 @@ module ActiveSupport
f.content_type = entity['content_type']
f
end
+
+ private
+
+ def current_thread_backend
+ Thread.current[:xml_mini_backend]
+ end
+
+ def current_thread_backend=(name)
+ Thread.current[:xml_mini_backend] = name && cast_backend_name_to_module(name)
+ end
+
+ def cast_backend_name_to_module(name)
+ if name.is_a?(Module)
+ name
+ else
+ require "active_support/xml_mini/#{name.downcase}"
+ ActiveSupport.const_get("XmlMini_#{name}")
+ end
+ end
end
XmlMini.backend = 'REXML'
diff --git a/activesupport/lib/active_support/xml_mini/jdom.rb b/activesupport/lib/active_support/xml_mini/jdom.rb
index 4551dd2f2d..27c64c4dca 100644
--- a/activesupport/lib/active_support/xml_mini/jdom.rb
+++ b/activesupport/lib/active_support/xml_mini/jdom.rb
@@ -37,6 +37,12 @@ module ActiveSupport
{}
else
@dbf = DocumentBuilderFactory.new_instance
+ # secure processing of java xml
+ # http://www.ibm.com/developerworks/xml/library/x-tipcfsx/index.html
+ @dbf.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false)
+ @dbf.setFeature("http://xml.org/sax/features/external-general-entities", false)
+ @dbf.setFeature("http://xml.org/sax/features/external-parameter-entities", false)
+ @dbf.setFeature(javax.xml.XMLConstants::FEATURE_SECURE_PROCESSING, true)
xml_string_reader = StringReader.new(data)
xml_input_source = InputSource.new(xml_string_reader)
doc = @dbf.new_document_builder.parse(xml_input_source)
diff --git a/activesupport/lib/active_support/xml_mini/libxmlsax.rb b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
index acc018fd2d..70a95299ec 100644
--- a/activesupport/lib/active_support/xml_mini/libxmlsax.rb
+++ b/activesupport/lib/active_support/xml_mini/libxmlsax.rb
@@ -32,7 +32,7 @@ module ActiveSupport
end
def on_start_element(name, attrs = {})
- new_hash = { CONTENT_KEY => '' }.merge(attrs)
+ new_hash = { CONTENT_KEY => '' }.merge!(attrs)
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
diff --git a/activesupport/lib/active_support/xml_mini/nokogirisax.rb b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
index 30b94aac47..be2d6a4cb1 100644
--- a/activesupport/lib/active_support/xml_mini/nokogirisax.rb
+++ b/activesupport/lib/active_support/xml_mini/nokogirisax.rb
@@ -38,7 +38,7 @@ module ActiveSupport
end
def start_element(name, attrs = [])
- new_hash = { CONTENT_KEY => '' }.merge(Hash[attrs])
+ new_hash = { CONTENT_KEY => '' }.merge!(Hash[attrs])
new_hash[HASH_SIZE_KEY] = new_hash.size + 1
case current_hash[name]
diff --git a/activesupport/test/abstract_unit.rb b/activesupport/test/abstract_unit.rb
index 8a67b148c3..65de48a7f6 100644
--- a/activesupport/test/abstract_unit.rb
+++ b/activesupport/test/abstract_unit.rb
@@ -8,18 +8,29 @@ ensure
end
require 'active_support/core_ext/kernel/reporting'
-require 'active_support/core_ext/string/encoding'
silence_warnings do
Encoding.default_internal = "UTF-8"
Encoding.default_external = "UTF-8"
end
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'empty_bool'
ENV['NO_RELOAD'] = '1'
require 'active_support'
+Thread.abort_on_exception = true
+
# Show backtraces for deprecated behavior for quicker cleanup.
ActiveSupport::Deprecation.debug = true
+
+# Skips the current run on Rubinius using Minitest::Assertions#skip
+def rubinius_skip(message = '')
+ skip message if RUBY_ENGINE == 'rbx'
+end
+
+# Skips the current run on JRuby using Minitest::Assertions#skip
+def jruby_skip(message = '')
+ skip message if RUBY_ENGINE == 'jruby'
+end
diff --git a/activesupport/test/autoloading_fixtures/html/some_class.rb b/activesupport/test/autoloading_fixtures/html/some_class.rb
new file mode 100644
index 0000000000..b43d15d891
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/html/some_class.rb
@@ -0,0 +1,4 @@
+module HTML
+ class SomeClass
+ end
+end
diff --git a/activesupport/test/autoloading_fixtures/should_not_be_required.rb b/activesupport/test/autoloading_fixtures/should_not_be_required.rb
new file mode 100644
index 0000000000..1fcf170cc5
--- /dev/null
+++ b/activesupport/test/autoloading_fixtures/should_not_be_required.rb
@@ -0,0 +1 @@
+ShouldNotBeAutoloaded = 0
diff --git a/activesupport/test/caching_test.rb b/activesupport/test/caching_test.rb
index 9f76f4c90b..51007402a1 100644
--- a/activesupport/test/caching_test.rb
+++ b/activesupport/test/caching_test.rb
@@ -1,6 +1,7 @@
require 'logger'
require 'abstract_unit'
require 'active_support/cache'
+require 'dependencies_test_helpers'
class CacheKeyTest < ActiveSupport::TestCase
def test_entry_legacy_optional_ivars
@@ -83,7 +84,7 @@ class CacheKeyTest < ActiveSupport::TestCase
def test_expand_cache_key_of_true
assert_equal 'true', ActiveSupport::Cache.expand_cache_key(true)
end
-
+
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
@@ -197,6 +198,16 @@ module CacheStoreBehavior
assert_equal 'baz', @cache.fetch('foo') { 'baz' }
end
+ def test_fetch_with_cache_miss_passes_key_to_block
+ cache_miss = false
+ assert_equal 3, @cache.fetch('foo') { |key| cache_miss = true; key.length }
+ assert cache_miss
+
+ cache_miss = false
+ assert_equal 3, @cache.fetch('foo') { |key| cache_miss = true; key.length }
+ assert !cache_miss
+ end
+
def test_fetch_with_forced_cache_miss
@cache.write('foo', 'bar')
@cache.expects(:read).never
@@ -246,6 +257,26 @@ module CacheStoreBehavior
assert_equal({"fu" => "baz"}, @cache.read_multi('foo', 'fu'))
end
+ def test_fetch_multi
+ @cache.write('foo', 'bar')
+ @cache.write('fud', 'biz')
+
+ values = @cache.fetch_multi('foo', 'fu', 'fud') {|value| value * 2 }
+
+ assert_equal(["bar", "fufu", "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")
+
+ @cache.write('bar', "BAM!")
+
+ values = @cache.fetch_multi(foo, bar) {|object| object.title }
+ assert_equal(["FOO!", "BAM!"], values)
+ end
+
def test_read_and_write_compressed_small_data
@cache.write('foo', 'bar', :compress => true)
assert_equal 'bar', @cache.read('foo')
@@ -296,8 +327,8 @@ module CacheStoreBehavior
def test_exist
@cache.write('foo', 'bar')
- assert @cache.exist?('foo')
- assert !@cache.exist?('bar')
+ assert_equal true, @cache.exist?('foo')
+ assert_equal false, @cache.exist?('bar')
end
def test_nil_exist
@@ -394,7 +425,7 @@ module CacheStoreBehavior
end
# https://rails.lighthouseapp.com/projects/8994/tickets/6225-memcachestore-cant-deal-with-umlauts-and-special-characters
-# The error is caused by charcter encodings that can't be compared with ASCII-8BIT regular expressions and by special
+# The error is caused by character encodings that can't be compared with ASCII-8BIT regular expressions and by special
# characters like the umlaut in UTF-8.
module EncodedKeyCacheBehavior
Encoding.list.each do |encoding|
@@ -552,6 +583,45 @@ module LocalCacheBehavior
end
end
+module AutoloadingCacheBehavior
+ include DependenciesTestHelpers
+ def test_simple_autoloading
+ with_autoloading_fixtures do
+ @cache.write('foo', E.new)
+ end
+
+ remove_constants(:E)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of E, @cache.read('foo')
+ end
+
+ remove_constants(:E)
+ ActiveSupport::Dependencies.clear
+ end
+
+ def test_two_classes_autoloading
+ with_autoloading_fixtures do
+ @cache.write('foo', [E.new, ClassFolder.new])
+ end
+
+ remove_constants(:E, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ loaded = @cache.read('foo')
+ assert_kind_of Array, loaded
+ assert_equal 2, loaded.size
+ assert_kind_of E, loaded[0]
+ assert_kind_of ClassFolder, loaded[1]
+ end
+
+ remove_constants(:E, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+ end
+end
+
class FileStoreTest < ActiveSupport::TestCase
def setup
Dir.mkdir(cache_dir) unless File.exist?(cache_dir)
@@ -575,6 +645,7 @@ class FileStoreTest < ActiveSupport::TestCase
include LocalCacheBehavior
include CacheDeleteMatchedBehavior
include CacheIncrementDecrementBehavior
+ include AutoloadingCacheBehavior
def test_clear
filepath = File.join(cache_dir, ".gitkeep")
@@ -594,7 +665,7 @@ class FileStoreTest < ActiveSupport::TestCase
assert_equal "views/index?id=1", @cache_with_pathname.send(:file_path_key, key)
end
- # Test that generated cache keys are short enough to have Tempfile stuff added to them and
+ # Test that generated cache keys are short enough to have Tempfile stuff added to them and
# remain valid
def test_filename_max_size
key = "#{'A' * ActiveSupport::Cache::FileStore::FILENAME_MAX_SIZE}"
@@ -621,17 +692,48 @@ class FileStoreTest < ActiveSupport::TestCase
end
end
+ def test_delete_does_not_delete_empty_parent_dir
+ sub_cache_dir = File.join(cache_dir, 'subdir/')
+ sub_cache_store = ActiveSupport::Cache::FileStore.new(sub_cache_dir)
+ assert_nothing_raised(Exception) do
+ assert sub_cache_store.write('foo', 'bar')
+ assert sub_cache_store.delete('foo')
+ end
+ assert File.exist?(cache_dir), "Parent of top level cache dir was deleted!"
+ assert File.exist?(sub_cache_dir), "Top level cache dir was deleted!"
+ assert Dir.entries(sub_cache_dir).reject {|f| ActiveSupport::Cache::FileStore::EXCLUDED_DIRS.include?(f)}.empty?
+ end
+
def test_log_exception_when_cache_read_fails
File.expects(:exist?).raises(StandardError, "failed")
@cache.send(:read_entry, "winston", {})
- assert_present @buffer.string
+ assert @buffer.string.present?
+ end
+
+ def test_cleanup_removes_all_expired_entries
+ time = Time.now
+ @cache.write('foo', 'bar', expires_in: 10)
+ @cache.write('baz', 'qux')
+ @cache.write('quux', 'corge', expires_in: 20)
+ Time.stubs(:now).returns(time + 15)
+ @cache.cleanup
+ assert_not @cache.exist?('foo')
+ assert @cache.exist?('baz')
+ assert @cache.exist?('quux')
+ end
+
+ def test_write_with_unless_exist
+ assert_equal true, @cache.write(1, "aaaaaaaaaa")
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
+ @cache.write(1, nil)
+ assert_equal false, @cache.write(1, "aaaaaaaaaa", unless_exist: true)
end
end
class MemoryStoreTest < ActiveSupport::TestCase
def setup
- @record_size = ActiveSupport::Cache::Entry.new("aaaaaaaaaa").size
- @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10)
+ @record_size = ActiveSupport::Cache.lookup_store(:memory_store).send(:cached_size, 1, ActiveSupport::Cache::Entry.new("aaaaaaaaaa"))
+ @cache = ActiveSupport::Cache.lookup_store(:memory_store, :expires_in => 60, :size => @record_size * 10 + 1)
end
include CacheStoreBehavior
@@ -681,6 +783,30 @@ class MemoryStoreTest < ActiveSupport::TestCase
assert !@cache.exist?(1), "no entry"
end
+ def test_prune_size_on_write_based_on_key_length
+ @cache.write(1, "aaaaaaaaaa") && sleep(0.001)
+ @cache.write(2, "bbbbbbbbbb") && sleep(0.001)
+ @cache.write(3, "cccccccccc") && sleep(0.001)
+ @cache.write(4, "dddddddddd") && sleep(0.001)
+ @cache.write(5, "eeeeeeeeee") && sleep(0.001)
+ @cache.write(6, "ffffffffff") && sleep(0.001)
+ @cache.write(7, "gggggggggg") && sleep(0.001)
+ @cache.write(8, "hhhhhhhhhh") && sleep(0.001)
+ @cache.write(9, "iiiiiiiiii") && sleep(0.001)
+ long_key = '*' * 2 * @record_size
+ @cache.write(long_key, "llllllllll")
+ assert @cache.exist?(long_key)
+ assert @cache.exist?(9)
+ assert @cache.exist?(8)
+ assert @cache.exist?(7)
+ assert @cache.exist?(6)
+ assert !@cache.exist?(5), "no entry"
+ assert !@cache.exist?(4), "no entry"
+ assert !@cache.exist?(3), "no entry"
+ assert !@cache.exist?(2), "no entry"
+ assert !@cache.exist?(1), "no entry"
+ end
+
def test_pruning_is_capped_at_a_max_time
def @cache.delete_entry (*args)
sleep(0.01)
@@ -735,6 +861,7 @@ class MemCacheStoreTest < ActiveSupport::TestCase
include LocalCacheBehavior
include CacheIncrementDecrementBehavior
include EncodedKeyCacheBehavior
+ include AutoloadingCacheBehavior
def test_raw_values
cache = ActiveSupport::Cache.lookup_store(:mem_cache_store, :raw => true)
@@ -845,12 +972,12 @@ class CacheStoreLoggerTest < ActiveSupport::TestCase
def test_logging
@cache.fetch('foo') { 'bar' }
- assert_present @buffer.string
+ assert @buffer.string.present?
end
def test_mute_logging
@cache.mute { @cache.fetch('foo') { 'bar' } }
- assert_blank @buffer.string
+ assert @buffer.string.blank?
end
end
@@ -879,29 +1006,28 @@ class CacheEntryTest < ActiveSupport::TestCase
assert_equal value.bytesize, entry.size
end
- def test_restoring_version_3_entries
- version_3_entry = ActiveSupport::Cache::Entry.allocate
- version_3_entry.instance_variable_set(:@value, "hello")
- version_3_entry.instance_variable_set(:@created_at, Time.now - 60)
- entry = Marshal.load(Marshal.dump(version_3_entry))
+ def test_restoring_version_4beta1_entries
+ version_4beta1_entry = ActiveSupport::Cache::Entry.allocate
+ version_4beta1_entry.instance_variable_set(:@v, "hello")
+ version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i + 60)
+ entry = Marshal.load(Marshal.dump(version_4beta1_entry))
assert_equal "hello", entry.value
assert_equal false, entry.expired?
end
- def test_restoring_compressed_version_3_entries
- version_3_entry = ActiveSupport::Cache::Entry.allocate
- version_3_entry.instance_variable_set(:@value, Zlib::Deflate.deflate(Marshal.dump("hello")))
- version_3_entry.instance_variable_set(:@compressed, true)
- entry = Marshal.load(Marshal.dump(version_3_entry))
+ def test_restoring_compressed_version_4beta1_entries
+ version_4beta1_entry = ActiveSupport::Cache::Entry.allocate
+ version_4beta1_entry.instance_variable_set(:@v, Zlib::Deflate.deflate(Marshal.dump("hello")))
+ version_4beta1_entry.instance_variable_set(:@c, true)
+ entry = Marshal.load(Marshal.dump(version_4beta1_entry))
assert_equal "hello", entry.value
end
- def test_restoring_expired_version_3_entries
- version_3_entry = ActiveSupport::Cache::Entry.allocate
- version_3_entry.instance_variable_set(:@value, "hello")
- version_3_entry.instance_variable_set(:@created_at, Time.now - 60)
- version_3_entry.instance_variable_set(:@expires_in, 58.9)
- entry = Marshal.load(Marshal.dump(version_3_entry))
+ def test_restoring_expired_version_4beta1_entries
+ version_4beta1_entry = ActiveSupport::Cache::Entry.allocate
+ version_4beta1_entry.instance_variable_set(:@v, "hello")
+ version_4beta1_entry.instance_variable_set(:@x, Time.now.to_i - 1)
+ entry = Marshal.load(Marshal.dump(version_4beta1_entry))
assert_equal "hello", entry.value
assert_equal true, entry.expired?
end
diff --git a/activesupport/test/callback_inheritance_test.rb b/activesupport/test/callback_inheritance_test.rb
index 6be8ea8b84..1adfe4edf4 100644
--- a/activesupport/test/callback_inheritance_test.rb
+++ b/activesupport/test/callback_inheritance_test.rb
@@ -109,7 +109,6 @@ class BasicCallbacksTest < ActiveSupport::TestCase
@index = GrandParent.new("index").dispatch
@update = GrandParent.new("update").dispatch
@delete = GrandParent.new("delete").dispatch
- @unknown = GrandParent.new("unknown").dispatch
end
def test_basic_conditional_callback1
@@ -130,7 +129,6 @@ class InheritedCallbacksTest < ActiveSupport::TestCase
@index = Parent.new("index").dispatch
@update = Parent.new("update").dispatch
@delete = Parent.new("delete").dispatch
- @unknown = Parent.new("unknown").dispatch
end
def test_inherited_excluded
diff --git a/activesupport/test/callbacks_test.rb b/activesupport/test/callbacks_test.rb
index 8810302f40..f8e2ce22fa 100644
--- a/activesupport/test/callbacks_test.rb
+++ b/activesupport/test/callbacks_test.rb
@@ -66,6 +66,16 @@ module CallbacksTest
end
end
+ class CallbackClass
+ def self.before(model)
+ model.history << [:before_save, :class]
+ end
+
+ def self.after(model)
+ model.history << [:after_save, :class]
+ end
+ end
+
class Person < Record
[:before_save, :after_save].each do |callback_method|
callback_method_sym = callback_method.to_sym
@@ -73,6 +83,7 @@ module CallbacksTest
send(callback_method, callback_string(callback_method_sym))
send(callback_method, callback_proc(callback_method_sym))
send(callback_method, callback_object(callback_method_sym.to_s.gsub(/_save/, '')))
+ send(callback_method, CallbackClass)
send(callback_method) { |model| model.history << [callback_method_sym, :block] }
end
@@ -86,10 +97,14 @@ module CallbacksTest
skip_callback :save, :after, :before_save_method, :unless => :yes
skip_callback :save, :after, :before_save_method, :if => :no
skip_callback :save, :before, :before_save_method, :unless => :no
+ skip_callback :save, :before, CallbackClass , :if => :yes
def yes; true; end
def no; false; end
end
+ class PersonForProgrammaticSkipping < Person
+ end
+
class ParentController
include ActiveSupport::Callbacks
@@ -297,7 +312,7 @@ module CallbacksTest
end
end
end
-
+
class AroundPersonResult < MySuper
attr_reader :result
@@ -308,7 +323,7 @@ module CallbacksTest
def tweedle_dum
@result = yield
end
-
+
def tweedle_1
:tweedle_1
end
@@ -316,7 +331,7 @@ module CallbacksTest
def tweedle_2
:tweedle_2
end
-
+
def save
run_callbacks :save do
:running
@@ -410,7 +425,7 @@ module CallbacksTest
], around.history
end
end
-
+
class AroundCallbackResultTest < ActiveSupport::TestCase
def test_save_around
around = AroundPersonResult.new
@@ -430,6 +445,26 @@ module CallbacksTest
[:before_save, :object],
[:before_save, :block],
[:after_save, :block],
+ [:after_save, :class],
+ [:after_save, :object],
+ [:after_save, :proc],
+ [:after_save, :string],
+ [:after_save, :symbol]
+ ], person.history
+ end
+
+ def test_skip_person_programmatically
+ PersonForProgrammaticSkipping._save_callbacks.each do |save_callback|
+ if "before" == save_callback.kind.to_s
+ PersonForProgrammaticSkipping.skip_callback("save", save_callback.kind, save_callback.filter)
+ end
+ end
+ person = PersonForProgrammaticSkipping.new
+ assert_equal [], person.history
+ person.save
+ assert_equal [
+ [:after_save, :block],
+ [:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
[:after_save, :string],
@@ -449,8 +484,10 @@ module CallbacksTest
[:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
+ [:before_save, :class],
[:before_save, :block],
[:after_save, :block],
+ [:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
[:after_save, :string],
@@ -488,7 +525,7 @@ module CallbacksTest
class CallbackTerminator
include ActiveSupport::Callbacks
- define_callbacks :save, :terminator => "result == :halt"
+ define_callbacks :save, :terminator => ->(_,result) { result == :halt }
set_callback :save, :before, :first
set_callback :save, :before, :second
@@ -607,6 +644,45 @@ module CallbacksTest
end
end
+ class OneTwoThreeSave
+ include ActiveSupport::Callbacks
+
+ define_callbacks :save
+
+ attr_accessor :record
+
+ def initialize
+ @record = []
+ end
+
+ def save
+ run_callbacks :save do
+ @record << "yielded"
+ end
+ end
+
+ def first
+ @record << "one"
+ end
+
+ def second
+ @record << "two"
+ end
+
+ def third
+ @record << "three"
+ end
+ end
+
+ class DuplicatingCallbacks < OneTwoThreeSave
+ set_callback :save, :before, :first, :second
+ set_callback :save, :before, :first, :third
+ end
+
+ class DuplicatingCallbacksInSameCall < OneTwoThreeSave
+ set_callback :save, :before, :first, :second, :first, :third
+ end
+
class UsingObjectTest < ActiveSupport::TestCase
def test_before_object
u = UsingObjectBefore.new
@@ -642,7 +718,7 @@ module CallbacksTest
def test_termination_invokes_hook
terminator = CallbackTerminator.new
terminator.save
- assert_equal ":second", terminator.halted
+ assert_equal :second, terminator.halted
end
def test_block_never_called_if_terminated
@@ -676,8 +752,10 @@ module CallbacksTest
[:before_save, :string],
[:before_save, :proc],
[:before_save, :object],
+ [:before_save, :class],
[:before_save, :block],
[:after_save, :block],
+ [:after_save, :class],
[:after_save, :object],
[:after_save, :proc],
[:after_save, :string],
@@ -694,20 +772,253 @@ module CallbacksTest
end
end
- class PerKeyOptionDeprecationTest < ActiveSupport::TestCase
+ class ExcludingDuplicatesCallbackTest < ActiveSupport::TestCase
+ def test_excludes_duplicates_in_separate_calls
+ model = DuplicatingCallbacks.new
+ model.save
+ assert_equal ["two", "one", "three", "yielded"], model.record
+ end
- def test_per_key_option_deprecaton
- assert_raise NotImplementedError do
- Phone.class_eval do
- set_callback :save, :before, :before_save1, :per_key => {:if => "true"}
- end
+ def test_excludes_duplicates_in_one_call
+ model = DuplicatingCallbacksInSameCall.new
+ model.save
+ assert_equal ["two", "one", "three", "yielded"], model.record
+ end
+ end
+
+ class CallbackProcTest < ActiveSupport::TestCase
+ def build_class(callback)
+ Class.new {
+ include ActiveSupport::Callbacks
+ define_callbacks :foo
+ set_callback :foo, :before, callback
+ def run; run_callbacks :foo; end
+ }
+ end
+
+ def test_proc_arity_0
+ calls = []
+ klass = build_class(->() { calls << :foo })
+ klass.new.run
+ assert_equal [:foo], calls
+ end
+
+ def test_proc_arity_1
+ calls = []
+ klass = build_class(->(o) { calls << o })
+ instance = klass.new
+ instance.run
+ assert_equal [instance], calls
+ end
+
+ def test_proc_arity_2
+ assert_raises(ArgumentError) do
+ klass = build_class(->(x,y) { })
+ klass.new.run
end
- assert_raise NotImplementedError do
- Phone.class_eval do
- skip_callback :save, :before, :before_save1, :per_key => {:if => "true"}
- end
+ end
+
+ def test_proc_negative_called_with_empty_list
+ calls = []
+ klass = build_class(->(*args) { calls << args })
+ klass.new.run
+ assert_equal [[]], calls
+ end
+ end
+
+ class ConditionalTests < ActiveSupport::TestCase
+ def build_class(callback)
+ Class.new {
+ include ActiveSupport::Callbacks
+ define_callbacks :foo
+ set_callback :foo, :before, :foo, :if => callback
+ def foo; end
+ def run; run_callbacks :foo; end
+ }
+ end
+
+ # FIXME: do we really want to support classes as conditionals? There were
+ # no tests for it previous to this.
+ def test_class_conditional_with_scope
+ z = []
+ callback = Class.new {
+ define_singleton_method(:foo) { |o| z << o }
+ }
+ klass = Class.new {
+ include ActiveSupport::Callbacks
+ define_callbacks :foo, :scope => [:name]
+ set_callback :foo, :before, :foo, :if => callback
+ def run; run_callbacks :foo; end
+ private
+ def foo; end
+ }
+ object = klass.new
+ object.run
+ assert_equal [object], z
+ end
+
+ # FIXME: do we really want to support classes as conditionals? There were
+ # no tests for it previous to this.
+ def test_class
+ z = []
+ klass = build_class Class.new {
+ define_singleton_method(:before) { |o| z << o }
+ }
+ object = klass.new
+ object.run
+ assert_equal [object], z
+ end
+
+ def test_proc_negative_arity # passes an empty list if *args
+ z = []
+ object = build_class(->(*args) { z << args }).new
+ object.run
+ assert_equal [], z.flatten
+ end
+
+ def test_proc_arity0
+ z = []
+ object = build_class(->() { z << 0 }).new
+ object.run
+ assert_equal [0], z
+ end
+
+ def test_proc_arity1
+ z = []
+ object = build_class(->(x) { z << x }).new
+ object.run
+ assert_equal [object], z
+ end
+
+ def test_proc_arity2
+ assert_raises(ArgumentError) do
+ object = build_class(->(a,b) { }).new
+ object.run
end
end
end
-
+
+ class ResetCallbackTest < ActiveSupport::TestCase
+ def build_class(memo)
+ klass = Class.new {
+ include ActiveSupport::Callbacks
+ define_callbacks :foo
+ set_callback :foo, :before, :hello
+ def run; run_callbacks :foo; end
+ }
+ klass.class_eval {
+ define_method(:hello) { memo << :hi }
+ }
+ klass
+ end
+
+ def test_reset_callbacks
+ events = []
+ klass = build_class events
+ klass.new.run
+ assert_equal 1, events.length
+
+ klass.reset_callbacks :foo
+ klass.new.run
+ assert_equal 1, events.length
+ end
+
+ def test_reset_impacts_subclasses
+ events = []
+ klass = build_class events
+ subclass = Class.new(klass) { set_callback :foo, :before, :world }
+ subclass.class_eval { define_method(:world) { events << :world } }
+
+ subclass.new.run
+ assert_equal 2, events.length
+
+ klass.reset_callbacks :foo
+ subclass.new.run
+ assert_equal 3, events.length
+ end
+ end
+
+ class CallbackTypeTest < ActiveSupport::TestCase
+ def build_class(callback, n = 10)
+ Class.new {
+ include ActiveSupport::Callbacks
+ define_callbacks :foo
+ n.times { set_callback :foo, :before, callback }
+ def run; run_callbacks :foo; end
+ def self.skip(thing); skip_callback :foo, :before, thing; end
+ }
+ end
+
+ def test_add_class
+ calls = []
+ callback = Class.new {
+ define_singleton_method(:before) { |o| calls << o }
+ }
+ build_class(callback).new.run
+ assert_equal 10, calls.length
+ end
+
+ def test_add_lambda
+ calls = []
+ build_class(->(o) { calls << o }).new.run
+ assert_equal 10, calls.length
+ end
+
+ def test_add_symbol
+ calls = []
+ klass = build_class(:bar)
+ klass.class_eval { define_method(:bar) { calls << klass } }
+ klass.new.run
+ assert_equal 1, calls.length
+ end
+
+ def test_add_eval
+ calls = []
+ klass = build_class("bar")
+ klass.class_eval { define_method(:bar) { calls << klass } }
+ klass.new.run
+ assert_equal 1, calls.length
+ end
+
+ def test_skip_class # removes one at a time
+ calls = []
+ callback = Class.new {
+ define_singleton_method(:before) { |o| calls << o }
+ }
+ klass = build_class(callback)
+ 9.downto(0) { |i|
+ klass.skip callback
+ klass.new.run
+ assert_equal i, calls.length
+ calls.clear
+ }
+ end
+
+ def test_skip_lambda # removes nothing
+ calls = []
+ callback = ->(o) { calls << o }
+ klass = build_class(callback)
+ 10.times { klass.skip callback }
+ klass.new.run
+ assert_equal 10, calls.length
+ end
+
+ def test_skip_symbol # removes all
+ calls = []
+ klass = build_class(:bar)
+ klass.class_eval { define_method(:bar) { calls << klass } }
+ klass.skip :bar
+ klass.new.run
+ assert_equal 0, calls.length
+ end
+
+ def test_skip_eval # removes nothing
+ calls = []
+ klass = build_class("bar")
+ klass.class_eval { define_method(:bar) { calls << klass } }
+ klass.skip "bar"
+ klass.new.run
+ assert_equal 1, calls.length
+ end
+ end
end
diff --git a/activesupport/test/clean_backtrace_test.rb b/activesupport/test/clean_backtrace_test.rb
index b14950acb3..dd67a45cf6 100644
--- a/activesupport/test/clean_backtrace_test.rb
+++ b/activesupport/test/clean_backtrace_test.rb
@@ -36,6 +36,27 @@ class BacktraceCleanerSilencerTest < ActiveSupport::TestCase
end
end
+class BacktraceCleanerMultipleSilencersTest < ActiveSupport::TestCase
+ def setup
+ @bc = ActiveSupport::BacktraceCleaner.new
+ @bc.add_silencer { |line| line =~ /mongrel/ }
+ @bc.add_silencer { |line| line =~ /yolo/ }
+ end
+
+ test "backtrace should not contain lines that match the silencers" do
+ assert_equal \
+ [ "/other/class.rb" ],
+ @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ])
+ end
+
+ test "backtrace should only contain lines that match the silencers" do
+ assert_equal \
+ [ "/mongrel/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ],
+ @bc.clean([ "/mongrel/class.rb", "/other/class.rb", "/mongrel/stuff.rb", "/other/yolo.rb" ],
+ :noise)
+ end
+end
+
class BacktraceCleanerFilterAndSilencerTest < ActiveSupport::TestCase
def setup
@bc = ActiveSupport::BacktraceCleaner.new
diff --git a/activesupport/test/concern_test.rb b/activesupport/test/concern_test.rb
index 912ce30c29..a74ee880b2 100644
--- a/activesupport/test/concern_test.rb
+++ b/activesupport/test/concern_test.rb
@@ -56,10 +56,6 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Baz)
assert_equal "baz", @klass.new.baz
assert @klass.included_modules.include?(ConcernTest::Baz)
-
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
end
def test_class_methods_are_extended
@@ -68,12 +64,6 @@ class ConcernTest < ActiveSupport::TestCase
assert_equal ConcernTest::Baz::ClassMethods, (class << @klass; self.included_modules; end)[0]
end
- def test_instance_methods_are_included
- @klass.send(:include, Baz)
- assert_equal "baz", @klass.new.baz
- assert @klass.included_modules.include?(ConcernTest::Baz)
- end
-
def test_included_block_is_ran
@klass.send(:include, Baz)
assert_equal true, @klass.included_ran
@@ -91,4 +81,18 @@ class ConcernTest < ActiveSupport::TestCase
@klass.send(:include, Foo)
assert_equal [ConcernTest::Foo, ConcernTest::Bar, ConcernTest::Baz], @klass.included_modules[0..2]
end
+
+ def test_raise_on_multiple_included_calls
+ assert_raises(ActiveSupport::Concern::MultipleIncludedBlocks) do
+ Module.new do
+ extend ActiveSupport::Concern
+
+ included do
+ end
+
+ included do
+ end
+ end
+ end
+ end
end
diff --git a/activesupport/test/configurable_test.rb b/activesupport/test/configurable_test.rb
index 215a6e06b0..d00273a028 100644
--- a/activesupport/test/configurable_test.rb
+++ b/activesupport/test/configurable_test.rb
@@ -38,7 +38,7 @@ class ConfigurableActiveSupport < ActiveSupport::TestCase
assert_equal :bar, Parent.config.foo
end
- test "configuration accessors is not available on instance" do
+ test "configuration accessors are not available on instance" do
instance = Parent.new
assert !instance.respond_to?(:bar)
diff --git a/activesupport/test/constantize_test_cases.rb b/activesupport/test/constantize_test_cases.rb
index ec05213409..bbeb710a0c 100644
--- a/activesupport/test/constantize_test_cases.rb
+++ b/activesupport/test/constantize_test_cases.rb
@@ -24,52 +24,52 @@ end
module ConstantizeTestCases
def run_constantize_tests_on
- assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") }
- assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") }
- assert_nothing_raised { assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice") }
- assert_nothing_raised { assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice") }
- assert_nothing_raised { assert_equal Ace::Gas::Case, yield("Ace::Gas::Case") }
- assert_nothing_raised { assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice") }
- assert_nothing_raised { assert_equal Case::Dice, yield("Case::Dice") }
- assert_nothing_raised { assert_equal Case::Dice, yield("Object::Case::Dice") }
- assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") }
- assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") }
- assert_nothing_raised { assert_equal Object, yield("") }
- assert_nothing_raised { assert_equal Object, yield("::") }
- assert_raise(NameError) { yield("UnknownClass") }
- assert_raise(NameError) { yield("UnknownClass::Ace") }
- assert_raise(NameError) { yield("UnknownClass::Ace::Base") }
- assert_raise(NameError) { yield("An invalid string") }
- assert_raise(NameError) { yield("InvalidClass\n") }
- assert_raise(NameError) { yield("Ace::ConstantizeTestCases") }
- assert_raise(NameError) { yield("Ace::Base::ConstantizeTestCases") }
- assert_raise(NameError) { yield("Ace::Gas::Base") }
- assert_raise(NameError) { yield("Ace::Gas::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::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 Case::Dice, yield("Case::Dice")
+ assert_equal Case::Dice, yield("Object::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") }
+ assert_raises(NameError) { yield("An invalid string") }
+ assert_raises(NameError) { yield("InvalidClass\n") }
+ assert_raises(NameError) { yield("Ace::ConstantizeTestCases") }
+ assert_raises(NameError) { yield("Ace::Base::ConstantizeTestCases") }
+ assert_raises(NameError) { yield("Ace::Gas::Base") }
+ assert_raises(NameError) { yield("Ace::Gas::ConstantizeTestCases") }
+ assert_raises(NameError) { yield("") }
+ assert_raises(NameError) { yield("::") }
end
def run_safe_constantize_tests_on
- assert_nothing_raised { assert_equal Ace::Base::Case, yield("Ace::Base::Case") }
- assert_nothing_raised { assert_equal Ace::Base::Case, yield("::Ace::Base::Case") }
- assert_nothing_raised { assert_equal Ace::Base::Case::Dice, yield("Ace::Base::Case::Dice") }
- assert_nothing_raised { assert_equal Ace::Base::Fase::Dice, yield("Ace::Base::Fase::Dice") }
- assert_nothing_raised { assert_equal Ace::Gas::Case, yield("Ace::Gas::Case") }
- assert_nothing_raised { assert_equal Ace::Gas::Case::Dice, yield("Ace::Gas::Case::Dice") }
- assert_nothing_raised { assert_equal Case::Dice, yield("Case::Dice") }
- assert_nothing_raised { assert_equal Case::Dice, yield("Object::Case::Dice") }
- assert_nothing_raised { assert_equal ConstantizeTestCases, yield("ConstantizeTestCases") }
- assert_nothing_raised { assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases") }
- assert_nothing_raised { assert_equal Object, yield("") }
- assert_nothing_raised { assert_equal Object, yield("::") }
- assert_nothing_raised { assert_equal nil, yield("UnknownClass") }
- assert_nothing_raised { assert_equal nil, yield("UnknownClass::Ace") }
- assert_nothing_raised { assert_equal nil, yield("UnknownClass::Ace::Base") }
- assert_nothing_raised { assert_equal nil, yield("An invalid string") }
- assert_nothing_raised { assert_equal nil, yield("InvalidClass\n") }
- assert_nothing_raised { assert_equal nil, yield("blargle") }
- assert_nothing_raised { assert_equal nil, yield("Ace::ConstantizeTestCases") }
- assert_nothing_raised { assert_equal nil, yield("Ace::Base::ConstantizeTestCases") }
- assert_nothing_raised { assert_equal nil, yield("Ace::Gas::Base") }
- assert_nothing_raised { assert_equal nil, yield("Ace::Gas::ConstantizeTestCases") }
- assert_nothing_raised { assert_equal nil, yield("#<Class:0x7b8b718b>::Nested_1") }
+ 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::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 Case::Dice, yield("Case::Dice")
+ assert_equal Case::Dice, yield("Object::Case::Dice")
+ assert_equal ConstantizeTestCases, yield("ConstantizeTestCases")
+ assert_equal ConstantizeTestCases, yield("::ConstantizeTestCases")
+ assert_nil yield("")
+ assert_nil yield("::")
+ assert_nil yield("UnknownClass")
+ assert_nil yield("UnknownClass::Ace")
+ assert_nil yield("UnknownClass::Ace::Base")
+ assert_nil yield("An invalid string")
+ assert_nil yield("InvalidClass\n")
+ assert_nil yield("blargle")
+ assert_nil yield("Ace::ConstantizeTestCases")
+ assert_nil yield("Ace::Base::ConstantizeTestCases")
+ assert_nil yield("Ace::Gas::Base")
+ assert_nil yield("Ace::Gas::ConstantizeTestCases")
+ assert_nil yield("#<Class:0x7b8b718b>::Nested_1")
end
end
diff --git a/activesupport/test/core_ext/array_ext_test.rb b/activesupport/test/core_ext/array_ext_test.rb
index 0b33a63460..6cd0eb39b7 100644
--- a/activesupport/test/core_ext/array_ext_test.rb
+++ b/activesupport/test/core_ext/array_ext_test.rb
@@ -1,6 +1,7 @@
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'
@@ -95,6 +96,10 @@ class ArrayExtToSentenceTests < ActiveSupport::TestCase
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
@@ -111,6 +116,14 @@ class ArrayExtToSTests < ActiveSupport::TestCase
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|
@@ -346,36 +359,6 @@ class ArrayExtractOptionsTests < ActiveSupport::TestCase
end
end
-class ArrayUniqByTests < ActiveSupport::TestCase
- def test_uniq_by
- ActiveSupport::Deprecation.silence do
- assert_equal [1,2], [1,2,3,4].uniq_by { |i| i.odd? }
- assert_equal [1,2], [1,2,3,4].uniq_by(&:even?)
- assert_equal((-5..0).to_a, (-5..5).to_a.uniq_by{ |i| i**2 })
- end
- end
-
- def test_uniq_by!
- a = [1,2,3,4]
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i.odd? }
- end
- assert_equal [1,2], a
-
- a = [1,2,3,4]
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i.even? }
- end
- assert_equal [1,2], a
-
- a = (-5..5).to_a
- ActiveSupport::Deprecation.silence do
- a.uniq_by! { |i| i**2 }
- end
- assert_equal((-5..0).to_a, a)
- end
-end
-
class ArrayWrapperTests < ActiveSupport::TestCase
class FakeCollection
def to_ary
diff --git a/activesupport/test/core_ext/bigdecimal_test.rb b/activesupport/test/core_ext/bigdecimal_test.rb
index a5987044b9..b386e55d6c 100644
--- a/activesupport/test/core_ext/bigdecimal_test.rb
+++ b/activesupport/test/core_ext/bigdecimal_test.rb
@@ -1,5 +1,4 @@
require 'abstract_unit'
-require 'bigdecimal'
require 'active_support/core_ext/big_decimal'
class BigDecimalTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/class/attribute_accessor_test.rb b/activesupport/test/core_ext/class/attribute_accessor_test.rb
index 8d827f054e..0d5f39a72b 100644
--- a/activesupport/test/core_ext/class/attribute_accessor_test.rb
+++ b/activesupport/test/core_ext/class/attribute_accessor_test.rb
@@ -44,16 +44,18 @@ class ClassAttributeAccessorTest < ActiveSupport::TestCase
end
def test_should_raise_name_error_if_attribute_name_is_invalid
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- cattr_reader "invalid attribute name"
+ cattr_reader "1nvalid"
end
end
+ assert_equal "invalid class attribute name: 1nvalid", exception.message
- assert_raises NameError do
+ exception = assert_raises NameError do
Class.new do
- cattr_writer "invalid attribute name"
+ cattr_writer "1nvalid"
end
end
+ assert_equal "invalid class attribute name: 1nvalid", exception.message
end
end
diff --git a/activesupport/test/core_ext/class/attribute_test.rb b/activesupport/test/core_ext/class/attribute_test.rb
index 1c3ba8a7a0..e7a1334db3 100644
--- a/activesupport/test/core_ext/class/attribute_test.rb
+++ b/activesupport/test/core_ext/class/attribute_test.rb
@@ -27,7 +27,7 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_equal 1, Class.new(@sub).setting
end
- test 'query method' do
+ test 'predicate method' do
assert_equal false, @klass.setting?
@klass.setting = 1
assert_equal true, @klass.setting?
@@ -48,7 +48,7 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_equal 1, object.setting
end
- test 'instance query' do
+ test 'instance predicate' do
object = @klass.new
assert_equal false, object.setting?
object.setting = 1
@@ -73,6 +73,11 @@ class ClassAttributeTest < ActiveSupport::TestCase
assert_raise(NoMethodError) { object.setting = 'boom' }
end
+ test 'disabling instance predicate' do
+ object = Class.new { class_attribute :setting, instance_predicate: false }.new
+ assert_raise(NoMethodError) { object.setting? }
+ end
+
test 'works well with singleton classes' do
object = @klass.new
object.singleton_class.setting = 'foo'
diff --git a/activesupport/test/core_ext/date_and_time_behavior.rb b/activesupport/test/core_ext/date_and_time_behavior.rb
index 9927856aa2..b4ef5a0597 100644
--- a/activesupport/test/core_ext/date_and_time_behavior.rb
+++ b/activesupport/test/core_ext/date_and_time_behavior.rb
@@ -95,6 +95,11 @@ module DateAndTimeBehavior
end
def test_next_week
+ # M | T | W | T | F | S | S # M | T | W | T | F | S | S #
+ # | 22/2 | | | | | # 28/2 | | | | | | # monday in next week `next_week`
+ # | 22/2 | | | | | # | | | | 4/3 | | # friday in next week `next_week(:friday)`
+ # 23/10 | | | | | | # 30/10 | | | | | | # monday in next week `next_week`
+ # 23/10 | | | | | | # | | 1/11 | | | | # wednesday in next week `next_week(:wednesday)`
assert_equal date_time_init(2005,2,28,0,0,0), date_time_init(2005,2,22,15,15,10).next_week
assert_equal date_time_init(2005,3,4,0,0,0), date_time_init(2005,2,22,15,15,10).next_week(:friday)
assert_equal date_time_init(2006,10,30,0,0,0), date_time_init(2006,10,23,0,0,0).next_week
diff --git a/activesupport/test/core_ext/date_ext_test.rb b/activesupport/test/core_ext/date_ext_test.rb
index 7ae1f67785..eab3aa7a6e 100644
--- a/activesupport/test/core_ext/date_ext_test.rb
+++ b/activesupport/test/core_ext/date_ext_test.rb
@@ -25,6 +25,7 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal "February 21st, 2005", date.to_s(:long_ordinal)
assert_equal "2005-02-21", date.to_s(:db)
assert_equal "21 Feb 2005", date.to_s(:rfc822)
+ assert_equal "2005-02-21", date.to_s(:iso8601)
end
def test_readable_inspect
@@ -33,8 +34,12 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
def test_to_time
- assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time
- assert_equal Time.local_time(2039, 2, 21), Date.new(2039, 2, 21).to_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time, Date.new(2005, 2, 21).to_time.class
+ assert_equal Time.local(2005, 2, 21), Date.new(2005, 2, 21).to_time
+ assert_equal Time.local(2005, 2, 21).utc_offset, Date.new(2005, 2, 21).to_time.utc_offset
+ end
+
silence_warnings do
0.upto(138) do |year|
[:utc, :local].each do |format|
@@ -44,6 +49,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_compare_to_time
+ assert Date.yesterday < Time.now
+ end
+
def test_to_datetime
assert_equal DateTime.civil(2005, 2, 21), Date.new(2005, 2, 21).to_datetime
assert_equal 0, Date.new(2005, 2, 21).to_datetime.offset # use UTC offset
@@ -239,6 +248,10 @@ class DateExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,21,0,0,0), Date.new(2005,2,21).beginning_of_day
end
+ def test_middle_of_day
+ assert_equal Time.local(2005,2,21,12,0,0), Date.new(2005,2,21).middle_of_day
+ end
+
def test_beginning_of_day_when_zone_is_set
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
with_env_tz 'UTC' do
@@ -354,3 +367,4 @@ class DateExtBehaviorTest < ActiveSupport::TestCase
end
end
end
+
diff --git a/activesupport/test/core_ext/date_time_ext_test.rb b/activesupport/test/core_ext/date_time_ext_test.rb
index b1d1e8ecb4..0a40aeb96c 100644
--- a/activesupport/test/core_ext/date_time_ext_test.rb
+++ b/activesupport/test/core_ext/date_time_ext_test.rb
@@ -18,6 +18,12 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal "Mon, 21 Feb 2005 14:30:00 +0000", datetime.to_s(:rfc822)
assert_equal "February 21st, 2005 14:30", datetime.to_s(:long_ordinal)
assert_match(/^2005-02-21T14:30:00(Z|\+00:00)$/, datetime.to_s)
+
+ with_env_tz "US/Central" do
+ assert_equal "2009-02-05T14:30:05-06:00", DateTime.civil(2009, 2, 5, 14, 30, 5, Rational(-21600, 86400)).to_s(:iso8601)
+ assert_equal "2008-06-09T04:05:01-05:00", DateTime.civil(2008, 6, 9, 4, 5, 1, Rational(-18000, 86400)).to_s(:iso8601)
+ assert_equal "2009-02-05T14:30:05+00:00", DateTime.civil(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
+ end
end
def test_readable_inspect
@@ -41,11 +47,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_to_time
- assert_equal Time.utc(2005, 2, 21, 10, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
- assert_equal Time.utc_time(2039, 2, 21, 10, 11, 12), DateTime.new(2039, 2, 21, 10, 11, 12, 0).to_time
- # DateTimes with offsets other than 0 are returned unaltered
- assert_equal DateTime.new(2005, 2, 21, 10, 11, 12, Rational(-5, 24)), DateTime.new(2005, 2, 21, 10, 11, 12, Rational(-5, 24)).to_time
- # Fractional seconds are preserved
+ with_env_tz 'US/Eastern' do
+ assert_equal Time, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.class
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12), DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time
+ assert_equal Time.local(2005, 2, 21, 5, 11, 12).utc_offset, DateTime.new(2005, 2, 21, 10, 11, 12, 0).to_time.utc_offset
+ end
+ end
+
+ def test_to_time_preserves_fractional_seconds
assert_equal Time.utc(2005, 2, 21, 10, 11, 12, 256), DateTime.new(2005, 2, 21, 10, 11, 12 + Rational(256, 1000000), 0).to_time
end
@@ -61,10 +70,22 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 86399,DateTime.civil(2005,1,1,23,59,59).seconds_since_midnight
end
+ def test_seconds_until_end_of_day
+ assert_equal 0, DateTime.civil(2005,1,1,23,59,59).seconds_until_end_of_day
+ assert_equal 1, DateTime.civil(2005,1,1,23,59,58).seconds_until_end_of_day
+ assert_equal 60, DateTime.civil(2005,1,1,23,58,59).seconds_until_end_of_day
+ assert_equal 3660, DateTime.civil(2005,1,1,22,58,59).seconds_until_end_of_day
+ assert_equal 86399, DateTime.civil(2005,1,1,0,0,0).seconds_until_end_of_day
+ end
+
def test_beginning_of_day
assert_equal DateTime.civil(2005,2,4,0,0,0), DateTime.civil(2005,2,4,10,10,10).beginning_of_day
end
+ def test_middle_of_day
+ assert_equal DateTime.civil(2005,2,4,12,0,0), DateTime.civil(2005,2,4,10,10,10).middle_of_day
+ end
+
def test_end_of_day
assert_equal DateTime.civil(2005,2,4,23,59,59), DateTime.civil(2005,2,4,10,10,10).end_of_day
end
@@ -77,6 +98,14 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005,2,4,19,59,59), DateTime.civil(2005,2,4,19,30,10).end_of_hour
end
+ def test_beginning_of_minute
+ assert_equal DateTime.civil(2005,2,4,19,30,0), DateTime.civil(2005,2,4,19,30,10).beginning_of_minute
+ end
+
+ def test_end_of_minute
+ assert_equal DateTime.civil(2005,2,4,19,30,59), DateTime.civil(2005,2,4,19,30,10).end_of_minute
+ end
+
def test_end_of_month
assert_equal DateTime.civil(2005,3,31,23,59,59), DateTime.civil(2005,3,20,10,10,10).end_of_month
assert_equal DateTime.civil(2005,2,28,23,59,59), DateTime.civil(2005,2,20,10,10,10).end_of_month
@@ -110,6 +139,9 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal DateTime.civil(2005,2,22,16), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16)
assert_equal DateTime.civil(2005,2,22,16,45), DateTime.civil(2005,2,22,15,15,10).change(:hour => 16, :min => 45)
assert_equal DateTime.civil(2005,2,22,15,45), DateTime.civil(2005,2,22,15,15,10).change(:min => 45)
+
+ # datetime with fractions of a second
+ assert_equal DateTime.civil(2005,2,1,15,15,10.7), DateTime.civil(2005,2,22,15,15,10.7).change(:day => 1)
end
def test_advance
@@ -234,6 +266,10 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_acts_like_date
+ assert DateTime.new.acts_like_date?
+ end
+
def test_acts_like_time
assert DateTime.new.acts_like_time?
end
@@ -301,6 +337,16 @@ class DateTimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 946684800, DateTime.civil(1999,12,31,19,0,0,Rational(-5,24)).to_i
end
+ def test_usec
+ assert_equal 0, DateTime.civil(2000).usec
+ assert_equal 500000, DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)).usec
+ end
+
+ def test_nsec
+ 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
diff --git a/activesupport/test/core_ext/duration_test.rb b/activesupport/test/core_ext/duration_test.rb
index c8312aa653..ed267cf4b9 100644
--- a/activesupport/test/core_ext/duration_test.rb
+++ b/activesupport/test/core_ext/duration_test.rb
@@ -21,12 +21,13 @@ class DurationTest < ActiveSupport::TestCase
assert ActiveSupport::Duration === 1.day
assert !(ActiveSupport::Duration === 1.day.to_i)
assert !(ActiveSupport::Duration === 'foo')
- assert !(ActiveSupport::Duration === ActiveSupport::BasicObject.new)
+ assert !(ActiveSupport::Duration === ActiveSupport::ProxyObject.new)
end
def test_equals
assert 1.day == 1.day
assert 1.day == 1.day.to_i
+ assert 1.day.to_i == 1.day
assert !(1.day == 'foo')
end
@@ -37,6 +38,8 @@ class DurationTest < ActiveSupport::TestCase
assert_equal '6 months and -2 days', (6.months - 2.days).inspect
assert_equal '10 seconds', 10.seconds.inspect
assert_equal '10 years, 2 months, and 1 day', (10.years + 2.months + 1.day).inspect
+ assert_equal '10 years, 2 months, and 1 day', (10.years + 1.month + 1.day + 1.month).inspect
+ assert_equal '10 years, 2 months, and 1 day', (1.day + 10.years + 2.months).inspect
assert_equal '7 days', 1.week.inspect
assert_equal '14 days', 1.fortnight.inspect
end
@@ -50,14 +53,12 @@ class DurationTest < ActiveSupport::TestCase
end
def test_argument_error
- begin
- 1.second.ago('')
- flunk("no exception was raised")
- rescue ArgumentError => e
- assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
- rescue Exception => e
- flunk("ArgumentError should be raised, but we got #{e.class} instead")
- end
+ 1.second.ago('')
+ flunk("no exception was raised")
+ rescue ArgumentError => e
+ assert_equal 'expected a time or date, got ""', e.message, "ensure ArgumentError is not being raised by dependencies.rb"
+ rescue Exception => e
+ flunk("ArgumentError should be raised, but we got #{e.class} instead")
end
def test_fractional_weeks
@@ -131,7 +132,7 @@ class DurationTest < ActiveSupport::TestCase
assert_equal Time.local(2009,3,29,0,0,0) + 1.day, Time.local(2009,3,30,0,0,0)
end
end
-
+
def test_delegation_with_block_works
counter = 0
assert_nothing_raised do
diff --git a/activesupport/test/core_ext/enumerable_test.rb b/activesupport/test/core_ext/enumerable_test.rb
index 0a1abac767..6781e3c20e 100644
--- a/activesupport/test/core_ext/enumerable_test.rb
+++ b/activesupport/test/core_ext/enumerable_test.rb
@@ -24,10 +24,8 @@ class EnumerableTests < ActiveSupport::TestCase
def test_group_by
names = %w(marcel sam david jeremy)
klass = Struct.new(:name)
- objects = (1..50).inject([]) do |people,|
- p = klass.new
- p.name = names.sort_by { rand }.first
- people << p
+ objects = (1..50).map do
+ klass.new names.sample
end
enum = GenericEnumerable.new(objects)
diff --git a/activesupport/test/core_ext/hash_ext_test.rb b/activesupport/test/core_ext/hash_ext_test.rb
index b208767490..b059bc3e89 100644
--- a/activesupport/test/core_ext/hash_ext_test.rb
+++ b/activesupport/test/core_ext/hash_ext_test.rb
@@ -3,11 +3,12 @@ require 'active_support/core_ext/hash'
require 'bigdecimal'
require 'active_support/core_ext/string/access'
require 'active_support/ordered_hash'
+require 'active_support/core_ext/object/conversions'
require 'active_support/core_ext/object/deep_dup'
require 'active_support/inflections'
class HashExtTest < ActiveSupport::TestCase
- class IndifferentHash < HashWithIndifferentAccess
+ class IndifferentHash < ActiveSupport::HashWithIndifferentAccess
end
class SubclassingArray < Array
@@ -427,6 +428,18 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 2, hash['b']
end
+ def test_indifferent_replace
+ hash = HashWithIndifferentAccess.new
+ hash[:a] = 42
+
+ replaced = hash.replace(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
@@ -467,6 +480,42 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal hash.delete('a'), nil
end
+ def test_indifferent_select
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| v == 1}
+
+ assert_equal({ 'a' => 1 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_select_returns_a_hash_when_unchanged
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).select {|k,v| true}
+
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_select_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.select! {|k,v| v == 1}
+
+ assert_equal({ 'a' => 1 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
+ def test_indifferent_reject
+ hash = ActiveSupport::HashWithIndifferentAccess.new(@strings).reject {|k,v| v != 1}
+
+ assert_equal({ 'a' => 1 }, hash)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, hash
+ end
+
+ def test_indifferent_reject_bang
+ indifferent_strings = ActiveSupport::HashWithIndifferentAccess.new(@strings)
+ indifferent_strings.reject! {|k,v| v != 1}
+
+ assert_equal({ 'a' => 1 }, indifferent_strings)
+ assert_instance_of ActiveSupport::HashWithIndifferentAccess, indifferent_strings
+ end
+
def test_indifferent_to_hash
# Should convert to a Hash with String keys.
assert_equal @strings, @mixed.with_indifferent_access.to_hash
@@ -477,6 +526,10 @@ class HashExtTest < ActiveSupport::TestCase
roundtrip = mixed_with_default.with_indifferent_access.to_hash
assert_equal @strings, roundtrip
assert_equal '1234', roundtrip.default
+ 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)
+ assert_not new_to_hash["a"]["b"].instance_of?(HashWithIndifferentAccess)
end
def test_lookup_returns_the_same_object_that_is_stored_in_hash_indifferent_access
@@ -486,9 +539,21 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal [1], hash[:a]
end
+ def test_with_indifferent_access_has_no_side_effects_on_existing_hash
+ hash = {content: [{:foo => :bar, 'bar' => 'baz'}]}
+ hash.with_indifferent_access
+
+ assert_equal [:foo, "bar"], hash[:content].first.keys
+ end
+
def test_indifferent_hash_with_array_of_hashes
hash = { "urls" => { "url" => [ { "address" => "1" }, { "address" => "2" } ] }}.with_indifferent_access
assert_equal "1", hash[:urls][:url].first[:address]
+
+ hash = hash.to_hash
+ assert_not hash.instance_of?(HashWithIndifferentAccess)
+ assert_not hash["urls"].instance_of?(HashWithIndifferentAccess)
+ assert_not hash["urls"]["url"].first.instance_of?(HashWithIndifferentAccess)
end
def test_should_preserve_array_subclass_when_value_is_array
@@ -642,10 +707,6 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal expected, merged
end
- def test_diff
- assert_equal({ :a => 2 }, { :a => 2, :b => 5 }.diff({ :a => 1, :b => 5 }))
- end
-
def test_slice
original = { :a => 'x', :b => 'y', :c => 10 }
expected = { :a => 'x', :b => 'y' }
@@ -720,6 +781,24 @@ class HashExtTest < ActiveSupport::TestCase
assert_equal 'bender', slice['login']
end
+ def test_slice_bang_does_not_override_default
+ hash = Hash.new(0)
+ hash.update(a: 1, b: 2)
+
+ hash.slice!(:a)
+
+ assert_equal 0, hash[:c]
+ end
+
+ def test_slice_bang_does_not_override_default_proc
+ hash = Hash.new { |h, k| h[k] = [] }
+ hash.update(a: 1, b: 2)
+
+ hash.slice!(:a)
+
+ assert_equal [], hash[:c]
+ end
+
def test_extract
original = {:a => 1, :b => 2, :c => 3, :d => 4}
expected = {:a => 1, :b => 2}
@@ -1000,12 +1079,10 @@ class HashToXmlTest < ActiveSupport::TestCase
<replies-close-in type="integer">2592000000</replies-close-in>
<written-on type="date">2003-07-16</written-on>
<viewed-at type="datetime">2003-07-16T09:28:00+0000</viewed-at>
- <content type="yaml">--- \n1: should be an integer\n:message: Have a nice day\narray: \n- should-have-dashes: true\n should_have_underscores: true\n</content>
<author-email-address>david@loudthinking.com</author-email-address>
<parent-id></parent-id>
<ad-revenue type="decimal">1.5</ad-revenue>
<optimum-viewing-angle type="float">135</optimum-viewing-angle>
- <resident type="symbol">yes</resident>
</topic>
EOT
@@ -1018,12 +1095,10 @@ class HashToXmlTest < ActiveSupport::TestCase
:replies_close_in => 2592000000,
:written_on => Date.new(2003, 7, 16),
:viewed_at => Time.utc(2003, 7, 16, 9, 28),
- :content => { :message => "Have a nice day", 1 => "should be an integer", "array" => [{ "should-have-dashes" => true, "should_have_underscores" => true }] },
:author_email_address => "david@loudthinking.com",
:parent_id => nil,
:ad_revenue => BigDecimal("1.50"),
:optimum_viewing_angle => 135.0,
- :resident => :yes
}.stringify_keys
assert_equal expected_topic_hash, Hash.from_xml(topic_xml)["topic"]
@@ -1037,7 +1112,6 @@ class HashToXmlTest < ActiveSupport::TestCase
<approved type="boolean"></approved>
<written-on type="date"></written-on>
<viewed-at type="datetime"></viewed-at>
- <content type="yaml"></content>
<parent-id></parent-id>
</topic>
EOT
@@ -1048,7 +1122,6 @@ class HashToXmlTest < ActiveSupport::TestCase
:approved => nil,
:written_on => nil,
:viewed_at => nil,
- :content => nil,
:parent_id => nil
}.stringify_keys
@@ -1275,6 +1348,28 @@ class HashToXmlTest < ActiveSupport::TestCase
assert_equal expected_product_hash, Hash.from_xml(product_xml)["product"]
end
+ def test_from_xml_raises_on_disallowed_type_attributes
+ assert_raise ActiveSupport::XMLConverter::DisallowedType do
+ Hash.from_xml '<product><name type="foo">value</name></product>', %w(foo)
+ end
+ end
+
+ def test_from_xml_disallows_symbol_and_yaml_types_by_default
+ assert_raise ActiveSupport::XMLConverter::DisallowedType do
+ Hash.from_xml '<product><name type="symbol">value</name></product>'
+ end
+
+ assert_raise ActiveSupport::XMLConverter::DisallowedType do
+ Hash.from_xml '<product><name type="yaml">value</name></product>'
+ end
+ 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_value_for_unknown_key
hash_wia = HashWithIndifferentAccess.new(3)
assert_equal 3, hash_wia[:new_key]
@@ -1315,7 +1410,7 @@ class HashToXmlTest < ActiveSupport::TestCase
def test_empty_string_works_for_typecast_xml_value
assert_nothing_raised do
- Hash.__send__(:typecast_xml_value, "")
+ ActiveSupport::XMLConverter.new("").to_h
end
end
diff --git a/activesupport/test/core_ext/kernel_test.rb b/activesupport/test/core_ext/kernel_test.rb
index 439bc87323..b8951de402 100644
--- a/activesupport/test/core_ext/kernel_test.rb
+++ b/activesupport/test/core_ext/kernel_test.rb
@@ -38,6 +38,18 @@ class KernelTest < ActiveSupport::TestCase
# Skip if we can't STDERR.tell
end
+ 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'
+ end
+ assert_equal old_stdout_position, STDOUT.tell
+ assert_equal old_stderr_position, STDERR.tell
+ rescue Errno::ESPIPE
+ # Skip if we can't STDERR.tell
+ end
+
def test_silence_stderr_with_return_value
assert_equal 1, silence_stderr { 1 }
end
@@ -51,6 +63,8 @@ class KernelTest < ActiveSupport::TestCase
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') }
end
end
diff --git a/activesupport/test/core_ext/marshal_test.rb b/activesupport/test/core_ext/marshal_test.rb
new file mode 100644
index 0000000000..8f3f710dfd
--- /dev/null
+++ b/activesupport/test/core_ext/marshal_test.rb
@@ -0,0 +1,124 @@
+require 'abstract_unit'
+require 'active_support/core_ext/marshal'
+require 'dependencies_test_helpers'
+
+class MarshalTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include DependenciesTestHelpers
+
+ def teardown
+ ActiveSupport::Dependencies.clear
+ remove_constants(:E, :ClassFolder)
+ end
+
+ test "that Marshal#load still works" do
+ sanity_data = ["test", [1, 2, 3], {a: [1, 2, 3]}, ActiveSupport::TestCase]
+ sanity_data.each do |obj|
+ dumped = Marshal.dump(obj)
+ assert_equal Marshal.load_without_autoloading(dumped), Marshal.load(dumped)
+ end
+ end
+
+ test "that a missing class is autoloaded from string" do
+ dumped = nil
+ with_autoloading_fixtures do
+ dumped = Marshal.dump(E.new)
+ end
+
+ remove_constants(:E)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of E, Marshal.load(dumped)
+ end
+ end
+
+ test "that classes in sub modules work" do
+ dumped = nil
+ with_autoloading_fixtures do
+ dumped = Marshal.dump(ClassFolder::ClassFolderSubclass.new)
+ end
+
+ remove_constants(:ClassFolder)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of ClassFolder::ClassFolderSubclass, Marshal.load(dumped)
+ end
+ end
+
+ test "that more than one missing class is autoloaded" do
+ dumped = nil
+ with_autoloading_fixtures do
+ dumped = Marshal.dump([E.new, ClassFolder.new])
+ end
+
+ remove_constants(:E, :ClassFolder)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ loaded = Marshal.load(dumped)
+ assert_equal 2, loaded.size
+ assert_kind_of E, loaded[0]
+ assert_kind_of ClassFolder, loaded[1]
+ end
+ end
+
+ test "that a real missing class is causing an exception" do
+ dumped = nil
+ with_autoloading_fixtures do
+ dumped = Marshal.dump(E.new)
+ end
+
+ remove_constants(:E)
+ ActiveSupport::Dependencies.clear
+
+ assert_raise(NameError) do
+ Marshal.load(dumped)
+ end
+ end
+
+ test "when first class is autoloaded and second not" do
+ dumped = nil
+ class SomeClass
+ end
+
+ with_autoloading_fixtures do
+ dumped = Marshal.dump([E.new, SomeClass.new])
+ end
+
+ remove_constants(:E)
+ self.class.send(:remove_const, :SomeClass)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_raise(NameError) do
+ Marshal.load(dumped)
+ end
+
+ assert_nothing_raised("E failed to load while we expect only SomeClass to fail loading") do
+ E.new
+ end
+
+ assert_raise(NameError, "We expected SomeClass to not be loaded but it is!") do
+ SomeClass.new
+ end
+ end
+ end
+
+ test "loading classes from files trigger autoloading" do
+ Tempfile.open("object_serializer_test") do |f|
+ with_autoloading_fixtures do
+ Marshal.dump(E.new, f)
+ end
+
+ f.rewind
+ remove_constants(:E)
+ ActiveSupport::Dependencies.clear
+
+ with_autoloading_fixtures do
+ assert_kind_of E, Marshal.load(f)
+ end
+ end
+ end
+end \ No newline at end of file
diff --git a/activesupport/test/core_ext/module/qualified_const_test.rb b/activesupport/test/core_ext/module/qualified_const_test.rb
index 343a848a42..37c9228a64 100644
--- a/activesupport/test/core_ext/module/qualified_const_test.rb
+++ b/activesupport/test/core_ext/module/qualified_const_test.rb
@@ -89,13 +89,20 @@ class QualifiedConstTest < ActiveSupport::TestCase
end
test "reject absolute paths" do
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")}
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X")}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_defined?("::X::Y")}
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")}
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X")}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_get("::X::Y")}
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)}
- assert_raise(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X", nil)}
+ assert_raise_with_message(NameError, "wrong constant name ::X") { Object.qualified_const_set("::X::Y", nil)}
+ end
+
+ private
+
+ def assert_raise_with_message(expected_exception, expected_message, &block)
+ exception = assert_raise(expected_exception, &block)
+ assert_equal expected_message, exception.message
end
end
diff --git a/activesupport/test/core_ext/module_test.rb b/activesupport/test/core_ext/module_test.rb
index 82249ddd1b..283b13ff8b 100644
--- a/activesupport/test/core_ext/module_test.rb
+++ b/activesupport/test/core_ext/module_test.rb
@@ -66,6 +66,23 @@ Tester = Struct.new(:client) do
delegate :name, :to => :client, :prefix => false
end
+Product = Struct.new(:name) do
+ delegate :name, :to => :manufacturer, :prefix => true
+ delegate :name, :to => :type, :prefix => true
+
+ def manufacturer
+ @manufacturer ||= begin
+ nil.unknown_method
+ end
+ end
+
+ def type
+ @type ||= begin
+ nil.type_name
+ end
+ end
+end
+
class ParameterSet
delegate :[], :[]=, :to => :@params
@@ -82,6 +99,21 @@ class Name
end
end
+class SideEffect
+ attr_reader :ints
+
+ delegate :to_i, :to => :shift, :allow_nil => true
+ delegate :to_s, :to => :shift
+
+ def initialize
+ @ints = [1, 2, 3]
+ end
+
+ def shift
+ @ints.shift
+ end
+end
+
class ModuleTest < ActiveSupport::TestCase
def setup
@david = Someone.new("David", Somewhere.new("Paulina", "Chicago"))
@@ -171,6 +203,17 @@ class ModuleTest < ActiveSupport::TestCase
assert_nil rails.name
end
+ # Ensures with check for nil, not for a falseish target.
+ def test_delegation_with_allow_nil_and_false_value
+ project = Project.new(false, false)
+ assert_raise(NoMethodError) { project.name }
+ end
+
+ def test_delegation_with_allow_nil_and_invalid_value
+ rails = Project.new("Rails", "David")
+ assert_raise(NoMethodError) { rails.name }
+ end
+
def test_delegation_with_allow_nil_and_nil_value_and_prefix
Project.class_eval do
delegate :name, :to => :person, :allow_nil => true, :prefix => true
@@ -181,7 +224,7 @@ class ModuleTest < ActiveSupport::TestCase
def test_delegation_without_allow_nil_and_nil_value
david = Someone.new("David")
- assert_raise(RuntimeError) { david.street }
+ assert_raise(Module::DelegationError) { david.street }
end
def test_delegation_to_method_that_exists_on_nil
@@ -228,6 +271,26 @@ class ModuleTest < ActiveSupport::TestCase
"[#{e.backtrace.inspect}] did not include [#{file_and_line}]"
end
+ def test_delegation_invokes_the_target_exactly_once
+ se = SideEffect.new
+
+ assert_equal 1, se.to_i
+ assert_equal [2, 3], se.ints
+
+ assert_equal '2', se.to_s
+ assert_equal [3], se.ints
+ end
+
+ def test_delegation_doesnt_mask_nested_no_method_error_on_nil_receiver
+ product = Product.new('Widget')
+
+ # Nested NoMethodError is a different name from the delegation
+ assert_raise(NoMethodError) { product.manufacturer_name }
+
+ # Nested NoMethodError is the same name as the delegation
+ assert_raise(NoMethodError) { product.type_name }
+ end
+
def test_parent
assert_equal Yz::Zy, Yz::Zy::Cd.parent
assert_equal Yz, Yz::Zy.parent
@@ -242,12 +305,6 @@ class ModuleTest < ActiveSupport::TestCase
def test_local_constants
assert_equal %w(Constant1 Constant3), Ab.local_constants.sort.map(&:to_s)
end
-
- def test_local_constant_names
- ActiveSupport::Deprecation.silence do
- assert_equal %w(Constant1 Constant3), Ab.local_constant_names.sort.map(&:to_s)
- end
- end
end
module BarMethodAliaser
diff --git a/activesupport/test/core_ext/numeric_ext_test.rb b/activesupport/test/core_ext/numeric_ext_test.rb
index 435f4aa5a1..1da72eb17d 100644
--- a/activesupport/test/core_ext/numeric_ext_test.rb
+++ b/activesupport/test/core_ext/numeric_ext_test.rb
@@ -77,7 +77,7 @@ class NumericExtTimeAndDateTimeTest < ActiveSupport::TestCase
assert_equal @dtnow.advance(:days => 1).advance(:months => 2), @dtnow + 1.day + 2.months
end
- def test_duration_after_convertion_is_no_longer_accurate
+ def test_duration_after_conversion_is_no_longer_accurate
assert_equal 30.days.to_i.since(@now), 1.month.to_i.since(@now)
assert_equal 365.25.days.to_f.since(@now), 1.year.to_f.since(@now)
assert_equal 30.days.to_i.since(@dtnow), 1.month.to_i.since(@dtnow)
@@ -153,22 +153,16 @@ end
class NumericExtSizeTest < ActiveSupport::TestCase
def test_unit_in_terms_of_another
- relationships = {
- 1024.bytes => 1.kilobyte,
- 1024.kilobytes => 1.megabyte,
- 3584.0.kilobytes => 3.5.megabytes,
- 3584.0.megabytes => 3.5.gigabytes,
- 1.kilobyte ** 4 => 1.terabyte,
- 1024.kilobytes + 2.megabytes => 3.megabytes,
- 2.gigabytes / 4 => 512.megabytes,
- 256.megabytes * 20 + 5.gigabytes => 10.gigabytes,
- 1.kilobyte ** 5 => 1.petabyte,
- 1.kilobyte ** 6 => 1.exabyte
- }
-
- relationships.each do |left, right|
- assert_equal right, left
- end
+ assert_equal 1024.bytes, 1.kilobyte
+ assert_equal 1024.kilobytes, 1.megabyte
+ assert_equal 3584.0.kilobytes, 3.5.megabytes
+ assert_equal 3584.0.megabytes, 3.5.gigabytes
+ assert_equal 1.kilobyte ** 4, 1.terabyte
+ assert_equal 1024.kilobytes + 2.megabytes, 3.megabytes
+ assert_equal 2.gigabytes / 4, 512.megabytes
+ assert_equal 256.megabytes * 20 + 5.gigabytes, 10.gigabytes
+ assert_equal 1.kilobyte ** 5, 1.petabyte
+ assert_equal 1.kilobyte ** 6, 1.exabyte
end
def test_units_as_bytes_independently
@@ -203,7 +197,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
def terabytes(number)
gigabytes(number) * 1024
end
-
+
def test_to_s__phone
assert_equal("555-1234", 5551234.to_s(:phone))
assert_equal("800-555-1212", 8005551212.to_s(:phone))
@@ -217,7 +211,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal("22-555-1212", 225551212.to_s(:phone))
assert_equal("+45-22-555-1212", 225551212.to_s(:phone, :country_code => 45))
end
-
+
def test_to_s__currency
assert_equal("$1,234,567,890.50", 1234567890.50.to_s(:currency))
assert_equal("$1,234,567,890.51", 1234567890.506.to_s(:currency))
@@ -228,8 +222,8 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal("$1,234,567,890.5", 1234567890.50.to_s(:currency, :precision => 1))
assert_equal("&pound;1234567890,50", 1234567890.50.to_s(:currency, :unit => "&pound;", :separator => ",", :delimiter => ""))
end
-
-
+
+
def test_to_s__rounded
assert_equal("-111.235", -111.2346.to_s(:rounded))
assert_equal("111.235", 111.2346.to_s(:rounded))
@@ -246,7 +240,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal("11.00", 10.995.to_s(:rounded, :precision => 2))
assert_equal("0.00", -0.001.to_s(:rounded, :precision => 2))
end
-
+
def test_to_s__percentage
assert_equal("100.000%", 100.to_s(:percentage))
assert_equal("100%", 100.to_s(:percentage, :precision => 0))
@@ -274,7 +268,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :separator => ',', :delimiter => '.')
assert_equal '12.345.678,05', 12345678.05.to_s(:delimited, :delimiter => '.', :separator => ',')
end
-
+
def test_to_s__rounded_with_custom_delimiter_and_separator
assert_equal '31,83', 31.825.to_s(:rounded, :precision => 2, :separator => ',')
@@ -350,7 +344,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal '1.23 GB', 1234567890.to_s(:human_size, :prefix => :si)
assert_equal '1.23 TB', 1234567890123.to_s(:human_size, :prefix => :si)
end
-
+
def test_to_s__human_size_with_options_hash
assert_equal '1.2 MB', 1234567.to_s(:human_size, :precision => 2)
assert_equal '3 Bytes', 3.14159265.to_s(:human_size, :precision => 4)
@@ -366,13 +360,13 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
assert_equal '1.012 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :significant => false)
assert_equal '1 KB', kilobytes(1.0123).to_s(:human_size, :precision => 0, :significant => true) #ignores significant it precision is 0
end
-
+
def test_to_s__human_size_with_custom_delimiter_and_separator
assert_equal '1,01 KB', kilobytes(1.0123).to_s(:human_size, :precision => 3, :separator => ',')
assert_equal '1,01 KB', kilobytes(1.0100).to_s(:human_size, :precision => 4, :separator => ',')
assert_equal '1.000,1 TB', terabytes(1000.1).to_s(:human_size, :precision => 5, :delimiter => '.', :separator => ',')
end
-
+
def test_number_to_human
assert_equal '-123', -123.to_s(:human)
assert_equal '-0.5', -0.5.to_s(:human)
@@ -436,7 +430,7 @@ class NumericExtFormattingTest < ActiveSupport::TestCase
def test_to_s__injected_on_proper_types
assert_equal Fixnum, 1230.class
assert_equal '1.23 Thousand', 1230.to_s(:human)
-
+
assert_equal Float, Float(1230).class
assert_equal '1.23 Thousand', Float(1230).to_s(:human)
diff --git a/activesupport/test/core_ext/object/inclusion_test.rb b/activesupport/test/core_ext/object/inclusion_test.rb
index 22888333f5..478706eeae 100644
--- a/activesupport/test/core_ext/object/inclusion_test.rb
+++ b/activesupport/test/core_ext/object/inclusion_test.rb
@@ -2,16 +2,6 @@ require 'abstract_unit'
require 'active_support/core_ext/object/inclusion'
class InTest < ActiveSupport::TestCase
- def test_in_multiple_args
- assert :b.in?(:a,:b)
- assert !:c.in?(:a,:b)
- end
-
- def test_in_multiple_arrays
- assert [1,2].in?([1,2],[2,3])
- assert ![1,2].in?([1,3],[2,1])
- end
-
def test_in_array
assert 1.in?([1,2])
assert !3.in?([1,2])
diff --git a/activesupport/test/core_ext/object/json_test.rb b/activesupport/test/core_ext/object/json_test.rb
new file mode 100644
index 0000000000..d3d31530df
--- /dev/null
+++ b/activesupport/test/core_ext/object/json_test.rb
@@ -0,0 +1,9 @@
+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_query_test.rb b/activesupport/test/core_ext/object/to_query_test.rb
index 8d1a8c628c..92f996f9a4 100644
--- a/activesupport/test/core_ext/object/to_query_test.rb
+++ b/activesupport/test/core_ext/object/to_query_test.rb
@@ -47,7 +47,7 @@ class ToQueryTest < ActiveSupport::TestCase
end
private
- def assert_query_equal(expected, actual, message = nil)
+ def assert_query_equal(expected, actual)
assert_equal expected.split('&'), actual.to_query.split('&')
end
end
diff --git a/activesupport/test/core_ext/object_and_class_ext_test.rb b/activesupport/test/core_ext/object_and_class_ext_test.rb
index ec7dd6d4fb..8d748791e3 100644
--- a/activesupport/test/core_ext/object_and_class_ext_test.rb
+++ b/activesupport/test/core_ext/object_and_class_ext_test.rb
@@ -120,6 +120,10 @@ class ObjectTryTest < ActiveSupport::TestCase
assert_raise(NoMethodError) { @string.try!(method, 'llo', 'y') }
end
+ def test_try_only_block_bang
+ assert_equal @string.reverse, @string.try! { |s| s.reverse }
+ end
+
def test_valid_method
assert_equal 5, @string.try(:size)
end
diff --git a/activesupport/test/core_ext/proc_test.rb b/activesupport/test/core_ext/proc_test.rb
deleted file mode 100644
index c4d5592196..0000000000
--- a/activesupport/test/core_ext/proc_test.rb
+++ /dev/null
@@ -1,14 +0,0 @@
-require 'abstract_unit'
-require 'active_support/core_ext/proc'
-
-class ProcTests < ActiveSupport::TestCase
- def test_bind_returns_method_with_changed_self
- assert_deprecated do
- block = Proc.new { self }
- assert_equal self, block.call
- bound_block = block.bind("hello")
- assert_not_equal block, bound_block
- assert_equal "hello", bound_block.call
- end
- end
-end
diff --git a/activesupport/test/core_ext/range_ext_test.rb b/activesupport/test/core_ext/range_ext_test.rb
index f0cdc0bfd4..854b0a38bd 100644
--- a/activesupport/test/core_ext/range_ext_test.rb
+++ b/activesupport/test/core_ext/range_ext_test.rb
@@ -12,6 +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
+ end
def test_overlaps_last_inclusive
assert((1..5).overlaps?(5..10))
@@ -49,7 +54,7 @@ class RangeTest < ActiveSupport::TestCase
assert((1...10) === (1...10))
end
- def test_should_compare_other_with_exlusive_end
+ def test_should_compare_other_with_exclusive_end
assert((1..10) === (1...10))
end
@@ -85,4 +90,26 @@ class RangeTest < ActiveSupport::TestCase
time_range_2 = Time.utc(2005, 12, 10, 17, 31)..Time.utc(2005, 12, 10, 18, 00)
assert !time_range_1.overlaps?(time_range_2)
end
+
+ def test_each_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).each {}
+ end
+ end
+
+ def test_step_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).step(1) {}
+ end
+ end
+
+ def test_include_on_time_with_zone
+ twz = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone['Eastern Time (US & Canada)'] , Time.utc(2006,11,28,10,30))
+ assert_raises TypeError do
+ ((twz - 1.hour)..twz).include?(twz)
+ end
+ end
+
end
diff --git a/activesupport/test/core_ext/string_ext_test.rb b/activesupport/test/core_ext/string_ext_test.rb
index 6720ed42f0..3cb66d4eec 100644
--- a/activesupport/test/core_ext/string_ext_test.rb
+++ b/activesupport/test/core_ext/string_ext_test.rb
@@ -11,13 +11,6 @@ require 'active_support/core_ext/string/strip'
require 'active_support/core_ext/string/output_safety'
require 'active_support/core_ext/string/indent'
-module Ace
- module Base
- class Case
- end
- end
-end
-
class StringInflectionsTest < ActiveSupport::TestCase
include InflectorTestCases
include ConstantizeTestCases
@@ -87,6 +80,12 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal('capital', 'Capital'.camelize(:lower))
end
+ def test_dasherize
+ UnderscoresToDashes.each do |underscored, dasherized|
+ assert_equal(dasherized, underscored.dasherize)
+ end
+ end
+
def test_underscore
CamelToUnderscore.each do |camel, underscore|
assert_equal(underscore, camel.underscore)
@@ -161,30 +160,6 @@ class StringInflectionsTest < ActiveSupport::TestCase
assert_equal 97, 'abc'.ord
end
- def test_string_to_time
- assert_equal Time.utc(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time
- assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time(:local)
- assert_equal Time.utc(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time
- assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:local)
- assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time
- assert_equal Time.local_time(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:local)
- assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time
- assert_nil "".to_time
- end
-
- def test_string_to_datetime
- assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_datetime
- assert_equal 0, "2039-02-27 23:50".to_datetime.offset # use UTC offset
- assert_equal ::Date::ITALY, "2039-02-27 23:50".to_datetime.start # use Ruby's default start value
- assert_equal DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00"), "2039-02-27T23:50:19.275038-04:00".to_datetime
- assert_nil "".to_datetime
- end
-
- def test_string_to_date
- assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date
- assert_nil "".to_date
- end
-
def test_access
s = "hello"
assert_equal "h", s.at(0)
@@ -247,10 +222,11 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_string_squish
- original = %{ A string with tabs(\t\t), newlines(\n\n), and
- many spaces( ). }
+ original = %{\u180E\u180E A string surrounded by unicode mongolian vowel separators,
+ with tabs(\t\t), newlines(\n\n), unicode nextlines(\u0085\u0085) and many spaces( ). \u180E\u180E}
- expected = "A string with tabs( ), newlines( ), and many spaces( )."
+ expected = "A string surrounded by unicode mongolian vowel separators, " +
+ "with tabs( ), newlines( ), unicode nextlines( ) and many spaces( )."
# Make sure squish returns what we expect:
assert_equal original.squish, expected
@@ -287,13 +263,18 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
def test_truncate_multibyte
- assert_equal "\354\225\204\353\246\254\353\236\221 \354\225\204\353\246\254 ...".force_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('UTF-8').truncate(10)
+ 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)
end
def test_truncate_should_not_be_html_safe
assert !"Hello World!".truncate(12).html_safe?
end
+
+ def test_remove
+ assert_equal "Summer", "Fast Summer".remove(/Fast /)
+ assert_equal "Summer", "Fast Summer".remove!(/Fast /)
+ end
def test_constantize
run_constantize_tests_on do |string|
@@ -308,6 +289,186 @@ class StringInflectionsTest < ActiveSupport::TestCase
end
end
+class StringConversionsTest < ActiveSupport::TestCase
+ 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)
+ assert_equal Time.local(2005, 2, 27, 23, 50), "2005-02-27 23:50".to_time
+ assert_equal Time.utc(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time(:utc)
+ assert_equal Time.local(2005, 2, 27, 23, 50, 19, 275038), "2005-02-27T23:50:19.275038".to_time
+ assert_equal Time.utc(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time(:utc)
+ assert_equal Time.local(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_time
+ assert_equal Time.local(2011, 2, 27, 17, 50), "2011-02-27 13:50 -0100".to_time
+ assert_equal Time.utc(2011, 2, 27, 23, 50), "2011-02-27 22:50 -0100".to_time(:utc)
+ assert_equal Time.local(2005, 2, 27, 22, 50), "2005-02-27 14:50 -0500".to_time
+ assert_nil "".to_time
+ end
+ end
+
+ def test_string_to_time_utc_offset
+ with_env_tz "US/Eastern" do
+ assert_equal 0, "2005-02-27 23:50".to_time(:utc).utc_offset
+ assert_equal(-18000, "2005-02-27 23:50".to_time.utc_offset)
+ assert_equal 0, "2005-02-27 22:50 -0100".to_time(:utc).utc_offset
+ assert_equal(-18000, "2005-02-27 22:50 -0100".to_time.utc_offset)
+ end
+ end
+
+ def test_partial_string_to_time
+ with_env_tz "Europe/Moscow" do
+ now = Time.now
+ assert_equal Time.local(now.year, now.month, now.day, 23, 50), "23:50".to_time
+ assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "23:50".to_time(:utc)
+ assert_equal Time.local(now.year, now.month, now.day, 18, 50), "13:50 -0100".to_time
+ assert_equal Time.utc(now.year, now.month, now.day, 23, 50), "22:50 -0100".to_time(:utc)
+ end
+ end
+
+ def test_standard_time_string_to_time_when_current_time_is_standard_time
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 1, 1))
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time
+ assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time
+ assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc)
+ end
+ end
+
+ def test_standard_time_string_to_time_when_current_time_is_daylight_savings
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 7, 1))
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 -0800".to_time
+ assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 -0800".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 -0500".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 -0500".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 5, 0), "2012-01-01 10:00 UTC".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "2012-01-01 10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 13, 0), "2012-01-01 10:00 PST".to_time
+ assert_equal Time.utc(2012, 1, 1, 18, 0), "2012-01-01 10:00 PST".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "2012-01-01 10:00 EST".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "2012-01-01 10:00 EST".to_time(:utc)
+ end
+ end
+
+ def test_daylight_savings_string_to_time_when_current_time_is_standard_time
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 1, 1))
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time
+ assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time
+ assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc)
+ end
+ end
+
+ def test_daylight_savings_string_to_time_when_current_time_is_daylight_savings
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 7, 1))
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 -0700".to_time
+ assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 -0700".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 -0400".to_time
+ assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 -0400".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 6, 0), "2012-07-01 10:00 UTC".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "2012-07-01 10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 13, 0), "2012-07-01 10:00 PDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 17, 0), "2012-07-01 10:00 PDT".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 10, 0), "2012-07-01 10:00 EDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 14, 0), "2012-07-01 10:00 EDT".to_time(:utc)
+ end
+ end
+
+ def test_partial_string_to_time_when_current_time_is_standard_time
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 1, 1))
+ assert_equal Time.local(2012, 1, 1, 10, 0), "10:00".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 6, 0), "10:00 -0100".to_time
+ assert_equal Time.utc(2012, 1, 1, 11, 0), "10:00 -0100".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 -0500".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 -0500".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 5, 0), "10:00 UTC".to_time
+ assert_equal Time.utc(2012, 1, 1, 10, 0), "10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 13, 0), "10:00 PST".to_time
+ assert_equal Time.utc(2012, 1, 1, 18, 0), "10:00 PST".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 12, 0), "10:00 PDT".to_time
+ assert_equal Time.utc(2012, 1, 1, 17, 0), "10:00 PDT".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 10, 0), "10:00 EST".to_time
+ assert_equal Time.utc(2012, 1, 1, 15, 0), "10:00 EST".to_time(:utc)
+ assert_equal Time.local(2012, 1, 1, 9, 0), "10:00 EDT".to_time
+ assert_equal Time.utc(2012, 1, 1, 14, 0), "10:00 EDT".to_time(:utc)
+ end
+ end
+
+ def test_partial_string_to_time_when_current_time_is_daylight_savings
+ with_env_tz "US/Eastern" do
+ Time.stubs(:now).returns(Time.local(2012, 7, 1))
+ assert_equal Time.local(2012, 7, 1, 10, 0), "10:00".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 7, 0), "10:00 -0100".to_time
+ assert_equal Time.utc(2012, 7, 1, 11, 0), "10:00 -0100".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 -0500".to_time
+ assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 -0500".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 6, 0), "10:00 UTC".to_time
+ assert_equal Time.utc(2012, 7, 1, 10, 0), "10:00 UTC".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 14, 0), "10:00 PST".to_time
+ assert_equal Time.utc(2012, 7, 1, 18, 0), "10:00 PST".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 13, 0), "10:00 PDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 17, 0), "10:00 PDT".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 11, 0), "10:00 EST".to_time
+ assert_equal Time.utc(2012, 7, 1, 15, 0), "10:00 EST".to_time(:utc)
+ assert_equal Time.local(2012, 7, 1, 10, 0), "10:00 EDT".to_time
+ assert_equal Time.utc(2012, 7, 1, 14, 0), "10:00 EDT".to_time(:utc)
+ end
+ end
+
+ def test_string_to_datetime
+ assert_equal DateTime.civil(2039, 2, 27, 23, 50), "2039-02-27 23:50".to_datetime
+ assert_equal 0, "2039-02-27 23:50".to_datetime.offset # use UTC offset
+ assert_equal ::Date::ITALY, "2039-02-27 23:50".to_datetime.start # use Ruby's default start value
+ assert_equal DateTime.civil(2039, 2, 27, 23, 50, 19 + Rational(275038, 1000000), "-04:00"), "2039-02-27T23:50:19.275038-04:00".to_datetime
+ assert_nil "".to_datetime
+ end
+
+ def test_partial_string_to_datetime
+ now = DateTime.now
+ assert_equal DateTime.civil(now.year, now.month, now.day, 23, 50), "23:50".to_datetime
+ assert_equal DateTime.civil(now.year, now.month, now.day, 23, 50, 0, "-04:00"), "23:50 -0400".to_datetime
+ end
+
+ def test_string_to_date
+ assert_equal Date.new(2005, 2, 27), "2005-02-27".to_date
+ 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
def test_acts_like_string
assert 'Bambi'.acts_like_string?
@@ -315,22 +476,24 @@ class StringBehaviourTest < ActiveSupport::TestCase
end
class CoreExtStringMultibyteTest < ActiveSupport::TestCase
- UNICODE_STRING = 'ã“ã«ã¡ã‚'
- ASCII_STRING = 'ohayo'
- BYTE_STRING = "\270\236\010\210\245"
+ UTF8_STRING = 'ã“ã«ã¡ã‚'
+ ASCII_STRING = 'ohayo'.encode('US-ASCII')
+ EUC_JP_STRING = 'ã•よãªã‚‰'.encode('EUC-JP')
+ INVALID_UTF8_STRING = "\270\236\010\210\245"
def test_core_ext_adds_mb_chars
- assert_respond_to UNICODE_STRING, :mb_chars
+ assert_respond_to UTF8_STRING, :mb_chars
end
def test_string_should_recognize_utf8_strings
- assert UNICODE_STRING.is_utf8?
+ assert UTF8_STRING.is_utf8?
assert ASCII_STRING.is_utf8?
- assert !BYTE_STRING.is_utf8?
+ assert !EUC_JP_STRING.is_utf8?
+ assert !INVALID_UTF8_STRING.is_utf8?
end
def test_mb_chars_returns_instance_of_proxy_class
- assert_kind_of ActiveSupport::Multibyte.proxy_class, UNICODE_STRING.mb_chars
+ assert_kind_of ActiveSupport::Multibyte.proxy_class, UTF8_STRING.mb_chars
end
end
@@ -486,12 +649,6 @@ class OutputSafetyTest < ActiveSupport::TestCase
assert_equal 'foo'.to_yaml, 'foo'.html_safe.to_yaml(:foo => 1)
end
- test 'knows whether it is encoding aware' do
- assert_deprecated do
- assert 'ruby'.encoding_aware?
- end
- end
-
test "call to_param returns a normal string" do
string = @string.html_safe
assert string.html_safe?
diff --git a/activesupport/test/core_ext/struct_test.rb b/activesupport/test/core_ext/struct_test.rb
new file mode 100644
index 0000000000..0dff7b32d2
--- /dev/null
+++ b/activesupport/test/core_ext/struct_test.rb
@@ -0,0 +1,10 @@
+require 'abstract_unit'
+require 'active_support/core_ext/struct'
+
+class StructExt < ActiveSupport::TestCase
+ def test_to_h
+ x = Struct.new(:foo, :bar)
+ z = x.new(1, 2)
+ assert_equal({ foo: 1, bar: 2 }, z.to_h)
+ end
+end
diff --git a/activesupport/test/core_ext/thread_test.rb b/activesupport/test/core_ext/thread_test.rb
new file mode 100644
index 0000000000..6a7c6e0604
--- /dev/null
+++ b/activesupport/test/core_ext/thread_test.rb
@@ -0,0 +1,75 @@
+require 'abstract_unit'
+require 'active_support/core_ext/thread'
+
+class ThreadExt < ActiveSupport::TestCase
+ def test_main_thread_variable_in_enumerator
+ assert_equal Thread.main, Thread.current
+
+ Thread.current.thread_variable_set :foo, "bar"
+
+ thread, value = Fiber.new {
+ Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)]
+ }.resume
+
+ assert_equal Thread.current, thread
+ assert_equal Thread.current.thread_variable_get(:foo), value
+ end
+
+ def test_thread_variable_in_enumerator
+ Thread.new {
+ Thread.current.thread_variable_set :foo, "bar"
+
+ thread, value = Fiber.new {
+ Fiber.yield [Thread.current, Thread.current.thread_variable_get(:foo)]
+ }.resume
+
+ assert_equal Thread.current, thread
+ assert_equal Thread.current.thread_variable_get(:foo), value
+ }.join
+ end
+
+ def test_thread_variables
+ assert_equal [], Thread.new { Thread.current.thread_variables }.join.value
+
+ t = Thread.new {
+ Thread.current.thread_variable_set(:foo, "bar")
+ Thread.current.thread_variables
+ }
+ assert_equal [:foo], t.join.value
+ end
+
+ def test_thread_variable?
+ assert_not Thread.new { Thread.current.thread_variable?("foo") }.join.value
+ t = Thread.new {
+ Thread.current.thread_variable_set("foo", "bar")
+ }.join
+
+ assert t.thread_variable?("foo")
+ assert t.thread_variable?(:foo)
+ assert_not t.thread_variable?(:bar)
+ end
+
+ def test_thread_variable_strings_and_symbols_are_the_same_key
+ t = Thread.new {}.join
+ t.thread_variable_set("foo", "bar")
+ assert_equal "bar", t.thread_variable_get(:foo)
+ end
+
+ def test_thread_variable_frozen
+ t = Thread.new { }.join
+ t.freeze
+ assert_raises(RuntimeError) do
+ t.thread_variable_set(:foo, "bar")
+ end
+ end
+
+ def test_thread_variable_frozen_after_set
+ t = Thread.new { }.join
+ t.thread_variable_set :foo, "bar"
+ t.freeze
+ assert_raises(RuntimeError) do
+ t.thread_variable_set(:baz, "qux")
+ end
+ end
+
+end
diff --git a/activesupport/test/core_ext/time_ext_test.rb b/activesupport/test/core_ext/time_ext_test.rb
index 6d6757a1b6..41a1df084e 100644
--- a/activesupport/test/core_ext/time_ext_test.rb
+++ b/activesupport/test/core_ext/time_ext_test.rb
@@ -57,6 +57,54 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_seconds_until_end_of_day
+ assert_equal 0, Time.local(2005,1,1,23,59,59).seconds_until_end_of_day
+ assert_equal 1, Time.local(2005,1,1,23,59,58).seconds_until_end_of_day
+ assert_equal 60, Time.local(2005,1,1,23,58,59).seconds_until_end_of_day
+ assert_equal 3660, Time.local(2005,1,1,22,58,59).seconds_until_end_of_day
+ assert_equal 86399, Time.local(2005,1,1,0,0,0).seconds_until_end_of_day
+ end
+
+ def test_seconds_until_end_of_day_at_daylight_savings_time_start
+ with_env_tz 'US/Eastern' do
+ # dt: US: 2005 April 3rd 2:00am ST => April 3rd 3:00am DT
+ assert_equal 21*3600, Time.local(2005,4,3,1,59,59).seconds_until_end_of_day, 'just before DST start'
+ assert_equal 21*3600-2, Time.local(2005,4,3,3,0,1).seconds_until_end_of_day, 'just after DST start'
+ end
+
+ with_env_tz 'NZ' do
+ # dt: New Zealand: 2006 October 1st 2:00am ST => October 1st 3:00am DT
+ assert_equal 21*3600, Time.local(2006,10,1,1,59,59).seconds_until_end_of_day, 'just before DST start'
+ assert_equal 21*3600-2, Time.local(2006,10,1,3,0,1).seconds_until_end_of_day, 'just after DST start'
+ end
+ end
+
+ def test_seconds_until_end_of_day_at_daylight_savings_time_end
+ with_env_tz 'US/Eastern' do
+ # st: US: 2005 October 30th 2:00am DT => October 30th 1:00am ST
+ # avoid setting a time between 1:00 and 2:00 since that requires specifying whether DST is active
+ assert_equal 24*3600, Time.local(2005,10,30,0,59,59).seconds_until_end_of_day, 'just before DST end'
+ assert_equal 22*3600-2, Time.local(2005,10,30,2,0,1).seconds_until_end_of_day, 'just after DST end'
+
+ # now set a time between 1:00 and 2:00 by specifying whether DST is active
+ # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
+ assert_equal 24*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,true,ENV['TZ']).seconds_until_end_of_day, 'before DST end'
+ assert_equal 23*3600-30*60-1, Time.local(0,30,1,30,10,2005,0,0,false,ENV['TZ']).seconds_until_end_of_day, 'after DST end'
+ end
+
+ with_env_tz 'NZ' do
+ # st: New Zealand: 2006 March 19th 3:00am DT => March 19th 2:00am ST
+ # avoid setting a time between 2:00 and 3:00 since that requires specifying whether DST is active
+ assert_equal 23*3600, Time.local(2006,3,19,1,59,59).seconds_until_end_of_day, 'just before DST end'
+ assert_equal 21*3600-2, Time.local(2006,3,19,3,0,1).seconds_until_end_of_day, 'just after DST end'
+
+ # now set a time between 2:00 and 3:00 by specifying whether DST is active
+ # uses: Time.local( sec, min, hour, day, month, year, wday, yday, isdst, tz )
+ assert_equal 23*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,true, ENV['TZ']).seconds_until_end_of_day, 'before DST end'
+ assert_equal 22*3600-30*60-1, Time.local(0,30,2,19,3,2006,0,0,false,ENV['TZ']).seconds_until_end_of_day, 'after DST end'
+ end
+ end
+
def test_beginning_of_day
assert_equal Time.local(2005,2,4,0,0,0), Time.local(2005,2,4,10,10,10).beginning_of_day
with_env_tz 'US/Eastern' do
@@ -69,10 +117,26 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
end
+ def test_middle_of_day
+ assert_equal Time.local(2005,2,4,12,0,0), Time.local(2005,2,4,10,10,10).middle_of_day
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2006,4,2,12,0,0), Time.local(2006,4,2,10,10,10).middle_of_day, 'start DST'
+ assert_equal Time.local(2006,10,29,12,0,0), Time.local(2006,10,29,10,10,10).middle_of_day, 'ends DST'
+ end
+ with_env_tz 'NZ' do
+ assert_equal Time.local(2006,3,19,12,0,0), Time.local(2006,3,19,10,10,10).middle_of_day, 'ends DST'
+ assert_equal Time.local(2006,10,1,12,0,0), Time.local(2006,10,1,10,10,10).middle_of_day, 'start DST'
+ end
+ end
+
def test_beginning_of_hour
assert_equal Time.local(2005,2,4,19,0,0), Time.local(2005,2,4,19,30,10).beginning_of_hour
end
+ def test_beginning_of_minute
+ assert_equal Time.local(2005,2,4,19,30,0), Time.local(2005,2,4,19,30,10).beginning_of_minute
+ end
+
def test_end_of_day
assert_equal Time.local(2007,8,12,23,59,59,Rational(999999999, 1000)), Time.local(2007,8,12,10,10,10).end_of_day
with_env_tz 'US/Eastern' do
@@ -89,6 +153,10 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal Time.local(2005,2,4,19,59,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_hour
end
+ def test_end_of_minute
+ assert_equal Time.local(2005,2,4,19,30,59,Rational(999999999, 1000)), Time.local(2005,2,4,19,30,10).end_of_minute
+ end
+
def test_last_year
assert_equal Time.local(2004,6,5,10), Time.local(2005,6,5,10,0,0).last_year
end
@@ -453,6 +521,9 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
with_env_tz "US/Central" do
assert_equal "Thu, 05 Feb 2009 14:30:05 -0600", Time.local(2009, 2, 5, 14, 30, 5).to_s(:rfc822)
assert_equal "Mon, 09 Jun 2008 04:05:01 -0500", Time.local(2008, 6, 9, 4, 5, 1).to_s(:rfc822)
+ assert_equal "2009-02-05T14:30:05-06:00", Time.local(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
+ assert_equal "2008-06-09T04:05:01-05:00", Time.local(2008, 6, 9, 4, 5, 1).to_s(:iso8601)
+ assert_equal "2009-02-05T14:30:05Z", Time.utc(2009, 2, 5, 14, 30, 5).to_s(:iso8601)
end
end
@@ -478,7 +549,11 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
end
def test_to_time
- assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time, Time.local(2005, 2, 21, 17, 44, 30).to_time.class
+ assert_equal Time.local(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30).to_time
+ assert_equal Time.local(2005, 2, 21, 17, 44, 30).utc_offset, Time.local(2005, 2, 21, 17, 44, 30).to_time.utc_offset
+ end
end
# NOTE: this test seems to fail (changeset 1958) only on certain platforms,
@@ -518,46 +593,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal 29, Time.days_in_month(2)
end
- def test_time_with_datetime_fallback
- assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:local, 2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0)
- assert_equal Time.time_with_datetime_fallback(:local, 2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 1900, 2, 21, 17, 44, 30), DateTime.civil(1900, 2, 21, 17, 44, 30, 0)
- assert_equal Time.time_with_datetime_fallback(:utc, 2005), Time.utc(2005)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039), DateTime.civil(2039, 1, 1, 0, 0, 0, 0)
- assert_equal Time.time_with_datetime_fallback(:utc, 2005, 2, 21, 17, 44, 30, 1), Time.utc(2005, 2, 21, 17, 44, 30, 1) #with usec
- # This won't overflow on 64bit linux
- unless time_is_64bits?
- assert_equal Time.time_with_datetime_fallback(:local, 1900, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1900, 2, 21, 17, 44, 30)
- assert_equal Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1),
- DateTime.civil(2039, 2, 21, 17, 44, 30, 0, 0)
- assert_equal ::Date::ITALY, Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).start # use Ruby's default start value
- end
- silence_warnings do
- 0.upto(138) do |year|
- [:utc, :local].each do |format|
- assert_equal year, Time.time_with_datetime_fallback(format, year).year
- end
- end
- end
- end
-
- def test_utc_time
- assert_equal Time.utc_time(2005, 2, 21, 17, 44, 30), Time.utc(2005, 2, 21, 17, 44, 30)
- assert_equal Time.utc_time(2039, 2, 21, 17, 44, 30), DateTime.civil(2039, 2, 21, 17, 44, 30, 0)
- assert_equal Time.utc_time(1901, 2, 21, 17, 44, 30), DateTime.civil(1901, 2, 21, 17, 44, 30, 0)
- end
-
- def test_local_time
- assert_equal Time.local_time(2005, 2, 21, 17, 44, 30), Time.local(2005, 2, 21, 17, 44, 30)
- assert_equal Time.local_time(2039, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 2039, 2, 21, 17, 44, 30)
-
- unless time_is_64bits?
- assert_equal Time.local_time(1901, 2, 21, 17, 44, 30), DateTime.civil_from_format(:local, 1901, 2, 21, 17, 44, 30)
- end
- end
-
def test_last_month_on_31st
assert_equal Time.local(2004, 2, 29), Time.local(2004, 3, 31).last_month
end
@@ -669,6 +704,82 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
assert_equal(-1, Time.utc(2000) <=> ActiveSupport::TimeWithZone.new( Time.utc(2000, 1, 1, 0, 0, 1), ActiveSupport::TimeZone['UTC'] ))
end
+ def test_at_with_datetime
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0))
+
+ # Only test this if the underlying Time.at raises a TypeError
+ begin
+ Time.at_without_coercion(Time.now, 0)
+ rescue TypeError
+ assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(DateTime.civil(2000, 1, 1, 0, 0, 0), 0)) }
+ end
+ end
+
+ def test_at_with_datetime_returns_local_time
+ with_env_tz 'US/Eastern' do
+ dt = DateTime.civil(2000, 1, 1, 0, 0, 0, '+0')
+ assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(dt)
+ assert_equal 'EST', Time.at(dt).zone
+ assert_equal(-18000, Time.at(dt).utc_offset)
+
+ # Daylight savings
+ dt = DateTime.civil(2000, 7, 1, 1, 0, 0, '+1')
+ assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(dt)
+ assert_equal 'EDT', Time.at(dt).zone
+ assert_equal(-14400, Time.at(dt).utc_offset)
+ end
+ end
+
+ def test_at_with_time_with_zone
+ assert_equal Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']))
+
+ # Only test this if the underlying Time.at raises a TypeError
+ begin
+ Time.at_without_coercion(Time.now, 0)
+ rescue TypeError
+ assert_raise(TypeError) { assert_equal(Time.utc(2000, 1, 1, 0, 0, 0), Time.at(ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['UTC']), 0)) }
+ end
+ end
+
+ def test_at_with_time_with_zone_returns_local_time
+ with_env_tz 'US/Eastern' do
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ assert_equal Time.local(1999, 12, 31, 19, 0, 0), Time.at(twz)
+ assert_equal 'EST', Time.at(twz).zone
+ assert_equal(-18000, Time.at(twz).utc_offset)
+
+ # Daylight savings
+ twz = ActiveSupport::TimeWithZone.new(Time.utc(2000, 7, 1, 0, 0, 0), ActiveSupport::TimeZone['London'])
+ assert_equal Time.local(2000, 6, 30, 20, 0, 0), Time.at(twz)
+ assert_equal 'EDT', Time.at(twz).zone
+ assert_equal(-14400, Time.at(twz).utc_offset)
+ end
+ end
+
+ def test_at_with_time_microsecond_precision
+ assert_equal Time.at(Time.utc(2000, 1, 1, 0, 0, 0, 111)).to_f, Time.utc(2000, 1, 1, 0, 0, 0, 111).to_f
+ end
+
+ def test_at_with_utc_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.utc(2000), Time.at(Time.utc(2000))
+ assert_equal 'UTC', Time.at(Time.utc(2000)).zone
+ assert_equal(0, Time.at(Time.utc(2000)).utc_offset)
+ end
+ end
+
+ def test_at_with_local_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time.local(2000), Time.at(Time.local(2000))
+ assert_equal 'EST', Time.at(Time.local(2000)).zone
+ assert_equal(-18000, Time.at(Time.local(2000)).utc_offset)
+
+ assert_equal Time.local(2000, 7, 1), Time.at(Time.local(2000, 7, 1))
+ assert_equal 'EDT', Time.at(Time.local(2000, 7, 1)).zone
+ assert_equal(-14400, Time.at(Time.local(2000, 7, 1)).utc_offset)
+ end
+ end
+
def test_eql?
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone['UTC']) )
assert_equal true, Time.utc(2000).eql?( ActiveSupport::TimeWithZone.new(Time.utc(2000), ActiveSupport::TimeZone["Hawaii"]) )
@@ -738,9 +849,6 @@ class TimeExtCalculationsTest < ActiveSupport::TestCase
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
- def time_is_64bits?
- Time.time_with_datetime_fallback(:utc, 2039, 2, 21, 17, 44, 30, 1).is_a?(Time)
- end
end
class TimeExtMarshalingTest < ActiveSupport::TestCase
diff --git a/activesupport/test/core_ext/time_with_zone_test.rb b/activesupport/test/core_ext/time_with_zone_test.rb
index 1293f104e5..5494824a40 100644
--- a/activesupport/test/core_ext/time_with_zone_test.rb
+++ b/activesupport/test/core_ext/time_with_zone_test.rb
@@ -75,11 +75,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
def test_to_json_with_use_standard_json_time_format_config_set_to_true
old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, true
- assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(@twz)
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(@twz)
ensure
ActiveSupport.use_standard_json_time_format = old
end
+ def test_to_json_when_wrapping_a_date_time
+ twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000), @time_zone)
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(twz)
+ end
+
def test_nsec
local = Time.local(2011,6,7,23,59,59,Rational(999999999, 1000))
with_zone = ActiveSupport::TimeWithZone.new(nil, ActiveSupport::TimeZone["Hawaii"], local)
@@ -326,8 +331,23 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 946684800, twz.to_i
end
+ def test_to_r
+ result = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii']).to_r
+ assert_equal Rational(946684800, 1), result
+ assert_kind_of Rational, result
+ end
+
+ def test_time_at
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000, 1, 1), ActiveSupport::TimeZone['Hawaii'])
+ assert_equal time, Time.at(time)
+ end
+
def test_to_time
- assert_equal @twz, @twz.to_time
+ with_env_tz 'US/Eastern' do
+ assert_equal Time, @twz.to_time.class
+ assert_equal Time.local(1999, 12, 31, 19), @twz.to_time
+ assert_equal Time.local(1999, 12, 31, 19).utc_offset, @twz.to_time.utc_offset
+ end
end
def test_to_date
@@ -430,6 +450,16 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal 0, twz.usec
end
+ def test_usec_returns_sec_fraction_when_datetime_is_wrapped
+ twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
+ assert_equal 500000, twz.usec
+ end
+
+ def test_nsec_returns_sec_fraction_when_datetime_is_wrapped
+ twz = ActiveSupport::TimeWithZone.new(DateTime.civil(2000, 1, 1, 0, 0, Rational(1,2)), @time_zone)
+ assert_equal 500000000, twz.nsec
+ end
+
def test_utc_to_local_conversion_saves_period_in_instance_variable
assert_nil @twz.instance_variable_get('@period')
@twz.time
@@ -535,6 +565,20 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Fri, 31 Dec 1999 19:59:59 EST -05:00", twz.end_of_hour.inspect
end
+ def test_beginning_of_minute
+ utc = Time.utc(2000, 1, 1, 0, 30, 10)
+ twz = ActiveSupport::TimeWithZone.new(utc, @time_zone)
+ assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect
+ assert_equal "Fri, 31 Dec 1999 19:00:00 EST -05:00", twz.beginning_of_hour.inspect
+ end
+
+ def test_end_of_minute
+ utc = Time.utc(2000, 1, 1, 0, 30, 10)
+ twz = ActiveSupport::TimeWithZone.new(utc, @time_zone)
+ assert_equal "Fri, 31 Dec 1999 19:30:10 EST -05:00", twz.inspect
+ assert_equal "Fri, 31 Dec 1999 19:30:59 EST -05:00", twz.end_of_minute.inspect
+ end
+
def test_since
assert_equal "Fri, 31 Dec 1999 19:00:01 EST -05:00", @twz.since(1).inspect
end
@@ -750,6 +794,14 @@ class TimeWithZoneTest < ActiveSupport::TestCase
assert_equal "Sun, 15 Jul 2007 10:30:00 EDT -04:00", (twz - 1.year).inspect
end
+ def test_no_method_error_has_proper_context
+ e = assert_raises(NoMethodError) {
+ @twz.this_method_does_not_exist
+ }
+ 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
@@ -945,6 +997,15 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
Time.zone = nil
end
+ def test_time_in_time_zone_doesnt_affect_receiver
+ with_env_tz 'Europe/London' do
+ time = Time.local(2000, 7, 1)
+ time_with_zone = time.in_time_zone('Eastern Time (US & Canada)')
+ assert_equal Time.utc(2000, 6, 30, 23, 0, 0), time_with_zone
+ assert_not time.utc?, 'time expected to be local, but is UTC'
+ end
+ end
+
protected
def with_env_tz(new_tz = 'US/Eastern')
old_tz, ENV['TZ'] = ENV['TZ'], new_tz
@@ -953,3 +1014,131 @@ class TimeWithZoneMethodsForTimeAndDateTimeTest < ActiveSupport::TestCase
old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
end
end
+
+class TimeWithZoneMethodsForDate < ActiveSupport::TestCase
+ 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
+ end
+ with_tz_default 'Hawaii' do
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone.inspect
+ end
+ with_tz_default nil do
+ assert_equal @d.to_time, @d.in_time_zone
+ end
+ end
+
+ def test_nil_time_zone
+ with_tz_default nil do
+ assert !@d.in_time_zone.respond_to?(:period), 'no period method'
+ end
+ end
+
+ def test_in_time_zone_with_argument
+ with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone)
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone('Alaska').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @d.in_time_zone('Hawaii').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @d.in_time_zone('UTC').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @d.in_time_zone(-9.hours).inspect
+ end
+ end
+
+ def test_in_time_zone_with_invalid_argument
+ assert_raise(ArgumentError) { @d.in_time_zone("No such timezone exists") }
+ 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
+ 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
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone.inspect
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone.inspect
+ end
+ with_tz_default 'Hawaii' do
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone.inspect
+ assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone.inspect
+ assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone.inspect
+ end
+ with_tz_default nil do
+ assert_equal @s.to_time, @s.in_time_zone
+ assert_equal @u.to_time, @u.in_time_zone
+ assert_equal @z.to_time, @z.in_time_zone
+ end
+ end
+
+ def test_nil_time_zone
+ with_tz_default nil do
+ assert !@s.in_time_zone.respond_to?(:period), 'no period method'
+ assert !@u.in_time_zone.respond_to?(:period), 'no period method'
+ assert !@z.in_time_zone.respond_to?(:period), 'no period method'
+ end
+ end
+
+ def test_in_time_zone_with_argument
+ with_tz_default 'Eastern Time (US & Canada)' do # Time.zone will not affect #in_time_zone(zone)
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone('Alaska').inspect
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone('Alaska').inspect
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone('Alaska').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 HST -10:00', @s.in_time_zone('Hawaii').inspect
+ assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @u.in_time_zone('Hawaii').inspect
+ assert_equal 'Fri, 31 Dec 1999 14:00:00 HST -10:00', @z.in_time_zone('Hawaii').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @s.in_time_zone('UTC').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @u.in_time_zone('UTC').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 UTC +00:00', @z.in_time_zone('UTC').inspect
+ assert_equal 'Sat, 01 Jan 2000 00:00:00 AKST -09:00', @s.in_time_zone(-9.hours).inspect
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @u.in_time_zone(-9.hours).inspect
+ assert_equal 'Fri, 31 Dec 1999 15:00:00 AKST -09:00', @z.in_time_zone(-9.hours).inspect
+ end
+ end
+
+ def test_in_time_zone_with_invalid_argument
+ assert_raise(ArgumentError) { @s.in_time_zone("No such timezone exists") }
+ assert_raise(ArgumentError) { @u.in_time_zone("No such timezone exists") }
+ assert_raise(ArgumentError) { @z.in_time_zone("No such timezone exists") }
+ assert_raise(ArgumentError) { @s.in_time_zone(-15.hours) }
+ assert_raise(ArgumentError) { @u.in_time_zone(-15.hours) }
+ assert_raise(ArgumentError) { @z.in_time_zone(-15.hours) }
+ assert_raise(ArgumentError) { @s.in_time_zone(Object.new) }
+ 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/dependencies/raises_exception_without_blame_file.rb b/activesupport/test/dependencies/raises_exception_without_blame_file.rb
new file mode 100644
index 0000000000..4b2da6ff30
--- /dev/null
+++ b/activesupport/test/dependencies/raises_exception_without_blame_file.rb
@@ -0,0 +1,5 @@
+exception = Exception.new('I am not blamable!')
+class << exception
+ undef_method(:blame_file!)
+end
+raise exception
diff --git a/activesupport/test/dependencies_test.rb b/activesupport/test/dependencies_test.rb
index e5bc806397..2392b71960 100644
--- a/activesupport/test/dependencies_test.rb
+++ b/activesupport/test/dependencies_test.rb
@@ -1,6 +1,7 @@
require 'abstract_unit'
require 'pp'
require 'active_support/dependencies'
+require 'dependencies_test_helpers'
module ModuleWithMissing
mattr_accessor :missing_count
@@ -19,25 +20,7 @@ class DependenciesTest < ActiveSupport::TestCase
ActiveSupport::Dependencies.clear
end
- def with_loading(*from)
- old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load
- this_dir = File.dirname(__FILE__)
- parent_dir = File.dirname(this_dir)
- path_copy = $LOAD_PATH.dup
- $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir)
- prior_autoload_paths = ActiveSupport::Dependencies.autoload_paths
- ActiveSupport::Dependencies.autoload_paths = from.collect { |f| "#{this_dir}/#{f}" }
- yield
- ensure
- $LOAD_PATH.replace(path_copy)
- ActiveSupport::Dependencies.autoload_paths = prior_autoload_paths
- ActiveSupport::Dependencies.mechanism = old_mechanism
- ActiveSupport::Dependencies.explicitly_unloadable_constants = []
- end
-
- def with_autoloading_fixtures(&block)
- with_loading 'autoloading_fixtures', &block
- end
+ include DependenciesTestHelpers
def test_depend_on_path
skip "LoadError#path does not exist" if RUBY_VERSION < '2.0.0'
@@ -52,6 +35,17 @@ class DependenciesTest < ActiveSupport::TestCase
assert_equal expected.path, e.path
end
+ def test_require_dependency_accepts_an_object_which_implements_to_path
+ o = Object.new
+ def o.to_path; 'dependencies/service_one'; end
+ assert_nothing_raised {
+ require_dependency o
+ }
+ assert defined?(ServiceOne)
+ ensure
+ remove_constants(:ServiceOne)
+ end
+
def test_tracking_loaded_files
require_dependency 'dependencies/service_one'
require_dependency 'dependencies/service_two'
@@ -93,6 +87,14 @@ class DependenciesTest < ActiveSupport::TestCase
end
end
+ def test_dependency_which_raises_doesnt_blindly_call_blame_file!
+ with_loading do
+ filename = 'dependencies/raises_exception_without_blame_file'
+
+ assert_raises(Exception) { require_dependency filename }
+ end
+ end
+
def test_warnings_should_be_enabled_on_first_load
with_loading 'dependencies' do
old_warnings, ActiveSupport::Dependencies.warnings_on_first_load = ActiveSupport::Dependencies.warnings_on_first_load, true
@@ -147,7 +149,8 @@ class DependenciesTest < ActiveSupport::TestCase
def test_circular_autoloading_detection
with_autoloading_fixtures do
- assert_raise(RuntimeError, "Circular dependency detected while autoloading constant Circular1") { Circular1 }
+ e = assert_raise(RuntimeError) { Circular1 }
+ assert_equal "Circular dependency detected while autoloading constant Circular1", e.message
end
end
@@ -379,12 +382,10 @@ class DependenciesTest < ActiveSupport::TestCase
end
def test_smart_name_error_strings
- begin
- Object.module_eval "ImaginaryObject"
- flunk "No raise!!"
- rescue NameError => e
- assert e.message.include?("uninitialized constant ImaginaryObject")
- end
+ Object.module_eval "ImaginaryObject"
+ flunk "No raise!!"
+ rescue NameError => e
+ assert e.message.include?("uninitialized constant ImaginaryObject")
end
def test_loadable_constants_for_path_should_handle_empty_autoloads
@@ -544,7 +545,6 @@ class DependenciesTest < ActiveSupport::TestCase
m = Module.new
m.module_eval "def a() CountingLoader; end"
extend m
- kls = nil
with_autoloading_fixtures do
kls = nil
assert_nothing_raised { kls = a }
@@ -658,6 +658,14 @@ class DependenciesTest < ActiveSupport::TestCase
Object.class_eval { remove_const :E }
end
+ def test_constants_in_capitalized_nesting_marked_as_autoloaded
+ with_autoloading_fixtures do
+ ActiveSupport::Dependencies.load_missing_constant(HTML, "SomeClass")
+
+ assert ActiveSupport::Dependencies.autoloaded?("HTML::SomeClass")
+ end
+ end
+
def test_unloadable
with_autoloading_fixtures do
Object.const_set :M, Module.new
@@ -896,7 +904,7 @@ class DependenciesTest < ActiveSupport::TestCase
def test_autoload_doesnt_shadow_name_error
with_autoloading_fixtures do
Object.send(:remove_const, :RaisesNameError) if defined?(::RaisesNameError)
- 2.times do |i|
+ 2.times do
begin
::RaisesNameError::FooBarBaz.object_id
flunk 'should have raised NameError when autoloaded file referenced FooBarBaz'
@@ -927,10 +935,30 @@ class DependenciesTest < ActiveSupport::TestCase
assert ! defined?(DeleteMe)
end
+ def test_remove_constant_does_not_trigger_loading_autoloads
+ constant = 'ShouldNotBeAutoloaded'
+ Object.class_eval do
+ autoload constant, File.expand_path('../autoloading_fixtures/should_not_be_required', __FILE__)
+ end
+
+ assert_nil ActiveSupport::Dependencies.remove_constant(constant), "Kernel#autoload has been triggered by remove_constant"
+ assert !defined?(ShouldNotBeAutoloaded)
+ end
+
+ def test_remove_constant_does_not_autoload_already_removed_parents_as_a_side_effect
+ with_autoloading_fixtures do
+ _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context"
+ _ = ::A::B # assignment to silence parse-time warning "possibly useless use of :: in void context"
+ ActiveSupport::Dependencies.remove_constant('A')
+ ActiveSupport::Dependencies.remove_constant('A::B')
+ assert !defined?(A)
+ end
+ end
+
def test_load_once_constants_should_not_be_unloaded
with_autoloading_fixtures do
ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_paths
- ::A.to_s
+ _ = ::A # assignment to silence parse-time warning "possibly useless use of :: in void context"
assert defined?(A)
ActiveSupport::Dependencies.clear
assert defined?(A)
diff --git a/activesupport/test/dependencies_test_helpers.rb b/activesupport/test/dependencies_test_helpers.rb
new file mode 100644
index 0000000000..9268512a97
--- /dev/null
+++ b/activesupport/test/dependencies_test_helpers.rb
@@ -0,0 +1,27 @@
+module DependenciesTestHelpers
+ def with_loading(*from)
+ old_mechanism, ActiveSupport::Dependencies.mechanism = ActiveSupport::Dependencies.mechanism, :load
+ this_dir = File.dirname(__FILE__)
+ parent_dir = File.dirname(this_dir)
+ path_copy = $LOAD_PATH.dup
+ $LOAD_PATH.unshift(parent_dir) unless $LOAD_PATH.include?(parent_dir)
+ prior_autoload_paths = ActiveSupport::Dependencies.autoload_paths
+ ActiveSupport::Dependencies.autoload_paths = from.collect { |f| "#{this_dir}/#{f}" }
+ yield
+ ensure
+ $LOAD_PATH.replace(path_copy)
+ ActiveSupport::Dependencies.autoload_paths = prior_autoload_paths
+ ActiveSupport::Dependencies.mechanism = old_mechanism
+ ActiveSupport::Dependencies.explicitly_unloadable_constants = []
+ end
+
+ def with_autoloading_fixtures(&block)
+ with_loading 'autoloading_fixtures', &block
+ end
+
+ def remove_constants(*constants)
+ constants.each do |constant|
+ Object.send(:remove_const, constant) if Object.const_defined?(constant)
+ end
+ end
+end \ No newline at end of file
diff --git a/activesupport/test/deprecation_test.rb b/activesupport/test/deprecation_test.rb
index c081103cc7..9674851b9d 100644
--- a/activesupport/test/deprecation_test.rb
+++ b/activesupport/test/deprecation_test.rb
@@ -9,7 +9,7 @@ class Deprecatee
def old_request; @request end
def partially(foo = nil)
- ActiveSupport::Deprecation.warn('calling with foo=nil is out', caller) if foo.nil?
+ ActiveSupport::Deprecation.warn('calling with foo=nil is out') if foo.nil?
end
def not() 2 end
@@ -75,6 +75,11 @@ class DeprecationTest < ActiveSupport::TestCase
end
end
+ def test_deprecate_object
+ deprecated_object = ActiveSupport::Deprecation::DeprecatedObjectProxy.new(Object.new, ':bomb:')
+ assert_deprecated(/:bomb:/) { deprecated_object.to_s }
+ end
+
def test_nil_behavior_is_ignored
ActiveSupport::Deprecation.behavior = nil
assert_deprecated(/foo=nil/) { @dtc.partially }
@@ -93,6 +98,22 @@ class DeprecationTest < ActiveSupport::TestCase
assert_match(/foo=nil/, @b)
end
+ def test_raise_behaviour
+ ActiveSupport::Deprecation.behavior = :raise
+
+ message = 'Revise this deprecated stuff now!'
+ callstack = %w(foo bar baz)
+
+ begin
+ ActiveSupport::Deprecation.behavior.first.call(message, callstack)
+ rescue ActiveSupport::DeprecationException => e
+ assert_equal message, e.message
+ assert_equal callstack, e.backtrace
+ else
+ flunk 'the :raise deprecation behaviour should raise the expected exception'
+ end
+ end
+
def test_default_stderr_behavior
ActiveSupport::Deprecation.behavior = :stderr
behavior = ActiveSupport::Deprecation.behavior.first
@@ -119,9 +140,10 @@ class DeprecationTest < ActiveSupport::TestCase
ActiveSupport::Deprecation.behavior = :silence
behavior = ActiveSupport::Deprecation.behavior.first
- assert_blank capture(:stderr) {
+ stderr_output = capture(:stderr) {
assert_nil behavior.call('Some error!', ['call stack!'])
}
+ assert stderr_output.blank?
end
def test_deprecated_instance_variable_proxy
@@ -138,6 +160,7 @@ class DeprecationTest < ActiveSupport::TestCase
def test_deprecated_constant_proxy
assert_not_deprecated { Deprecatee::B::C }
assert_deprecated('Deprecatee::A') { assert_equal Deprecatee::B::C, Deprecatee::A }
+ assert_not_deprecated { assert_equal Deprecatee::B::C.class, Deprecatee::A.class }
end
def test_assert_deprecation_without_match
@@ -254,10 +277,10 @@ class DeprecationTest < ActiveSupport::TestCase
klass::OLD.to_s
end
end
-
+
def test_deprecated_instance_variable_with_instance_deprecator
deprecator = deprecator_with_messages
-
+
klass = Class.new() do
def initialize(deprecator)
@request = ActiveSupport::Deprecation::DeprecatedInstanceVariableProxy.new(self, :request, :@request, deprecator)
diff --git a/activesupport/test/descendants_tracker_test_cases.rb b/activesupport/test/descendants_tracker_test_cases.rb
index 066ec8549b..69e046998e 100644
--- a/activesupport/test/descendants_tracker_test_cases.rb
+++ b/activesupport/test/descendants_tracker_test_cases.rb
@@ -1,3 +1,5 @@
+require 'set'
+
module DescendantsTrackerTestCases
class Parent
extend ActiveSupport::DescendantsTracker
@@ -18,15 +20,15 @@ module DescendantsTrackerTestCases
ALL = [Parent, Child1, Child2, Grandchild1, Grandchild2]
def test_descendants
- assert_equal [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
- assert_equal [Grandchild1, Grandchild2], Child1.descendants
- assert_equal [], Child2.descendants
+ assert_equal_sets [Child1, Grandchild1, Grandchild2, Child2], Parent.descendants
+ assert_equal_sets [Grandchild1, Grandchild2], Child1.descendants
+ assert_equal_sets [], Child2.descendants
end
def test_direct_descendants
- assert_equal [Child1, Child2], Parent.direct_descendants
- assert_equal [Grandchild1, Grandchild2], Child1.direct_descendants
- assert_equal [], Child2.direct_descendants
+ assert_equal_sets [Child1, Child2], Parent.direct_descendants
+ assert_equal_sets [Grandchild1, Grandchild2], Child1.direct_descendants
+ assert_equal_sets [], Child2.direct_descendants
end
def test_clear
@@ -40,6 +42,10 @@ module DescendantsTrackerTestCases
protected
+ def assert_equal_sets(expected, actual)
+ assert_equal Set.new(expected), Set.new(actual)
+ end
+
def mark_as_autoloaded(*klasses)
# If ActiveSupport::Dependencies is not loaded, forget about autoloading.
# This allows using AS::DescendantsTracker without AS::Dependencies.
@@ -56,4 +62,4 @@ module DescendantsTrackerTestCases
ActiveSupport::Dependencies.autoloaded_constants = old_autoloaded if defined? ActiveSupport::Dependencies
ActiveSupport::DescendantsTracker.class_eval("@@direct_descendants").replace(old_descendants)
end
-end \ No newline at end of file
+end
diff --git a/activesupport/test/descendants_tracker_with_autoloading_test.rb b/activesupport/test/descendants_tracker_with_autoloading_test.rb
index 9180f1f977..a2ae066a21 100644
--- a/activesupport/test/descendants_tracker_with_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_with_autoloading_test.rb
@@ -6,7 +6,7 @@ require 'descendants_tracker_test_cases'
class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
- def test_clear_with_autoloaded_parent_children_and_granchildren
+ def test_clear_with_autoloaded_parent_children_and_grandchildren
mark_as_autoloaded(*ALL) do
ActiveSupport::DescendantsTracker.clear
ALL.each do |k|
@@ -15,20 +15,20 @@ class DescendantsTrackerWithAutoloadingTest < ActiveSupport::TestCase
end
end
- def test_clear_with_autoloaded_children_and_granchildren
+ def test_clear_with_autoloaded_children_and_grandchildren
mark_as_autoloaded Child1, Grandchild1, Grandchild2 do
ActiveSupport::DescendantsTracker.clear
- assert_equal [Child2], Parent.descendants
- assert_equal [], Child2.descendants
+ assert_equal_sets [Child2], Parent.descendants
+ assert_equal_sets [], Child2.descendants
end
end
- def test_clear_with_autoloaded_granchildren
+ def test_clear_with_autoloaded_grandchildren
mark_as_autoloaded Grandchild1, Grandchild2 do
ActiveSupport::DescendantsTracker.clear
- assert_equal [Child1, Child2], Parent.descendants
- assert_equal [], Child1.descendants
- assert_equal [], Child2.descendants
+ assert_equal_sets [Child1, Child2], Parent.descendants
+ assert_equal_sets [], Child1.descendants
+ assert_equal_sets [], Child2.descendants
end
end
end
diff --git a/activesupport/test/descendants_tracker_without_autoloading_test.rb b/activesupport/test/descendants_tracker_without_autoloading_test.rb
index 74669aaca1..00b449af51 100644
--- a/activesupport/test/descendants_tracker_without_autoloading_test.rb
+++ b/activesupport/test/descendants_tracker_without_autoloading_test.rb
@@ -4,4 +4,14 @@ require 'descendants_tracker_test_cases'
class DescendantsTrackerWithoutAutoloadingTest < ActiveSupport::TestCase
include DescendantsTrackerTestCases
+
+ # Regression test for #8422. https://github.com/rails/rails/issues/8442
+ def test_clear_without_autoloaded_singleton_parent
+ mark_as_autoloaded do
+ parent_instance = Parent.new
+ parent_instance.singleton_class.descendants
+ ActiveSupport::DescendantsTracker.clear
+ assert !ActiveSupport::DescendantsTracker.class_variable_get(:@@direct_descendants).key?(parent_instance.singleton_class)
+ end
+ end
end
diff --git a/activesupport/test/fixtures/xml/jdom_doctype.dtd b/activesupport/test/fixtures/xml/jdom_doctype.dtd
new file mode 100644
index 0000000000..89480496ef
--- /dev/null
+++ b/activesupport/test/fixtures/xml/jdom_doctype.dtd
@@ -0,0 +1 @@
+<!ENTITY a "external entity">
diff --git a/activesupport/test/fixtures/xml/jdom_entities.txt b/activesupport/test/fixtures/xml/jdom_entities.txt
new file mode 100644
index 0000000000..0337fdaa08
--- /dev/null
+++ b/activesupport/test/fixtures/xml/jdom_entities.txt
@@ -0,0 +1 @@
+<!ENTITY a "hello">
diff --git a/activesupport/test/fixtures/xml/jdom_include.txt b/activesupport/test/fixtures/xml/jdom_include.txt
new file mode 100644
index 0000000000..239ca3afaf
--- /dev/null
+++ b/activesupport/test/fixtures/xml/jdom_include.txt
@@ -0,0 +1 @@
+include me
diff --git a/activesupport/test/gzip_test.rb b/activesupport/test/gzip_test.rb
index 75a0505899..0e3cf3b429 100644
--- a/activesupport/test/gzip_test.rb
+++ b/activesupport/test/gzip_test.rb
@@ -4,6 +4,12 @@ require 'active_support/core_ext/object/blank'
class GzipTest < ActiveSupport::TestCase
def test_compress_should_decompress_to_the_same_value
assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World"))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::NO_COMPRESSION))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_SPEED))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", Zlib::BEST_COMPRESSION))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::FILTERED))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, Zlib::HUFFMAN_ONLY))
+ assert_equal "Hello World", ActiveSupport::Gzip.decompress(ActiveSupport::Gzip.compress("Hello World", nil, nil))
end
def test_compress_should_return_a_binary_string
@@ -12,4 +18,16 @@ class GzipTest < ActiveSupport::TestCase
assert_equal Encoding.find('binary'), compressed.encoding
assert !compressed.blank?, "a compressed blank string should not be blank"
end
+
+ def test_compress_should_return_gzipped_string_by_compression_level
+ source_string = "Hello World"*100
+
+ gzipped_by_speed = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_SPEED)
+ assert_equal 1, Zlib::GzipReader.new(StringIO.new(gzipped_by_speed)).level
+
+ gzipped_by_best_compression = ActiveSupport::Gzip.compress(source_string, Zlib::BEST_COMPRESSION)
+ assert_equal 9, Zlib::GzipReader.new(StringIO.new(gzipped_by_best_compression)).level
+
+ assert_equal true, (gzipped_by_best_compression.bytesize < gzipped_by_speed.bytesize)
+ end
end
diff --git a/activesupport/test/i18n_test.rb b/activesupport/test/i18n_test.rb
index ddbba444cf..5ef59b6e6b 100644
--- a/activesupport/test/i18n_test.rb
+++ b/activesupport/test/i18n_test.rb
@@ -62,7 +62,7 @@ class I18nTest < ActiveSupport::TestCase
end
def test_date_order
- assert_equal [:year, :month, :day], I18n.translate(:'date.order')
+ assert_equal %w(year month day), I18n.translate(:'date.order')
end
def test_time_am
diff --git a/activesupport/test/inflector_test.rb b/activesupport/test/inflector_test.rb
index aa41e57928..32739d45a9 100644
--- a/activesupport/test/inflector_test.rb
+++ b/activesupport/test/inflector_test.rb
@@ -61,9 +61,7 @@ class InflectorTest < ActiveSupport::TestCase
assert_equal(plural, ActiveSupport::Inflector.pluralize(plural))
assert_equal(plural.capitalize, ActiveSupport::Inflector.pluralize(plural.capitalize))
end
- end
- SingularToPlural.each do |singular, plural|
define_method "test_singularize_singular_#{singular}" do
assert_equal(singular, ActiveSupport::Inflector.singularize(singular))
assert_equal(singular.capitalize, ActiveSupport::Inflector.singularize(singular.capitalize))
@@ -78,9 +76,10 @@ class InflectorTest < ActiveSupport::TestCase
ActiveSupport::Inflector.inflections.uncountable "series" # Return to normal
end
- MixtureToTitleCase.each do |before, titleized|
- define_method "test_titleize_#{before}" do
- assert_equal(titleized, ActiveSupport::Inflector.titleize(before))
+ MixtureToTitleCase.each_with_index do |(before, titleized), index|
+ define_method "test_titleize_mixture_to_title_case_#{index}" do
+ assert_equal(titleized, ActiveSupport::Inflector.titleize(before), "mixture \
+ to TitleCase failed for #{before}")
end
end
@@ -231,25 +230,35 @@ class InflectorTest < ActiveSupport::TestCase
end
end
+# FIXME: get following tests to pass on jruby, currently skipped
+#
+# Currently this fails because ActiveSupport::Multibyte::Unicode#tidy_bytes
+# required a specific Encoding::Converter(UTF-8 to UTF8-MAC) which unavailable on JRuby
+# causing our tests to error out.
+# related bug http://jira.codehaus.org/browse/JRUBY-7194
def test_parameterize
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_and_normalize
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizedAndNormalized.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string))
end
end
def test_parameterize_with_custom_separator
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterizeWithUnderscore.each do |some_string, parameterized_string|
assert_equal(parameterized_string, ActiveSupport::Inflector.parameterize(some_string, '_'))
end
end
def test_parameterize_with_multi_character_separator
+ jruby_skip "UTF-8 to UTF8-MAC Converter is unavailable"
StringToParameterized.each do |some_string, parameterized_string|
assert_equal(parameterized_string.gsub('-', '__sep__'), ActiveSupport::Inflector.parameterize(some_string, '__sep__'))
end
@@ -326,7 +335,7 @@ class InflectorTest < ActiveSupport::TestCase
end
def test_underscore_as_reverse_of_dasherize
- UnderscoresToDashes.each do |underscored, dasherized|
+ UnderscoresToDashes.each_key do |underscored|
assert_equal(underscored, ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.dasherize(underscored)))
end
end
@@ -361,7 +370,7 @@ class InflectorTest < ActiveSupport::TestCase
inflect.singular(/s$/, '')
inflect.singular(/es$/, '')
-
+
inflect.irregular('el', 'los')
end
@@ -421,33 +430,36 @@ class InflectorTest < ActiveSupport::TestCase
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_irregularity_between_#{singular}_and_#{plural}") do
- inflect.irregular(singular, plural)
- assert_equal singular, ActiveSupport::Inflector.singularize(plural)
- assert_equal plural, ActiveSupport::Inflector.pluralize(singular)
+ Irregularities.each do |singular, plural|
+ define_method("test_irregularity_between_#{singular}_and_#{plural}") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal singular, ActiveSupport::Inflector.singularize(plural)
+ assert_equal plural, ActiveSupport::Inflector.pluralize(singular)
+ end
end
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do
- inflect.irregular(singular, plural)
- assert_equal plural, ActiveSupport::Inflector.pluralize(plural)
+ Irregularities.each do |singular, plural|
+ define_method("test_pluralize_of_irregularity_#{plural}_should_be_the_same") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal plural, ActiveSupport::Inflector.pluralize(plural)
+ end
end
end
end
- Irregularities.each do |irregularity|
- singular, plural = *irregularity
- ActiveSupport::Inflector.inflections do |inflect|
- define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do
- inflect.irregular(singular, plural)
- assert_equal singular, ActiveSupport::Inflector.singularize(singular)
+ Irregularities.each do |singular, plural|
+ define_method("test_singularize_of_irregularity_#{singular}_should_be_the_same") do
+ with_dup do
+ ActiveSupport::Inflector.inflections do |inflect|
+ inflect.irregular(singular, plural)
+ assert_equal singular, ActiveSupport::Inflector.singularize(singular)
+ end
end
end
end
diff --git a/activesupport/test/inflector_test_cases.rb b/activesupport/test/inflector_test_cases.rb
index ca4efd2e59..cc36d62138 100644
--- a/activesupport/test/inflector_test_cases.rb
+++ b/activesupport/test/inflector_test_cases.rb
@@ -105,7 +105,6 @@ module InflectorTestCases
"prize" => "prizes",
"edge" => "edges",
- "cow" => "kine",
"database" => "databases",
# regression tests against improper inflection regexes
@@ -310,5 +309,6 @@ module InflectorTestCases
'move' => 'moves',
'cow' => 'kine',
'zombie' => 'zombies',
+ 'genus' => 'genera'
}
end
diff --git a/activesupport/test/json/decoding_test.rb b/activesupport/test/json/decoding_test.rb
index d1454902e5..07d7e530ca 100644
--- a/activesupport/test/json/decoding_test.rb
+++ b/activesupport/test/json/decoding_test.rb
@@ -4,6 +4,12 @@ require 'active_support/json'
require 'active_support/time'
class TestJSONDecoding < ActiveSupport::TestCase
+ class Foo
+ def self.json_create(object)
+ "Foo"
+ end
+ end
+
TESTS = {
%q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
%q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
@@ -52,36 +58,48 @@ class TestJSONDecoding < ActiveSupport::TestCase
# tests escaping of "\n" char with Yaml backend
%q({"a":"\n"}) => {"a"=>"\n"},
%q({"a":"\u000a"}) => {"a"=>"\n"},
- %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"}
+ %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"},
+ # prevent json unmarshalling
+ %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"},
+ # json "fragments" - these are invalid JSON, but ActionPack relies on this
+ %q("a string") => "a string",
+ %q(1.1) => 1.1,
+ %q(1) => 1,
+ %q(-1) => -1,
+ %q(true) => true,
+ %q(false) => false,
+ %q(null) => nil
}
- backends = [:ok_json]
- backends << :json_gem if defined?(::JSON)
- backends << :yajl if defined?(::Yajl)
-
- backends.each do |backend|
- TESTS.each do |json, expected|
- test "json decodes #{json} with the #{backend} backend" do
- ActiveSupport.parse_json_times = true
- silence_warnings do
- ActiveSupport::JSON.with_backend backend do
- assert_equal expected, ActiveSupport::JSON.decode(json)
- end
- end
+ 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}"
end
+ ActiveSupport.parse_json_times = prev
end
+ end
- test "json decodes time json with time parsing disabled with the #{backend} backend" do
- ActiveSupport.parse_json_times = false
- expected = {"a" => "2007-01-01 01:12:34 Z"}
- ActiveSupport::JSON.with_backend backend do
- assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
- 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
end
def test_failed_json_decoding
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) }
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) }
assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) }
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) }
+ end
+
+ def test_cannot_pass_unsupported_options
+ assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
end
end
diff --git a/activesupport/test/json/encoding_test.rb b/activesupport/test/json/encoding_test.rb
index 7ed71f9abc..d549113ff4 100644
--- a/activesupport/test/json/encoding_test.rb
+++ b/activesupport/test/json/encoding_test.rb
@@ -1,4 +1,5 @@
# encoding: utf-8
+require 'securerandom'
require 'abstract_unit'
require 'active_support/core_ext/string/inflections'
require 'active_support/json'
@@ -22,6 +23,15 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
end
+ class CustomWithOptions
+ attr_accessor :foo, :bar
+
+ def as_json(options={})
+ options[:only] = %w(foo bar)
+ super(options)
+ end
+ end
+
TrueTests = [[ true, %(true) ]]
FalseTests = [[ false, %(false) ]]
NilTests = [[ nil, %(null) ]]
@@ -36,8 +46,8 @@ class TestJSONEncoding < ActiveSupport::TestCase
StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")],
[ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
[ 'http://test.host/posts/1', %("http://test.host/posts/1")],
- [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f",
- %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F") ]]
+ [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\342\200\250\342\200\251",
+ %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000B\\f\\r\\u000E\\u000F\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001A\\u001B\\u001C\\u001D\\u001E\\u001F\\u2028\\u2029") ]]
ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
[ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
@@ -73,6 +83,8 @@ class TestJSONEncoding < ActiveSupport::TestCase
constants.grep(/Tests$/).each do |class_tests|
define_method("test_#{class_tests[0..-6].underscore}") do
begin
+ prev = ActiveSupport.use_standard_json_time_format
+
ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/
ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
self.class.const_get(class_tests).each do |pair|
@@ -80,16 +92,16 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
ensure
ActiveSupport.escape_html_entities_in_json = false
- ActiveSupport.use_standard_json_time_format = false
+ ActiveSupport.use_standard_json_time_format = prev
end
end
end
- def test_json_variable
- assert_deprecated do
- assert_equal ActiveSupport::JSON::Variable.new('foo'), 'foo'
- assert_equal ActiveSupport::JSON::Variable.new('alert("foo")'), 'alert("foo")'
- end
+ def test_process_status
+ # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
+ # creating a child process, hence this to populate $?
+ system("not_a_real_program_#{SecureRandom.hex}")
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
end
def test_hash_encoding
@@ -103,21 +115,34 @@ class TestJSONEncoding < ActiveSupport::TestCase
def test_utf8_string_encoded_properly
result = ActiveSupport::JSON.encode('€2.99')
- assert_equal '"\\u20ac2.99"', result
+ assert_equal '"€2.99"', result
assert_equal(Encoding::UTF_8, result.encoding)
result = ActiveSupport::JSON.encode('✎☺')
- assert_equal '"\\u270e\\u263a"', result
+ assert_equal '"✎☺"', result
assert_equal(Encoding::UTF_8, result.encoding)
end
def test_non_utf8_string_transcodes
s = '二'.encode('Shift_JIS')
result = ActiveSupport::JSON.encode(s)
- assert_equal '"\\u4e8c"', result
+ assert_equal '"二"', result
assert_equal Encoding::UTF_8, result.encoding
end
+ def test_wide_utf8_chars
+ w = '𠜎'
+ result = ActiveSupport::JSON.encode(w)
+ assert_equal '"𠜎"', result
+ end
+
+ def test_wide_utf8_roundtrip
+ hash = { string: "ð’‘" }
+ json = ActiveSupport::JSON.encode(hash)
+ decoded_hash = ActiveSupport::JSON.decode(json)
+ assert_equal "ð’‘", decoded_hash['string']
+ end
+
def test_exception_raised_when_encoding_circular_reference_in_array
a = [1]
a << a
@@ -150,23 +175,28 @@ class TestJSONEncoding < ActiveSupport::TestCase
end
def test_time_to_json_includes_local_offset
+ prev = ActiveSupport.use_standard_json_time_format
ActiveSupport.use_standard_json_time_format = true
with_env_tz 'US/Eastern' do
assert_equal %("2005-02-01T15:15:10-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
end
ensure
- ActiveSupport.use_standard_json_time_format = false
+ ActiveSupport.use_standard_json_time_format = prev
end
def test_hash_with_time_to_json
+ prev = ActiveSupport.use_standard_json_time_format
+ ActiveSupport.use_standard_json_time_format = false
assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
+ ensure
+ ActiveSupport.use_standard_json_time_format = prev
end
def test_nested_hash_with_float
assert_nothing_raised do
hash = {
"CHI" => {
- :dislay_name => "chicago",
+ :display_name => "chicago",
:latitude => 123.234
}
}
@@ -248,6 +278,16 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
end
+ def test_to_json_should_not_keep_options_around
+ f = CustomWithOptions.new
+ f.foo = "hello"
+ f.bar = "world"
+
+ hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
+ assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
+ "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, JSON.parse(hash.to_json))
+ end
+
def test_struct_encoding
Struct.new('UserNameAndEmail', :name, :email)
Struct.new('UserNameAndDate', :name, :date)
@@ -270,12 +310,12 @@ class TestJSONEncoding < ActiveSupport::TestCase
assert_equal({"name" => "David",
"sub" => {
"name" => "David",
- "date" => "2010/01/01" }}, JSON.parse(json_custom))
+ "date" => "2010-01-01" }}, JSON.parse(json_custom))
assert_equal({"name" => "David", "email" => "sample@example.com"},
JSON.parse(json_strings))
- assert_equal({"name" => "David", "date" => "2010/01/01"},
+ assert_equal({"name" => "David", "date" => "2010-01-01"},
JSON.parse(json_string_and_date))
end
diff --git a/activesupport/test/load_paths_test.rb b/activesupport/test/load_paths_test.rb
index 979e25bdf3..ac617a9fd8 100644
--- a/activesupport/test/load_paths_test.rb
+++ b/activesupport/test/load_paths_test.rb
@@ -10,7 +10,7 @@ class LoadPathsTest < ActiveSupport::TestCase
}
load_paths_count[File.expand_path('../../lib', __FILE__)] -= 1
- filtered = load_paths_count.select { |k, v| v > 1 }
- assert filtered.empty?, filtered.inspect
+ load_paths_count.select! { |k, v| v > 1 }
+ assert load_paths_count.empty?, load_paths_count.inspect
end
end
diff --git a/activesupport/test/logger_test.rb b/activesupport/test/logger_test.rb
index eedeca30a8..d2801849ca 100644
--- a/activesupport/test/logger_test.rb
+++ b/activesupport/test/logger_test.rb
@@ -120,4 +120,14 @@ class LoggerTest < ActiveSupport::TestCase
byte_string.force_encoding("ASCII-8BIT")
assert byte_string.include?(BYTE_STRING)
end
+
+ def test_silencing_everything_but_errors
+ @logger.silence do
+ @logger.debug "NOT THERE"
+ @logger.error "THIS IS HERE"
+ end
+
+ assert !@output.string.include?("NOT THERE")
+ assert @output.string.include?("THIS IS HERE")
+ end
end
diff --git a/activesupport/test/message_encryptor_test.rb b/activesupport/test/message_encryptor_test.rb
index b544742300..509c453b5c 100644
--- a/activesupport/test/message_encryptor_test.rb
+++ b/activesupport/test/message_encryptor_test.rb
@@ -29,9 +29,9 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def test_encrypting_twice_yields_differing_cipher_text
- first_messqage = @encryptor.encrypt_and_sign(@data).split("--").first
+ first_message = @encryptor.encrypt_and_sign(@data).split("--").first
second_message = @encryptor.encrypt_and_sign(@data).split("--").first
- assert_not_equal first_messqage, second_message
+ assert_not_equal first_message, second_message
end
def test_messing_with_either_encrypted_values_causes_failure
@@ -56,9 +56,14 @@ class MessageEncryptorTest < ActiveSupport::TestCase
end
def test_alternative_serialization_method
- encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), :serializer => JSONSerializer.new)
+ prev = ActiveSupport.use_standard_json_time_format
+ ActiveSupport.use_standard_json_time_format = true
+ encryptor = ActiveSupport::MessageEncryptor.new(SecureRandom.hex(64), SecureRandom.hex(64), :serializer => JSONSerializer.new)
message = encryptor.encrypt_and_sign({ :foo => 123, 'bar' => Time.utc(2010) })
- assert_equal encryptor.decrypt_and_verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ assert_equal exp, encryptor.decrypt_and_verify(message)
+ ensure
+ ActiveSupport.use_standard_json_time_format = prev
end
private
diff --git a/activesupport/test/message_verifier_test.rb b/activesupport/test/message_verifier_test.rb
index 5adff41653..a8633f7299 100644
--- a/activesupport/test/message_verifier_test.rb
+++ b/activesupport/test/message_verifier_test.rb
@@ -45,9 +45,14 @@ class MessageVerifierTest < ActiveSupport::TestCase
end
def test_alternative_serialization_method
+ prev = ActiveSupport.use_standard_json_time_format
+ ActiveSupport.use_standard_json_time_format = true
verifier = ActiveSupport::MessageVerifier.new("Hey, I'm a secret!", :serializer => JSONSerializer.new)
message = verifier.generate({ :foo => 123, 'bar' => Time.utc(2010) })
- assert_equal verifier.verify(message), { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ exp = { "foo" => 123, "bar" => "2010-01-01T00:00:00Z" }
+ assert_equal exp, verifier.verify(message)
+ ensure
+ ActiveSupport.use_standard_json_time_format = prev
end
def assert_not_verified(message)
diff --git a/activesupport/test/multibyte_chars_test.rb b/activesupport/test/multibyte_chars_test.rb
index ef289692bc..2bf73291a2 100644
--- a/activesupport/test/multibyte_chars_test.rb
+++ b/activesupport/test/multibyte_chars_test.rb
@@ -47,7 +47,10 @@ class MultibyteCharsTest < ActiveSupport::TestCase
end
def test_methods_are_forwarded_to_wrapped_string_for_byte_strings
- assert_equal BYTE_STRING.class, BYTE_STRING.mb_chars.class
+ original_encoding = BYTE_STRING.encoding
+ assert_equal BYTE_STRING.length, BYTE_STRING.mb_chars.length
+ ensure
+ BYTE_STRING.force_encoding(original_encoding)
end
def test_forwarded_method_with_non_string_result_should_be_returned_vertabim
@@ -673,6 +676,9 @@ class MultibyteCharsExtrasTest < ActiveSupport::TestCase
assert_equal "𥤤", chars(byte_string).tidy_bytes(true)
end
+ def test_class_is_not_forwarded
+ assert_equal BYTE_STRING.dup.mb_chars.class, ActiveSupport::Multibyte::Chars
+ end
private
diff --git a/activesupport/test/notifications/instrumenter_test.rb b/activesupport/test/notifications/instrumenter_test.rb
new file mode 100644
index 0000000000..f46e96f636
--- /dev/null
+++ b/activesupport/test/notifications/instrumenter_test.rb
@@ -0,0 +1,58 @@
+require 'abstract_unit'
+require 'active_support/notifications/instrumenter'
+
+module ActiveSupport
+ module Notifications
+ class InstrumenterTest < ActiveSupport::TestCase
+ class TestNotifier
+ attr_reader :starts, :finishes
+
+ def initialize
+ @starts = []
+ @finishes = []
+ end
+
+ def start(*args); @starts << args; end
+ def finish(*args); @finishes << args; end
+ end
+
+ attr_reader :instrumenter, :notifier, :payload
+
+ def setup
+ super
+ @notifier = TestNotifier.new
+ @instrumenter = Instrumenter.new @notifier
+ @payload = { :foo => Object.new }
+ end
+
+ def test_instrument
+ called = false
+ instrumenter.instrument("foo", payload) {
+ called = true
+ }
+
+ assert called
+ end
+
+ def test_instrument_yields_the_payload_for_further_modification
+ assert_equal 2, instrumenter.instrument("awesome") { |p| p[:result] = 1 + 1 }
+ assert_equal 1, notifier.finishes.size
+ name, _, payload = notifier.finishes.first
+ assert_equal "awesome", name
+ assert_equal Hash[:result => 2], payload
+ end
+
+ def test_start
+ instrumenter.start("foo", payload)
+ assert_equal [["foo", instrumenter.id, payload]], notifier.starts
+ assert_predicate notifier.finishes, :empty?
+ end
+
+ def test_finish
+ instrumenter.finish("foo", payload)
+ assert_equal [["foo", instrumenter.id, payload]], notifier.finishes
+ assert_predicate notifier.starts, :empty?
+ end
+ end
+ end
+end
diff --git a/activesupport/test/notifications_test.rb b/activesupport/test/notifications_test.rb
index bcb393c7bc..f729f0a95b 100644
--- a/activesupport/test/notifications_test.rb
+++ b/activesupport/test/notifications_test.rb
@@ -81,6 +81,20 @@ module Notifications
end
end
+ class TestSubscriber
+ attr_reader :starts, :finishes, :publishes
+
+ def initialize
+ @starts = []
+ @finishes = []
+ @publishes = []
+ end
+
+ def start(*args); @starts << args; end
+ def finish(*args); @finishes << args; end
+ def publish(*args); @publishes << args; end
+ end
+
class SyncPubSubTest < TestCase
def test_events_are_published_to_a_listener
@notifier.publish :foo
@@ -99,7 +113,7 @@ module Notifications
@notifier.publish :foo
@notifier.publish :foo
- @notifier.subscribe("not_existant") do |*args|
+ @notifier.subscribe("not_existent") do |*args|
@events << ActiveSupport::Notifications::Event.new(*args)
end
@@ -144,6 +158,14 @@ module Notifications
assert_equal [[:foo]], @another
end
+ def test_publish_with_subscriber
+ subscriber = TestSubscriber.new
+ @notifier.subscribe nil, subscriber
+ @notifier.publish :foo
+
+ assert_equal [[:foo]], subscriber.publishes
+ end
+
private
def event(*args)
args
@@ -157,7 +179,7 @@ module Notifications
assert_equal 2, instrument(:awesome) { 1 + 1 }
end
- def test_instrument_yields_the_paylod_for_further_modification
+ def test_instrument_yields_the_payload_for_further_modification
assert_equal 2, instrument(:awesome) { |p| p[:result] = 1 + 1 }
assert_equal 1, @events.size
assert_equal :awesome, @events.first.name
diff --git a/activesupport/test/number_helper_test.rb b/activesupport/test/number_helper_test.rb
index 5f54587f93..1fadef3637 100644
--- a/activesupport/test/number_helper_test.rb
+++ b/activesupport/test/number_helper_test.rb
@@ -301,6 +301,13 @@ module ActiveSupport
end
end
+ def test_number_to_human_with_custom_units_that_are_missing_the_needed_key
+ [@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
+ assert_equal '123', number_helper.number_to_human(123, units: { thousand: 'k'})
+ assert_equal '123', number_helper.number_to_human(123, units: {})
+ end
+ end
+
def test_number_to_human_with_custom_format
[@instance_with_helpers, TestClassWithClassNumberHelpers, ActiveSupport::NumberHelper].each do |number_helper|
assert_equal '123 times Thousand', number_helper.number_to_human(123456, :format => "%n times %u")
diff --git a/activesupport/test/ordered_hash_test.rb b/activesupport/test/ordered_hash_test.rb
index 14ba4e0076..0b54026c64 100644
--- a/activesupport/test/ordered_hash_test.rb
+++ b/activesupport/test/ordered_hash_test.rb
@@ -1,7 +1,8 @@
require 'abstract_unit'
require 'active_support/json'
-require 'active_support/core_ext/object/to_json'
+require 'active_support/core_ext/object/json'
require 'active_support/core_ext/hash/indifferent_access'
+require 'active_support/core_ext/array/extract_options'
class OrderedHashTest < ActiveSupport::TestCase
def setup
@@ -243,11 +244,7 @@ class OrderedHashTest < ActiveSupport::TestCase
end
def test_each_after_yaml_serialization
- values = []
- @deserialized_ordered_hash = YAML.load(YAML.dump(@ordered_hash))
-
- @deserialized_ordered_hash.each {|key, value| values << value}
- assert_equal @values, values
+ assert_equal @values, YAML.load(YAML.dump(@ordered_hash)).values
end
def test_each_when_yielding_to_block_with_splat
diff --git a/activesupport/test/ordered_options_test.rb b/activesupport/test/ordered_options_test.rb
index f60f9a58e3..fdc745b23b 100644
--- a/activesupport/test/ordered_options_test.rb
+++ b/activesupport/test/ordered_options_test.rb
@@ -7,13 +7,13 @@ class OrderedOptionsTest < ActiveSupport::TestCase
assert_nil a[:not_set]
- a[:allow_concurreny] = true
+ a[:allow_concurrency] = true
assert_equal 1, a.size
- assert a[:allow_concurreny]
+ assert a[:allow_concurrency]
- a[:allow_concurreny] = false
+ a[:allow_concurrency] = false
assert_equal 1, a.size
- assert !a[:allow_concurreny]
+ assert !a[:allow_concurrency]
a["else_where"] = 56
assert_equal 2, a.size
@@ -23,10 +23,10 @@ class OrderedOptionsTest < ActiveSupport::TestCase
def test_looping
a = ActiveSupport::OrderedOptions.new
- a[:allow_concurreny] = true
+ a[:allow_concurrency] = true
a["else_where"] = 56
- test = [[:allow_concurreny, true], [:else_where, 56]]
+ test = [[:allow_concurrency, true], [:else_where, 56]]
a.each_with_index do |(key, value), index|
assert_equal test[index].first, key
@@ -39,13 +39,13 @@ class OrderedOptionsTest < ActiveSupport::TestCase
assert_nil a.not_set
- a.allow_concurreny = true
+ a.allow_concurrency = true
assert_equal 1, a.size
- assert a.allow_concurreny
+ assert a.allow_concurrency
- a.allow_concurreny = false
+ a.allow_concurrency = false
assert_equal 1, a.size
- assert !a.allow_concurreny
+ assert !a.allow_concurrency
a.else_where = 56
assert_equal 2, a.size
diff --git a/activesupport/test/queueing/synchronous_queue_test.rb b/activesupport/test/queueing/synchronous_queue_test.rb
deleted file mode 100644
index 86c39d0f6c..0000000000
--- a/activesupport/test/queueing/synchronous_queue_test.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'abstract_unit'
-require 'active_support/queueing'
-
-class SynchronousQueueTest < ActiveSupport::TestCase
- class Job
- attr_reader :ran
- def run; @ran = true end
- end
-
- class ExceptionRaisingJob
- def run; raise end
- end
-
- def setup
- @queue = ActiveSupport::SynchronousQueue.new
- end
-
- def test_runs_jobs_immediately
- job = Job.new
- @queue.push job
- assert job.ran
-
- assert_raises RuntimeError do
- @queue.push ExceptionRaisingJob.new
- end
- end
-end
diff --git a/activesupport/test/queueing/test_queue_test.rb b/activesupport/test/queueing/test_queue_test.rb
deleted file mode 100644
index 451fb68d3e..0000000000
--- a/activesupport/test/queueing/test_queue_test.rb
+++ /dev/null
@@ -1,146 +0,0 @@
-require 'abstract_unit'
-require 'active_support/queueing'
-
-class TestQueueTest < ActiveSupport::TestCase
- def setup
- @queue = ActiveSupport::TestQueue.new
- end
-
- class ExceptionRaisingJob
- def run
- raise
- end
- end
-
- def test_drain_raises_exceptions_from_running_jobs
- @queue.push ExceptionRaisingJob.new
- assert_raises(RuntimeError) { @queue.drain }
- end
-
- def test_jobs
- @queue.push 1
- @queue.push 2
- assert_equal [1,2], @queue.jobs
- end
-
- class EquivalentJob
- def initialize
- @initial_id = self.object_id
- end
-
- def run
- end
-
- def ==(other)
- other.same_initial_id?(@initial_id)
- end
-
- def same_initial_id?(other_id)
- other_id == @initial_id
- end
- end
-
- def test_contents
- job = EquivalentJob.new
- assert @queue.empty?
- @queue.push job
- refute @queue.empty?
- assert_equal job, @queue.pop
- end
-
- class ProcessingJob
- def self.clear_processed
- @processed = []
- end
-
- def self.processed
- @processed
- end
-
- def initialize(object)
- @object = object
- end
-
- def run
- self.class.processed << @object
- end
- end
-
- def test_order
- ProcessingJob.clear_processed
- job1 = ProcessingJob.new(1)
- job2 = ProcessingJob.new(2)
-
- @queue.push job1
- @queue.push job2
- @queue.drain
-
- assert_equal [1,2], ProcessingJob.processed
- end
-
- class ThreadTrackingJob
- attr_reader :thread_id
-
- def run
- @thread_id = Thread.current.object_id
- end
-
- def ran?
- @thread_id
- end
- end
-
- def test_drain
- @queue.push ThreadTrackingJob.new
- job = @queue.jobs.last
- @queue.drain
-
- assert @queue.empty?
- assert job.ran?, "The job runs synchronously when the queue is drained"
- assert_equal job.thread_id, Thread.current.object_id
- end
-
- class IdentifiableJob
- def initialize(id)
- @id = id
- end
-
- def ==(other)
- other.same_id?(@id)
- end
-
- def same_id?(other_id)
- other_id == @id
- end
-
- def run
- end
- end
-
- def test_queue_can_be_observed
- jobs = (1..10).map do |id|
- IdentifiableJob.new(id)
- end
-
- jobs.each do |job|
- @queue.push job
- end
-
- assert_equal jobs, @queue.jobs
- end
-
- def test_adding_an_unmarshallable_job
- anonymous_class_instance = Struct.new(:run).new
-
- assert_raises TypeError do
- @queue.push anonymous_class_instance
- end
- end
-
- def test_attempting_to_add_a_reference_to_itself
- job = {reference: @queue}
- assert_raises TypeError do
- @queue.push job
- end
- end
-end
diff --git a/activesupport/test/queueing/threaded_consumer_test.rb b/activesupport/test/queueing/threaded_consumer_test.rb
deleted file mode 100644
index fc43cb555a..0000000000
--- a/activesupport/test/queueing/threaded_consumer_test.rb
+++ /dev/null
@@ -1,92 +0,0 @@
-require 'abstract_unit'
-require 'active_support/queueing'
-require "active_support/log_subscriber/test_helper"
-
-class TestThreadConsumer < ActiveSupport::TestCase
- class Job
- attr_reader :id
- def initialize(id = 1, &block)
- @id = id
- @block = block
- end
-
- def run
- @block.call if @block
- end
- end
-
- def setup
- @logger = ActiveSupport::LogSubscriber::TestHelper::MockLogger.new
- @queue = ActiveSupport::Queue.new(logger: @logger)
- end
-
- def teardown
- @queue.drain
- end
-
- test "the jobs are executed" do
- ran = false
- job = Job.new { ran = true }
-
- @queue.push job
- @queue.drain
-
- assert_equal true, ran
- end
-
- test "the jobs are not executed synchronously" do
- run, ran = Queue.new, Queue.new
- job = Job.new { ran.push run.pop }
-
- @queue.consumer.start
- @queue.push job
- assert ran.empty?
-
- run.push true
- assert_equal true, ran.pop
- end
-
- test "shutting down the queue synchronously drains the jobs" do
- ran = false
- job = Job.new do
- sleep 0.1
- ran = true
- end
-
- @queue.consumer.start
- @queue.push job
- assert_equal false, ran
-
- @queue.consumer.shutdown
- assert_equal true, ran
- end
-
- test "log job that raises an exception" do
- job = Job.new { raise "RuntimeError: Error!" }
-
- @queue.push job
- @queue.drain
-
- assert_equal 1, @logger.logged(:error).size
- assert_match 'Job Error: RuntimeError: Error!', @logger.logged(:error).last
- end
-
- test "test overriding exception handling" do
- @queue.consumer.instance_eval do
- def handle_exception(job, exception)
- @last_error = exception.message
- end
-
- def last_error
- @last_error
- end
- end
-
- job = Job.new { raise "RuntimeError: Error!" }
-
- @queue.push job
- @queue.drain
-
- assert_equal "RuntimeError: Error!", @queue.consumer.last_error
- end
-end
diff --git a/activesupport/test/rescuable_test.rb b/activesupport/test/rescuable_test.rb
index 3f8d09c18e..ec9d231125 100644
--- a/activesupport/test/rescuable_test.rb
+++ b/activesupport/test/rescuable_test.rb
@@ -21,7 +21,7 @@ class Stargate
rescue_from WraithAttack, :with => :sos
- rescue_from NuclearExplosion do
+ rescue_from 'NuclearExplosion' do
@result = 'alldead'
end
@@ -97,10 +97,9 @@ class RescuableTest < ActiveSupport::TestCase
assert_equal expected, result
end
- def test_children_should_inherit_rescue_defintions_from_parents_and_child_rescue_should_be_appended
+ def test_children_should_inherit_rescue_definitions_from_parents_and_child_rescue_should_be_appended
expected = ["WraithAttack", "WraithAttack", "NuclearExplosion", "MadRonon", "CoolError"]
result = @cool_stargate.send(:rescue_handlers).collect {|e| e.first}
assert_equal expected, result
end
-
end
diff --git a/activesupport/test/spec_type_test.rb b/activesupport/test/spec_type_test.rb
deleted file mode 100644
index 95a982d8fd..0000000000
--- a/activesupport/test/spec_type_test.rb
+++ /dev/null
@@ -1,23 +0,0 @@
-require "abstract_unit"
-require "active_record"
-
-class SomeRandomModel < ActiveRecord::Base; end
-
-class SpecTypeTest < ActiveSupport::TestCase
-
- def assert_support actual
- assert_equal ActiveSupport::TestCase, actual
- end
-
- def assert_spec actual
- assert_equal MiniTest::Spec, actual
- end
-
- def test_spec_type_resolves_for_actitive_record_constants
- assert_support MiniTest::Spec.spec_type(SomeRandomModel)
- end
-
- def test_spec_type_doesnt_resolve_random_strings
- assert_spec MiniTest::Spec.spec_type("Unmatched String")
- end
-end
diff --git a/activesupport/test/string_inquirer_test.rb b/activesupport/test/string_inquirer_test.rb
index 94d5fe197d..a2ed577eb0 100644
--- a/activesupport/test/string_inquirer_test.rb
+++ b/activesupport/test/string_inquirer_test.rb
@@ -10,7 +10,7 @@ class StringInquirerTest < ActiveSupport::TestCase
end
def test_miss
- refute @string_inquirer.development?
+ assert_not @string_inquirer.development?
end
def test_missing_question_mark
diff --git a/activesupport/test/subscriber_test.rb b/activesupport/test/subscriber_test.rb
new file mode 100644
index 0000000000..253411aa3d
--- /dev/null
+++ b/activesupport/test/subscriber_test.rb
@@ -0,0 +1,40 @@
+require 'abstract_unit'
+require 'active_support/subscriber'
+
+class TestSubscriber < ActiveSupport::Subscriber
+ attach_to :doodle
+
+ cattr_reader :event
+
+ def self.clear
+ @@event = nil
+ end
+
+ def open_party(event)
+ @@event = event
+ end
+
+ private
+
+ def private_party(event)
+ @@event = event
+ end
+end
+
+class SubscriberTest < ActiveSupport::TestCase
+ def setup
+ TestSubscriber.clear
+ end
+
+ def test_attaches_subscribers
+ ActiveSupport::Notifications.instrument("open_party.doodle")
+
+ assert_equal "open_party.doodle", TestSubscriber.event.name
+ end
+
+ def test_does_not_attach_private_methods
+ ActiveSupport::Notifications.instrument("private_party.doodle")
+
+ assert_nil TestSubscriber.event
+ end
+end
diff --git a/activesupport/test/test_case_test.rb b/activesupport/test/test_case_test.rb
deleted file mode 100644
index c02bfa8497..0000000000
--- a/activesupport/test/test_case_test.rb
+++ /dev/null
@@ -1,109 +0,0 @@
-require 'abstract_unit'
-
-module ActiveSupport
- class TestCaseTest < ActiveSupport::TestCase
- class FakeRunner
- attr_reader :puked
-
- def initialize
- @puked = []
- end
-
- def puke(klass, name, e)
- @puked << [klass, name, e]
- end
-
- def options
- nil
- end
- end
-
- def test_standard_error_raised_within_setup_callback_is_puked
- tc = Class.new(TestCase) do
- def self.name; nil; end
-
- setup :bad_callback
- def bad_callback; raise 'oh noes' end
- def test_true; assert true end
- end
-
- test_name = 'test_true'
- fr = FakeRunner.new
-
- test = tc.new test_name
- test.run fr
- klass, name, exception = *fr.puked.first
-
- assert_equal tc, klass
- assert_equal test_name, name
- assert_equal 'oh noes', exception.message
- end
-
- def test_standard_error_raised_within_teardown_callback_is_puked
- tc = Class.new(TestCase) do
- def self.name; nil; end
-
- teardown :bad_callback
- def bad_callback; raise 'oh noes' end
- def test_true; assert true end
- end
-
- test_name = 'test_true'
- fr = FakeRunner.new
-
- test = tc.new test_name
- test.run fr
- klass, name, exception = *fr.puked.first
-
- assert_equal tc, klass
- assert_equal test_name, name
- assert_equal 'oh noes', exception.message
- end
-
- def test_passthrough_exception_raised_within_test_method_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
-
- def test_which_raises_interrupt; raise Interrupt; end
- end
-
- test_name = 'test_which_raises_interrupt'
- fr = FakeRunner.new
-
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
- end
-
- def test_passthrough_exception_raised_within_setup_callback_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
-
- setup :callback_which_raises_interrupt
- def callback_which_raises_interrupt; raise Interrupt; end
- def test_true; assert true end
- end
-
- test_name = 'test_true'
- fr = FakeRunner.new
-
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
- end
-
- def test_passthrough_exception_raised_within_teardown_callback_is_not_rescued
- tc = Class.new(TestCase) do
- def self.name; nil; end
-
- teardown :callback_which_raises_interrupt
- def callback_which_raises_interrupt; raise Interrupt; end
- def test_true; assert true end
- end
-
- test_name = 'test_true'
- fr = FakeRunner.new
-
- test = tc.new test_name
- assert_raises(Interrupt) { test.run fr }
- end
- end
-end
diff --git a/activesupport/test/test_test.rb b/activesupport/test/test_test.rb
index 9516556844..0c8cc1f883 100644
--- a/activesupport/test/test_test.rb
+++ b/activesupport/test/test_test.rb
@@ -15,6 +15,17 @@ class AssertDifferenceTest < ActiveSupport::TestCase
@object.num = 0
end
+ def test_assert_not
+ assert_equal true, assert_not(nil)
+ assert_equal true, assert_not(false)
+
+ e = assert_raises(MiniTest::Assertion) { assert_not true }
+ assert_equal 'Expected true to be nil or false', e.message
+
+ e = assert_raises(MiniTest::Assertion) { assert_not true, 'custom' }
+ assert_equal 'custom', e.message
+ end
+
def test_assert_no_difference
assert_no_difference '@object.num' do
# ...
@@ -76,58 +87,18 @@ class AssertDifferenceTest < ActiveSupport::TestCase
end
end
-class AssertBlankTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ]
- NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ]
-
- def test_assert_blank_true
- BLANK.each { |v| assert_blank v }
- end
-
- def test_assert_blank_false
- NOT_BLANK.each { |v|
- begin
- assert_blank v
- fail 'should not get to here'
- rescue Exception => e
- assert_match(/is not blank/, e.message)
- end
- }
- end
-end
-
-class AssertPresentTest < ActiveSupport::TestCase
- BLANK = [ EmptyTrue.new, nil, false, '', ' ', " \n\t \r ", [], {} ]
- NOT_BLANK = [ EmptyFalse.new, Object.new, true, 0, 1, 'x', [nil], { nil => 0 } ]
-
- def test_assert_present_true
- NOT_BLANK.each { |v| assert_present v }
- end
-
- def test_assert_present_false
- BLANK.each { |v|
- begin
- assert_present v
- fail 'should not get to here'
- rescue Exception => e
- assert_match(/is blank/, e.message)
- end
- }
- end
-end
-
class AlsoDoingNothingTest < ActiveSupport::TestCase
end
# Setup and teardown callbacks.
class SetupAndTeardownTest < ActiveSupport::TestCase
setup :reset_callback_record, :foo
- teardown :foo, :sentinel, :foo
+ teardown :foo, :sentinel
def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:foo], @called_back
- assert_equal [:foo, :sentinel, :foo], self.class._teardown_callbacks.map(&:raw_filter)
+ assert_equal [:foo, :sentinel], self.class._teardown_callbacks.map(&:raw_filter)
end
def setup
@@ -147,7 +118,7 @@ class SetupAndTeardownTest < ActiveSupport::TestCase
end
def sentinel
- assert_equal [:foo, :foo], @called_back
+ assert_equal [:foo], @called_back
end
end
@@ -159,7 +130,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
def test_inherited_setup_callbacks
assert_equal [:reset_callback_record, :foo, :bar], self.class._setup_callbacks.map(&:raw_filter)
assert_equal [:foo, :bar], @called_back
- assert_equal [:foo, :sentinel, :foo, :bar], self.class._teardown_callbacks.map(&:raw_filter)
+ assert_equal [:foo, :sentinel, :bar], self.class._teardown_callbacks.map(&:raw_filter)
end
protected
@@ -168,7 +139,7 @@ class SubclassSetupAndTeardownTest < SetupAndTeardownTest
end
def sentinel
- assert_equal [:foo, :bar, :bar, :foo], @called_back
+ assert_equal [:foo, :bar, :bar], @called_back
end
end
@@ -182,7 +153,6 @@ class TestCaseTaggedLoggingTest < ActiveSupport::TestCase
end
def test_logs_tagged_with_current_test_case
- tagged_logger.info 'test'
- assert_equal "[#{self.class.name}] [#{__name__}] test\n", @out.string
+ assert_match "#{self.class}: #{name}\n", @out.string
end
end
diff --git a/activesupport/test/testing/constant_lookup_test.rb b/activesupport/test/testing/constant_lookup_test.rb
index c56c032cde..aca2951450 100644
--- a/activesupport/test/testing/constant_lookup_test.rb
+++ b/activesupport/test/testing/constant_lookup_test.rb
@@ -1,7 +1,8 @@
require 'abstract_unit'
+require 'dependencies_test_helpers'
class Foo; end
-class Bar < Foo;
+class Bar < Foo
def index; end
def self.index; end
end
@@ -10,6 +11,7 @@ module FooBar; end
class ConstantLookupTest < ActiveSupport::TestCase
include ActiveSupport::Testing::ConstantLookup
+ include DependenciesTestHelpers
def find_foo(name)
self.class.determine_constant_from_test_name(name) do |constant|
@@ -56,4 +58,12 @@ class ConstantLookupTest < ActiveSupport::TestCase
assert_nil find_module("DoesntExist::Nadda::Nope")
assert_nil find_module("DoesntExist::Nadda::Nope::NotHere")
end
+
+ def test_does_not_swallow_exception_on_no_method_error
+ assert_raises(NoMethodError) {
+ with_autoloading_fixtures {
+ self.class.determine_constant_from_test_name("RaisesNoMethodError")
+ }
+ }
+ end
end
diff --git a/activesupport/test/testing/performance_test.rb b/activesupport/test/testing/performance_test.rb
deleted file mode 100644
index 53073cb8db..0000000000
--- a/activesupport/test/testing/performance_test.rb
+++ /dev/null
@@ -1,59 +0,0 @@
-require 'abstract_unit'
-require 'active_support/testing/performance'
-
-
-module ActiveSupport
- module Testing
- class PerformanceTest < ActiveSupport::TestCase
- def test_amount_format
- amount_metric = ActiveSupport::Testing::Performance::Metrics[:amount].new
- assert_equal "0", amount_metric.format(0)
- assert_equal "1", amount_metric.format(1.23)
- assert_equal "40,000,000", amount_metric.format(40000000)
- end
-
- def test_time_format
- time_metric = ActiveSupport::Testing::Performance::Metrics[:time].new
- assert_equal "0 ms", time_metric.format(0)
- assert_equal "40 ms", time_metric.format(0.04)
- assert_equal "41 ms", time_metric.format(0.0415)
- assert_equal "1.23 sec", time_metric.format(1.23)
- assert_equal "40000.00 sec", time_metric.format(40000)
- assert_equal "-5000 ms", time_metric.format(-5)
- end
-
- def test_space_format
- space_metric = ActiveSupport::Testing::Performance::Metrics[:digital_information_unit].new
- assert_equal "0 Bytes", space_metric.format(0)
- assert_equal "0 Bytes", space_metric.format(0.4)
- assert_equal "1 Byte", space_metric.format(1.23)
- assert_equal "123 Bytes", space_metric.format(123)
- assert_equal "123 Bytes", space_metric.format(123.45)
- assert_equal "12 KB", space_metric.format(12345)
- assert_equal "1.2 MB", space_metric.format(1234567)
- assert_equal "9.3 GB", space_metric.format(10**10)
- assert_equal "91 TB", space_metric.format(10**14)
- assert_equal "910000 TB", space_metric.format(10**18)
- end
-
- def test_environment_format_without_rails
- metric = ActiveSupport::Testing::Performance::Metrics[:time].new
- benchmarker = ActiveSupport::Testing::Performance::Benchmarker.new(self, metric)
- assert_equal "#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL},#{RUBY_PLATFORM}", benchmarker.environment
- end
-
- def test_environment_format_with_rails
- rails, version = Module.new, Module.new
- version.const_set :STRING, "4.0.0"
- rails.const_set :VERSION, version
- Object.const_set :Rails, rails
-
- metric = ActiveSupport::Testing::Performance::Metrics[:time].new
- benchmarker = ActiveSupport::Testing::Performance::Benchmarker.new(self, metric)
- assert_equal "rails-4.0.0,#{RUBY_ENGINE}-#{RUBY_VERSION}.#{RUBY_PATCHLEVEL},#{RUBY_PLATFORM}", benchmarker.environment
- ensure
- Object.send :remove_const, :Rails
- end
- end
- end
-end \ No newline at end of file
diff --git a/activesupport/test/time_zone_test.rb b/activesupport/test/time_zone_test.rb
index bfd6863e40..84c3154e53 100644
--- a/activesupport/test/time_zone_test.rb
+++ b/activesupport/test/time_zone_test.rb
@@ -178,8 +178,8 @@ class TimeZoneTest < ActiveSupport::TestCase
def test_parse_with_old_date
zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
- twz = zone.parse('1850-12-31 19:00:00')
- assert_equal [0,0,19,31,12,1850], twz.to_a[0,6]
+ twz = zone.parse('1883-12-31 19:00:00')
+ assert_equal [0,0,19,31,12,1883], twz.to_a[0,6]
assert_equal zone, twz.time_zone
end
@@ -204,21 +204,48 @@ class TimeZoneTest < ActiveSupport::TestCase
end
def test_parse_should_not_black_out_system_timezone_dst_jump
- zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
- zone.stubs(:now).returns(zone.now)
- Time.stubs(:parse).with('2012-03-25 03:29', zone.now).
- returns(Time.local(0,29,4,25,3,2012,nil,nil,true,"+03:00"))
- twz = zone.parse('2012-03-25 03:29')
- assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0,6]
+ with_env_tz('EET') do
+ zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
+ twz = zone.parse('2012-03-25 03:29:00')
+ assert_equal [0, 29, 3, 25, 3, 2012], twz.to_a[0,6]
+ end
end
def test_parse_should_black_out_app_timezone_dst_jump
- zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
- zone.stubs(:now).returns(zone.now)
- Time.stubs(:parse).with('2012-03-11 02:29', zone.now).
- returns(Time.local(0,29,2,11,3,2012,nil,nil,false,"+02:00"))
- twz = zone.parse('2012-03-11 02:29')
- assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0,6]
+ with_env_tz('EET') do
+ zone = ActiveSupport::TimeZone['Pacific Time (US & Canada)']
+ twz = zone.parse('2012-03-11 02:29:00')
+ assert_equal [0, 29, 3, 11, 3, 2012], twz.to_a[0,6]
+ end
+ end
+
+ def test_parse_with_missing_time_components
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ zone.stubs(:now).returns zone.local(1999, 12, 31, 12, 59, 59)
+ twz = zone.parse('2012-12-01')
+ assert_equal Time.utc(2012, 12, 1), twz.time
+ end
+
+ def test_parse_with_javascript_date
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ twz = zone.parse("Mon May 28 2012 00:00:00 GMT-0700 (PDT)")
+ assert_equal Time.utc(2012, 5, 28, 7, 0, 0), twz.utc
+ end
+
+ def test_parse_doesnt_use_local_dst
+ with_env_tz 'US/Eastern' do
+ zone = ActiveSupport::TimeZone['UTC']
+ twz = zone.parse('2013-03-10 02:00:00')
+ assert_equal Time.utc(2013, 3, 10, 2, 0, 0), twz.time
+ end
+ end
+
+ def test_parse_handles_dst_jump
+ with_env_tz 'US/Eastern' do
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
+ twz = zone.parse('2013-03-10 02:00:00')
+ assert_equal Time.utc(2013, 3, 10, 3, 0, 0), twz.time
+ end
end
def test_utc_offset_lazy_loaded_from_tzinfo_when_not_passed_in_to_initialize
diff --git a/activesupport/test/transliterate_test.rb b/activesupport/test/transliterate_test.rb
index b5d8142458..e0f85f4e7c 100644
--- a/activesupport/test/transliterate_test.rb
+++ b/activesupport/test/transliterate_test.rb
@@ -3,7 +3,6 @@ require 'abstract_unit'
require 'active_support/inflector/transliterate'
class TransliterateTest < ActiveSupport::TestCase
-
def test_transliterate_should_not_change_ascii_chars
(0..127).each do |byte|
char = [byte].pack("U")
@@ -17,19 +16,20 @@ class TransliterateTest < ActiveSupport::TestCase
# some reason or other are floating in the middle of all the letters.
string = (0xC0..0x17E).to_a.reject {|c| [0xD7, 0xF7].include?(c)}.pack("U*")
string.each_char do |char|
- assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(string)
+ assert_match %r{^[a-zA-Z']*$}, ActiveSupport::Inflector.transliterate(char)
end
end
def test_transliterate_should_work_with_custom_i18n_rules_and_uncomposed_utf8
char = [117, 776].pack("U*") # "ü" as ASCII "u" plus COMBINING DIAERESIS
I18n.backend.store_translations(:de, :i18n => {:transliterate => {:rule => {"ü" => "ue"}}})
- I18n.locale = :de
+ default_locale, I18n.locale = I18n.locale, :de
assert_equal "ue", ActiveSupport::Inflector.transliterate(char)
+ ensure
+ I18n.locale = default_locale
end
def test_transliterate_should_allow_a_custom_replacement_char
assert_equal "a*b", ActiveSupport::Inflector.transliterate("aç´¢b", "*")
end
-
end
diff --git a/activesupport/test/ts_isolated.rb b/activesupport/test/ts_isolated.rb
deleted file mode 100644
index 2c217157d3..0000000000
--- a/activesupport/test/ts_isolated.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'minitest/autorun'
-require 'active_support/test_case'
-require 'rbconfig'
-require 'active_support/core_ext/kernel/reporting'
-
-class TestIsolated < ActiveSupport::TestCase
- ruby = File.join(*RbConfig::CONFIG.values_at('bindir', 'RUBY_INSTALL_NAME'))
-
- Dir["#{File.dirname(__FILE__)}/**/*_test.rb"].each do |file|
- define_method("test #{file}") do
- command = "#{ruby} -Ilib:test #{file}"
- result = silence_stderr { `#{command}` }
- assert $?.to_i.zero?, "#{command}\n#{result}"
- end
- end
-end
diff --git a/activesupport/test/xml_mini/jdom_engine_test.rb b/activesupport/test/xml_mini/jdom_engine_test.rb
index f77d78d42c..ed4de8aba2 100644
--- a/activesupport/test/xml_mini/jdom_engine_test.rb
+++ b/activesupport/test/xml_mini/jdom_engine_test.rb
@@ -3,9 +3,12 @@ if RUBY_PLATFORM =~ /java/
require 'active_support/xml_mini'
require 'active_support/core_ext/hash/conversions'
+
class JDOMEngineTest < ActiveSupport::TestCase
include ActiveSupport
+ FILES_DIR = File.dirname(__FILE__) + '/../fixtures/xml'
+
def setup
@default_backend = XmlMini.backend
XmlMini.backend = 'JDOM'
@@ -30,10 +33,41 @@ if RUBY_PLATFORM =~ /java/
assert_equal 'image/png', file.content_type
end
+ def test_not_allowed_to_expand_entities_to_files
+ attack_xml = <<-EOT
+ <!DOCTYPE member [
+ <!ENTITY a SYSTEM "file://#{FILES_DIR}/jdom_include.txt">
+ ]>
+ <member>x&a;</member>
+ EOT
+ assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ end
+
+ def test_not_allowed_to_expand_parameter_entities_to_files
+ attack_xml = <<-EOT
+ <!DOCTYPE member [
+ <!ENTITY % b SYSTEM "file://#{FILES_DIR}/jdom_entities.txt">
+ %b;
+ ]>
+ <member>x&a;</member>
+ EOT
+ assert_raise Java::OrgXmlSax::SAXParseException do
+ assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ end
+ end
+
+
+ def test_not_allowed_to_load_external_doctypes
+ attack_xml = <<-EOT
+ <!DOCTYPE member SYSTEM "file://#{FILES_DIR}/jdom_doctype.dtd">
+ <member>x&a;</member>
+ EOT
+ assert_equal 'x', Hash.from_xml(attack_xml)["member"]
+ end
+
def test_exception_thrown_on_expansion_attack
- assert_raise NativeException do
+ assert_raise Java::OrgXmlSax::SAXParseException do
attack_xml = <<-EOT
- <?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE member [
<!ENTITY a "&b;&b;&b;&b;&b;&b;&b;&b;&b;&b;">
<!ENTITY b "&c;&c;&c;&c;&c;&c;&c;&c;&c;&c;">
@@ -142,10 +176,11 @@ if RUBY_PLATFORM =~ /java/
end
private
- def assert_equal_rexml(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, XmlMini.parse(xml))
- end
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
else
diff --git a/activesupport/test/xml_mini/libxml_engine_test.rb b/activesupport/test/xml_mini/libxml_engine_test.rb
index 5debb2fd59..a8df2e1f7b 100644
--- a/activesupport/test/xml_mini/libxml_engine_test.rb
+++ b/activesupport/test/xml_mini/libxml_engine_test.rb
@@ -1,12 +1,11 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
-
begin
require 'libxml'
rescue LoadError
# Skip libxml tests
else
+require 'abstract_unit'
+require 'active_support/xml_mini'
+require 'active_support/core_ext/hash/conversions'
class LibxmlEngineTest < ActiveSupport::TestCase
include ActiveSupport
@@ -142,7 +141,7 @@ class LibxmlEngineTest < ActiveSupport::TestCase
morning
</root>
eoxml
- XmlMini.parse(io)
+ assert_equal_rexml(io)
end
def test_children_with_simple_cdata
@@ -194,10 +193,12 @@ class LibxmlEngineTest < ActiveSupport::TestCase
private
- def assert_equal_rexml(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, XmlMini.parse(xml))
- end
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
end
diff --git a/activesupport/test/xml_mini/libxmlsax_engine_test.rb b/activesupport/test/xml_mini/libxmlsax_engine_test.rb
index 94250d48ec..d6d90639e2 100644
--- a/activesupport/test/xml_mini/libxmlsax_engine_test.rb
+++ b/activesupport/test/xml_mini/libxmlsax_engine_test.rb
@@ -1,12 +1,11 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
-
begin
require 'libxml'
rescue LoadError
# Skip libxml tests
else
+require 'abstract_unit'
+require 'active_support/xml_mini'
+require 'active_support/core_ext/hash/conversions'
class LibXMLSAXEngineTest < ActiveSupport::TestCase
include ActiveSupport
@@ -142,7 +141,7 @@ class LibXMLSAXEngineTest < ActiveSupport::TestCase
morning
</root>
eoxml
- XmlMini.parse(io)
+ assert_equal_rexml(io)
end
def test_children_with_simple_cdata
@@ -185,10 +184,12 @@ class LibXMLSAXEngineTest < ActiveSupport::TestCase
end
private
- def assert_equal_rexml(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, XmlMini.parse(xml))
- end
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
end
diff --git a/activesupport/test/xml_mini/nokogiri_engine_test.rb b/activesupport/test/xml_mini/nokogiri_engine_test.rb
index 3f37c7cbb6..2e962576b5 100644
--- a/activesupport/test/xml_mini/nokogiri_engine_test.rb
+++ b/activesupport/test/xml_mini/nokogiri_engine_test.rb
@@ -1,12 +1,11 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
-
begin
require 'nokogiri'
rescue LoadError
# Skip nokogiri tests
else
+require 'abstract_unit'
+require 'active_support/xml_mini'
+require 'active_support/core_ext/hash/conversions'
class NokogiriEngineTest < ActiveSupport::TestCase
include ActiveSupport
@@ -156,7 +155,7 @@ class NokogiriEngineTest < ActiveSupport::TestCase
morning
</root>
eoxml
- XmlMini.parse(io)
+ assert_equal_rexml(io)
end
def test_children_with_simple_cdata
@@ -207,10 +206,12 @@ class NokogiriEngineTest < ActiveSupport::TestCase
end
private
- def assert_equal_rexml(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, XmlMini.parse(xml))
- end
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
end
diff --git a/activesupport/test/xml_mini/nokogirisax_engine_test.rb b/activesupport/test/xml_mini/nokogirisax_engine_test.rb
index d6ae7f12ae..4f078f31e0 100644
--- a/activesupport/test/xml_mini/nokogirisax_engine_test.rb
+++ b/activesupport/test/xml_mini/nokogirisax_engine_test.rb
@@ -1,12 +1,11 @@
-require 'abstract_unit'
-require 'active_support/xml_mini'
-require 'active_support/core_ext/hash/conversions'
-
begin
require 'nokogiri'
rescue LoadError
# Skip nokogiri tests
else
+require 'abstract_unit'
+require 'active_support/xml_mini'
+require 'active_support/core_ext/hash/conversions'
class NokogiriSAXEngineTest < ActiveSupport::TestCase
include ActiveSupport
@@ -57,9 +56,9 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase
end
end
- def test_setting_nokogiri_as_backend
- XmlMini.backend = 'Nokogiri'
- assert_equal XmlMini_Nokogiri, XmlMini.backend
+ def test_setting_nokogirisax_as_backend
+ XmlMini.backend = 'NokogiriSAX'
+ assert_equal XmlMini_NokogiriSAX, XmlMini.backend
end
def test_blank_returns_empty_hash
@@ -157,7 +156,7 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase
morning
</root>
eoxml
- XmlMini.parse(io)
+ assert_equal_rexml(io)
end
def test_children_with_simple_cdata
@@ -208,10 +207,12 @@ class NokogiriSAXEngineTest < ActiveSupport::TestCase
end
private
- def assert_equal_rexml(xml)
- hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
- assert_equal(hash, XmlMini.parse(xml))
- end
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
end
diff --git a/activesupport/test/xml_mini/rexml_engine_test.rb b/activesupport/test/xml_mini/rexml_engine_test.rb
index c4770405f2..0c1f11803c 100644
--- a/activesupport/test/xml_mini/rexml_engine_test.rb
+++ b/activesupport/test/xml_mini/rexml_engine_test.rb
@@ -24,6 +24,14 @@ class REXMLEngineTest < ActiveSupport::TestCase
morning
</root>
eoxml
- XmlMini.parse(io)
+ assert_equal_rexml(io)
end
+
+ private
+ def assert_equal_rexml(xml)
+ parsed_xml = XmlMini.parse(xml)
+ xml.rewind if xml.respond_to?(:rewind)
+ hash = XmlMini.with_backend('REXML') { XmlMini.parse(xml) }
+ assert_equal(hash, parsed_xml)
+ end
end
diff --git a/activesupport/test/xml_mini_test.rb b/activesupport/test/xml_mini_test.rb
index 504fc96493..d992028323 100644
--- a/activesupport/test/xml_mini_test.rb
+++ b/activesupport/test/xml_mini_test.rb
@@ -99,4 +99,74 @@ module XmlMiniTest
end
# TODO: test the remaining functions hidden in #to_tag.
end
+
+ class WithBackendTest < ActiveSupport::TestCase
+ module REXML end
+ module LibXML end
+ module Nokogiri end
+
+ setup do
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
+ end
+
+ test "#with_backend should switch backend and then switch back" do
+ @xml.backend = REXML
+ @xml.with_backend(LibXML) do
+ assert_equal LibXML, @xml.backend
+ @xml.with_backend(Nokogiri) do
+ assert_equal Nokogiri, @xml.backend
+ end
+ assert_equal LibXML, @xml.backend
+ end
+ assert_equal REXML, @xml.backend
+ end
+
+ test "backend switch inside #with_backend block" do
+ @xml.with_backend(LibXML) do
+ @xml.backend = REXML
+ assert_equal REXML, @xml.backend
+ end
+ assert_equal REXML, @xml.backend
+ end
+ end
+
+ class ThreadSafetyTest < ActiveSupport::TestCase
+ module REXML end
+ module LibXML end
+
+ setup do
+ @xml, @default_backend = ActiveSupport::XmlMini, ActiveSupport::XmlMini.backend
+ end
+
+ teardown do
+ ActiveSupport::XmlMini.backend = @default_backend
+ end
+
+ test "#with_backend should be thread-safe" do
+ @xml.backend = REXML
+ t = Thread.new do
+ @xml.with_backend(LibXML) { sleep 1 }
+ end
+ sleep 0.1 while t.status != "sleep"
+
+ # We should get `old_backend` here even while another
+ # thread is using `new_backend`.
+ assert_equal REXML, @xml.backend
+ end
+
+ test "nested #with_backend should be thread-safe" do
+ @xml.with_backend(REXML) do
+ t = Thread.new do
+ @xml.with_backend(LibXML) { sleep 1 }
+ end
+ sleep 0.1 while t.status != "sleep"
+
+ assert_equal REXML, @xml.backend
+ end
+ end
+ end
end
diff --git a/ci/travis.rb b/ci/travis.rb
index b03ac4fe35..7e68993332 100755
--- a/ci/travis.rb
+++ b/ci/travis.rb
@@ -20,7 +20,8 @@ class Build
'am' => 'actionmailer',
'amo' => 'activemodel',
'as' => 'activesupport',
- 'ar' => 'activerecord'
+ 'ar' => 'activerecord',
+ 'av' => 'actionview'
}
attr_reader :component, :options
@@ -109,7 +110,7 @@ end
# puts " #{`uname -a`}"
# puts " #{`ruby -v`}"
# puts " #{`mysql --version`}"
-# # puts " #{`pg_config --version`}"
+# puts " #{`pg_config --version`}"
# puts " SQLite3: #{`sqlite3 -version`}"
# `gem env`.each_line {|line| print " #{line}"}
# puts " Bundled gems:"
@@ -117,7 +118,7 @@ end
# puts " Local gems:"
# `gem list`.each_line {|line| print " #{line}"}
-failures = results.select { |key, value| value == false }
+failures = results.select { |key, value| !value }
if failures.empty?
puts
diff --git a/guides/code/getting_started/test/fixtures/.gitkeep b/guides/.document
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/test/fixtures/.gitkeep
+++ b/guides/.document
diff --git a/guides/CHANGELOG.md b/guides/CHANGELOG.md
index a1a480e911..38e407b198 100644
--- a/guides/CHANGELOG.md
+++ b/guides/CHANGELOG.md
@@ -1,5 +1,9 @@
-## Rails 4.0.0 (unreleased) ##
+* Removed repetitive th tags. Instead of them added one th tag with a colspan attribute.
-* Guides updated to reflect new test locations. *Mike Moore*
+ *Sıtkı Bağdat*
-* Guides have a responsive design. *Joe Fiorini*
+* Added the Rails maintenance policy to the guides.
+
+ *Matias Korhonen*
+
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/guides/CHANGELOG.md) for previous changes.
diff --git a/guides/Rakefile b/guides/Rakefile
index 7881a3d9b3..d6dd950d01 100644
--- a/guides/Rakefile
+++ b/guides/Rakefile
@@ -13,6 +13,12 @@ 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`"
+ end
+ unless `convert` =~ /convert/
+ abort "Please install ImageMagick`"
+ end
ENV['KINDLE'] = '1'
Rake::Task['guides:generate:html'].invoke
end
diff --git a/guides/assets/images/akshaysurve.jpg b/guides/assets/images/akshaysurve.jpg
new file mode 100644
index 0000000000..cfc3333958
--- /dev/null
+++ b/guides/assets/images/akshaysurve.jpg
Binary files differ
diff --git a/guides/assets/images/challenge.png b/guides/assets/images/challenge.png
deleted file mode 100644
index 30be3d7028..0000000000
--- a/guides/assets/images/challenge.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/customized_error_messages.png b/guides/assets/images/customized_error_messages.png
deleted file mode 100644
index fcf47b4be0..0000000000
--- a/guides/assets/images/customized_error_messages.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/edge_badge.png b/guides/assets/images/edge_badge.png
index a35dc9f8ee..a3c1843d1d 100644
--- a/guides/assets/images/edge_badge.png
+++ b/guides/assets/images/edge_badge.png
Binary files differ
diff --git a/guides/assets/images/error_messages.png b/guides/assets/images/error_messages.png
deleted file mode 100644
index 1189e486d4..0000000000
--- a/guides/assets/images/error_messages.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/feature_tile.gif b/guides/assets/images/feature_tile.gif
index 75469361db..5268ef8373 100644
--- a/guides/assets/images/feature_tile.gif
+++ b/guides/assets/images/feature_tile.gif
Binary files differ
diff --git a/guides/assets/images/footer_tile.gif b/guides/assets/images/footer_tile.gif
index bb33fc1ff0..3fe21a8275 100644
--- a/guides/assets/images/footer_tile.gif
+++ b/guides/assets/images/footer_tile.gif
Binary files differ
diff --git a/guides/assets/images/fxn.png b/guides/assets/images/fxn.png
index 9b531ee584..733d380cba 100644
--- a/guides/assets/images/fxn.png
+++ b/guides/assets/images/fxn.png
Binary files differ
diff --git a/guides/assets/images/getting_started/challenge.png b/guides/assets/images/getting_started/challenge.png
new file mode 100644
index 0000000000..4a30e49e6d
--- /dev/null
+++ b/guides/assets/images/getting_started/challenge.png
Binary files differ
diff --git a/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png b/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png
new file mode 100644
index 0000000000..6c78e52173
--- /dev/null
+++ b/guides/assets/images/getting_started/forbidden_attributes_for_new_post.png
Binary files differ
diff --git a/guides/assets/images/getting_started/new_post.png b/guides/assets/images/getting_started/new_post.png
index b573cb164c..b20b0192d4 100644
--- a/guides/assets/images/getting_started/new_post.png
+++ b/guides/assets/images/getting_started/new_post.png
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..569dd846a8
--- /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 407ea2ea06..35ee4f348f 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 d461807c5d..1cbddfa0f1 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/template_is_missing_posts_new.png b/guides/assets/images/getting_started/template_is_missing_posts_new.png
index 6860aaeca7..f03db05fb8 100644
--- a/guides/assets/images/getting_started/template_is_missing_posts_new.png
+++ b/guides/assets/images/getting_started/template_is_missing_posts_new.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_create_for_posts.png b/guides/assets/images/getting_started/unknown_action_create_for_posts.png
index 1eca14b988..8fdd4c574a 100644
--- a/guides/assets/images/getting_started/unknown_action_create_for_posts.png
+++ b/guides/assets/images/getting_started/unknown_action_create_for_posts.png
Binary files differ
diff --git a/guides/assets/images/getting_started/unknown_action_new_for_posts.png b/guides/assets/images/getting_started/unknown_action_new_for_posts.png
index fd72586573..7e72feee38 100644
--- a/guides/assets/images/getting_started/unknown_action_new_for_posts.png
+++ b/guides/assets/images/getting_started/unknown_action_new_for_posts.png
Binary files differ
diff --git a/guides/assets/images/header_tile.gif b/guides/assets/images/header_tile.gif
index e2c878d492..6b1af15eab 100644
--- a/guides/assets/images/header_tile.gif
+++ b/guides/assets/images/header_tile.gif
Binary files differ
diff --git a/guides/assets/images/icons/README b/guides/assets/images/icons/README
index f12b2a730c..09da77fc86 100644
--- a/guides/assets/images/icons/README
+++ b/guides/assets/images/icons/README
@@ -1,5 +1,5 @@
Replaced the plain DocBook XSL admonition icons with Jimmac's DocBook
icons (http://jimmac.musichall.cz/ikony.php3). I dropped transparency
-from the Jimmac icons to get round MS IE and FOP PNG incompatibilies.
+from the Jimmac icons to get round MS IE and FOP PNG incompatibilities.
Stuart Rackham
diff --git a/guides/assets/images/icons/callouts/11.png b/guides/assets/images/icons/callouts/11.png
index 9244a1ac4b..3b7b9318e7 100644
--- a/guides/assets/images/icons/callouts/11.png
+++ b/guides/assets/images/icons/callouts/11.png
Binary files differ
diff --git a/guides/assets/images/icons/callouts/12.png b/guides/assets/images/icons/callouts/12.png
index ae56459f4c..7b95925e9d 100644
--- a/guides/assets/images/icons/callouts/12.png
+++ b/guides/assets/images/icons/callouts/12.png
Binary files differ
diff --git a/guides/assets/images/icons/callouts/13.png b/guides/assets/images/icons/callouts/13.png
index 1181f9f892..4b99fe8efc 100644
--- a/guides/assets/images/icons/callouts/13.png
+++ b/guides/assets/images/icons/callouts/13.png
Binary files differ
diff --git a/guides/assets/images/icons/callouts/15.png b/guides/assets/images/icons/callouts/15.png
index 39304de94f..70e4bba615 100644
--- a/guides/assets/images/icons/callouts/15.png
+++ b/guides/assets/images/icons/callouts/15.png
Binary files differ
diff --git a/guides/assets/images/icons/caution.png b/guides/assets/images/icons/caution.png
index 031e19c776..7227b54b32 100644
--- a/guides/assets/images/icons/caution.png
+++ b/guides/assets/images/icons/caution.png
Binary files differ
diff --git a/guides/assets/images/icons/example.png b/guides/assets/images/icons/example.png
index 1b0e482059..de23c0aa87 100644
--- a/guides/assets/images/icons/example.png
+++ b/guides/assets/images/icons/example.png
Binary files differ
diff --git a/guides/assets/images/jaimeiniesta.jpg b/guides/assets/images/jaimeiniesta.jpg
deleted file mode 100644
index 445f048d92..0000000000
--- a/guides/assets/images/jaimeiniesta.jpg
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/radar.png b/guides/assets/images/radar.png
index f61e08763f..421b62b623 100644
--- a/guides/assets/images/radar.png
+++ b/guides/assets/images/radar.png
Binary files differ
diff --git a/guides/assets/images/rails4_features.png b/guides/assets/images/rails4_features.png
new file mode 100644
index 0000000000..b3bd5ef69e
--- /dev/null
+++ b/guides/assets/images/rails4_features.png
Binary files differ
diff --git a/guides/assets/images/rails_guides_kindle_cover.jpg b/guides/assets/images/rails_guides_kindle_cover.jpg
index 9eb16720a9..f068bd9a04 100644
--- a/guides/assets/images/rails_guides_kindle_cover.jpg
+++ b/guides/assets/images/rails_guides_kindle_cover.jpg
Binary files differ
diff --git a/guides/assets/images/rails_welcome.png b/guides/assets/images/rails_welcome.png
deleted file mode 100644
index 8ad2d351de..0000000000
--- a/guides/assets/images/rails_welcome.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/validation_error_messages.png b/guides/assets/images/validation_error_messages.png
deleted file mode 100644
index 30e4ca4a3d..0000000000
--- a/guides/assets/images/validation_error_messages.png
+++ /dev/null
Binary files differ
diff --git a/guides/assets/images/vijaydev.jpg b/guides/assets/images/vijaydev.jpg
index e21d3cabfc..fe5e4f1cb4 100644
--- a/guides/assets/images/vijaydev.jpg
+++ b/guides/assets/images/vijaydev.jpg
Binary files differ
diff --git a/guides/assets/javascripts/guides.js b/guides/assets/javascripts/guides.js
index 7e494fb6d8..e4d25dfb21 100644
--- a/guides/assets/javascripts/guides.js
+++ b/guides/assets/javascripts/guides.js
@@ -1,57 +1,53 @@
-function guideMenu(){
- if (document.getElementById('guides').style.display == "none") {
- document.getElementById('guides').style.display = "block";
- } else {
- document.getElementById('guides').style.display = "none";
- }
-}
-
-$.fn.selectGuide = function(guide){
+$.fn.selectGuide = function(guide) {
$("select", this).val(guide);
-}
+};
-guidesIndex = {
- bind: function(){
+var guidesIndex = {
+ bind: function() {
var currentGuidePath = window.location.pathname;
var currentGuide = currentGuidePath.substring(currentGuidePath.lastIndexOf("/")+1);
$(".guides-index-small").
on("change", "select", guidesIndex.navigate).
selectGuide(currentGuide);
- $(".more-info-button:visible").click(function(e){
+ $(document).on("click", ".more-info-button", function(e){
e.stopPropagation();
- if($(".more-info-links").is(":visible")){
+ if ($(".more-info-links").is(":visible")) {
$(".more-info-links").addClass("s-hidden").unwrap();
} else {
$(".more-info-links").wrap("<div class='more-info-container'></div>").removeClass("s-hidden");
}
- $(document).on("click", function(e){
- var $button = $(".more-info-button");
- var element;
+ });
+ $("#guidesMenu").on("click", function(e) {
+ $("#guides").toggle();
+ return false;
+ });
+ $(document).on("click", function(e){
+ e.stopPropagation();
+ var $button = $(".more-info-button");
+ var element;
- // Cross browser find the element that had the event
- if (e.target) element = e.target;
- else if (e.srcElement) element = e.srcElement;
+ // Cross browser find the element that had the event
+ if (e.target) element = e.target;
+ else if (e.srcElement) element = e.srcElement;
- // Defeat the older Safari bug:
- // http://www.quirksmode.org/js/events_properties.html
- if (element.nodeType == 3) element = element.parentNode;
+ // Defeat the older Safari bug:
+ // http://www.quirksmode.org/js/events_properties.html
+ if (element.nodeType === 3) element = element.parentNode;
- var $element = $(element);
+ var $element = $(element);
- var $container = $element.parents(".more-info-container");
+ var $container = $element.parents(".more-info-container");
- // We've captured a click outside the popup
- if($container.length == 0){
- $container = $button.next(".more-info-container");
- $container.find(".more-info-links").addClass("s-hidden").unwrap();
- $(document).off("click");
- }
- });
+ // We've captured a click outside the popup
+ if($container.length === 0){
+ $container = $button.next(".more-info-container");
+ $container.find(".more-info-links").addClass("s-hidden").unwrap();
+ }
});
},
navigate: function(e){
var $list = $(e.target);
- url = $list.val();
+ var url = $list.val();
window.location = url;
}
-}
+};
diff --git a/guides/assets/stylesheets/main.css b/guides/assets/stylesheets/main.css
index 9f5e101d1c..898f9ff05b 100644
--- a/guides/assets/stylesheets/main.css
+++ b/guides/assets/stylesheets/main.css
@@ -26,11 +26,13 @@ dl { margin: 0 0 1.5em 0; }
dl dt { font-weight: bold; }
dd { margin-left: 1.5em;}
-pre,code { margin: 1.5em 0; overflow: auto; color: #222;}
-pre,code {
- font-size: 1em;
- font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
- line-height: 1.5;
+pre, code {
+ font-size: 1em;
+ font-family: "Anonymous Pro", "Inconsolata", "Menlo", "Consolas", "Bitstream Vera Sans Mono", "Courier New", monospace;
+ line-height: 1.5;
+ margin: 1.5em 0;
+ overflow: auto;
+ color: #222;
}
pre,tt,code,.note>p {
white-space: pre-wrap; /* css-3 */
@@ -81,6 +83,10 @@ table th {
padding: 0.5em 1em;
}
+img {
+ max-width: 100%;
+}
+
/* Structure and Layout
--------------------------------------- */
@@ -92,14 +98,14 @@ body {
line-height: 1.5em;
background: #fff;
color: #999;
- }
+}
.wrapper {
text-align: left;
margin: 0 auto;
max-width: 960px;
padding: 0 1em;
- }
+}
.red-button {
display: inline-block;
@@ -123,6 +129,7 @@ body {
font-family: Helvetica, Arial, Sans-Serif;
text-decoration: none;
vertical-align: middle;
+ cursor: pointer;
}
.red-button:active {
border-top: none;
@@ -164,7 +171,6 @@ body {
.more-info:last-child:after {
content: "";
}
-
}
@media screen and (max-width: 1024px) {
@@ -218,7 +224,7 @@ body {
color: #FFF;
padding: 1.5em 0;
z-index: 99;
- }
+}
#feature {
background: #d5e9f6 url(../images/feature_tile.gif) repeat-x;
@@ -229,17 +235,17 @@ body {
#container {
color: #333;
padding: 0.5em 0 1.5em 0;
- }
+}
#mainCol {
max-width: 630px;
margin-left: 2em;
- }
+}
#subCol {
position: absolute;
z-index: 0;
- top: 0;
+ top: 21px;
right: 0;
background: #FFF;
padding: 1em 1.5em 1em 1.25em;
@@ -247,7 +253,7 @@ body {
font-size: 0.9285em;
line-height: 1.3846em;
margin-right: 1em;
- }
+}
@media screen and (max-width: 800px) {
@@ -265,7 +271,7 @@ body {
#footer {
padding: 2em 0;
background: #222 url(../images/footer_tile.gif) repeat-x;
- }
+}
#footer .wrapper {
padding-left: 1em;
max-width: 960px;
@@ -284,12 +290,11 @@ body {
a, a:link, a:visited {
color: #ee3f3f;
text-decoration: underline;
- }
+}
#mainCol a, #subCol a, #feature a {color: #980905;}
#mainCol a code, #subCol a code, #feature a code {color: #980905;}
-
/* Navigation
--------------------------------------- */
@@ -313,7 +318,6 @@ a, a:link, a:visited {
background: #980905;
position: relative;
color: white;
- cursor: pointer;
}
.guides-index .guides-index-item {
@@ -345,7 +349,6 @@ a, a:link, a:visited {
}
@media screen and (max-width: 480px) {
-
.nav {
float: none;
width: 100%;
@@ -408,7 +411,8 @@ a, a:link, a:visited {
padding: 0;
}
#guides dt {padding:0; margin: 0.5em 0 0;}
-#guides a {color: #FFF; background: none !important;}
+#guides a {color: #FFF; background: none !important; text-decoration: none;}
+#guides a:hover {text-decoration: underline;}
#guides .L, #guides .R {float: left; width: 50%; margin: 0; padding: 0;}
#guides .R {float: right;}
#guides hr {
@@ -427,14 +431,14 @@ h1 {
line-height: 1em;
margin: 0.6em 0 .2em;
font-weight: bold;
- }
+}
h2 {
font-size: 2.1428em;
line-height: 1em;
margin: 0.7em 0 .2333em;
font-weight: bold;
- }
+}
@media screen and (max-width: 480px) {
h2 {
@@ -447,7 +451,7 @@ h3 {
line-height: 1.286em;
margin: 0.875em 0 0.2916em;
font-weight: bold;
- }
+}
@media screen and (max-width: 480px) {
h3 {
@@ -460,7 +464,7 @@ h4 {
line-height: 1.2em;
margin: 1.6667em 0 .3887em;
font-weight: bold;
- }
+}
h5 {
font-size: 1em;
@@ -474,7 +478,7 @@ h6 {
line-height: 1.5em;
margin: 1em 0 .5em;
font-weight: normal;
- }
+}
.section {
padding-bottom: 0.25em;
@@ -542,13 +546,19 @@ h6 {
}
#mainCol dt, #subCol dt {
- font-size: 1em;
+ font-size: 1.2857em;
padding: 0.125em 0 0.25em 0;
margin-bottom: 0;
/*background: url(../images/book_icon.gif) no-repeat left top;
padding: 0.125em 0 0.25em 28px;*/
}
+@media screen and (max-width: 480px) {
+ #mainCol dt, #subCol dt {
+ font-size: 1em;
+ }
+}
+
#mainCol dd.work-in-progress, #subCol dd.work-in-progress {
background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top;
border: none;
@@ -568,7 +578,7 @@ h6 {
#mainCol div.warning, #subCol dd.warning {
background: #f9d9d8 url(../images/tab_red.gif) no-repeat left top;
border: none;
- padding: 1.25em 1.25em 1.25em 48px;
+ padding: 1.25em 1.25em 0.25em 48px;
margin-left: 0;
margin-top: 0.25em;
}
@@ -609,10 +619,10 @@ div.code_container {
}
#mainCol div.todo {
- background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top;
- border: none;
- padding: 1em 1em 0.25em 48px;
- margin: 0.25em 0 1.5em 0;
+ background: #fff9d8 url(../images/tab_yellow.gif) no-repeat left top;
+ border: none;
+ padding: 1em 1em 0.25em 48px;
+ margin: 0.25em 0 1.5em 0;
}
.note code, .info code, .todo code {border:none; background: none; padding: 0;}
@@ -640,11 +650,11 @@ div.code_container {
--------------------------------------- */
.clearfix:after {
- content: ".";
- display: block;
- height: 0;
- clear: both;
- visibility: hidden;
+ content: ".";
+ display: block;
+ height: 0;
+ clear: both;
+ visibility: hidden;
}
.clearfix {display: inline-block;}
@@ -655,13 +665,13 @@ div.code_container {
/* Same bottom margin for special boxes than for regular paragraphs, this way
intermediate whitespace looks uniform. */
div.code_container, div.important, div.caution, div.warning, div.note, div.info {
- margin-bottom: 1.5em;
+ margin-bottom: 1.5em;
}
/* Remove bottom margin of paragraphs in special boxes, otherwise they get a
spurious blank area below with the box background. */
div.important p, div.caution p, div.warning p, div.note p, div.info p {
- margin-bottom: 1em;
+ margin-bottom: 1em;
}
/* Edge Badge
@@ -683,19 +693,18 @@ table td, table th { padding: 9px 10px; text-align: left; }
/* Mobile */
@media only screen and (max-width: 767px) {
-
- table.responsive { margin-bottom: 0; }
-
- .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; }
- .pinned table { border-right: none; border-left: none; width: 100%; }
- .pinned table th, .pinned table td { white-space: nowrap; }
- .pinned td:last-child { border-bottom: 0; }
-
- div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; }
- div.table-wrapper div.scrollable table { margin-left: 35%; }
- div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
-
- table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; }
- table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; }
-
+ table.responsive { margin-bottom: 0; }
+
+ .pinned { position: absolute; left: 0; top: 0; background: #fff; width: 35%; overflow: hidden; overflow-x: scroll; border-right: 1px solid #ccc; border-left: 1px solid #ccc; }
+ .pinned table { border-right: none; border-left: none; width: 100%; }
+ .pinned table th, .pinned table td { white-space: nowrap; }
+ .pinned td:last-child { border-bottom: 0; }
+
+ div.table-wrapper { position: relative; margin-bottom: 20px; overflow: hidden; border-right: 1px solid #ccc; }
+ div.table-wrapper div.scrollable table { margin-left: 35%; }
+ div.table-wrapper div.scrollable { overflow: scroll; overflow-y: hidden; }
+
+ table.responsive td, table.responsive th { position: relative; white-space: nowrap; overflow: hidden; }
+ table.responsive th:first-child, table.responsive td:first-child, table.responsive td:first-child, table.responsive.pinned td { display: none; }
+
}
diff --git a/guides/assets/stylesheets/print.css b/guides/assets/stylesheets/print.css
index 628da105d4..bdc8ec948d 100644
--- a/guides/assets/stylesheets/print.css
+++ b/guides/assets/stylesheets/print.css
@@ -36,7 +36,7 @@ hr {
}
h1,h2,h3,h4,h5,h6 { font-family: "Helvetica Neue", Arial, "Lucida Grande", sans-serif; }
-code { font:.9em "Courier New", Monaco, Courier, monospace; }
+code { font:.9em "Courier New", Monaco, Courier, monospace; display:inline}
img { float:left; margin:1.5em 1.5em 1.5em 0; }
a img { border:none; }
diff --git a/guides/bug_report_templates/action_controller_gem.rb b/guides/bug_report_templates/action_controller_gem.rb
new file mode 100644
index 0000000000..693bc320b3
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_gem.rb
@@ -0,0 +1,42 @@
+# Activate the gem you are reporting the issue against.
+gem 'rails', '4.0.0'
+
+require 'rails'
+require 'action_controller/railtie'
+
+class TestApp < Rails::Application
+ config.root = File.dirname(__FILE__)
+ config.session_store :cookie_store, key: 'cookie_store_key'
+ config.secret_token = 'secret_token'
+ config.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+class BugTest < MiniTest::Unit::TestCase
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end \ No newline at end of file
diff --git a/guides/bug_report_templates/action_controller_master.rb b/guides/bug_report_templates/action_controller_master.rb
new file mode 100644
index 0000000000..5d88749118
--- /dev/null
+++ b/guides/bug_report_templates/action_controller_master.rb
@@ -0,0 +1,51 @@
+unless File.exist?('Gemfile')
+ File.write('Gemfile', <<-GEMFILE)
+ source 'https://rubygems.org'
+ gem 'rails', github: 'rails/rails'
+ GEMFILE
+
+ system 'bundle'
+end
+
+require 'bundler'
+Bundler.setup(:default)
+
+require 'rails'
+require 'action_controller/railtie'
+
+class TestApp < Rails::Application
+ config.root = File.dirname(__FILE__)
+ config.session_store :cookie_store, key: 'cookie_store_key'
+ config.secret_token = 'secret_token'
+ config.secret_key_base = 'secret_key_base'
+
+ config.logger = Logger.new($stdout)
+ Rails.logger = config.logger
+
+ routes.draw do
+ get '/' => 'test#index'
+ end
+end
+
+class TestController < ActionController::Base
+ def index
+ render text: 'Home'
+ end
+end
+
+require 'minitest/autorun'
+require 'rack/test'
+
+class BugTest < Minitest::Test
+ include Rack::Test::Methods
+
+ def test_returns_success
+ get '/'
+ assert last_response.ok?
+ end
+
+ private
+ def app
+ Rails.application
+ end
+end
diff --git a/guides/bug_report_templates/active_record_gem.rb b/guides/bug_report_templates/active_record_gem.rb
new file mode 100644
index 0000000000..a8868a1877
--- /dev/null
+++ b/guides/bug_report_templates/active_record_gem.rb
@@ -0,0 +1,37 @@
+# Activate the gem you are reporting the issue against.
+gem 'activerecord', '4.0.0'
+require 'active_record'
+require 'minitest/autorun'
+require 'logger'
+
+# This connection will do for database-independent bug reports.
+ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+ActiveRecord::Schema.define do
+ create_table :posts do |t|
+ end
+
+ create_table :comments do |t|
+ t.integer :post_id
+ end
+end
+
+class Post < ActiveRecord::Base
+ has_many :comments
+end
+
+class Comment < ActiveRecord::Base
+ belongs_to :post
+end
+
+class BugTest < MiniTest::Unit::TestCase
+ def test_association_stuff
+ post = Post.create!
+ post.comments << Comment.create!
+
+ assert_equal 1, post.comments.count
+ assert_equal 1, Comment.count
+ assert_equal post.id, Comment.first.post.id
+ end
+end
diff --git a/guides/bug_report_templates/active_record_master.rb b/guides/bug_report_templates/active_record_master.rb
new file mode 100644
index 0000000000..2435444dc9
--- /dev/null
+++ b/guides/bug_report_templates/active_record_master.rb
@@ -0,0 +1,48 @@
+unless File.exist?('Gemfile')
+ File.write('Gemfile', <<-GEMFILE)
+ source 'https://rubygems.org'
+ gem 'rails', github: 'rails/rails'
+ gem 'sqlite3'
+ GEMFILE
+
+ system 'bundle'
+end
+
+require 'bundler'
+Bundler.setup(:default)
+
+require 'active_record'
+require 'minitest/autorun'
+require 'logger'
+
+# This connection will do for database-independent bug reports.
+ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
+ActiveRecord::Base.logger = Logger.new(STDOUT)
+
+ActiveRecord::Schema.define do
+ create_table :posts do |t|
+ end
+
+ create_table :comments do |t|
+ t.integer :post_id
+ end
+end
+
+class Post < ActiveRecord::Base
+ has_many :comments
+end
+
+class Comment < ActiveRecord::Base
+ belongs_to :post
+end
+
+class BugTest < Minitest::Test
+ def test_association_stuff
+ post = Post.create!
+ post.comments << Comment.create!
+
+ assert_equal 1, post.comments.count
+ assert_equal 1, Comment.count
+ assert_equal post.id, Comment.first.post.id
+ end
+end
diff --git a/guides/code/getting_started/.gitignore b/guides/code/getting_started/.gitignore
new file mode 100644
index 0000000000..6a502e997f
--- /dev/null
+++ b/guides/code/getting_started/.gitignore
@@ -0,0 +1,16 @@
+# 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
index 670a8523b0..eef6180804 100644
--- a/guides/code/getting_started/Gemfile
+++ b/guides/code/getting_started/Gemfile
@@ -1,38 +1,43 @@
source 'https://rubygems.org'
-gem 'rails', '3.2.3'
-
-# Bundle edge Rails instead:
-# gem 'rails', :git => 'git://github.com/rails/rails.git'
+gem 'rails', '4.0.0'
+# Use sqlite3 as the database for Active Record
gem 'sqlite3'
+# Use SCSS for stylesheets
+gem 'sass-rails'
-# Gems used only for assets and not required
-# in production environments by default.
-group :assets do
- gem 'sass-rails', '~> 3.2.3'
- gem 'coffee-rails', '~> 3.2.1'
+# Use CoffeeScript for .js.coffee assets and views
+gem 'coffee-rails'
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- # gem 'therubyracer', :platform => :ruby
+# See https://github.com/sstephenson/execjs#readme for more supported runtimes
+# gem 'therubyracer', platforms: :ruby
- gem 'uglifier', '>= 1.0.3'
-end
+# Use Uglifier as compressor for JavaScript assets
+gem 'uglifier', '>= 1.0.3'
gem 'jquery-rails'
-# To use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.0.0'
+# Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
+gem 'turbolinks'
-# To use Jbuilder templates for JSON
-# gem 'jbuilder'
+group :doc do
+ # bundle exec rake doc:rails generates the API under doc/api.
+ gem 'sdoc', require: false
+end
+
+# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
+gem 'jbuilder', '~> 1.2'
+
+# To use ActiveModel has_secure_password
+# gem 'bcrypt-ruby', '~> 3.1.2'
# Use unicorn as the app server
# gem 'unicorn'
# Deploy with Capistrano
-# gem 'capistrano'
+# gem 'capistrano', group: :development
# To use debugger
# gem 'debugger'
diff --git a/guides/code/getting_started/Gemfile.lock b/guides/code/getting_started/Gemfile.lock
new file mode 100644
index 0000000000..2d5c50ef5c
--- /dev/null
+++ b/guides/code/getting_started/Gemfile.lock
@@ -0,0 +1,120 @@
+GEM
+ remote: https://rubygems.org/
+ specs:
+ actionmailer (4.0.0)
+ actionpack (= 4.0.0)
+ mail (~> 2.5.3)
+ actionpack (4.0.0)
+ activesupport (= 4.0.0)
+ builder (~> 3.1.0)
+ erubis (~> 2.7.0)
+ rack (~> 1.5.2)
+ rack-test (~> 0.6.2)
+ activemodel (4.0.0)
+ activesupport (= 4.0.0)
+ builder (~> 3.1.0)
+ activerecord (4.0.0)
+ activemodel (= 4.0.0)
+ activerecord-deprecated_finders (~> 1.0.2)
+ activesupport (= 4.0.0)
+ arel (~> 4.0.0)
+ activerecord-deprecated_finders (1.0.3)
+ activesupport (4.0.0)
+ i18n (~> 0.6, >= 0.6.4)
+ minitest (~> 4.2)
+ multi_json (~> 1.3)
+ thread_safe (~> 0.1)
+ tzinfo (~> 0.3.37)
+ arel (4.0.0)
+ atomic (1.1.10)
+ builder (3.1.4)
+ coffee-rails (4.0.0)
+ coffee-script (>= 2.2.0)
+ railties (>= 4.0.0.beta, < 5.0)
+ coffee-script (2.2.0)
+ coffee-script-source
+ execjs
+ coffee-script-source (1.6.3)
+ erubis (2.7.0)
+ execjs (1.4.0)
+ multi_json (~> 1.0)
+ hike (1.2.3)
+ i18n (0.6.4)
+ jbuilder (1.4.2)
+ activesupport (>= 3.0.0)
+ multi_json (>= 1.2.0)
+ jquery-rails (3.0.2)
+ railties (>= 3.0, < 5.0)
+ thor (>= 0.14, < 2.0)
+ json (1.8.0)
+ mail (2.5.4)
+ mime-types (~> 1.16)
+ treetop (~> 1.4.8)
+ mime-types (1.23)
+ minitest (4.7.5)
+ multi_json (1.7.7)
+ polyglot (0.3.3)
+ rack (1.5.2)
+ rack-test (0.6.2)
+ rack (>= 1.0)
+ rails (4.0.0)
+ actionmailer (= 4.0.0)
+ actionpack (= 4.0.0)
+ activerecord (= 4.0.0)
+ activesupport (= 4.0.0)
+ bundler (>= 1.3.0, < 2.0)
+ railties (= 4.0.0)
+ sprockets-rails (~> 2.0.0)
+ railties (4.0.0)
+ actionpack (= 4.0.0)
+ activesupport (= 4.0.0)
+ rake (>= 0.8.7)
+ thor (>= 0.18.1, < 2.0)
+ rake (10.1.0)
+ rdoc (3.12.2)
+ json (~> 1.4)
+ sass (3.2.9)
+ sass-rails (4.0.0)
+ railties (>= 4.0.0.beta, < 5.0)
+ sass (>= 3.1.10)
+ sprockets-rails (~> 2.0.0)
+ sdoc (0.3.20)
+ json (>= 1.1.3)
+ rdoc (~> 3.10)
+ sprockets (2.10.0)
+ hike (~> 1.2)
+ multi_json (~> 1.0)
+ rack (~> 1.0)
+ tilt (~> 1.1, != 1.3.0)
+ sprockets-rails (2.0.0)
+ actionpack (>= 3.0)
+ activesupport (>= 3.0)
+ sprockets (~> 2.8)
+ sqlite3 (1.3.7)
+ thor (0.18.1)
+ thread_safe (0.1.0)
+ atomic
+ tilt (1.4.1)
+ treetop (1.4.14)
+ polyglot
+ polyglot (>= 0.3.1)
+ turbolinks (1.2.0)
+ coffee-rails
+ tzinfo (0.3.37)
+ uglifier (2.1.1)
+ execjs (>= 0.3.0)
+ multi_json (~> 1.0, >= 1.0.2)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ coffee-rails
+ jbuilder (~> 1.2)
+ jquery-rails
+ rails (= 4.0.0)
+ sass-rails
+ sdoc
+ sqlite3
+ turbolinks
+ uglifier (>= 1.0.3)
diff --git a/guides/code/getting_started/README.rdoc b/guides/code/getting_started/README.rdoc
index b5d7b6436b..dd4e97e22e 100644
--- a/guides/code/getting_started/README.rdoc
+++ b/guides/code/getting_started/README.rdoc
@@ -1,259 +1,28 @@
-== Welcome to Rails
+== README
-Rails is a web-application framework that includes everything needed to create
-database-backed web applications according to the Model-View-Control pattern.
+This README would normally document whatever steps are necessary to get the
+application up and running.
-This pattern splits the view (also called the presentation) into "dumb"
-templates that are primarily responsible for inserting pre-built data in between
-HTML tags. The model contains the "smart" domain objects (such as Account,
-Product, Person, Post) that holds all the business logic and knows how to
-persist themselves to a database. The controller handles the incoming requests
-(such as Save New Account, Update Product, Show Post) by manipulating the model
-and directing data to the view.
+Things you may want to cover:
-In Rails, the model is handled by what's called an object-relational mapping
-layer entitled Active Record. This layer allows you to present the data from
-database rows as objects and embellish these data objects with business logic
-methods. You can read more about Active Record in
-link:files/vendor/rails/activerecord/README.html.
+* Ruby version
-The controller and view are handled by the Action Pack, which handles both
-layers by its two parts: Action View and Action Controller. These two layers
-are bundled in a single package due to their heavy interdependence. This is
-unlike the relationship between the Active Record and Action Pack that is much
-more separate. Each of these packages can be used independently outside of
-Rails. You can read more about Action Pack in
-link:files/vendor/rails/actionpack/README.html.
+* System dependencies
+* Configuration
-== Getting Started
+* Database creation
-1. At the command prompt, create a new Rails application:
- <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name)
+* Database initialization
-2. Change directory to <tt>myapp</tt> and start the web server:
- <tt>cd myapp; rails server</tt> (run with --help for options)
+* How to run the test suite
-3. Go to http://localhost:3000/ and you'll see:
- "Welcome aboard: You're riding Ruby on Rails!"
+* Services (job queues, cache servers, search engines, etc.)
-4. Follow the guidelines to start developing your application. You can find
-the following resources handy:
+* Deployment instructions
-* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
-* Ruby on Rails Tutorial Book: http://www.railstutorial.org/
+* ...
-== Debugging Rails
-
-Sometimes your application goes wrong. Fortunately there are a lot of tools that
-will help you debug it and get it back on the rails.
-
-First area to check is the application log files. Have "tail -f" commands
-running on the server.log and development.log. Rails will automatically display
-debugging and runtime information to these files. Debugging info will also be
-shown in the browser on requests from 127.0.0.1.
-
-You can also log your own messages directly into the log file from your code
-using the Ruby logger class from inside your controllers. Example:
-
- class WeblogController < ActionController::Base
- def destroy
- @weblog = Weblog.find(params[:id])
- @weblog.destroy
- logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
- end
- end
-
-The result will be a message in your log file along the lines of:
-
- Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
-
-More information on how to use the logger is at http://www.ruby-doc.org/core/
-
-Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
-several books available online as well:
-
-* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
-* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
-
-These two books will bring you up to speed on the Ruby language and also on
-programming in general.
-
-
-== Debugger
-
-Debugger support is available through the debugger command when you start your
-Mongrel or WEBrick server with --debugger. This means that you can break out of
-execution at any point in the code, investigate and change the model, and then,
-resume execution! You need to install the 'debugger' gem to run the server in debugging
-mode. Add gem 'debugger' to your Gemfile and run <tt>bundle</tt> to install it. Example:
-
- class WeblogController < ActionController::Base
- def index
- @posts = Post.all
- debugger
- end
- end
-
-So the controller will accept the action, run the first line, then present you
-with a IRB prompt in the server window. Here you can do things like:
-
- >> @posts.inspect
- => "[#<Post:0x14a6be8
- @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>,
- #<Post:0x14a6620
- @attributes={"title"=>"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
- >> @posts.first.title = "hello from a debugger"
- => "hello from a debugger"
-
-...and even better, you can examine how your runtime objects actually work:
-
- >> f = @posts.first
- => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
- >> f.
- Display all 152 possibilities? (y or n)
-
-Finally, when you're ready to resume execution, you can enter "cont".
-
-
-== Console
-
-The console is a Ruby shell, which allows you to interact with your
-application's domain model. Here you'll have all parts of the application
-configured, just like it is when the application is running. You can inspect
-domain models, change values, and save to the database. Starting the script
-without arguments will launch it in the development environment.
-
-To start the console, run <tt>rails console</tt> from the application
-directory.
-
-Options:
-
-* Passing the <tt>-s, --sandbox</tt> argument will rollback any modifications
- made to the database.
-* Passing an environment name as an argument will load the corresponding
- environment. Example: <tt>rails console production</tt>.
-
-To reload your controllers and models after launching the console run
-<tt>reload!</tt>
-
-More information about irb can be found at:
-link:http://www.rubycentral.org/pickaxe/irb.html
-
-
-== dbconsole
-
-You can go to the command line of your database directly through <tt>rails
-dbconsole</tt>. You would be connected to the database with the credentials
-defined in database.yml. Starting the script without arguments will connect you
-to the development database. Passing an argument will connect you to a different
-database, like <tt>rails dbconsole production</tt>. Currently works for MySQL,
-PostgreSQL and SQLite 3.
-
-== Description of Contents
-
-The default directory structure of a generated Ruby on Rails application:
-
- |-- app
- | |-- assets
- | |-- images
- | |-- javascripts
- | `-- stylesheets
- | |-- controllers
- | |-- helpers
- | |-- mailers
- | |-- models
- | `-- views
- | `-- layouts
- |-- config
- | |-- environments
- | |-- initializers
- | `-- locales
- |-- db
- |-- doc
- |-- lib
- | `-- tasks
- |-- log
- |-- public
- |-- script
- |-- test
- | |-- fixtures
- | |-- functional
- | |-- integration
- | |-- performance
- | `-- unit
- |-- tmp
- | |-- cache
- | |-- pids
- | |-- sessions
- | `-- sockets
- `-- vendor
- |-- assets
- `-- stylesheets
-
-app
- Holds all the code that's specific to this particular application.
-
-app/assets
- Contains subdirectories for images, stylesheets, and JavaScript files.
-
-app/controllers
- Holds controllers that should be named like weblogs_controller.rb for
- automated URL mapping. All controllers should descend from
- ApplicationController which itself descends from ActionController::Base.
-
-app/models
- Holds models that should be named like post.rb. Models descend from
- ActiveRecord::Base by default.
-
-app/views
- Holds the template files for the view that should be named like
- weblogs/index.html.erb for the WeblogsController#index action. All views use
- eRuby syntax by default.
-
-app/views/layouts
- Holds the template files for layouts to be used with views. This models the
- common header/footer method of wrapping views. In your views, define a layout
- using the <tt>layout :default</tt> and create a file named default.html.erb.
- Inside default.html.erb, call <% yield %> to render the view using this
- layout.
-
-app/helpers
- Holds view helpers that should be named like weblogs_helper.rb. These are
- generated for you automatically when using generators for controllers.
- Helpers can be used to wrap functionality for your views into methods.
-
-config
- Configuration files for the Rails environment, the routing map, the database,
- and other dependencies.
-
-db
- Contains the database schema in schema.rb. db/migrate contains all the
- sequence of Migrations for your schema.
-
-doc
- This directory is where your application documentation will be stored when
- generated using <tt>rake doc:app</tt>
-
-lib
- Application specific libraries. Basically, any kind of custom code that
- doesn't belong under controllers, models, or helpers. This directory is in
- the load path.
-
-public
- The directory available for the web server. Also contains the dispatchers and the
- default HTML files. This should be set as the DOCUMENT_ROOT of your web
- server.
-
-script
- Helper scripts for automation and generation.
-
-test
- Unit and functional tests along with fixtures. When using the rails generate
- command, template test files will be generated for you and placed in this
- directory.
-
-vendor
- External libraries that the application depends on. If the app has frozen rails,
- those gems also go here, under vendor/rails/. This directory is in the load path.
+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
index e1d1ec8615..05de8bb536 100644
--- a/guides/code/getting_started/Rakefile
+++ b/guides/code/getting_started/Rakefile
@@ -1,4 +1,3 @@
-#!/usr/bin/env rake
# 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.
diff --git a/guides/code/getting_started/app/assets/images/rails.png b/guides/code/getting_started/app/assets/images/rails.png
deleted file mode 100644
index d5edc04e65..0000000000
--- a/guides/code/getting_started/app/assets/images/rails.png
+++ /dev/null
Binary files differ
diff --git a/guides/code/getting_started/app/assets/javascripts/application.js b/guides/code/getting_started/app/assets/javascripts/application.js
index 93cdae76ca..5a4fbaa370 100644
--- a/guides/code/getting_started/app/assets/javascripts/application.js
+++ b/guides/code/getting_started/app/assets/javascripts/application.js
@@ -7,9 +7,9 @@
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
-// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
-// GO AFTER THE REQUIRES BELOW.
+// 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
new file mode 100644
index 0000000000..24f83d18bb
--- /dev/null
+++ b/guides/code/getting_started/app/assets/javascripts/comments.js.coffee
@@ -0,0 +1,3 @@
+# 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
new file mode 100644
index 0000000000..24f83d18bb
--- /dev/null
+++ b/guides/code/getting_started/app/assets/javascripts/posts.js.coffee
@@ -0,0 +1,3 @@
+# 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
new file mode 100644
index 0000000000..24f83d18bb
--- /dev/null
+++ b/guides/code/getting_started/app/assets/javascripts/welcome.js.coffee
@@ -0,0 +1,3 @@
+# 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
index 3b5cc6648e..3192ec897b 100644
--- a/guides/code/getting_started/app/assets/stylesheets/application.css
+++ b/guides/code/getting_started/app/assets/stylesheets/application.css
@@ -10,4 +10,4 @@
*
*= 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
new file mode 100644
index 0000000000..e730912783
--- /dev/null
+++ b/guides/code/getting_started/app/assets/stylesheets/comments.css.scss
@@ -0,0 +1,3 @@
+// 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
new file mode 100644
index 0000000000..1a7e15390c
--- /dev/null
+++ b/guides/code/getting_started/app/assets/stylesheets/posts.css.scss
@@ -0,0 +1,3 @@
+// 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
new file mode 100644
index 0000000000..77ce11a740
--- /dev/null
+++ b/guides/code/getting_started/app/assets/stylesheets/welcome.css.scss
@@ -0,0 +1,3 @@
+// 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
index e8065d9505..d83690e1b9 100644
--- a/guides/code/getting_started/app/controllers/application_controller.rb
+++ b/guides/code/getting_started/app/controllers/application_controller.rb
@@ -1,3 +1,5 @@
class ApplicationController < ActionController::Base
- protect_from_forgery
+ # 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
index cf3d1be42e..b2d9bcdf7f 100644
--- a/guides/code/getting_started/app/controllers/comments_controller.rb
+++ b/guides/code/getting_started/app/controllers/comments_controller.rb
@@ -1,9 +1,10 @@
class CommentsController < ApplicationController
- http_basic_authenticate_with :name => "dhh", :password => "secret", :only => :destroy
+
+ http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @comment = @post.comments.create(comment_params)
redirect_to post_path(@post)
end
@@ -14,4 +15,9 @@ class CommentsController < ApplicationController
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/test/functional/.gitkeep b/guides/code/getting_started/app/controllers/concerns/.keep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/test/functional/.gitkeep
+++ b/guides/code/getting_started/app/controllers/concerns/.keep
diff --git a/guides/code/getting_started/app/controllers/posts_controller.rb b/guides/code/getting_started/app/controllers/posts_controller.rb
index a8ac9aba5a..02689ad67b 100644
--- a/guides/code/getting_started/app/controllers/posts_controller.rb
+++ b/guides/code/getting_started/app/controllers/posts_controller.rb
@@ -1,6 +1,6 @@
class PostsController < ApplicationController
- http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show]
+ http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
@posts = Post.all
@@ -10,31 +10,31 @@ class PostsController < ApplicationController
@post = Post.find(params[:id])
end
- def new
- @post = Post.new
+ def edit
+ @post = Post.find(params[:id])
end
- def create
- @post = Post.new(params[:post])
+ def update
+ @post = Post.find(params[:id])
- if @post.save
- redirect_to :action => :show, :id => @post.id
+ if @post.update(post_params)
+ redirect_to action: :show, id: @post.id
else
- render 'new'
+ render 'edit'
end
end
- def edit
- @post = Post.find(params[:id])
+ def new
+ @post = Post.new
end
- def update
- @post = Post.find(params[:id])
+ def create
+ @post = Post.new(post_params)
- if @post.update_attributes(params[:post])
- redirect_to :action => :show, :id => @post.id
+ if @post.save
+ redirect_to action: :show, id: @post.id
else
- render 'edit'
+ render 'new'
end
end
@@ -42,6 +42,12 @@ class PostsController < ApplicationController
@post = Post.find(params[:id])
@post.destroy
- redirect_to :action => :index
+ redirect_to action: :index
end
+
+ private
+
+ def post_params
+ params.require(:post).permit(:title, :text)
+ end
end
diff --git a/guides/code/getting_started/test/integration/.gitkeep b/guides/code/getting_started/app/mailers/.keep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/test/integration/.gitkeep
+++ b/guides/code/getting_started/app/mailers/.keep
diff --git a/guides/code/getting_started/test/unit/.gitkeep b/guides/code/getting_started/app/models/.keep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/test/unit/.gitkeep
+++ b/guides/code/getting_started/app/models/.keep
diff --git a/guides/code/getting_started/vendor/plugins/.gitkeep b/guides/code/getting_started/app/models/concerns/.keep
index e69de29bb2..e69de29bb2 100644
--- a/guides/code/getting_started/vendor/plugins/.gitkeep
+++ b/guides/code/getting_started/app/models/concerns/.keep
diff --git a/guides/code/getting_started/app/models/post.rb b/guides/code/getting_started/app/models/post.rb
index 21387340b0..64e0d721fd 100644
--- a/guides/code/getting_started/app/models/post.rb
+++ b/guides/code/getting_started/app/models/post.rb
@@ -1,6 +1,7 @@
class Post < ActiveRecord::Base
- validates :title, :presence => true,
- :length => { :minimum => 5 }
-
- has_many :comments, :dependent => :destroy
+ 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
index 3d2bc1590e..593493339e 100644
--- a/guides/code/getting_started/app/views/comments/_comment.html.erb
+++ b/guides/code/getting_started/app/views/comments/_comment.html.erb
@@ -2,7 +2,7 @@
<strong>Commenter:</strong>
<%= comment.commenter %>
</p>
-
+
<p>
<strong>Comment:</strong>
<%= comment.body %>
@@ -10,6 +10,6 @@
<p>
<%= link_to 'Destroy Comment', [comment.post, comment],
- :method => :delete,
- :data => { :confirm => 'Are you sure?' } %>
+ method: :delete,
+ data: { confirm: 'Are you sure?' } %>
</p>
diff --git a/guides/code/getting_started/app/views/layouts/application.html.erb b/guides/code/getting_started/app/views/layouts/application.html.erb
index 6578a41da2..95368c37a3 100644
--- a/guides/code/getting_started/app/views/layouts/application.html.erb
+++ b/guides/code/getting_started/app/views/layouts/application.html.erb
@@ -2,8 +2,8 @@
<html>
<head>
<title>Blog</title>
- <%= stylesheet_link_tag "application", :media => "all" %>
- <%= javascript_include_tag "application" %>
+ <%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+ <%= javascript_include_tag "application", "data-turbolinks-track" => true %>
<%= csrf_meta_tags %>
</head>
<body>
diff --git a/guides/code/getting_started/app/views/posts/_form.html.erb b/guides/code/getting_started/app/views/posts/_form.html.erb
index f22139938c..f2f83585e1 100644
--- a/guides/code/getting_started/app/views/posts/_form.html.erb
+++ b/guides/code/getting_started/app/views/posts/_form.html.erb
@@ -1,21 +1,22 @@
<%= form_for @post do |f| %>
<% if @post.errors.any? %>
- <div id="errorExplanation">
- <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>
+ <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.label :title %><br>
<%= f.text_field :title %>
</p>
<p>
- <%= f.label :text %><br />
+ <%= f.label :text %><br>
<%= f.text_area :text %>
</p>
@@ -23,3 +24,4 @@
<%= 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
index 911a48569d..393e7430d0 100644
--- a/guides/code/getting_started/app/views/posts/edit.html.erb
+++ b/guides/code/getting_started/app/views/posts/edit.html.erb
@@ -1,5 +1,5 @@
-<h1>Editing post</h1>
-
+<h1>Edit post</h1>
+
<%= render 'form' %>
-
-<%= link_to 'Back', :action => :index %>
+
+<%= 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
index 9a0e90eadc..7369f0396f 100644
--- a/guides/code/getting_started/app/views/posts/index.html.erb
+++ b/guides/code/getting_started/app/views/posts/index.html.erb
@@ -1,7 +1,4 @@
-<h1>Listing posts</h1>
-
-<%= link_to 'New post', :action => :new %>
-
+<h1>Listing Posts</h1>
<table>
<tr>
<th>Title</th>
@@ -10,14 +7,15 @@
<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><%= link_to 'Edit', :action => :edit, :id => post.id %>
- <td><%= link_to 'Destroy', { :action => :destroy, :id => post.id }, :method => :delete, :data => { :confirm => 'Are you sure?' } %>
+ <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
index ce9523a721..efa81038ec 100644
--- a/guides/code/getting_started/app/views/posts/new.html.erb
+++ b/guides/code/getting_started/app/views/posts/new.html.erb
@@ -1,5 +1,5 @@
<h1>New post</h1>
-
+
<%= render 'form' %>
-
-<%= link_to 'Back', :action => :index %>
+
+<%= 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
index 65809033ed..e99e9edbb3 100644
--- a/guides/code/getting_started/app/views/posts/show.html.erb
+++ b/guides/code/getting_started/app/views/posts/show.html.erb
@@ -2,7 +2,7 @@
<strong>Title:</strong>
<%= @post.title %>
</p>
-
+
<p>
<strong>Text:</strong>
<%= @post.text %>
@@ -10,9 +10,9 @@
<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
index e04680ea7e..56be8dd3cc 100644
--- a/guides/code/getting_started/app/views/welcome/index.html.erb
+++ b/guides/code/getting_started/app/views/welcome/index.html.erb
@@ -1,2 +1,4 @@
<h1>Hello, Rails!</h1>
-<%= link_to "My Blog", :controller => "posts" %>
+
+<%= 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
new file mode 100755
index 0000000000..45cf37fba4
--- /dev/null
+++ b/guides/code/getting_started/bin/bundle
@@ -0,0 +1,4 @@
+#!/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
new file mode 100755
index 0000000000..728cd85aa5
--- /dev/null
+++ b/guides/code/getting_started/bin/rails
@@ -0,0 +1,4 @@
+#!/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
new file mode 100755
index 0000000000..17240489f6
--- /dev/null
+++ b/guides/code/getting_started/bin/rake
@@ -0,0 +1,4 @@
+#!/usr/bin/env ruby
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/guides/code/getting_started/config/application.rb b/guides/code/getting_started/config/application.rb
index d2cd5c028b..3d7604b659 100644
--- a/guides/code/getting_started/config/application.rb
+++ b/guides/code/getting_started/config/application.rb
@@ -2,12 +2,9 @@ require File.expand_path('../boot', __FILE__)
require 'rails/all'
-if defined?(Bundler)
- # If you precompile assets before deploying to production, use this line
- Bundler.require(*Rails.groups(:assets => %w(development test)))
- # If you want your assets lazily compiled in production, use this line
- # Bundler.require(:default, :assets, Rails.env)
-end
+# 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
@@ -17,39 +14,5 @@ module Blog
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
-
- # Activate observers that should always be running.
- # config.active_record.observers = :cacher, :garbage_collector, :forum_observer
-
- # 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.
- # config.time_zone = 'Central Time (US & Canada)'
-
- # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
- # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
- # config.i18n.default_locale = :de
-
- # Configure the default encoding used in templates for Ruby 1.9.
- config.encoding = "utf-8"
-
- # Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters += [:password]
-
- # Use SQL instead of Active Record's schema dumper when creating the database.
- # This is necessary if your schema can't be completely dumped by the schema dumper,
- # like if you have constraints or database-specific column types.
- # config.active_record.schema_format = :sql
-
- # Enforce whitelist mode for mass assignment.
- # This will create an empty whitelist of attributes available for mass-assignment for all models
- # in your app. As such, your models will need to explicitly whitelist or blacklist accessible
- # parameters by using an attr_accessible or attr_protected declaration.
- # config.active_record.whitelist_attributes = true
-
- # Enable the asset pipeline.
- config.assets.enabled = true
-
- # Version of your assets, change this if you want to expire all your assets.
- config.assets.version = '1.0'
end
end
diff --git a/guides/code/getting_started/config/boot.rb b/guides/code/getting_started/config/boot.rb
index 3596736667..5e5f0c1fac 100644
--- a/guides/code/getting_started/config/boot.rb
+++ b/guides/code/getting_started/config/boot.rb
@@ -1,4 +1,4 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/guides/code/getting_started/config/environment.rb b/guides/code/getting_started/config/environment.rb
index 8f728b7ce7..e7e341c960 100644
--- a/guides/code/getting_started/config/environment.rb
+++ b/guides/code/getting_started/config/environment.rb
@@ -1,5 +1,5 @@
-# Load the rails application
+# Load the Rails application.
require File.expand_path('../application', __FILE__)
-# Initialize the rails application
+# 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
index cec2b20c0b..7e5692b08b 100644
--- a/guides/code/getting_started/config/environments/development.rb
+++ b/guides/code/getting_started/config/environments/development.rb
@@ -2,10 +2,13 @@ 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
+ # 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
@@ -19,16 +22,9 @@ Blog::Application.configure do
# Only use best-standards-support built into browsers.
config.action_dispatch.best_standards_support = :builtin
- # Raise exception on mass assignment protection for ActiveRecord models.
- config.active_record.mass_assignment_sanitizer = :strict
-
- # Log the query plan for queries taking more than this (works
- # with SQLite, MySQL, and PostgreSQL).
- config.active_record.auto_explain_threshold_in_seconds = 0.5
-
- # Do not compress assets.
- config.assets.compress = false
+ # Raise an error on page load if there are pending migrations.
+ config.active_record.migration_error = :page_load
- # Expands the lines which load the assets.
+ # 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
index ecc35b030b..368a735122 100644
--- a/guides/code/getting_started/config/environments/production.rb
+++ b/guides/code/getting_started/config/environments/production.rb
@@ -4,24 +4,36 @@ Blog::Application.configure do
# Code is not reloaded between requests.
config.cache_classes = true
+ # Eager load code on boot. This eager loads most of Rails and
+ # your application in memory, allowing both thread web servers
+ # 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.compress = true
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
- # Don't fallback to assets pipeline if a precompiled asset is missed.
+ # 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
- # Defaults to nil
- # config.assets.manifest = YOUR_PATH
+ # 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
@@ -30,8 +42,8 @@ Blog::Application.configure do
# Force all access to the app over SSL, use Strict-Transport-Security, and use secure cookies.
# config.force_ssl = true
- # See everything in the log (default is :info).
- # config.log_level = :debug
+ # 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 ]
@@ -45,15 +57,14 @@ Blog::Application.configure do
# 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 are already added).
+ # 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 )
- # Disable delivery errors, bad email addresses will be ignored.
+ # 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 threaded mode.
- # config.threadsafe!
-
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found).
config.i18n.fallbacks = true
@@ -61,7 +72,9 @@ Blog::Application.configure do
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
- # Log the query plan for queries taking more than this (works
- # with SQLite, MySQL, and PostgreSQL).
- # config.active_record.auto_explain_threshold_in_seconds = 0.5
+ # 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
index f2bc932fb3..00adaa5015 100644
--- a/guides/code/getting_started/config/environments/test.rb
+++ b/guides/code/getting_started/config/environments/test.rb
@@ -2,11 +2,16 @@ 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
+ # 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!
+ # 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"
@@ -26,9 +31,6 @@ Blog::Application.configure do
# ActionMailer::Base.deliveries array.
config.action_mailer.delivery_method = :test
- # Raise exception on mass assignment protection for Active Record models.
- config.active_record.mass_assignment_sanitizer = :strict
-
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
end
diff --git a/guides/code/getting_started/config/initializers/filter_parameter_logging.rb b/guides/code/getting_started/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/guides/code/getting_started/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# 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
index 5d8d9be237..ac033bf9dc 100644
--- a/guides/code/getting_started/config/initializers/inflections.rb
+++ b/guides/code/getting_started/config/initializers/inflections.rb
@@ -1,15 +1,16 @@
# Be sure to restart your server when you modify this file.
-# Add new inflection rules using the following format
-# (all these examples are active by default):
-# ActiveSupport::Inflector.inflections do |inflect|
+# 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 do |inflect|
+# 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
new file mode 100644
index 0000000000..d89dac7c6a
--- /dev/null
+++ b/guides/code/getting_started/config/initializers/locale.rb
@@ -0,0 +1,9 @@
+# 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/secret_token.rb b/guides/code/getting_started/config/initializers/secret_token.rb
index f36ebdda18..aaf57731be 100644
--- a/guides/code/getting_started/config/initializers/secret_token.rb
+++ b/guides/code/getting_started/config/initializers/secret_token.rb
@@ -2,8 +2,11 @@
# 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.
-# Make sure your secret key is kept private
+# 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_token = '685a9bf865b728c6549a191c90851c1b5ec41ecb60b9e94ad79dd3f824749798aa7b5e94431901960bee57809db0947b481570f7f13376b7ca190fa28099c459'
+Blog::Application.config.secret_key_base = 'e8aab50cec8a06a75694111a4cbaf6e22fc288ccbc6b268683aae7273043c69b15ca07d10c92a788dd6077a54762cbfcc55f19c3459f7531221b3169f8171a53'
diff --git a/guides/code/getting_started/config/initializers/wrap_parameters.rb b/guides/code/getting_started/config/initializers/wrap_parameters.rb
index 999df20181..33725e95fd 100644
--- a/guides/code/getting_started/config/initializers/wrap_parameters.rb
+++ b/guides/code/getting_started/config/initializers/wrap_parameters.rb
@@ -1,14 +1,14 @@
# 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]
+ wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
end
-# Disable root element in JSON by default.
-ActiveSupport.on_load(:active_record) do
- self.include_root_in_json = false
-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
index 179c14ca52..0653957166 100644
--- a/guides/code/getting_started/config/locales/en.yml
+++ b/guides/code/getting_started/config/locales/en.yml
@@ -1,5 +1,23 @@
-# Sample localization file for English. Add more files in this directory for other locales.
-# See https://github.com/svenfuchs/rails-i18n/tree/master/rails%2Flocale for starting points.
+# 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
index 04a6bd374e..0155b613a3 100644
--- a/guides/code/getting_started/config/routes.rb
+++ b/guides/code/getting_started/config/routes.rb
@@ -1,63 +1,7 @@
Blog::Application.routes.draw do
-
resources :posts do
resources :comments
end
- # The priority is based upon order of creation:
- # first created -> highest priority.
-
- # Sample of regular route:
- # match 'products/:id' => 'catalog#view'
- # Keep in mind you can assign values other than :controller and :action
-
- # Sample of named route:
- # match 'products/:id/purchase' => 'catalog#purchase', :as => :purchase
- # This route can be invoked with purchase_url(:id => product.id)
-
- # Sample resource route (maps HTTP verbs to controller actions automatically):
- # resources :products
-
- # Sample resource route with options:
- # resources :products do
- # member do
- # get 'short'
- # post 'toggle'
- # end
- #
- # collection do
- # get 'sold'
- # end
- # end
-
- # Sample resource route with sub-resources:
- # resources :products do
- # resources :comments, :sales
- # resource :seller
- # end
-
- # Sample resource route with more complex sub-resources
- # resources :products do
- # resources :comments
- # resources :sales do
- # get 'recent', :on => :collection
- # end
- # end
-
- # Sample resource route within a namespace:
- # namespace :admin do
- # # Directs /admin/products/* to Admin::ProductsController
- # # (app/controllers/admin/products_controller.rb)
- # resources :products
- # end
-
- # You can have the root of your site routed with "root"
- # just remember to delete public/index.html.
- root :to => "welcome#index"
-
- # See how all your routes lay out with "rake routes"
-
- # This is a legacy wild controller route that's not recommended for RESTful applications.
- # Note: This route will make all actions in every controller accessible via GET requests.
- # match ':controller(/:action(/:id))(.:format)'
+ root "welcome#index"
end
diff --git a/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb b/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb
deleted file mode 100644
index adda8078c1..0000000000
--- a/guides/code/getting_started/db/migrate/20110901012815_create_comments.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-class CreateComments < ActiveRecord::Migration
- def change
- create_table :comments do |t|
- t.string :commenter
- t.text :body
- t.references :post
-
- t.timestamps
- end
- add_index :comments, :post_id
- end
-end
diff --git a/guides/code/getting_started/db/migrate/20120420083127_create_posts.rb b/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb
index 602bef31ab..602bef31ab 100644
--- a/guides/code/getting_started/db/migrate/20120420083127_create_posts.rb
+++ b/guides/code/getting_started/db/migrate/20130122042648_create_posts.rb
diff --git a/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb
new file mode 100644
index 0000000000..3e51f9c0f7
--- /dev/null
+++ b/guides/code/getting_started/db/migrate/20130122045842_create_comments.rb
@@ -0,0 +1,11 @@
+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
index cfb56ca9b9..101fe712a1 100644
--- a/guides/code/getting_started/db/schema.rb
+++ b/guides/code/getting_started/db/schema.rb
@@ -9,34 +9,25 @@
# 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 to check this file into your version control system.
+# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(:version => 20120420083127) do
+ActiveRecord::Schema.define(version: 20130122045842) do
- create_table "comments", :force => true do |t|
+ create_table "comments", force: true do |t|
t.string "commenter"
t.text "body"
t.integer "post_id"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at"
+ t.datetime "updated_at"
end
- add_index "comments", ["post_id"], :name => "index_comments_on_post_id"
+ add_index "comments", ["post_id"], name: "index_comments_on_post_id"
- create_table "posts", :force => true do |t|
+ create_table "posts", force: true do |t|
t.string "title"
t.text "text"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
+ t.datetime "created_at"
+ t.datetime "updated_at"
end
- create_table "tags", :force => true do |t|
- t.string "name"
- t.integer "post_id"
- t.datetime "created_at", :null => false
- t.datetime "updated_at", :null => false
- end
-
- add_index "tags", ["post_id"], :name => "index_tags_on_post_id"
-
end
diff --git a/guides/code/getting_started/doc/README_FOR_APP b/guides/code/getting_started/doc/README_FOR_APP
deleted file mode 100644
index fe41f5cc24..0000000000
--- a/guides/code/getting_started/doc/README_FOR_APP
+++ /dev/null
@@ -1,2 +0,0 @@
-Use this README file to introduce your application and point to useful places in the API for learning more.
-Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
diff --git a/railties/lib/rails/generators/rails/app/templates/app/mailers/.empty_directory b/guides/code/getting_started/lib/assets/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/mailers/.empty_directory
+++ b/guides/code/getting_started/lib/assets/.keep
diff --git a/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory b/guides/code/getting_started/lib/tasks/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/models/.empty_directory
+++ b/guides/code/getting_started/lib/tasks/.keep
diff --git a/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory b/guides/code/getting_started/log/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/stylesheets/.empty_directory
+++ b/guides/code/getting_started/log/.keep
diff --git a/guides/code/getting_started/public/404.html b/guides/code/getting_started/public/404.html
index 9a48320a5f..3d287b135d 100644
--- a/guides/code/getting_started/public/404.html
+++ b/guides/code/getting_started/public/404.html
@@ -2,17 +2,48 @@
<html>
<head>
<title>The page you were looking for doesn't exist (404)</title>
- <style type="text/css">
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ <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;
+ }
+
+ 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-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>
@@ -22,5 +53,6 @@
<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
index 83660ab187..3b946bf4a4 100644
--- a/guides/code/getting_started/public/422.html
+++ b/guides/code/getting_started/public/422.html
@@ -2,17 +2,48 @@
<html>
<head>
<title>The change you wanted was rejected (422)</title>
- <style type="text/css">
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ <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;
+ }
+
+ 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-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>
@@ -22,5 +53,6 @@
<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
index f3648a0dbc..ccc4ad5656 100644
--- a/guides/code/getting_started/public/500.html
+++ b/guides/code/getting_started/public/500.html
@@ -2,17 +2,48 @@
<html>
<head>
<title>We're sorry, but something went wrong (500)</title>
- <style type="text/css">
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ <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;
+ }
+
+ 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-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>
@@ -21,5 +52,6 @@
<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/script/rails b/guides/code/getting_started/script/rails
deleted file mode 100755
index f8da2cffd4..0000000000
--- a/guides/code/getting_started/script/rails
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env ruby
-# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
-
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-require File.expand_path('../../config/boot', __FILE__)
-require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory b/guides/code/getting_started/test/controllers/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/fixtures/.empty_directory
+++ b/guides/code/getting_started/test/controllers/.keep
diff --git a/guides/code/getting_started/test/functional/comments_controller_test.rb b/guides/code/getting_started/test/controllers/comments_controller_test.rb
index 2ec71b4ec5..2ec71b4ec5 100644
--- a/guides/code/getting_started/test/functional/comments_controller_test.rb
+++ b/guides/code/getting_started/test/controllers/comments_controller_test.rb
diff --git a/guides/code/getting_started/test/controllers/posts_controller_test.rb b/guides/code/getting_started/test/controllers/posts_controller_test.rb
new file mode 100644
index 0000000000..7a6ee4f1db
--- /dev/null
+++ b/guides/code/getting_started/test/controllers/posts_controller_test.rb
@@ -0,0 +1,7 @@
+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
new file mode 100644
index 0000000000..dff8e9d2c5
--- /dev/null
+++ b/guides/code/getting_started/test/controllers/welcome_controller_test.rb
@@ -0,0 +1,9 @@
+require 'test_helper'
+
+class WelcomeControllerTest < ActionController::TestCase
+ test "should get index" do
+ get :index
+ assert_response :success
+ end
+
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory b/guides/code/getting_started/test/fixtures/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/functional/.empty_directory
+++ b/guides/code/getting_started/test/fixtures/.keep
diff --git a/guides/code/getting_started/test/fixtures/comments.yml b/guides/code/getting_started/test/fixtures/comments.yml
index d33da386bf..9e409d8a61 100644
--- a/guides/code/getting_started/test/fixtures/comments.yml
+++ b/guides/code/getting_started/test/fixtures/comments.yml
@@ -1,11 +1,11 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
commenter: MyString
body: MyText
- post:
+ post_id:
two:
commenter: MyString
body: MyText
- post:
+ post_id:
diff --git a/guides/code/getting_started/test/fixtures/posts.yml b/guides/code/getting_started/test/fixtures/posts.yml
index e1edfd385e..46b01c3bb4 100644
--- a/guides/code/getting_started/test/fixtures/posts.yml
+++ b/guides/code/getting_started/test/fixtures/posts.yml
@@ -1,4 +1,4 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/Fixtures.html
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
one:
title: MyString
diff --git a/guides/code/getting_started/test/functional/posts_controller_test.rb b/guides/code/getting_started/test/functional/posts_controller_test.rb
deleted file mode 100644
index b8f7b07820..0000000000
--- a/guides/code/getting_started/test/functional/posts_controller_test.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'test_helper'
-
-class PostsControllerTest < ActionController::TestCase
- setup do
- @post = posts(:one)
- end
-
- test "should get index" do
- get :index
- assert_response :success
- assert_not_nil assigns(:posts)
- end
-
- test "should get new" do
- get :new
- assert_response :success
- end
-
- test "should create post" do
- assert_difference('Post.count') do
- post :create, post: @post.attributes
- end
-
- assert_redirected_to post_path(assigns(:post))
- end
-
- test "should show post" do
- get :show, id: @post.to_param
- assert_response :success
- end
-
- test "should get edit" do
- get :edit, id: @post.to_param
- assert_response :success
- end
-
- test "should update post" do
- put :update, id: @post.to_param, post: @post.attributes
- assert_redirected_to post_path(assigns(:post))
- end
-
- test "should destroy post" do
- assert_difference('Post.count', -1) do
- delete :destroy, id: @post.to_param
- end
-
- assert_redirected_to posts_path
- end
-end
diff --git a/guides/code/getting_started/test/functional/welcome_controller_test.rb b/guides/code/getting_started/test/functional/welcome_controller_test.rb
deleted file mode 100644
index e4d5abae11..0000000000
--- a/guides/code/getting_started/test/functional/welcome_controller_test.rb
+++ /dev/null
@@ -1,8 +0,0 @@
-require 'test_helper'
-
-class WelcomeControllerTest < ActionController::TestCase
- test "should get index" do
- get :index
- assert_response :success
- end
-end
diff --git a/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory b/guides/code/getting_started/test/helpers/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/integration/.empty_directory
+++ b/guides/code/getting_started/test/helpers/.keep
diff --git a/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb b/guides/code/getting_started/test/helpers/comments_helper_test.rb
index 2518c16bd5..2518c16bd5 100644
--- a/guides/code/getting_started/test/unit/helpers/comments_helper_test.rb
+++ b/guides/code/getting_started/test/helpers/comments_helper_test.rb
diff --git a/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb b/guides/code/getting_started/test/helpers/posts_helper_test.rb
index 48549c2ea1..48549c2ea1 100644
--- a/guides/code/getting_started/test/unit/helpers/posts_helper_test.rb
+++ b/guides/code/getting_started/test/helpers/posts_helper_test.rb
diff --git a/guides/code/getting_started/test/helpers/welcome_helper_test.rb b/guides/code/getting_started/test/helpers/welcome_helper_test.rb
new file mode 100644
index 0000000000..d6ded5995f
--- /dev/null
+++ b/guides/code/getting_started/test/helpers/welcome_helper_test.rb
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class WelcomeHelperTest < ActionView::TestCase
+end
diff --git a/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory b/guides/code/getting_started/test/integration/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/unit/.empty_directory
+++ b/guides/code/getting_started/test/integration/.keep
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory b/guides/code/getting_started/test/mailers/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/mailers/.empty_directory
+++ b/guides/code/getting_started/test/mailers/.keep
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory b/guides/code/getting_started/test/models/.keep
index e69de29bb2..e69de29bb2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/models/.empty_directory
+++ b/guides/code/getting_started/test/models/.keep
diff --git a/guides/code/getting_started/test/unit/comment_test.rb b/guides/code/getting_started/test/models/comment_test.rb
index b6d6131a96..b6d6131a96 100644
--- a/guides/code/getting_started/test/unit/comment_test.rb
+++ b/guides/code/getting_started/test/models/comment_test.rb
diff --git a/guides/code/getting_started/test/unit/post_test.rb b/guides/code/getting_started/test/models/post_test.rb
index 6d9d463a71..6d9d463a71 100644
--- a/guides/code/getting_started/test/unit/post_test.rb
+++ b/guides/code/getting_started/test/models/post_test.rb
diff --git a/guides/code/getting_started/test/performance/browsing_test.rb b/guides/code/getting_started/test/performance/browsing_test.rb
deleted file mode 100644
index 2a849b7f2b..0000000000
--- a/guides/code/getting_started/test/performance/browsing_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory],
- # :output => 'tmp/performance', :formats => [:flat] }
-
- def test_homepage
- get '/'
- end
-end
diff --git a/guides/code/getting_started/test/test_helper.rb b/guides/code/getting_started/test/test_helper.rb
index 3daca18a71..f91a4375dc 100644
--- a/guides/code/getting_started/test/test_helper.rb
+++ b/guides/code/getting_started/test/test_helper.rb
@@ -3,6 +3,8 @@ 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
diff --git a/guides/code/getting_started/test/unit/helpers/home_helper_test.rb b/guides/code/getting_started/test/unit/helpers/home_helper_test.rb
deleted file mode 100644
index 4740a18dac..0000000000
--- a/guides/code/getting_started/test/unit/helpers/home_helper_test.rb
+++ /dev/null
@@ -1,4 +0,0 @@
-require 'test_helper'
-
-class HomeHelperTest < ActionView::TestCase
-end
diff --git a/guides/code/getting_started/test/unit/tag_test.rb b/guides/code/getting_started/test/unit/tag_test.rb
deleted file mode 100644
index b8498a117c..0000000000
--- a/guides/code/getting_started/test/unit/tag_test.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-require 'test_helper'
-
-class TagTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
diff --git a/guides/code/getting_started/vendor/assets/javascripts/.keep b/guides/code/getting_started/vendor/assets/javascripts/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/guides/code/getting_started/vendor/assets/javascripts/.keep
diff --git a/guides/code/getting_started/vendor/assets/stylesheets/.keep b/guides/code/getting_started/vendor/assets/stylesheets/.keep
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/guides/code/getting_started/vendor/assets/stylesheets/.keep
diff --git a/guides/rails_guides.rb b/guides/rails_guides.rb
index ab890f202c..9d488a8a15 100644
--- a/guides/rails_guides.rb
+++ b/guides/rails_guides.rb
@@ -5,7 +5,7 @@ $:.unshift pwd
def bundler?
# Note that rake sets the cwd to the one that contains the Rakefile
# being executed.
- File.exists?('Gemfile')
+ File.exist?('Gemfile')
end
begin
@@ -22,7 +22,7 @@ end
begin
require 'redcarpet'
-rescue Gem::LoadError
+rescue LoadError
# This can happen if doc:guides is executed in an application.
$stderr.puts('Generating guides requires Redcarpet 2.1.1+.')
$stderr.puts(<<ERROR) if bundler?
@@ -39,6 +39,25 @@ ERROR
exit 1
end
+begin
+ require 'nokogiri'
+rescue LoadError
+ # This can happen if doc:guides is executed in an application.
+ $stderr.puts('Generating guides requires Nokogiri.')
+ $stderr.puts(<<ERROR) if bundler?
+Please add
+
+ gem 'nokogiri'
+
+to the Gemfile, run
+
+ bundle install
+
+and try again.
+ERROR
+ exit 1
+end
+
require 'rails_guides/markdown'
require "rails_guides/generator"
RailsGuides::Generator.new.generate
diff --git a/guides/rails_guides/generator.rb b/guides/rails_guides/generator.rb
index 3b124ef236..aa900454c8 100644
--- a/guides/rails_guides/generator.rb
+++ b/guides/rails_guides/generator.rb
@@ -84,7 +84,7 @@ module RailsGuides
@warnings = ENV['WARNINGS'] == '1'
@all = ENV['ALL'] == '1'
@kindle = ENV['KINDLE'] == '1'
- @version = ENV['RAILS_VERSION'] || `git rev-parse --short HEAD`.chomp
+ @version = ENV['RAILS_VERSION'] || 'local'
@lang = ENV['GUIDES_LANGUAGE']
end
@@ -112,11 +112,9 @@ module RailsGuides
end
def generate_mobi
- opf = "#{output_dir}/rails_guides.opf"
+ require 'rails_guides/kindle'
out = "#{output_dir}/kindlegen.out"
-
- system "kindlegen #{opf} -o #{mobi} > #{out} 2>&1"
- puts "Guides compiled as Kindle book to #{mobi}"
+ Kindle.generate(output_dir, mobi, out)
puts "(kindlegen log at #{out})."
end
@@ -186,7 +184,7 @@ module RailsGuides
def generate?(source_file, output_file)
fin = File.join(source_dir, source_file)
fout = output_path_for(output_file)
- all || !File.exists?(fout) || File.mtime(fout) < File.mtime(fin)
+ all || !File.exist?(fout) || File.mtime(fout) < File.mtime(fin)
end
def generate_guide(guide, output_file)
diff --git a/guides/rails_guides/helpers.rb b/guides/rails_guides/helpers.rb
index a288d0f0f4..760b196abd 100644
--- a/guides/rails_guides/helpers.rb
+++ b/guides/rails_guides/helpers.rb
@@ -17,7 +17,7 @@ module RailsGuides
end
def documents_flat
- documents_by_section.map {|section| section['documents']}.flatten
+ documents_by_section.flat_map {|section| section['documents']}
end
def finished_documents(documents)
diff --git a/guides/rails_guides/kindle.rb b/guides/rails_guides/kindle.rb
new file mode 100644
index 0000000000..09eecd5634
--- /dev/null
+++ b/guides/rails_guides/kindle.rb
@@ -0,0 +1,119 @@
+#!/usr/bin/env ruby
+
+unless `which kindlerb`
+ abort "Please gem install kindlerb"
+end
+
+require 'nokogiri'
+require 'fileutils'
+require 'yaml'
+require 'date'
+
+module Kindle
+ extend self
+
+ def generate(output_dir, mobi_outfile, logfile)
+ output_dir = File.absolute_path(output_dir)
+ Dir.chdir output_dir do
+ puts "=> Using output dir: #{output_dir}"
+ puts "=> Arranging html pages in document order"
+ toc = File.read("toc.ncx")
+ doc = Nokogiri::XML(toc).xpath("//ncx:content", 'ncx' => "http://www.daisy.org/z3986/2005/ncx/")
+ html_pages = doc.select {|c| c[:src]}.map {|c| c[:src]}.uniq
+
+ generate_front_matter(html_pages)
+
+ generate_sections(html_pages)
+
+ generate_document_metadata(mobi_outfile)
+
+ puts "Creating MOBI document with kindlegen. This make take a while."
+ cmd = "kindlerb . > #{File.absolute_path logfile} 2>&1"
+ puts cmd
+ system(cmd)
+ puts "MOBI document generated at #{File.expand_path(mobi_outfile, output_dir)}"
+ end
+ end
+
+ def generate_front_matter(html_pages)
+ frontmatter = []
+ html_pages.delete_if {|x|
+ if x =~ /(toc|welcome|credits|copyright).html/
+ frontmatter << x unless x =~ /toc/
+ true
+ end
+ }
+ html = frontmatter.map {|x|
+ Nokogiri::HTML(File.open(x)).at("body").inner_html
+ }.join("\n")
+
+ fdoc = Nokogiri::HTML(html)
+ fdoc.search("h3").each do |h3|
+ h3.name = 'h4'
+ end
+ fdoc.search("h2").each do |h2|
+ h2.name = 'h3'
+ h2['id'] = h2.inner_text.gsub(/\s/, '-')
+ end
+ add_head_section fdoc, "Front Matter"
+ File.open("frontmatter.html",'w') {|f| f.puts fdoc.to_html}
+ html_pages.unshift "frontmatter.html"
+ end
+
+ def generate_sections(html_pages)
+ FileUtils::rm_rf("sections/")
+ html_pages.each_with_index do |page, section_idx|
+ FileUtils::mkdir_p("sections/%03d" % section_idx)
+ doc = Nokogiri::HTML(File.open(page))
+ title = doc.at("title").inner_text.gsub("Ruby on Rails Guides: ", '')
+ title = page.capitalize.gsub('.html', '') if title.strip == ''
+ File.open("sections/%03d/_section.txt" % section_idx, 'w') {|f| f.puts title}
+ doc.xpath("//h3[@id]").each_with_index do |h3,item_idx|
+ subsection = h3.inner_text
+ content = h3.xpath("./following-sibling::*").take_while {|x| x.name != "h3"}.map {|x| x.to_html}
+ item = Nokogiri::HTML(h3.to_html + content.join("\n"))
+ item_path = "sections/%03d/%03d.html" % [section_idx, item_idx]
+ add_head_section(item, subsection)
+ item.search("img").each do |img|
+ img['src'] = "#{Dir.pwd}/#{img['src']}"
+ end
+ item.xpath("//li/p").each {|p| p.swap(p.children); p.remove}
+ File.open(item_path, 'w') {|f| f.puts item.to_html}
+ end
+ end
+ end
+
+ def generate_document_metadata(mobi_outfile)
+ puts "=> Generating _document.yml"
+ x = Nokogiri::XML(File.open("rails_guides.opf")).remove_namespaces!
+ cover_jpg = "#{Dir.pwd}/images/rails_guides_kindle_cover.jpg"
+ cover_gif = cover_jpg.sub(/jpg$/, 'gif')
+ puts `convert #{cover_jpg} #{cover_gif}`
+ document = {
+ 'doc_uuid' => x.at("package")['unique-identifier'],
+ 'title' => x.at("title").inner_text.gsub(/\(.*$/, " v2"),
+ 'publisher' => x.at("publisher").inner_text,
+ 'author' => x.at("creator").inner_text,
+ 'subject' => x.at("subject").inner_text,
+ 'date' => x.at("date").inner_text,
+ 'cover' => cover_gif,
+ 'masthead' => nil,
+ 'mobi_outfile' => mobi_outfile
+ }
+ puts document.to_yaml
+ File.open("_document.yml", 'w'){|f| f.puts document.to_yaml}
+ end
+
+ def add_head_section(doc, title)
+ head = Nokogiri::XML::Node.new "head", doc
+ title_node = Nokogiri::XML::Node.new "title", doc
+ title_node.content = title
+ title_node.parent = head
+ css = Nokogiri::XML::Node.new "link", doc
+ css['rel'] = 'stylesheet'
+ css['type'] = 'text/css'
+ css['href'] = "#{Dir.pwd}/stylesheets/kindle.css"
+ css.parent = head
+ doc.at("body").before head
+ end
+end
diff --git a/guides/rails_guides/markdown.rb b/guides/rails_guides/markdown.rb
index 650489e6cb..547c6d2c15 100644
--- a/guides/rails_guides/markdown.rb
+++ b/guides/rails_guides/markdown.rb
@@ -1,3 +1,5 @@
+# encoding: utf-8
+
require 'redcarpet'
require 'nokogiri'
require 'rails_guides/markdown/renderer'
@@ -129,7 +131,7 @@ module RailsGuides
def generate_title
if heading = Nokogiri::HTML(@header).at(:h2)
- @title = "Ruby on Rails Guides: #{heading.text}".html_safe
+ @title = "#{heading.text} — Ruby on Rails Guides".html_safe
else
@title = "Ruby on Rails Guides"
end
diff --git a/guides/rails_guides/markdown/renderer.rb b/guides/rails_guides/markdown/renderer.rb
index 2f36af1fb3..2eb7ca17a3 100644
--- a/guides/rails_guides/markdown/renderer.rb
+++ b/guides/rails_guides/markdown/renderer.rb
@@ -9,7 +9,7 @@ module RailsGuides
<<-HTML
<div class="code_container">
<pre class="brush: #{brush_for(language)}; gutter: false; toolbar: false">
-#{ERB::Util.h(code).strip}
+#{ERB::Util.h(code)}
</pre>
</div>
HTML
@@ -65,7 +65,7 @@ HTML
# if a bulleted list follows the first item is not rendered
# as a list item, but as a paragraph starting with a plain
# asterisk.
- body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do |m|
+ body.gsub(/^(TIP|IMPORTANT|CAUTION|WARNING|NOTE|INFO|TODO)[.:](.*?)(\n(?=\n)|\Z)/m) do
css_class = case $1
when 'CAUTION', 'IMPORTANT'
'warning'
diff --git a/guides/source/2_2_release_notes.md b/guides/source/2_2_release_notes.md
index cef82f3784..7db4cf07e7 100644
--- a/guides/source/2_2_release_notes.md
+++ b/guides/source/2_2_release_notes.md
@@ -31,20 +31,20 @@ Documentation
The internal documentation of Rails, in the form of code comments, has been improved in numerous places. In addition, the [Ruby on Rails Guides](http://guides.rubyonrails.org/) project is the definitive source for information on major Rails components. In its first official release, the Guides page includes:
-* [Getting Started with Rails](http://guides.rubyonrails.org/getting_started.html)
-* [Rails Database Migrations](http://guides.rubyonrails.org/migrations.html)
-* [Active Record Associations](http://guides.rubyonrails.org/association_basics.html)
-* [Active Record Query Interface](http://guides.rubyonrails.org/active_record_querying.html)
-* [Layouts and Rendering in Rails](http://guides.rubyonrails.org/layouts_and_rendering.html)
-* [Action View Form Helpers](http://guides.rubyonrails.org/form_helpers.html)
-* [Rails Routing from the Outside In](http://guides.rubyonrails.org/routing.html)
-* [Action Controller Overview](http://guides.rubyonrails.org/action_controller_overview.html)
-* [Rails Caching](http://guides.rubyonrails.org/caching_with_rails.html)
-* [A Guide to Testing Rails Applications](http://guides.rubyonrails.org/testing.html)
-* [Securing Rails Applications](http://guides.rubyonrails.org/security.html)
-* [Debugging Rails Applications](http://guides.rubyonrails.org/debugging_rails_applications.html)
-* [Performance Testing Rails Applications](http://guides.rubyonrails.org/performance_testing.html)
-* [The Basics of Creating Rails Plugins](http://guides.rubyonrails.org/plugins.html)
+* [Getting Started with Rails](getting_started.html)
+* [Rails Database Migrations](migrations.html)
+* [Active Record Associations](association_basics.html)
+* [Active Record Query Interface](active_record_querying.html)
+* [Layouts and Rendering in Rails](layouts_and_rendering.html)
+* [Action View Form Helpers](form_helpers.html)
+* [Rails Routing from the Outside In](routing.html)
+* [Action Controller Overview](action_controller_overview.html)
+* [Rails Caching](caching_with_rails.html)
+* [A Guide to Testing Rails Applications](testing.html)
+* [Securing Rails Applications](security.html)
+* [Debugging Rails Applications](debugging_rails_applications.html)
+* [Performance Testing Rails Applications](performance_testing.html)
+* [The Basics of Creating Rails Plugins](plugins.html)
All told, the Guides provide tens of thousands of words of guidance for beginning and intermediate Rails developers.
@@ -200,7 +200,7 @@ Active Record association proxies now respect the scope of methods on the proxie
* More information:
* [Rails 2.2 Change: Private Methods on Association Proxies are Private](http://afreshcup.com/2008/10/24/rails-22-change-private-methods-on-association-proxies-are-private/)
-### Other ActiveRecord Changes
+### Other Active Record Changes
* `rake db:migrate:redo` now accepts an optional VERSION to target that specific migration to redo
* Set `config.active_record.timestamped_migrations = false` to have migrations with numeric prefix instead of UTC timestamp.
@@ -236,7 +236,7 @@ This will enable recognition of (among others) these routes:
* Lead Contributor: [S. Brent Faulkner](http://www.unwwwired.net/)
* More information:
- * [Rails Routing from the Outside In](http://guides.rubyonrails.org/routing.html#nested-resources)
+ * [Rails Routing from the Outside In](routing.html#nested-resources)
* [What's New in Edge Rails: Shallow Routes](http://ryandaigle.com/articles/2008/9/7/what-s-new-in-edge-rails-shallow-routes)
### Method Arrays for Member or Collection Routes
diff --git a/guides/source/2_3_release_notes.md b/guides/source/2_3_release_notes.md
index 7aef566e40..c2002fb8fa 100644
--- a/guides/source/2_3_release_notes.md
+++ b/guides/source/2_3_release_notes.md
@@ -40,7 +40,7 @@ Here's a summary of the rack-related changes:
* `ActiveRecord::QueryCache` middleware is automatically inserted onto the middleware stack if `ActiveRecord` has been loaded. This middleware sets up and flushes the per-request Active Record query cache.
* The Rails router and controller classes follow the Rack spec. You can call a controller directly with `SomeController.call(env)`. The router stores the routing parameters in `rack.routing_args`.
* `ActionController::Request` inherits from `Rack::Request`.
-* Instead of `config.action_controller.session = { :session_key => 'foo', ...` use `config.action_controller.session = { :key => 'foo', ...`.
+* Instead of `config.action_controller.session = { :session_key => 'foo', ...` use `config.action_controller.session = { :key => 'foo', ...`.
* Using the `ParamsParser` middleware preprocesses any XML, JSON, or YAML requests so they can be read normally with any `Rack::Request` object after it.
### Renewed Support for Rails Engines
@@ -134,7 +134,7 @@ Rails 2.3 will introduce the notion of _default scopes_ similar to named scopes,
### Batch Processing
-You can now process large numbers of records from an ActiveRecord model with less pressure on memory by using `find_in_batches`:
+You can now process large numbers of records from an Active Record model with less pressure on memory by using `find_in_batches`:
```ruby
Customer.find_in_batches(:conditions => {:active => true}) do |customer_group|
@@ -173,8 +173,8 @@ before_save :update_credit_rating, :if => :active,
Rails now has a `:having` option on find (as well as on `has_many` and `has_and_belongs_to_many` associations) for filtering records in grouped finds. As those with heavy SQL backgrounds know, this allows filtering based on grouped results:
```ruby
-developers = Developer.find(:all, :group => "salary",
- :having => "sum(salary) > 10000", :select => "salary")
+developers = Developer.find(:all, :group => "salary",
+ :having => "sum(salary) > 10000", :select => "salary")
```
* Lead Contributor: [Emilio Tagua](http://github.com/miloops)
@@ -237,7 +237,7 @@ If you're one of the people who has always been bothered by the special-case nam
### HTTP Digest Authentication Support
-Rails now has built-in support for HTTP digest authentication. To use it, you call `authenticate_or_request_with_http_digest` with a block that returns the user’s password (which is then hashed and compared against the transmitted credentials):
+Rails now has built-in support for HTTP digest authentication. To use it, you call `authenticate_or_request_with_http_digest` with a block that returns the user's password (which is then hashed and compared against the transmitted credentials):
```ruby
class PostsController < ApplicationController
@@ -451,11 +451,11 @@ select(:post, :category, Post::CATEGORIES, :disabled => 'private')
returns
```html
-<select name=“post[category]“>
+<select name="post[category]">
<option>story</option>
<option>joke</option>
<option>poem</option>
-<option disabled=“disabled“>private</option>
+<option disabled="disabled">private</option>
</select>
```
@@ -504,7 +504,7 @@ A lot of folks have adopted the notion of using try() to attempt operations on o
### Swappable Parsers for XMLmini
-The support for XML parsing in ActiveSupport has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed:
+The support for XML parsing in Active Support has been made more flexible by allowing you to swap in different parsers. By default, it uses the standard REXML implementation, but you can easily specify the faster LibXML or Nokogiri implementations for your own applications, provided you have the appropriate gems installed:
```ruby
XmlMini.backend = 'LibXML'
@@ -606,7 +606,7 @@ A few pieces of older code are deprecated in this release:
* If you're one of the (fairly rare) Rails developers who deploys in a fashion that depends on the inspector, reaper, and spawner scripts, you'll need to know that those scripts are no longer included in core Rails. If you need them, you'll be able to pick up copies via the [irs_process_scripts](http://github.com/rails/irs_process_scripts/tree) plugin.
* `render_component` goes from "deprecated" to "nonexistent" in Rails 2.3. If you still need it, you can install the [render_component plugin](http://github.com/rails/render_component/tree/master.)
* Support for Rails components has been removed.
-* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There’s a new request_profiler plugin that you can install to get the exact same functionality back.
+* If you were one of the people who got used to running `script/performance/request` to look at performance based on integration tests, you need to learn a new trick: that script has been removed from core Rails now. There's a new request_profiler plugin that you can install to get the exact same functionality back.
* `ActionController::Base#session_enabled?` is deprecated because sessions are lazy-loaded now.
* The `:digest` and `:secret` options to `protect_from_forgery` are deprecated and have no effect.
* Some integration test helpers have been removed. `response.headers["Status"]` and `headers["Status"]` will no longer return anything. Rack does not allow "Status" in its return headers. However you can still use the `status` and `status_message` helpers. `response.headers["cookie"]` and `headers["cookie"]` will no longer return any CGI cookies. You can inspect `headers["Set-Cookie"]` to see the raw cookie header or use the `cookies` helper to get a hash of the cookies sent to the client.
diff --git a/guides/source/3_0_release_notes.md b/guides/source/3_0_release_notes.md
index 388ba3fa30..cf9d694de7 100644
--- a/guides/source/3_0_release_notes.md
+++ b/guides/source/3_0_release_notes.md
@@ -73,13 +73,11 @@ You can see an example of how that works at [Rails Upgrade is now an Official Pl
Aside from Rails Upgrade tool, if you need more help, there are people on IRC and [rubyonrails-talk](http://groups.google.com/group/rubyonrails-talk) that are probably doing the same thing, possibly hitting the same issues. Be sure to blog your own experiences when upgrading so others can benefit from your knowledge!
-More information - [The Path to Rails 3: Approaching the upgrade](http://omgbloglol.com/post/353978923/the-path-to-rails-3-approaching-the-upgrade)
-
Creating a Rails 3.0 application
--------------------------------
```bash
-# You should have the 'rails' rubygem installed
+# You should have the 'rails' RubyGem installed
$ rails new myapp
$ cd myapp
```
@@ -342,7 +340,7 @@ Helpers that do something else, like `cache` or `content_for`, are not affected
* Helpers now output HTML 5 by default.
* Form label helper now pulls values from I18n with a single value, so `f.label :name` will pull the `:name` translation.
* I18n select label on should now be :en.helpers.select instead of :en.support.select.
-* You no longer need to place a minus sign at the end of a ruby interpolation inside an ERb template to remove the trailing carriage return in the HTML output.
+* You no longer need to place a minus sign at the end of a Ruby interpolation inside an ERB template to remove the trailing carriage return in the HTML output.
* Added `grouped_collection_select` helper to Action View.
* `content_for?` has been added allowing you to check for the existence of content in a view before rendering.
* passing `:value => nil` to form helpers will set the field's `value` attribute to nil as opposed to using the default value
@@ -475,7 +473,7 @@ As well as the following deprecations:
* `named_scope` in an Active Record class is deprecated and has been renamed to just `scope`.
* In `scope` methods, you should move to using the relation methods, instead of a `:conditions => {}` finder method, for example `scope :since, lambda {|time| where("created_at > ?", time) }`.
* `save(false)` is deprecated, in favor of `save(:validate => false)`.
-* I18n error messages for ActiveRecord should be changed from :en.activerecord.errors.template to `:en.errors.template`.
+* I18n error messages for Active Record should be changed from :en.activerecord.errors.template to `:en.errors.template`.
* `model.errors.on` is deprecated in favor of `model.errors[]`
* validates_presence_of => validates... :presence => true
* `ActiveRecord::Base.colorize_logging` and `config.active_record.colorize_logging` are deprecated in favor of `Rails::LogSubscriber.colorize_logging` or `config.colorize_logging`
@@ -580,7 +578,7 @@ Action Mailer has been given a new API with TMail being replaced out with the ne
* All mailers are now in `app/mailers` by default.
* Can now send email using new API with three methods: `attachments`, `headers` and `mail`.
-* ActionMailer now has native support for inline attachments using the `attachments.inline` method.
+* Action Mailer now has native support for inline attachments using the `attachments.inline` method.
* Action Mailer emailing methods now return `Mail::Message` objects, which can then be sent the `deliver` message to send itself.
* All delivery methods are now abstracted out to the Mail gem.
* The mail delivery method can accept a hash of all valid mail header fields with their value pair.
@@ -611,4 +609,3 @@ 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.)
-
diff --git a/guides/source/3_1_release_notes.md b/guides/source/3_1_release_notes.md
index 1d79ea5b5a..5c99892e39 100644
--- a/guides/source/3_1_release_notes.md
+++ b/guides/source/3_1_release_notes.md
@@ -129,11 +129,15 @@ config.static_cache_control = "public, max-age=3600"
end
```
+#### Remove :cache and :concat options in asset helpers references in views
+
+* With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views.
+
Creating a Rails 3.1 application
--------------------------------
```bash
-# You should have the 'rails' rubygem installed
+# You should have the 'rails' RubyGem installed
$ rails new myapp
$ cd myapp
```
diff --git a/guides/source/3_2_release_notes.md b/guides/source/3_2_release_notes.md
index 68a47be14f..babdc5050e 100644
--- a/guides/source/3_2_release_notes.md
+++ b/guides/source/3_2_release_notes.md
@@ -21,7 +21,7 @@ If you're upgrading an existing application, it's a great idea to have good test
Rails 3.2 requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible. Rails 3.2 is also compatible with Ruby 1.9.2.
-TIP: Note that Ruby 1.8.7 p248 and p249 have marshaling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.
+TIP: Note that Ruby 1.8.7 p248 and p249 have marshalling bugs that crash Rails. Ruby Enterprise Edition has these fixed since the release of 1.8.7-2010.02. On the 1.9 front, Ruby 1.9.1 is not usable because it outright segfaults, so if you want to use 1.9.x, jump on to 1.9.2 or 1.9.3 for smooth sailing.
### What to update in your apps
@@ -67,7 +67,7 @@ Creating a Rails 3.2 application
--------------------------------
```bash
-# You should have the 'rails' rubygem installed
+# You should have the 'rails' RubyGem installed
$ rails new myapp
$ cd myapp
```
@@ -101,7 +101,7 @@ Rails 3.2 comes with a development mode that's noticeably faster. Inspired by [A
### Automatic Query Explains
-Rails 3.2 comes with a nice feature that explains queries generated by ARel by defining an `explain` method in `ActiveRecord::Relation`. For example, you can run something like `puts Person.active.limit(5).explain` and the query ARel produces is explained. This allows to check for the proper indexes and further optimizations.
+Rails 3.2 comes with a nice feature that explains queries generated by Arel by defining an `explain` method in `ActiveRecord::Relation`. For example, you can run something like `puts Person.active.limit(5).explain` and the query Arel produces is explained. This allows to check for the proper indexes and further optimizations.
Queries that take more than half a second to run are *automatically* explained in the development mode. This threshold, of course, can be changed.
@@ -137,7 +137,7 @@ Railties
* Update `Rails::Rack::Logger` middleware to apply any tags set in `config.log_tags` to `ActiveSupport::TaggedLogging`. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications.
-* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time 'rails new' runs in the `.railsrc` configuration file in your home directory.
+* Default options to `rails new` can be set in `~/.railsrc`. You can specify extra command-line arguments to be used every time `rails new` runs in the `.railsrc` configuration file in your home directory.
* Add an alias `d` for `destroy`. This works for engines too.
@@ -185,11 +185,11 @@ Action Pack
end
```
- Rails will use 'layouts/single_car' when a request comes in :show action, and use 'layouts/application' (or 'layouts/cars', if exists) when a request comes in for any other actions.
+ Rails will use `layouts/single_car` when a request comes in `:show` action, and use `layouts/application` (or `layouts/cars`, if exists) when a request comes in for any other actions.
-* form\_for is changed to use "#{action}\_#{as}" as the css class and id if `:as` option is provided. Earlier versions used "#{as}\_#{action}".
+* `form\_for` is changed to use `#{action}\_#{as}` as the css class and id if `:as` option is provided. Earlier versions used `#{as}\_#{action}`.
-* `ActionController::ParamsWrapper` on ActiveRecord 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`.
+* `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`.
* Log "Filter chain halted as CALLBACKNAME rendered or redirected" every time a before callback halts.
@@ -219,7 +219,7 @@ Action Pack
* MIME type entries for PDF, ZIP and other formats were added.
-* Allow fresh_when/stale? to take a record instead of an options hash.
+* Allow `fresh_when/stale?` to take a record instead of an options hash.
* Changed log level of warning for missing CSRF token from `:debug` to `:warn`.
@@ -227,7 +227,7 @@ Action Pack
#### Deprecations
-* Deprecated implied layout lookup in controllers whose parent had a explicit layout set:
+* Deprecated implied layout lookup in controllers whose parent had an explicit layout set:
```ruby
class ApplicationController
@@ -240,11 +240,11 @@ Action Pack
In the example above, Posts controller will no longer automatically look up for a posts layout. If you need this functionality you could either remove `layout "application"` from `ApplicationController` or explicitly set it to `nil` in `PostsController`.
-* Deprecated `ActionController::UnknownAction` in favour of `AbstractController::ActionNotFound`.
+* Deprecated `ActionController::UnknownAction` in favor of `AbstractController::ActionNotFound`.
-* Deprecated `ActionController::DoubleRenderError` in favour of `AbstractController::DoubleRenderError`.
+* Deprecated `ActionController::DoubleRenderError` in favor of `AbstractController::DoubleRenderError`.
-* Deprecated `method_missing` in favour of `action_missing` for missing actions.
+* Deprecated `method_missing` in favor of `action_missing` for missing actions.
* Deprecated `ActionController#rescue_action`, `ActionController#initialize_template_class` and `ActionController#assign_shortcuts`.
@@ -254,7 +254,7 @@ Action Pack
* Added `ActionDispatch::RequestId` middleware that'll make a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method. This makes it easy to trace requests from end-to-end in the stack and to identify individual requests in mixed logs like Syslog.
-* The `ShowExceptions` middleware now accepts a exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
+* The `ShowExceptions` middleware now accepts an exceptions application that is responsible to render an exception when the application fails. The application is invoked with a copy of the exception in `env["action_dispatch.exception"]` and with the `PATH_INFO` rewritten to the status code.
* Allow rescue responses to be configured through a railtie as in `config.action_dispatch.rescue_responses`.
diff --git a/guides/source/4_0_release_notes.md b/guides/source/4_0_release_notes.md
index c909ef1496..c4ca1e921f 100644
--- a/guides/source/4_0_release_notes.md
+++ b/guides/source/4_0_release_notes.md
@@ -1,47 +1,28 @@
Ruby on Rails 4.0 Release Notes
===============================
-Highlights in Rails 4.0: (WIP)
+Highlights in Rails 4.0:
-* Ruby 1.9.3 only
+* Ruby 2.0 preferred; 1.9.3+ required
* Strong Parameters
-* Queue API
-* Caching Improvements
+* Turbolinks
+* Russian Doll Caching
-These release notes cover the major changes, but do not include each bug-fix and changes. If you want to see everything, check out the [list of commits](https://github.com/rails/rails/commits/master) in the main Rails repository on GitHub.
+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.0
----------------------
-TODO. This is a WIP guide.
+If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. A list of things to watch out for when upgrading is available in the [Upgrading to Rails](upgrading_ruby_on_rails.html#upgrading-from-rails-3-2-to-rails-4-0) guide.
-If you're upgrading an existing application, it's a great idea to have good test coverage before going in. You should also first upgrade to Rails 3.2 in case you haven't and make sure your application still runs as expected before attempting an update to Rails 4.0. Then take heed of the following changes:
-
-### Rails 4.0 requires at least Ruby 1.9.3
-
-Rails 4.0 requires Ruby 1.9.3 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible.
-
-### What to update in your apps
-
-* Update your Gemfile to depend on
- * `rails = 4.0.0`
- * `sass-rails ~> 3.2.3`
- * `coffee-rails ~> 3.2.1`
- * `uglifier >= 1.0.3`
-
-TODO: Update the versions above.
-
-* Rails 4.0 removes `vendor/plugins` completely. You have to replace these plugins by extracting them as gems and adding them in your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
-
-TODO: Configuration changes in environment files
Creating a Rails 4.0 application
--------------------------------
```
- You should have the 'rails' rubygem installed
+ You should have the 'rails' RubyGem installed
$ rails new myapp
$ cd myapp
```
@@ -69,816 +50,225 @@ $ ruby /path/to/rails/railties/bin/rails new myapp --dev
Major Features
--------------
+[![Rails 4.0](images/rails4_features.png)](http://guides.rubyonrails.org/images/rails4_features.png)
+
+### Upgrade
+
+ * **Ruby 1.9.3** ([commit](https://github.com/rails/rails/commit/a0380e808d3dbd2462df17f5d3b7fcd8bd812496)) - Ruby 2.0 preferred; 1.9.3+ required
+ * **[New deprecation policy](http://www.youtube.com/watch?v=z6YgD6tVPQs)** - Deprecated features are warnings in Rails 4.0 and will be removed in Rails 4.1.
+ * **ActionPack page and action caching** ([commit](https://github.com/rails/rails/commit/b0a7068564f0c95e7ef28fc39d0335ed17d93e90)) - Page and action caching are extracted to a separate gem. Page and action caching requires too much manual intervention (manually expiring caches when the underlying model objects are updated). Instead, use Russian doll caching.
+ * **ActiveRecord observers** ([commit](https://github.com/rails/rails/commit/ccecab3ba950a288b61a516bf9b6962e384aae0b)) - Observers are extracted to a separate gem. Observers are only needed for page and action caching, and can lead to spaghetti code.
+ * **ActiveRecord session store** ([commit](https://github.com/rails/rails/commit/0ffe19056c8e8b2f9ae9d487b896cad2ce9387ad)) - The ActiveRecord session store is extracted to a separate gem. Storing sessions in SQL is costly. Instead, use cookie sessions, memcache sessions, or a custom session store.
+ * **ActiveModel mass assignment protection** ([commit](https://github.com/rails/rails/commit/f8c9a4d3e88181cee644f91e1342bfe896ca64c6)) - Rails 3 mass assignment protection is deprecated. Instead, use strong parameters.
+ * **ActiveResource** ([commit](https://github.com/rails/rails/commit/f1637bf2bb00490203503fbd943b73406e043d1d)) - ActiveResource is extracted to a separate gem. ActiveResource was not widely used.
+ * **vendor/plugins removed** ([commit](https://github.com/rails/rails/commit/853de2bd9ac572735fa6cf59fcf827e485a231c3)) - Use a Gemfile to manage installed gems.
+
+### ActionPack
+
+ * **Strong parameters** ([commit](https://github.com/rails/rails/commit/a8f6d5c6450a7fe058348a7f10a908352bb6c7fc)) - Only allow whitelisted parameters to update model objects (`params.permit(:title, :text)`).
+ * **Routing concerns** ([commit](https://github.com/rails/rails/commit/0dd24728a088fcb4ae616bb5d62734aca5276b1b)) - In the routing DSL, factor out common subroutes (`comments` from `/posts/1/comments` and `/videos/1/comments`).
+ * **ActionController::Live** ([commit](https://github.com/rails/rails/commit/af0a9f9eefaee3a8120cfd8d05cbc431af376da3)) - Stream JSON with `response.stream`.
+ * **Declarative ETags** ([commit](https://github.com/rails/rails/commit/ed5c938fa36995f06d4917d9543ba78ed506bb8d)) - Add controller-level etag additions that will be part of the action etag computation
+ * **[Russian doll caching](http://37signals.com/svn/posts/3113-how-key-based-cache-expiration-works)** ([commit](https://github.com/rails/rails/commit/4154bf012d2bec2aae79e4a49aa94a70d3e91d49)) - Cache nested fragments of views. Each fragment expires based on a set of dependencies (a cache key). The cache key is usually a template version number and a model object.
+ * **Turbolinks** ([commit](https://github.com/rails/rails/commit/e35d8b18d0649c0ecc58f6b73df6b3c8d0c6bb74)) - Serve only one initial HTML page. When the user navigates to another page, use pushState to update the URL and use AJAX to update the title and body.
+ * **Decouple ActionView from ActionController** ([commit](https://github.com/rails/rails/commit/78b0934dd1bb84e8f093fb8ef95ca99b297b51cd)) - ActionView was decoupled from ActionPack and will be moved to a separated gem in Rails 4.1.
+ * **Do not depend on ActiveModel** ([commit](https://github.com/rails/rails/commit/166dbaa7526a96fdf046f093f25b0a134b277a68)) - ActionPack no longer depends on ActiveModel.
+
+### General
+
+ * **ActiveModel::Model** ([commit](https://github.com/rails/rails/commit/3b822e91d1a6c4eab0064989bbd07aae3a6d0d08)) - `ActiveModel::Model`, a mixin to make normal Ruby objects to work with ActionPack out of box (ex. for `form_for`)
+ * **New scope API** ([commit](https://github.com/rails/rails/commit/50cbc03d18c5984347965a94027879623fc44cce)) - Scopes must always use callables.
+ * **Schema cache dump** ([commit](https://github.com/rails/rails/commit/5ca4fc95818047108e69e22d200e7a4a22969477)) - To improve Rails boot time, instead of loading the schema directly from the database, load the schema from a dump file.
+ * **Support for specifying transaction isolation level** ([commit](https://github.com/rails/rails/commit/392eeecc11a291e406db927a18b75f41b2658253)) - Choose whether repeatable reads or improved performance (less locking) is more important.
+ * **Dalli** ([commit](https://github.com/rails/rails/commit/82663306f428a5bbc90c511458432afb26d2f238)) - Use Dalli memcache client for the memcache store.
+ * **Notifications start &amp; finish** ([commit](https://github.com/rails/rails/commit/f08f8750a512f741acb004d0cebe210c5f949f28)) - Active Support instrumentation reports start and finish notifications to subscribers.
+ * **Thread safe by default** ([commit](https://github.com/rails/rails/commit/5d416b907864d99af55ebaa400fff217e17570cd)) - Rails can run in threaded app servers without additional configuration. Note: Check that the gems you are using are threadsafe.
+ * **PATCH verb** ([commit](https://github.com/rails/rails/commit/eed9f2539e3ab5a68e798802f464b8e4e95e619e)) - In Rails, PATCH replaces PUT. PATCH is used for partial updates of resources.
+
+### Security
+
+ * **match do not catch all** ([commit](https://github.com/rails/rails/commit/90d2802b71a6e89aedfe40564a37bd35f777e541)) - In the routing DSL, match requires the HTTP verb or verbs to be specified.
+ * **html entities escaped by default** ([commit](https://github.com/rails/rails/commit/5f189f41258b83d49012ec5a0678d827327e7543)) - Strings rendered in erb are escaped unless wrapped with `raw` or `html_safe` is called.
+ * **New security headers** ([commit](https://github.com/rails/rails/commit/6794e92b204572d75a07bd6413bdae6ae22d5a82)) - Rails sends the following headers with every HTTP request: `X-Frame-Options` (prevents clickjacking by forbidding the browser from embedding the page in a frame), `X-XSS-Protection` (asks the browser to halt script injection) and `X-Content-Type-Options` (prevents the browser from opening a jpeg as an exe).
+
+Extraction of features to gems
+---------------------------
+
+In Rails 4.0, several features have been extracted into gems. You can simply add the extracted gems to your `Gemfile` to bring the functionality back.
+
+* Hash-based & Dynamic finder methods ([GitHub](https://github.com/rails/activerecord-deprecated_finders))
+* Mass assignment protection in Active Record models ([GitHub](https://github.com/rails/protected_attributes), [Pull Request](https://github.com/rails/rails/pull/7251))
+* ActiveRecord::SessionStore ([GitHub](https://github.com/rails/activerecord-session_store), [Pull Request](https://github.com/rails/rails/pull/7436))
+* Active Record Observers ([GitHub](https://github.com/rails/rails-observers), [Commit](https://github.com/rails/rails/commit/39e85b3b90c58449164673909a6f1893cba290b2))
+* Active Resource ([GitHub](https://github.com/rails/activeresource), [Pull Request](https://github.com/rails/rails/pull/572), [Blog](http://yetimedia.tumblr.com/post/35233051627/activeresource-is-dead-long-live-activeresource))
+* Action Caching ([GitHub](https://github.com/rails/actionpack-action_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
+* Page Caching ([GitHub](https://github.com/rails/actionpack-page_caching), [Pull Request](https://github.com/rails/rails/pull/7833))
+* Sprockets ([GitHub](https://github.com/rails/sprockets-rails))
+* Performance tests ([GitHub](https://github.com/rails/rails-perftest), [Pull Request](https://github.com/rails/rails/pull/8876))
+
Documentation
-------------
* Guides are rewritten in GitHub Flavored Markdown.
+* Guides have a responsive design.
+
Railties
--------
-* Allow scaffold/model/migration generators to accept a `polymorphic` modifier for `references`/`belongs_to`, for instance
-
- ```
- rails g model Product supplier:references{polymorphic}
- ```
-
- will generate the model with `belongs_to :supplier, polymorphic: true` association and appropriate migration.
-
-* Set `config.active_record.migration_error` to `:page_load` for development.
-
-* Add runner to `Rails::Railtie` as a hook called just after runner starts.
-
-* Add `/rails/info/routes` path which displays the same information as `rake routes`.
-
-* Improved `rake routes` output for redirects.
-
-* Load all environments available in `config.paths["config/environments"]`.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for detailed changes.
-* Add `config.queue_consumer` to change the job queue consumer from the default `ActiveSupport::ThreadedQueueConsumer`.
+### Notable changes
-* Add `Rails.queue` for processing jobs in the background.
+* New test locations `test/models`, `test/helpers`, `test/controllers`, and `test/mailers`. Corresponding rake tasks added as well. ([Pull Request](https://github.com/rails/rails/pull/7878))
-* Remove `Rack::SSL` in favour of `ActionDispatch::SSL`.
+* Your app's executables now live in the `bin/` directory. Run `rake rails:update:bin` to get `bin/bundle`, `bin/rails`, and `bin/rake`.
-* Allow to set class that will be used to run as a console, other than IRB, with `Rails.application.config.console=`. It's best to add it to console block.
+* Threadsafe on by default
- ```ruby
- # it can be added to config/application.rb
- console do
- # this block is called only when running console,
- # so we can safely require pry here
- require "pry"
- config.console = Pry
- end
- ```
-
-* Add a convenience method `hide!` to Rails generators to hide the current generator namespace from showing when running `rails generate`.
-
-* Scaffold now uses `content_tag_for` in `index.html.erb`.
-
-* `Rails::Plugin` is removed. Instead of adding plugins to `vendor/plugins`, use gems or bundler with path or git dependencies.
+* Ability to use a custom builder by passing `--builder` (or `-b`) to
+ `rails new` has been removed. Consider using application templates
+ instead. ([Pull Request](https://github.com/rails/rails/pull/9401))
### Deprecations
-Action Mailer
--------------
-
-* Allow to set default Action Mailer options via `config.action_mailer.default_options=`.
-
-* Raise an `ActionView::MissingTemplate` exception when no implicit template could be found.
-
-* Asynchronously send messages via the Rails Queue.
-
-* Delivery Options (such as SMTP Settings) can now be set dynamically per mailer action.
-
- Delivery options are set via <tt>:delivery_method_options</tt> key on mail.
-
- ```ruby
- def welcome_mailer(user,company)
- delivery_options = { user_name: company.smtp_user, password: company.smtp_password, address: company.smtp_host }
- mail(to: user.email, subject: "Welcome!", delivery_method_options: delivery_options)
- end
- ```
-
-* Allow for callbacks in mailers similar to ActionController::Base. You can now set up headers/attachments using `before_filter` or `after_filter`. You could also change delivery settings or prevent delivery in an after filter based on instance variables set in your mailer action. You have access to `ActionMailer::Base` instance methods like `message`, `attachments`, `headers`.
-
-Action Pack
------------
-
-### Action Controller
-
-* Add `ActionController::Flash.add_flash_types` method to allow people to register their own flash types. e.g.:
-
- ```ruby
- class ApplicationController
- add_flash_types :error, :warning
- end
- ```
-
- If you add the above code, you can use `<%= error %>` in an erb, and `redirect_to /foo, :error => 'message'` in a controller.
-
-* Remove Active Model dependency from Action Pack.
-
-* Support unicode characters in routes. Route will be automatically escaped, so instead of manually escaping:
-
- ```ruby
- get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯') => 'home#index'
- ```
-
- You just have to write the unicode route:
-
- ```ruby
- get 'ã“ã‚“ã«ã¡ã¯' => 'home#index'
- ```
-
-* Return proper format on exceptions.
-
-* Extracted redirect logic from `ActionController::ForceSSL::ClassMethods.force_ssl` into `ActionController::ForceSSL#force_ssl_redirect`.
-
-* URL path parameters with invalid encoding now raise `ActionController::BadRequest`.
-
-* Malformed query and request parameter hashes now raise `ActionController::BadRequest`.
-
-* `respond_to` and `respond_with` now raise `ActionController::UnknownFormat` instead of directly returning head 406. The exception is rescued and converted to 406 in the exception handling middleware.
-
-* JSONP now uses `application/javascript` instead of `application/json` as the MIME type.
-
-* Session arguments passed to process calls in functional tests are now merged into the existing session, whereas previously they would replace the existing session. This change may break some existing tests if they are asserting the exact contents of the session but should not break existing tests that only assert individual keys.
-
-* Forms of persisted records use always PATCH (via the `_method` hack).
-
-* For resources, both PATCH and PUT are routed to the `update` action.
-
-* Don't ignore `force_ssl` in development. This is a change of behavior - use an `:if` condition to recreate the old behavior.
-
- ```ruby
- class AccountsController < ApplicationController
- force_ssl :if => :ssl_configured?
-
- def ssl_configured?
- !Rails.env.development?
- end
- end
- ```
-
-#### Deprecations
-
-* Deprecated `ActionController::Integration` in favour of `ActionDispatch::Integration`.
-
-* Deprecated `ActionController::IntegrationTest` in favour of `ActionDispatch::IntegrationTest`.
-
-* Deprecated `ActionController::PerformanceTest` in favour of `ActionDispatch::PerformanceTest`.
-
-* Deprecated `ActionController::AbstractRequest` in favour of `ActionDispatch::Request`.
-
-* Deprecated `ActionController::Request` in favour of `ActionDispatch::Request`.
-
-* Deprecated `ActionController::AbstractResponse` in favour of `ActionDispatch::Response`.
-
-* Deprecated `ActionController::Response` in favour of `ActionDispatch::Response`.
-
-* Deprecated `ActionController::Routing` in favour of `ActionDispatch::Routing`.
-
-### Action Dispatch
-
-* Add Routing Concerns to declare common routes that can be reused inside others resources and routes.
-
- Code before:
-
- ```ruby
- resources :messages do
- resources :comments
- end
-
- resources :posts do
- resources :comments
- resources :images, only: :index
- end
- ```
-
- Code after:
-
- ```ruby
- concern :commentable do
- resources :comments
- end
-
- concern :image_attachable do
- resources :images, only: :index
- end
-
- resources :messages, concerns: :commentable
-
- resources :posts, concerns: [:commentable, :image_attachable]
- ```
-
-* Show routes in exception page while debugging a `RoutingError` in development.
-
-* Include `mounted_helpers` (helpers for accessing mounted engines) in `ActionDispatch::IntegrationTest` by default.
-
-* Added `ActionDispatch::SSL` middleware that when included force all the requests to be under HTTPS protocol.
-
-* Copy literal route constraints to defaults so that url generation know about them. The copied constraints are `:protocol`, `:subdomain`, `:domain`, `:host` and `:port`.
-
-* Allows `assert_redirected_to` to match against a regular expression.
-
-* Adds a backtrace to the routing error page in development.
-
-* `assert_generates`, `assert_recognizes`, and `assert_routing` all raise `Assertion` instead of `RoutingError`.
-
-* Allows the route helper root to take a string argument. For example, `root 'pages#main'` as a shortcut for `root to: 'pages#main'`.
-
-* Adds support for the PATCH verb: Request objects respond to `patch?`. Routes now have a new `patch` method, and understand `:patch` in the existing places where a verb is configured, like `:via`. Functional tests have a new method `patch` and integration tests have a new method `patch_via_redirect`.
-If `:patch` is the default verb for updates, edits are tunneled as `PATCH` rather than as `PUT` and routing acts accordingly.
-
-* Integration tests support the OPTIONS method.
-
-* `expires_in` accepts a `must_revalidate` flag. If true, "must-revalidate" is added to the `Cache-Control` header.
-
-* Default responder will now always use your overridden block in `respond_with` to render your response.
-
-* Turn off verbose mode of `rack-cache`, we still have `X-Rack-Cache` to check that info.
-
-#### Deprecations
-
-### Action View
-
-* Remove Active Model dependency from Action Pack.
-
-* Allow to use `mounted_helpers` (helpers for accessing mounted engines) in `ActionView::TestCase`.
-
-* Make current object and counter (when it applies) variables accessible when rendering templates with `:object` or `:collection`.
-
-* Allow to lazy load `default_form_builder` by passing a string instead of a constant.
-
-* Add index method to `FormBuilder` class.
-
-* Adds support for layouts when rendering a partial with a given collection.
-
-* Remove `:disable_with` in favor of `data-disable-with` option from `submit_tag`, `button_tag` and `button_to` helpers.
-
-* Remove `:mouseover` option from `image_tag` helper.
-
-* Templates without a handler extension now raises a deprecation warning but still defaults to `ERb`. In future releases, it will simply return the template content.
-
-* Add a `divider` option to `grouped_options_for_select` to generate a separator optgroup automatically, and deprecate prompt as third argument, in favor of using an options hash.
-
-* Add `time_field` and `time_field_tag` helpers which render an `input[type="time"]` tag.
-
-* Removed old `text_helper` apis for `highlight`, `excerpt` and `word_wrap`.
-
-* Remove the leading \n added by textarea on `assert_select`.
-
-* Changed default value for `config.action_view.embed_authenticity_token_in_remote_forms` to false. This change breaks remote forms that need to work also without JavaScript, so if you need such behavior, you can either set it to true or explicitly pass `:authenticity_token => true` in form options.
-
-* Make possible to use a block in `button_to` helper if button text is hard to fit into the name parameter:
-
- ```ruby
- <%= button_to [:make_happy, @user] do %>
- Make happy <strong><%= @user.name %></strong>
- <% end %>
- # => "<form method="post" action="/users/1/make_happy" class="button_to">
- # <div>
- # <button type="submit">
- # Make happy <strong>Name</strong>
- # </button>
- # </div>
- # </form>"
- ```
-
-* Replace `include_seconds` boolean argument with `:include_seconds => true` option in `distance_of_time_in_words` and `time_ago_in_words` signature.
-
-* Remove `button_to_function` and `link_to_function` helpers.
-
-* `truncate` now always returns an escaped HTML-safe string. The option `:escape` can be used as `false` to not escape the result.
-
-* `truncate` now accepts a block to show extra content when the text is truncated.
-
-* Add `week_field`, `week_field_tag`, `month_field`, `month_field_tag`, `datetime_local_field`, `datetime_local_field_tag`, `datetime_field` and `datetime_field_tag` helpers.
-
-* Add `color_field` and `color_field_tag` helpers.
-
-* Add `include_hidden` option to select tag. With `:include_hidden => false` select with multiple attribute doesn't generate hidden input with blank value.
-
-* Removed default size option from the `text_field`, `search_field`, `telephone_field`, `url_field`, `email_field` helpers.
-
-* Removed default cols and rows options from the `text_area` helper.
-
-* Adds `image_url`, `javascript_url`, `stylesheet_url`, `audio_url`, `video_url`, and `font_url` to assets tag helper. These URL helpers will return the full path to your assets. This is useful when you are going to reference this asset from external host.
-
-* Allow `value_method` and `text_method` arguments from `collection_select` and `options_from_collection_for_select` to receive an object that responds to `:call` such as a proc, to evaluate the option in the current element context. This works the same way with `collection_radio_buttons` and `collection_check_boxes`.
-
-* Add `date_field` and `date_field_tag` helpers which render an `input[type="date"]` tag.
-
-* Add `collection_check_boxes` form helper, similar to `collection_select`:
-
- ```ruby
- collection_check_boxes :post, :author_ids, Author.all, :id, :name
- # Outputs something like:
- <input id="post_author_ids_1" name="post[author_ids][]" type="checkbox" value="1" />
- <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 name="post[author_ids][]" type="hidden" value="" />
- ```
-
- The label/check_box pairs can be customized with a block.
-
-* Add `collection_radio_buttons` form helper, similar to `collection_select`:
-
- ```ruby
- collection_radio_buttons :post, :author_id, Author.all, :id, :name
- # Outputs something like:
- <input id="post_author_id_1" name="post[author_id]" type="radio" value="1" />
- <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>
- ```
-
- The label/radio_button pairs can be customized with a block.
-
-* `check_box` with an HTML5 attribute `:form` will now replicate the `:form` attribute to the hidden field as well.
-
-* label form helper accepts `:for => nil` to not generate the attribute.
-
-* Add `:format` option to `number_to_percentage`.
-
-* Add `config.action_view.logger` to configure logger for `Action View`.
-
-* `check_box` helper with `:disabled => true` will generate a `disabled` hidden field to conform with the HTML convention where disabled fields are not submitted with the form. This is a behavior change, previously the hidden tag had a value of the disabled checkbox.
-
-* `favicon_link_tag` helper will now use the favicon in `app/assets` by default.
+* `config.threadsafe!` is deprecated in favor of `config.eager_load` which provides a more fine grained control on what is eager loaded.
-* `ActionView::Helpers::TextHelper#highlight` now defaults to the HTML5 `mark` element.
+* `Rails::Plugin` has gone. Instead of adding plugins to `vendor/plugins` use gems or bundler with path or git dependencies.
-#### Deprecations
-
-### Sprockets
-
-Moved into a separate gem `sprockets-rails`.
-
-Active Record
+Action Mailer
-------------
-* Add `add_reference` and `remove_reference` schema statements. Aliases, `add_belongs_to` and `remove_belongs_to` are acceptable. References are reversible.
-
- ```ruby
- # Create a user_id column
- add_reference(:products, :user)
-
- # Create a supplier_id, supplier_type columns and appropriate index
- add_reference(:products, :supplier, polymorphic: true, index: true)
-
- # Remove polymorphic reference
- remove_reference(:products, :supplier, polymorphic: true)
- ```
-
-* Add `:default` and `:null` options to `column_exists?`.
-
- ```ruby
- column_exists?(:testings, :taggable_id, :integer, null: false)
- column_exists?(:testings, :taggable_type, :string, default: 'Photo')
- ```
-
-* `ActiveRecord::Relation#inspect` now makes it clear that you are dealing with a `Relation` object rather than an array:
-
- ```ruby
- User.where(:age => 30).inspect
- # => <ActiveRecord::Relation [#<User ...>, #<User ...>]>
-
- User.where(:age => 30).to_a.inspect
- # => [#<User ...>, #<User ...>]
- ```
-
- if more than 10 items are returned by the relation, inspect will only show the first 10 followed by ellipsis.
-
-* Add `:collation` and `:ctype` support to PostgreSQL. These are available for PostgreSQL 8.4 or later.
-
- ```yaml
- development:
- adapter: postgresql
- host: localhost
- database: rails_development
- username: foo
- password: bar
- encoding: UTF8
- collation: ja_JP.UTF8
- ctype: ja_JP.UTF8
- ```
-
-* `FinderMethods#exists?` now returns `false` with the `false` argument.
-
-* Added support for specifying the precision of a timestamp in the postgresql adapter. So, instead of having to incorrectly specify the precision using the `:limit` option, you may use `:precision`, as intended. For example, in a migration:
-
- ```ruby
- def change
- create_table :foobars do |t|
- t.timestamps :precision => 0
- end
- end
- ```
-
-* Allow `ActiveRecord::Relation#pluck` to accept multiple columns. Returns an array of arrays containing the typecasted values:
-
- ```ruby
- Person.pluck(:id, :name)
- # SELECT people.id, people.name FROM people
- # => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
- ```
-
-* Improve the derivation of HABTM join table name to take account of nesting. It now takes the table names of the two models, sorts them lexically and then joins them, stripping any common prefix from the second table name. Some examples:
-
- ```
- Top level models (Category <=> Product)
- Old: categories_products
- New: categories_products
-
- Top level models with a global table_name_prefix (Category <=> Product)
- Old: site_categories_products
- New: site_categories_products
-
- Nested models in a module without a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: categories_products
-
- Nested models in a module with a table_name_prefix method (Admin::Category <=> Admin::Product)
- Old: categories_products
- New: admin_categories_products
-
- Nested models in a parent model (Catalog::Category <=> Catalog::Product)
- Old: categories_products
- New: catalog_categories_products
-
- Nested models in different parent models (Catalog::Category <=> Content::Page)
- Old: categories_pages
- New: catalog_categories_content_pages
- ```
-
-* Move HABTM validity checks to `ActiveRecord::Reflection`. One side effect of this is to move when the exceptions are raised from the point of declaration to when the association is built. This is consistant with other association validity checks.
-
-* Added `stored_attributes` hash which contains the attributes stored using `ActiveRecord::Store`. This allows you to retrieve the list of attributes you've defined.
-
- ```ruby
- class User < ActiveRecord::Base
- store :settings, accessors: [:color, :homepage]
- end
-
- User.stored_attributes[:settings] # [:color, :homepage]
- ```
-
-* PostgreSQL default log level is now 'warning', to bypass the noisy notice messages. You can change the log level using the `min_messages` option available in your `config/database.yml`.
-
-* Add uuid datatype support to PostgreSQL adapter.
-
-* Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending.
-
-* Added `#destroy!` which acts like `#destroy` but will raise an `ActiveRecord::RecordNotDestroyed` exception instead of returning `false`.
-
-* Allow blocks for count with `ActiveRecord::Relation`, to work similar as `Array#count`: `Person.where("age > 26").count { |person| person.gender == 'female' }`
-
-* Added support to `CollectionAssociation#delete` for passing fixnum or string values as record ids. This finds the records responding to the ids and deletes them.
-
- ```ruby
- class Person < ActiveRecord::Base
- has_many :pets
- end
-
- person.pets.delete("1") # => [#<Pet id: 1>]
- person.pets.delete(2, 3) # => [#<Pet id: 2>, #<Pet id: 3>]
- ```
-
-* It's not possible anymore to destroy a model marked as read only.
-
-* Added ability to `ActiveRecord::Relation#from` to accept other `ActiveRecord::Relation` objects.
-
-* Added custom coders support for `ActiveRecord::Store`. Now you can set your custom coder like this:
-
- ```ruby
- store :settings, accessors: [ :color, :homepage ], coder: JSON
- ```
-
-* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by default to avoid silent data loss. This can be disabled by specifying `strict: false` in `config/database.yml`.
-
-* Added default order to `ActiveRecord::Base#first` to assure consistent results among different database engines. Introduced `ActiveRecord::Base#take` as a replacement to the old behavior.
-
-* Added an `:index` option to automatically create indexes for `references` and `belongs_to` statements in migrations. This can be either a boolean or a hash that is identical to options available to the `add_index` method:
-
- ```ruby
- create_table :messages do |t|
- t.references :person, :index => true
- end
- ```
-
- Is the same as:
-
- ```ruby
- create_table :messages do |t|
- t.references :person
- end
- add_index :messages, :person_id
- ```
-
- Generators have also been updated to use the new syntax.
-
-* Added bang methods for mutating `ActiveRecord::Relation` objects. For example, while `foo.where(:bar)` will return a new object leaving foo unchanged, `foo.where!(:bar)` will mutate the foo object.
-
-* Added `#find_by` and `#find_by!` to mirror the functionality provided by dynamic finders in a way that allows dynamic input more easily:
-
- ```ruby
- Post.find_by name: 'Spartacus', rating: 4
- Post.find_by "published_at < ?", 2.weeks.ago
- Post.find_by! name: 'Spartacus'
- ```
-
-* Added `ActiveRecord::Base#slice` to return a hash of the given methods with their names as keys and returned values as values.
-
-* Remove IdentityMap - IdentityMap has never graduated to be an "enabled-by-default" feature, due to some inconsistencies with associations, as described in this [commit](https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6). Hence the removal from the codebase, until such issues are fixed.
-
-* Added a feature to dump/load internal state of `SchemaCache` instance because we want to boot more quickly when we have many models.
-
- ```ruby
- # execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:dump
- => generate db/schema_cache.dump
-
- # add config.use_schema_cache_dump = true in config/production.rb. BTW, true is default.
-
- # boot rails.
- RAILS_ENV=production bundle exec rails server
- => use db/schema_cache.dump
-
- # If you remove clear dumped cache, execute rake task.
- RAILS_ENV=production bundle exec rake db:schema:cache:clear
- => remove db/schema_cache.dump
- ```
-
-* Added support for partial indices to `PostgreSQL` adapter.
-
-* The `add_index` method now supports a `where` option that receives a string with the partial index criteria.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionmailer/CHANGELOG.md) for detailed changes.
-* Added the `ActiveRecord::NullRelation` class implementing the null object pattern for the Relation class.
-
-* Implemented `ActiveRecord::Relation#none` method which returns a chainable relation with zero records (an instance of the `NullRelation` class). Any subsequent condition chained to the returned relation will continue generating an empty relation and will not fire any query to the database.
-
-* Added `create_join_table` migration helper to create HABTM join tables.
-
- ```ruby
- create_join_table :products, :categories
- # =>
- # create_table :categories_products, :id => false do |td|
- # td.integer :product_id, :null => false
- # td.integer :category_id, :null => false
- # end
- ```
-
-* The primary key is always initialized in the `@attributes` hash to nil (unless another value has been specified).
-
-* In previous releases, the following would generate a single query with an OUTER JOIN comments, rather than two separate queries:
-
- ```ruby
- Post.includes(:comments).where("comments.name = 'foo'")
- ```
-
- This behaviour relies on matching SQL string, which is an inherently flawed idea unless we write an SQL parser, which we do not wish to do. Therefore, it is now deprecated.
-
- To avoid deprecation warnings and for future compatibility, you must explicitly state which tables you reference, when using SQL snippets:
-
- ```ruby
- Post.includes(:comments).where("comments.name = 'foo'").references(:comments)
- ```
-
- Note that you do not need to explicitly specify references in the following cases, as they can be automatically inferred:
-
- ```ruby
- Post.where(comments: { name: 'foo' })
- Post.where('comments.name' => 'foo')
- Post.order('comments.name')
- ```
-
- You also do not need to worry about this unless you are doing eager loading. Basically, don't worry unless you see a deprecation warning or (in future releases) an SQL error due to a missing JOIN.
-
-* Support for the `schema_info` table has been dropped. Please switch to `schema_migrations`.
-
-* Connections *must* be closed at the end of a thread. If not, your connection pool can fill and an exception will be raised.
-
-* Added the `ActiveRecord::Model` module which can be included in a class as an alternative to inheriting from `ActiveRecord::Base`:
-
- ```ruby
- class Post
- include ActiveRecord::Model
- end
- ```
-
-* PostgreSQL hstore records can be created.
-
-* PostgreSQL hstore types are automatically deserialized from the database.
-
-* Added `#update_columns` method which updates the attributes from the passed-in hash without calling save, hence skipping validations and callbacks. `ActiveRecordError` will be raised when called on new objects or when at least one of the attributes is marked as read only.
-
- ```ruby
- post.attributes # => {"id"=>2, "title"=>"My title", "body"=>"My content", "author"=>"Peter"}
- post.update_columns({title: 'New title', author: 'Sebastian'}) # => true
- post.attributes # => {"id"=>2, "title"=>"New title", "body"=>"My content", "author"=>"Sebastian"}
- ```
+### Notable changes
### Deprecations
-* Deprecated most of the 'dynamic finder' methods. All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's how you can rewrite the code:
-
- ```ruby
- find_all_by_... can be rewritten using where(...)
- find_last_by_... can be rewritten using where(...).last
- scoped_by_... can be rewritten using where(...)
- find_or_initialize_by_... can be rewritten using where(...).first_or_initialize
- find_or_create_by_... can be rewritten using where(...).first_or_create
- find_or_create_by_...! can be rewritten using where(...).first_or_create!
- ```
-
- The implementation of the deprecated dynamic finders has been moved to the `active_record_deprecated_finders` gem.
-
-* Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do. For example this:
-
- ```ruby
- Post.find(:all, :conditions => { :comments_count => 10 }, :limit => 5)
- ```
-
- should be rewritten in the new style which has existed since Rails 3:
-
- ```ruby
- Post.where(comments_count: 10).limit(5)
- ```
-
- Note that as an interim step, it is possible to rewrite the above as:
-
- ```ruby
- Post.scoped(:where => { :comments_count => 10 }, :limit => 5)
- ```
-
- This could save you a lot of work if there is a lot of old-style finder usage in your application.
-
- Calling `Post.scoped(options)` is a shortcut for `Post.scoped.merge(options)`. `Relation#merge` now accepts a hash of options, but they must be identical to the names of the equivalent finder method. These are mostly identical to the old-style finder option names, except in the following cases:
-
- ```
- :conditions becomes :where
- :include becomes :includes
- :extend becomes :extending
- ```
-
- The code to implement the deprecated features has been moved out to the `active_record_deprecated_finders` gem. This gem is a dependency of Active Record in Rails 4.0. It will no longer be a dependency from Rails 4.1, but if your app relies on the deprecated features then you can add it to your own Gemfile. It will be maintained by the Rails core team until Rails 5.0 is released.
-
-* Deprecate eager-evaluated scopes.
-
- Don't use this:
-
- ```ruby
- scope :red, where(color: 'red')
- default_scope where(color: 'red')
- ```
-
- Use this:
-
- ```ruby
- scope :red, -> { where(color: 'red') }
- default_scope { where(color: 'red') }
- ```
-
- The former has numerous issues. It is a common newbie gotcha to do the following:
-
- ```ruby
- scope :recent, where(published_at: Time.now - 2.weeks)
- ```
-
- Or a more subtle variant:
-
- ```ruby
- scope :recent, -> { where(published_at: Time.now - 2.weeks) }
- scope :recent_red, recent.where(color: 'red')
- ```
-
- Eager scopes are also very complex to implement within Active Record, and there are still bugs. For example, the following does not do what you expect:
-
- ```ruby
- scope :remove_conditions, except(:where)
- where(...).remove_conditions # => still has conditions
- ```
-
-* Added deprecation for the `:dependent => :restrict` association option.
-
-* Up until now `has_many` and `has_one, :dependent => :restrict` option raised a `DeleteRestrictionError` at the time of destroying the object. Instead, it will add an error on the model.
-
-* To fix this warning, make sure your code isn't relying on a `DeleteRestrictionError` and then add `config.active_record.dependent_restrict_raises = false` to your application config.
-
-* New rails application would be generated with the `config.active_record.dependent_restrict_raises = false` in the application config.
-
-* The migration generator now creates a join table with (commented) indexes every time the migration name contains the word "join_table".
-
-* `ActiveRecord::SessionStore` is removed from Rails 4.0 and is now a separate [gem](https://github.com/rails/activerecord-session_store).
-
Active Model
------------
-* Changed `AM::Serializers::JSON.include_root_in_json` default value to false. Now, AM Serializers and AR objects have the same default behaviour.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activemodel/CHANGELOG.md) for detailed changes.
- ```ruby
- class User < ActiveRecord::Base; end
+### Notable changes
- class Person
- include ActiveModel::Model
- include ActiveModel::AttributeMethods
- include ActiveModel::Serializers::JSON
+* Add `ActiveModel::ForbiddenAttributesProtection`, a simple module to protect attributes from mass assignment when non-permitted attributes are passed.
- attr_accessor :name, :age
+* Added `ActiveModel::Model`, a mixin to make Ruby objects work with Action Pack out of box.
- def attributes
- instance_values
- end
- end
+### Deprecations
- user.as_json
- => {"id"=>1, "name"=>"Konata Izumi", "age"=>16, "awesome"=>true}
- # root is not included
+Active Support
+--------------
- person.as_json
- => {"name"=>"Francesco", "age"=>22}
- # root is not included
- ```
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activesupport/CHANGELOG.md) for detailed changes.
-* Passing false hash values to `validates` will no longer enable the corresponding validators.
+### Notable changes
-* `ConfirmationValidator` error messages will attach to `:#{attribute}_confirmation` instead of `attribute`.
+* Replace deprecated `memcache-client` gem with `dalli` in ActiveSupport::Cache::MemCacheStore.
-* Added `ActiveModel::Model`, a mixin to make Ruby objects work with Action Pack out of the box.
+* Optimize ActiveSupport::Cache::Entry to reduce memory and processing overhead.
-* `ActiveModel::Errors#to_json` supports a new parameter `:full_messages`.
+* Inflections can now be defined per locale. `singularize` and `pluralize` accept locale as an extra argument.
-* Trims down the API by removing `valid?` and `errors.full_messages`.
+* `Object#try` will now return nil instead of raise a NoMethodError if the receiving object does not implement the method, but you can still get the old behavior by using the new `Object#try!`.
-### Deprecations
+* `String#to_date` now raises `Argument Error: invalid date` instead of `NoMethodError: undefined method 'div' for nil:NilClass`
+ when given an invalid date. It is now the same as `Date.parse`, and it accepts more invalid dates than 3.x, such as:
-Active Resource
----------------
+ ```
+ # ActiveSupport 3.x
+ "asdf".to_date # => NoMethodError: undefined method `div' for nil:NilClass
+ "333".to_date # => NoMethodError: undefined method `div' for nil:NilClass
-* Active Resource is removed from Rails 4.0 and is now a separate [gem](https://github.com/rails/activeresource).
+ # ActiveSupport 4
+ "asdf".to_date # => ArgumentError: invalid date
+ "333".to_date # => Fri, 29 Nov 2013
+ ```
-Active Support
---------------
+### Deprecations
-* Add default values to all `ActiveSupport::NumberHelper` methods, to avoid errors with empty locales or missing values.
+* Deprecate `ActiveSupport::TestCase#pending` method, use `skip` from MiniTest instead.
-* `Time#change` now works with time values with offsets other than UTC or the local time zone.
+* `ActiveSupport::Benchmarkable#silence` has been deprecated due to its lack of thread safety. It will be removed without replacement in Rails 4.1.
-* Add `Time#prev_quarter` and `Time#next_quarter` short-hands for `months_ago(3)` and `months_since(3)`.
+* `ActiveSupport::JSON::Variable` is deprecated. Define your own `#as_json` and `#encode_json` methods for custom JSON string literals.
-* Remove obsolete and unused `require_association` method from dependencies.
+* Deprecates the compatibility method `Module#local_constant_names`, use `Module#local_constants` instead (which returns symbols).
-* Add `:instance_accessor` option for `config_accessor`.
+* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger`, or the logger from Ruby standard library.
- ```ruby
- class User
- include ActiveSupport::Configurable
- config_accessor :allowed_access, instance_accessor: false
- end
+* Deprecate `assert_present` and `assert_blank` in favor of `assert object.blank?` and `assert object.present?`
- User.new.allowed_access = true # => NoMethodError
- User.new.allowed_access # => NoMethodError
- ```
+Action Pack
+-----------
-* `ActionView::Helpers::NumberHelper` methods have been moved to `ActiveSupport::NumberHelper` and are now available via `Numeric#to_s`.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/actionpack/CHANGELOG.md) for detailed changes.
-* `Numeric#to_s` now accepts the formatting options :phone, :currency, :percentage, :delimited, :rounded, :human, and :human_size.
+### Notable changes
-* Add `Hash#transform_keys`, `Hash#transform_keys!`, `Hash#deep_transform_keys` and `Hash#deep_transform_keys!`.
+* Change the stylesheet of exception pages for development mode. Additionally display also the line of code and fragment that raised the exception in all exceptions pages.
-* Changed xml type datetime to dateTime (with upper case letter T).
+### Deprecations
-* Add `:instance_accessor` option for `class_attribute`.
-* `constantize` now looks in the ancestor chain.
+Active Record
+-------------
-* Add `Hash#deep_stringify_keys` and `Hash#deep_stringify_keys!` to convert all keys from a `Hash` instance into strings.
+Please refer to the [Changelog](https://github.com/rails/rails/blob/4-0-stable/activerecord/CHANGELOG.md) for detailed changes.
-* Add `Hash#deep_symbolize_keys` and `Hash#deep_symbolize_keys!` to convert all keys from a `Hash` instance into symbols.
+### Notable changes
-* `Object#try` can't call private methods.
+* Improve ways to write `change` migrations, making the old `up` & `down` methods no longer necessary.
-* AS::Callbacks#run_callbacks remove key argument.
+ * The methods `drop_table` and `remove_column` are now reversible, as long as the necessary information is given.
+ The method `remove_column` used to accept multiple column names; instead use `remove_columns` (which is not revertible).
+ The method `change_table` is also reversible, as long as its block doesn't call `remove`, `change` or `change_default`
-* `deep_dup` works more expectedly now and duplicates also values in `Hash` instances and elements in `Array` instances.
+ * New method `reversible` makes it possible to specify code to be run when migrating up or down.
+ See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#using-the-reversible-method)
-* Inflector no longer applies ice -> ouse to words like slice, police.
+ * New method `revert` will revert a whole migration or the given block.
+ If migrating down, the given migration / block is run normally.
+ See the [Guide on Migration](https://github.com/rails/rails/blob/master/guides/source/migrations.md#reverting-previous-migrations)
-* Add `ActiveSupport::Deprecations.behavior = :silence` to completely ignore Rails runtime deprecations.
+* Adds PostgreSQL array type support. Any datatype can be used to create an array column, with full migration and schema dumper support.
-* Make `Module#delegate` stop using send - can no longer delegate to private methods.
+* Add `Relation#load` to explicitly load the record and return `self`.
-* AS::Callbacks deprecate :rescuable option.
+* `Model.all` now returns an `ActiveRecord::Relation`, rather than an array of records. Use `Relation#to_a` if you really want an array. In some specific cases, this may cause breakage when upgrading.
-* Adds `Integer#ordinal` to get the ordinal suffix string of an integer.
+* Added `ActiveRecord::Migration.check_pending!` that raises an error if migrations are pending.
-* AS::Callbacks :per_key option is no longer supported.
+* Added custom coders support for `ActiveRecord::Store`. Now you can set your custom coder like this:
-* AS::Callbacks#define_callbacks add :skip_after_callbacks_if_terminated option.
+ store :settings, accessors: [ :color, :homepage ], coder: JSON
-* Add html_escape_once to ERB::Util, and delegate escape_once tag helper to it.
+* `mysql` and `mysql2` connections will set `SQL_MODE=STRICT_ALL_TABLES` by default to avoid silent data loss. This can be disabled by specifying `strict: false` in your `database.yml`.
-* Remove `ActiveSupport::TestCase#pending` method, use `skip` instead.
+* Remove IdentityMap.
-* Deletes the compatibility method `Module#method_names`, use `Module#methods` from now on (which returns symbols).
+* Remove automatic execution of EXPLAIN queries. The option `active_record.auto_explain_threshold_in_seconds` is no longer used and should be removed.
-* Deletes the compatibility method `Module#instance_method_names`, use `Module#instance_methods` from now on (which returns symbols).
+* Adds `ActiveRecord::NullRelation` and `ActiveRecord::Relation#none` implementing the null object pattern for the Relation class.
-* Unicode database updated to 6.1.0.
+* Added `create_join_table` migration helper to create HABTM join tables.
-* Adds `encode_big_decimal_as_string` option to force JSON serialization of BigDecimals as numeric instead of wrapping them in strings for safety.
+* Allows PostgreSQL hstore records to be created.
### Deprecations
-* `ActiveSupport::Callbacks`: deprecate usage of filter object with `#before` and `#after` methods as `around` callback.
+* Deprecated the old-style hash based finder API. This means that methods which previously accepted "finder options" no longer do.
-* `BufferedLogger` is deprecated. Use `ActiveSupport::Logger` or the `logger` from Ruby stdlib.
+* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated. Here's
+ how you can rewrite the code:
-* Deprecates the compatibility method `Module#local_constant_names` and use `Module#local_constants` instead (which returns symbols).
+ * `find_all_by_...` can be rewritten using `where(...)`.
+ * `find_last_by_...` can be rewritten using `where(...).last`.
+ * `scoped_by_...` can be rewritten using `where(...)`.
+ * `find_or_initialize_by_...` can be rewritten using `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` can be rewritten using `find_or_create_by(...)`.
+ * `find_or_create_by_...!` can be rewritten using `find_or_create_by!(...)`.
Credits
-------
diff --git a/guides/source/_welcome.html.erb b/guides/source/_welcome.html.erb
index 9d2e9c1d68..0a0a958e30 100644
--- a/guides/source/_welcome.html.erb
+++ b/guides/source/_welcome.html.erb
@@ -1,4 +1,4 @@
-<h2>Ruby on Rails Guides (<%= @version %>)</h2>
+<h2>Ruby on Rails Guides (<%= @edge ? @version[0, 7] : @version %>)</h2>
<% if @edge %>
<p>
@@ -10,10 +10,13 @@
</p>
<% else %>
<p>
- These are the new guides for Rails 3.2 based on <a href="https://github.com/rails/rails/tree/<%= @version %>"><%= @version %></a>.
+ These are the new guides for Rails 4.0 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.14/">http://guides.rubyonrails.org/v3.2.14/</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>.
</p>
diff --git a/guides/source/action_controller_overview.md b/guides/source/action_controller_overview.md
index 824ffb5d7a..cd4a1a0792 100644
--- a/guides/source/action_controller_overview.md
+++ b/guides/source/action_controller_overview.md
@@ -1,15 +1,18 @@
Action Controller Overview
==========================
-In this guide you will learn how controllers work and how they fit into the request cycle in your application. After reading this guide, you will be able to:
+In this guide you will learn how controllers work and how they fit into the request cycle in your application.
-* Follow the flow of a request through a controller
-* Understand why and how to store data in the session or cookies
-* Work with filters to execute code during request processing
-* Use Action Controller's built-in HTTP authentication
-* Stream data directly to the user's browser
-* Filter sensitive parameters so they do not appear in the application's log
-* Deal with exceptions that may be raised during request processing
+After reading this guide, you will know:
+
+* How to follow the flow of a request through a controller.
+* How to restrict parameters passed to your controller.
+* Why and how to store data in the session or cookies.
+* How to work with filters to execute code during request processing.
+* How to use Action Controller's built-in HTTP authentication.
+* How to stream data directly to the user's browser.
+* How to filter sensitive parameters so they do not appear in the application's log.
+* How to deal with exceptions that may be raised during request processing.
--------------------------------------------------------------------------------
@@ -24,6 +27,16 @@ A controller can thus be thought of as a middle man between models and views. It
NOTE: For more details on the routing process, see [Rails Routing from the Outside In](routing.html).
+Controller Naming Convention
+----------------------------
+
+The naming convention of controllers in Rails favors pluralization of the last word in the controller's name, although it is not strictly required (e.g. `ApplicationController`). For example, `ClientsController` is preferable to `ClientController`, `SiteAdminsController` is preferable to `SiteAdminController` or `SitesAdminsController`, and so on.
+
+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.
+
+
Methods and Actions
-------------------
@@ -36,7 +49,7 @@ class ClientsController < ApplicationController
end
```
-As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and run the `new` method. Note that the empty method from the example above could work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`:
+As an example, if a user goes to `/clients/new` in your application to add a new client, Rails will create an instance of `ClientsController` and run the `new` method. Note that the empty method from the example above would work just fine because Rails will by default render the `new.html.erb` view unless the action says otherwise. The `new` method could make available to the view a `@client` instance variable by creating a new `Client`:
```ruby
def new
@@ -56,7 +69,7 @@ Parameters
You will probably want to access data sent in by the user or other parameters in your controller actions. There are two kinds of parameters possible in a web application. The first are parameters that are sent as part of the URL, called query string parameters. The query string is everything after "?" in the URL. The second type of parameter is usually referred to as POST data. This information usually comes from an HTML form which has been filled in by the user. It's called POST data because it can only be sent as part of an HTTP POST request. Rails does not make any distinction between query string parameters and POST parameters, and both are available in the `params` hash in your controller:
```ruby
-class ClientsController < ActionController::Base
+class ClientsController < ApplicationController
# This action uses query string parameters because it gets run
# by an HTTP GET request, but this does not make any difference
# to the way in which the parameters are accessed. The URL for
@@ -66,7 +79,7 @@ class ClientsController < ActionController::Base
if params[:status] == "activated"
@clients = Client.activated
else
- @clients = Client.unactivated
+ @clients = Client.inactivated
end
end
@@ -81,7 +94,7 @@ class ClientsController < ActionController::Base
else
# This line overrides the default rendering behavior, which
# would have been to render the "create" view.
- render action: "new"
+ render "new"
end
end
end
@@ -110,23 +123,23 @@ To send a hash you include the key name inside the brackets:
</form>
```
-When this form is submitted, the value of `params[:client]` will be `{"name" => "Acme", "phone" => "12345", "address" => {"postcode" => "12345", "city" => "Carrot City"}}`. Note the nested hash in `params[:client][:address]`.
+When this form is submitted, the value of `params[:client]` will be `{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }`. Note the nested hash in `params[:client][:address]`.
-Note that the `params` hash is actually an instance of `HashWithIndifferentAccess` from Active Support, which acts like a hash that lets you use symbols and strings interchangeably as keys.
+Note that the `params` hash is actually an instance of `ActiveSupport::HashWithIndifferentAccess`, which acts like a hash but lets you use symbols and strings interchangeably as keys.
-### JSON/XML parameters
+### JSON parameters
-If you're writing a web service application, you might find yourself more comfortable on accepting parameters in JSON or XML format. Rails will automatically convert your parameters into `params` hash, which you'll be able to access like you would normally do with form data.
+If you're writing a web service application, you might find yourself more comfortable accepting parameters in JSON format. If the "Content-Type" header of your request is set to "application/json", Rails will automatically convert your parameters into the `params` hash, which you can access as you would normally.
-So for example, if you are sending this JSON parameter:
+So for example, if you are sending this JSON content:
```json
{ "company": { "name": "acme", "address": "123 Carrot Street" } }
```
-You'll get `params[:company]` as `{ :name => "acme", "address" => "123 Carrot Street" }`.
+You'll get `params[:company]` as `{ "name" => "acme", "address" => "123 Carrot Street" }`.
-Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON/XML parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as:
+Also, if you've turned on `config.wrap_parameters` in your initializer or calling `wrap_parameters` in your controller, you can safely omit the root element in the JSON parameter. The parameters will be cloned and wrapped in the key according to your controller's name by default. So the above parameter can be written as:
```json
{ "name": "acme", "address": "123 Carrot Street" }
@@ -135,17 +148,19 @@ Also, if you've turned on `config.wrap_parameters` in your initializer or callin
And assume that you're sending the data to `CompaniesController`, it would then be wrapped in `:company` key like this:
```ruby
-{ :name => "acme", :address => "123 Carrot Street", :company => { :name => "acme", :address => "123 Carrot Street" }}
+{ name: "acme", address: "123 Carrot Street", company: { name: "acme", address: "123 Carrot Street" } }
```
You can customize the name of the key or specific parameters you want to wrap by consulting the [API documentation](http://api.rubyonrails.org/classes/ActionController/ParamsWrapper.html)
+NOTE: Support for parsing XML parameters has been extracted into a gem named `actionpack-xml_parser`
+
### Routing Parameters
The `params` hash will always contain the `:controller` and `:action` keys, but you should use the methods `controller_name` and `action_name` instead to access these values. Any other parameters defined by the routing, such as `:id` will also be available. As an example, consider a listing of clients where the list can show either active or inactive clients. We can add a route which captures the `:status` parameter in a "pretty" URL:
```ruby
-match '/clients/:status' => 'clients#index', foo: "bar"
+get '/clients/:status' => 'clients#index', foo: 'bar'
```
In this case, when a user opens the URL `/clients/active`, `params[:status]` will be set to "active". When this route is used, `params[:foo]` will also be set to "bar" just like it was passed in the query string. In the same way `params[:action]` will contain "index".
@@ -166,22 +181,174 @@ These options will be used as a starting point when generating URLs, so it's pos
If you define `default_url_options` in `ApplicationController`, as in the example above, it would be used for all URL generation. The method can also be defined in one specific controller, in which case it only affects URLs generated there.
+### Strong Parameters
+
+With strong parameters, Action Controller parameters are forbidden to
+be used in Active Model mass assignments until they have been
+whitelisted. This means you'll have to make a conscious choice about
+which attributes to allow for mass updating and thus prevent
+accidentally exposing that which shouldn't be exposed.
+
+In addition, parameters can be marked as required and flow through a
+predefined raise/rescue flow to end up as a 400 Bad Request with no
+effort.
+
+```ruby
+class PeopleController < ActionController::Base
+ # This will raise an ActiveModel::ForbiddenAttributes exception
+ # because it's using mass assignment without an explicit permit
+ # step.
+ def create
+ Person.create(params[:person])
+ end
+
+ # This will pass with flying colors as long as there's a person key
+ # in the parameters, otherwise it'll raise a
+ # ActionController::ParameterMissing exception, which will get
+ # caught by ActionController::Base and turned into that 400 Bad
+ # Request reply.
+ def update
+ person = current_account.people.find(params[:id])
+ person.update!(person_params)
+ redirect_to person
+ end
+
+ private
+ # Using a private method to encapsulate the permissible parameters
+ # is just a good pattern since you'll be able to reuse the same
+ # permit list between create and update. Also, you can specialize
+ # this method with per-user checking of permissible attributes.
+ def person_params
+ params.require(:person).permit(:name, :age)
+ end
+end
+```
+
+#### Permitted Scalar Values
+
+Given
+
+```ruby
+params.permit(:id)
+```
+
+the key `:id` will pass the whitelisting if it appears in `params` and
+it has a permitted scalar value associated. Otherwise the key is going
+to be filtered out, so arrays, hashes, or any other objects cannot be
+injected.
+
+The permitted scalar types are `String`, `Symbol`, `NilClass`,
+`Numeric`, `TrueClass`, `FalseClass`, `Date`, `Time`, `DateTime`,
+`StringIO`, `IO`, `ActionDispatch::Http::UploadedFile` and
+`Rack::Test::UploadedFile`.
+
+To declare that the value in `params` must be an array of permitted
+scalar values map the key to an empty array:
+
+```ruby
+params.permit(id: [])
+```
+
+To whitelist an entire hash of parameters, the `permit!` method can be
+used:
+
+```ruby
+params.require(:log_entry).permit!
+```
+
+This will mark the `:log_entry` parameters hash and any subhash 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.
+
+#### Nested Parameters
+
+You can also use permit on nested parameters, like:
+
+```ruby
+params.permit(:name, { emails: [] },
+ friends: [ :name,
+ { family: [ :name ], hobbies: [] }])
+```
+
+This declaration whitelists the `name`, `emails` and `friends`
+attributes. It is expected that `emails` will be an array of permitted
+scalar values and that `friends` will be an array of resources with
+specific attributes : they should have a `name` attribute (any
+permitted scalar values allowed), a `hobbies` attribute as an array of
+permitted scalar values, and a `family` attribute which is restricted
+to having a `name` (any permitted scalar values allowed, too).
+
+#### More Examples
+
+You want to also use the permitted attributes in the `new`
+action. This raises the problem that you can't use `require` on the
+root key because normally it does not exist when calling `new`:
+
+```ruby
+# using `fetch` you can supply a default and use
+# the Strong Parameters API from there.
+params.fetch(:blog, {}).permit(:title, :author)
+```
+
+`accepts_nested_attributes_for` allows you to update and destroy
+associated records. This is based on the `id` and `_destroy`
+parameters:
+
+```ruby
+# permit :id and :_destroy
+params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])
+```
+
+Hashes with integer keys are treated differently and you can declare
+the attributes as if they were direct children. You get these kinds of
+parameters when you use `accepts_nested_attributes_for` in combination
+with a `has_many` association:
+
+```ruby
+# To whitelist the following data:
+# {"book" => {"title" => "Some Book",
+# "chapters_attributes" => { "1" => {"title" => "First Chapter"},
+# "2" => {"title" => "Second Chapter"}}}}
+
+params.require(:book).permit(:title, chapters_attributes: [:title])
+```
+
+#### Outside the Scope of Strong Parameters
+
+The strong parameter API was designed with the most common use cases
+in mind. It is not meant as a silver bullet to handle all your
+whitelisting problems. However you can easily mix the API with your
+own code to adapt to your situation.
+
+Imagine a scenario where you want to whitelist an attribute
+containing a hash with any keys. Using strong parameters you can't
+allow a hash with any keys but you can use a simple assignment to get
+the job done:
+
+```ruby
+def product_params
+ params.require(:product).permit(:name).tap do |whitelisted|
+ whitelisted[:data] = params[:product][:data]
+ end
+end
+```
Session
-------
Your application has a session for each user in which you can store small amounts of data that will be persisted between requests. The session is only available in the controller and the view and can use one of a number of different storage mechanisms:
-* ActionDispatch::Session::CookieStore - Stores everything on the client.
-* ActionDispatch::Session::CacheStore - Stores the data in the Rails cache.
-* @ActionDispatch::Session::ActiveRecordStore@ - Stores the data in a database using Active Record. (require `activerecord-session_store` gem).
-* @ActionDispatch::Session::MemCacheStore@ - Stores the data in a memcached cluster (this is a legacy implementation; consider using CacheStore instead).
+* `ActionDispatch::Session::CookieStore` - Stores everything on the client.
+* `ActionDispatch::Session::CacheStore` - Stores the data in the Rails cache.
+* `ActionDispatch::Session::ActiveRecordStore` - Stores the data in a database using Active Record. (require `activerecord-session_store` gem).
+* `ActionDispatch::Session::MemCacheStore` - Stores the data in a memcached cluster (this is a legacy implementation; consider using CacheStore instead).
All session stores use a cookie to store a unique ID for each session (you must use a cookie, Rails will not allow you to pass the session ID in the URL as this is less secure).
-For most stores this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof, but it is not encrypted, so anyone with access to it can read its contents but not edit it (Rails will not accept it if it has been edited).
+For most stores, this ID is used to look up the session data on the server, e.g. in a database table. There is one exception, and that is the default and recommended session store - the CookieStore - which stores all session data in the cookie itself (the ID is still available to you if you need it). This has the advantage of being very lightweight and it requires zero setup in a new application in order to use the session. The cookie data is cryptographically signed to make it tamper-proof. And it is also encrypted so anyone with access to it can't read its contents. (Rails will not accept it if it has been edited).
-The CookieStore can store around 4kB of data -- much less than the others -- but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.
+The CookieStore can store around 4kB of data - much less than the others - but this is usually enough. Storing large amounts of data in the session is discouraged no matter which session store your application uses. You should especially avoid storing complex objects (anything other than basic Ruby objects, the most common example being model instances) in the session, as the server might not be able to reassemble them between requests, which will result in an error.
If your user sessions don't store critical data or don't need to be around for long periods (for instance if you just use the flash for messaging), you can consider using ActionDispatch::Session::CacheStore. This will store sessions using the cache implementation you have configured for your application. The advantage of this is that you can use your existing cache infrastructure for storing sessions without requiring any additional setup or administration. The downside, of course, is that the sessions will be ephemeral and could disappear at any time.
@@ -192,7 +359,7 @@ If you need a different session storage mechanism, you can change it in the `con
```ruby
# 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 "script/rails g active_record:session_migration")
+# (create the session table with "rails g active_record:session_migration")
# YourApp::Application.config.session_store :active_record_store
```
@@ -219,7 +386,7 @@ Rails sets up (for the CookieStore) a secret key used for signing the session da
# 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_token = '49d3f3de9ed86c74b94ad6bd0...'
+YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
```
NOTE: Changing the secret when using the `CookieStore` will invalidate all existing sessions.
@@ -243,7 +410,7 @@ class ApplicationController < ActionController::Base
# logging out removes it.
def current_user
@_current_user ||= session[:current_user_id] &&
- User.find_by_id(session[:current_user_id])
+ User.find_by(id: session[:current_user_id])
end
end
```
@@ -371,13 +538,13 @@ end
Cookies
-------
-Your application can store small amounts of data on the client -- called cookies -- that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which -- much like the `session` -- works like a hash:
+Your application can store small amounts of data on the client - called cookies - that will be persisted across requests and even sessions. Rails provides easy access to cookies via the `cookies` method, which - much like the `session` - works like a hash:
```ruby
class CommentsController < ApplicationController
def new
# Auto-fill the commenter's name if it has been stored in a cookie
- @comment = Comment.new(name: cookies[:commenter_name])
+ @comment = Comment.new(author: cookies[:commenter_name])
end
def create
@@ -386,7 +553,7 @@ class CommentsController < ApplicationController
flash[:notice] = "Thanks for your comment!"
if params[:remember_name]
# Remember the commenter's name.
- cookies[:commenter_name] = @comment.name
+ cookies[:commenter_name] = @comment.author
else
# Delete cookie for the commenter's name cookie, if any.
cookies.delete(:commenter_name)
@@ -401,10 +568,10 @@ end
Note that while for session values you set the key to `nil`, to delete a cookie value you should use `cookies.delete(:key)`.
-Rendering xml and json data
+Rendering XML and JSON data
---------------------------
-ActionController makes it extremely easy to render `xml` or `json` data. If you generate a controller using scaffold then your controller would look something like this.
+ActionController makes it extremely easy to render `XML` or `JSON` data. If you've generated a controller using scaffolding, it would look something like this:
```ruby
class UsersController < ApplicationController
@@ -419,7 +586,7 @@ class UsersController < ApplicationController
end
```
-Notice that in the above case code is `render xml: @users` and not `render xml: @users.to_xml`. That is because if the input is not string then rails automatically invokes `to_xml` .
+You may notice in the above code that we're using `render xml: @users`, not `render xml: @users.to_xml`. If the object is not a String, then Rails will automatically invoke `to_xml` for us.
Filters
-------
@@ -428,11 +595,11 @@ Filters are methods that are run before, after or "around" a controller action.
Filters are inherited, so if you set a filter on `ApplicationController`, it will be run on every controller in your application.
-Before filters may halt the request cycle. A common before filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way:
+"Before" filters may halt the request cycle. A common "before" filter is one which requires that a user is logged in for an action to be run. You can define the filter method this way:
```ruby
class ApplicationController < ActionController::Base
- before_filter :require_login
+ before_action :require_login
private
@@ -442,25 +609,16 @@ class ApplicationController < ActionController::Base
redirect_to new_login_url # halts request cycle
end
end
-
- # The logged_in? method simply returns true if the user is logged
- # in and false otherwise. It does this by "booleanizing" the
- # current_user method we created previously using a double ! operator.
- # Note that this is not common in Ruby and is discouraged unless you
- # really mean to convert something into true or false.
- def logged_in?
- !!current_user
- end
end
```
-The method simply stores an error message in the flash and redirects to the login form if the user is not logged in. If a before filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter they are also cancelled.
+The method simply stores an error message in the flash and redirects to the login form if the user is not logged in. If a "before" filter renders or redirects, the action will not run. If there are additional filters scheduled to run after that filter, they are also cancelled.
-In this example the filter is added to `ApplicationController` and thus all controllers in the application inherit it. This will make everything in the application require the user to be logged in in order to use it. For obvious reasons (the user wouldn't be able to log in in the first place!), not all controllers or actions should require this. You can prevent this filter from running before particular actions with `skip_before_filter`:
+In this example the filter is added to `ApplicationController` and thus all controllers in the application inherit it. This will make everything in the application require the user to be logged in in order to use it. For obvious reasons (the user wouldn't be able to log in in the first place!), not all controllers or actions should require this. You can prevent this filter from running before particular actions with `skip_before_action`:
```ruby
class LoginsController < ApplicationController
- skip_before_filter :require_login, only: [:new, :create]
+ skip_before_action :require_login, only: [:new, :create]
end
```
@@ -468,17 +626,17 @@ Now, the `LoginsController`'s `new` and `create` actions will work as before wit
### After Filters and Around Filters
-In addition to before filters, you can also run filters after an action has been executed, or both before and after.
+In addition to "before" filters, you can also run filters after an action has been executed, or both before and after.
-After filters are similar to before filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, after filters cannot stop the action from running.
+"After" filters are similar to "before" filters, but because the action has already been run they have access to the response data that's about to be sent to the client. Obviously, "after" filters cannot stop the action from running.
-Around filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.
+"Around" filters are responsible for running their associated actions by yielding, similar to how Rack middlewares work.
For example, in a website where changes have an approval workflow an administrator could be able to preview them easily, just apply them within a transaction:
```ruby
-class ChangesController < ActionController::Base
- around_filter :wrap_in_transaction, only: :show
+class ChangesController < ApplicationController
+ around_action :wrap_in_transaction, only: :show
private
@@ -494,19 +652,19 @@ class ChangesController < ActionController::Base
end
```
-Note that an around filter also wraps rendering. In particular, if in the example above, the view itself reads from the database (e.g. via a scope), it will do so within the transaction and thus present the data to preview.
+Note that an "around" filter also wraps rendering. In particular, if in the example above, the view itself reads from the database (e.g. via a scope), it will do so within the transaction and thus present the data to preview.
You can choose not to yield and build the response yourself, in which case the action will not be run.
### Other Ways to Use Filters
-While the most common way to use filters is by creating private methods and using *_filter to add them, there are two other ways to do the same thing.
+While the most common way to use filters is by creating private methods and using *_action to add them, there are two other ways to do the same thing.
-The first is to use a block directly with the *_filter methods. The block receives the controller as an argument, and the `require_login` filter from above could be rewritten to use a block:
+The first is to use a block directly with the *_action methods. The block receives the controller as an argument, and the `require_login` filter from above could be rewritten to use a block:
```ruby
class ApplicationController < ActionController::Base
- before_filter do |controller|
+ before_action do |controller|
redirect_to new_login_url unless controller.send(:logged_in?)
end
end
@@ -518,7 +676,7 @@ The second way is to use a class (actually, any object that responds to the righ
```ruby
class ApplicationController < ActionController::Base
- before_filter LoginFilter
+ before_action LoginFilter
end
class LoginFilter
@@ -616,6 +774,8 @@ If you want to set custom headers for a response then `response.headers` is the
response.headers["Content-Type"] = "application/pdf"
```
+Note: in the above case it would make more sense to use the `content_type` setter directly.
+
HTTP Authentications
--------------------
@@ -629,7 +789,7 @@ Rails comes with two built-in HTTP authentication mechanisms:
HTTP basic authentication is an authentication scheme that is supported by the majority of browsers and other HTTP clients. As an example, consider an administration section which will only be available by entering a username and a password into the browser's HTTP basic dialog window. Using the built-in authentication is quite easy and only requires you to use one method, `http_basic_authenticate_with`.
```ruby
-class AdminController < ApplicationController
+class AdminsController < ApplicationController
http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end
```
@@ -641,18 +801,18 @@ With this in place, you can create namespaced controllers that inherit from `Adm
HTTP digest authentication is superior to the basic authentication as it does not require the client to send an unencrypted password over the network (though HTTP basic authentication is safe over HTTPS). Using digest authentication with Rails is quite easy and only requires using one method, `authenticate_or_request_with_http_digest`.
```ruby
-class AdminController < ApplicationController
+class AdminsController < ApplicationController
USERS = { "lifo" => "world" }
- before_filter :authenticate
+ before_action :authenticate
private
- def authenticate
- authenticate_or_request_with_http_digest do |username|
- USERS[username]
+ def authenticate
+ authenticate_or_request_with_http_digest do |username|
+ USERS[username]
+ end
end
- end
end
```
@@ -679,13 +839,13 @@ class ClientsController < ApplicationController
private
- def generate_pdf(client)
- Prawn::Document.new do
- text client.name, align: :center
- text "Address: #{client.address}"
- text "Email: #{client.email}"
- end.render
- end
+ def generate_pdf(client)
+ Prawn::Document.new do
+ text client.name, align: :center
+ text "Address: #{client.address}"
+ text "Email: #{client.email}"
+ end.render
+ end
end
```
@@ -711,7 +871,7 @@ This will read and stream the file 4kB at the time, avoiding loading the entire
If `:type` is not specified, it will be guessed from the file extension specified in `:filename`. If the content type is not registered for the extension, `application/octet-stream` will be used.
-WARNING: Be careful when using data coming from the client (params, cookies, etc.) to locate the file on disk, as this is a security risk that might allow someone to gain access to files they are not meant to see.
+WARNING: Be careful when using data coming from the client (params, cookies, etc.) to locate the file on disk, as this is a security risk that might allow someone to gain access to files they are not meant to.
TIP: It is not recommended that you stream static files through Rails if you can instead keep them in a public folder on your web server. It is much more efficient to let the user download the file directly using Apache or another web server, keeping the request from unnecessarily going through the whole Rails stack.
@@ -747,15 +907,122 @@ Now the user can request to get a PDF version of a client just by adding ".pdf"
GET /clients/1.pdf
```
-Parameter Filtering
--------------------
+### Live Streaming of Arbitrary Data
+
+Rails allows you to stream more than just files. In fact, you can stream anything
+you would like in a response object. The `ActionController::Live` module allows
+you to create a persistent connection with a browser. Using this module, you will
+be able to send arbitrary data to the browser at specific points in time.
+
+#### Incorporating Live Streaming
+
+Including `ActionController::Live` inside of your controller class will provide
+all actions inside of the controller the ability to stream data. You can mix in
+the module like so:
+
+```ruby
+class MyController < ActionController::Base
+ include ActionController::Live
+
+ def stream
+ response.headers['Content-Type'] = 'text/event-stream'
+ 100.times {
+ response.stream.write "hello world\n"
+ sleep 1
+ }
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code will keep a persistent connection with the browser and send 100
+messages of `"hello world\n"`, each one second apart.
+
+There are a couple of things to notice in the above example. We need to make
+sure to close the response stream. Forgetting to close the stream will leave
+the socket open forever. We also have to set the content type to `text/event-stream`
+before we write to the response stream. This is because headers cannot be written
+after the response has been committed (when `response.committed` returns a truthy
+value), which occurs when you `write` or `commit` the response stream.
+
+#### Example Usage
+
+Let's suppose that you were making a Karaoke machine and a user wants to get the
+lyrics for a particular song. Each `Song` has a particular number of lines and
+each line takes time `num_beats` to finish singing.
+
+If we wanted to return the lyrics in Karaoke fashion (only sending the line when
+the singer has finished the previous line), then we could use `ActionController::Live`
+as follows:
+
+```ruby
+class LyricsController < ActionController::Base
+ include ActionController::Live
-Rails keeps a log file for each environment in the `log` folder. These are extremely useful when debugging what's actually going on in your application, but in a live application you may not want every bit of information to be stored in the log file. 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.
+ def show
+ response.headers['Content-Type'] = 'text/event-stream'
+ song = Song.find(params[:id])
+
+ song.each do |line|
+ response.stream.write line.lyrics
+ sleep line.num_beats
+ end
+ ensure
+ response.stream.close
+ end
+end
+```
+
+The above code sends the next line only after the singer has completed the previous
+line.
+
+#### Streaming Considerations
+
+Streaming arbitrary data is an extremely powerful tool. As shown in the previous
+examples, you can choose when and what to send across a response stream. However,
+you should also note the following things:
+
+* Each response stream creates a new thread and copies over the thread local
+ variables from the original thread. Having too many thread local variables can
+ negatively impact performance. Similarly, a large number of threads can also
+ hinder performance.
+* Failing to close the response stream will leave the corresponding socket open
+ forever. Make sure to call `close` whenever you are using a response stream.
+* WEBrick servers buffer all responses, and so including `ActionController::Live`
+ will not work. You must use a web server which does not automatically buffer
+ responses.
+
+Log Filtering
+-------------
+
+Rails keeps a log file for each environment in the `log` folder. These are extremely useful when debugging what's actually going on in your application, but in a live application you may not want every bit of information to be stored in the log file.
+
+### 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.
```ruby
config.filter_parameters << :password
```
+### Redirects Filtering
+
+Sometimes it's desirable to filter out from log files some sensible locations your application is redirecting to.
+You can do that by using the `config.filter_redirect` configuration option:
+
+```ruby
+config.filter_redirect << 's3.amazonaws.com'
+```
+
+You can set it to a String, a Regexp, or an array of both.
+
+```ruby
+config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]
+```
+
+Matching URLs will be marked as '[FILTERED]'.
+
Rescue
------
@@ -781,9 +1048,9 @@ class ApplicationController < ActionController::Base
private
- def record_not_found
- render text: "404 Not Found", status: 404
- end
+ def record_not_found
+ render text: "404 Not Found", status: 404
+ end
end
```
@@ -795,15 +1062,15 @@ class ApplicationController < ActionController::Base
private
- def user_not_authorized
- flash[:error] = "You don't have access to this section."
- redirect_to :back
- end
+ def user_not_authorized
+ flash[:error] = "You don't have access to this section."
+ redirect_to :back
+ end
end
class ClientsController < ApplicationController
# Check that the user has the right authorization to access clients.
- before_filter :check_authorization
+ before_action :check_authorization
# Note how the actions don't have to worry about all the auth stuff.
def edit
@@ -812,10 +1079,10 @@ class ClientsController < ApplicationController
private
- # If the user is not authorized, just throw the exception.
- def check_authorization
- raise User::NotAuthorized unless current_user.admin?
- end
+ # If the user is not authorized, just throw the exception.
+ def check_authorization
+ raise User::NotAuthorized unless current_user.admin?
+ end
end
```
@@ -824,7 +1091,7 @@ NOTE: Certain exceptions are only rescuable from the `ApplicationController` cla
Force HTTPS protocol
--------------------
-Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. Since Rails 3.1 you can now use `force_ssl` method in your controller to enforce that:
+Sometime you might want to force a particular controller to only be accessible via an HTTPS protocol for security reasons. You can use the `force_ssl` method in your controller to enforce that:
```ruby
class DinnerController
@@ -832,7 +1099,7 @@ class DinnerController
end
```
-Just like the filter, you could also passing `:only` and `:except` to enforce the secure connection only to specific actions.
+Just like the filter, you could also pass `:only` and `:except` to enforce the secure connection only to specific actions:
```ruby
class DinnerController
@@ -842,4 +1109,4 @@ class DinnerController
end
```
-Please note that if you found yourself adding `force_ssl` to many controllers, you may found yourself wanting to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file.
+Please note that if you find yourself adding `force_ssl` to many controllers, you may want to force the whole application to use HTTPS instead. In that case, you can set the `config.force_ssl` in your environment file.
diff --git a/guides/source/action_mailer_basics.md b/guides/source/action_mailer_basics.md
index a938db6265..61fd762304 100644
--- a/guides/source/action_mailer_basics.md
+++ b/guides/source/action_mailer_basics.md
@@ -1,21 +1,29 @@
Action Mailer Basics
====================
-This guide should provide you with all you need to get started in sending and receiving emails from and to your application, and many internals of Action Mailer. It also covers how to test your mailers.
+This guide provides you with all you need to get started in sending and
+receiving emails from and to your application, and many internals of Action
+Mailer. It also covers how to test your mailers.
---------------------------------------------------------------------------------
+After reading this guide, you will know:
+
+* How to send and receive email within a Rails application.
+* How to generate and edit an Action Mailer class and mailer view.
+* How to configure Action Mailer for your environment.
+* How to test your Action Mailer classes.
-WARNING. This guide is based on Rails 3.2. Some of the code shown here will not work in earlier versions of Rails.
+--------------------------------------------------------------------------------
Introduction
------------
-Action Mailer allows you to send emails from your application using a mailer model and views. So, in Rails, emails are used by creating mailers that inherit from `ActionMailer::Base` and live in `app/mailers`. Those mailers have associated views that appear alongside controller views 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
--------------
-This section will provide a step-by-step guide to creating a mailer and its views.
+This section will provide a step-by-step guide to creating a mailer and its
+views.
### Walkthrough to Generating a Mailer
@@ -30,10 +38,25 @@ invoke test_unit
create test/mailers/user_mailer_test.rb
```
-So we got the mailer, the views, and the tests.
+As you can see, you can generate mailers just like you use other generators with
+Rails. Mailers are conceptually similar to controllers, and so we get a mailer,
+a directory for views, and a test.
+
+If you didn't want to use a generator, you could create your own file inside of
+app/mailers, just make sure that it inherits from `ActionMailer::Base`:
+
+```ruby
+class MyMailer < ActionMailer::Base
+end
+```
#### Edit the Mailer
+Mailers are very similar to Rails controllers. They also have methods called
+"actions" and use views to structure the content. Where a controller generates
+content like HTML to send back to the client, a Mailer creates a message to be
+delivered via email.
+
`app/mailers/user_mailer.rb` contains an empty mailer:
```ruby
@@ -42,7 +65,8 @@ class UserMailer < ActionMailer::Base
end
```
-Let's add a method called `welcome_email`, that will send an email to the user's registered email address:
+Let's add a method called `welcome_email`, that will send an email to the user's
+registered email address:
```ruby
class UserMailer < ActionMailer::Base
@@ -51,21 +75,25 @@ class UserMailer < ActionMailer::Base
def welcome_email(user)
@user = user
@url = 'http://example.com/login'
- mail(to: user.email, subject: 'Welcome to My Awesome Site')
+ mail(to: @user.email, subject: 'Welcome to My Awesome Site')
end
end
```
-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.
+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, 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
+* `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.
+Just like controllers, any instance variables we define in the method become
+available for use in the views.
#### Create a Mailer View
-Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This will be the template used for the email, formatted in HTML:
+Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This
+will be the template used for the email, formatted in HTML:
```html+erb
<!DOCTYPE html>
@@ -77,7 +105,7 @@ Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This
<h1>Welcome to example.com, <%= @user.name %></h1>
<p>
You have successfully signed up to example.com,
- your username is: <%= @user.login %>.<br/>
+ your username is: <%= @user.login %>.<br>
</p>
<p>
To login to the site, just follow this link: <%= @url %>.
@@ -87,7 +115,9 @@ Create a file called `welcome_email.html.erb` in `app/views/user_mailer/`. This
</html>
```
-It is also a good idea to make a text part for this email. To do this, create a file called `welcome_email.text.erb` in `app/views/user_mailer/`:
+Let's also make a text part for this email. Not all clients prefer HTML emails,
+and so sending both is best practice. To do this, create a file called
+`welcome_email.text.erb` in `app/views/user_mailer/`:
```erb
Welcome to example.com, <%= @user.name %>
@@ -101,22 +131,29 @@ To login to the site, just follow this link: <%= @url %>.
Thanks for joining and have a great day!
```
-When you call the `mail` method now, Action Mailer will detect the two templates (text and HTML) and automatically generate a `multipart/alternative` email.
+When you call the `mail` method now, Action Mailer will detect the two templates
+(text and HTML) and automatically generate a `multipart/alternative` email.
-#### Wire It Up So That the System Sends the Email When a User Signs Up
+#### Calling the Mailer
-There are several ways to do this, some people create Rails Observers to fire off emails, others do it inside of the User Model. However, in Rails 3, mailers are really just another way to render a view. Instead of rendering a view and sending out the HTTP protocol, they are just sending it out through the Email protocols instead. Due to this, it makes sense to just have your controller tell the mailer to send an email when a user is successfully created.
+Mailers are really just another way to render a view. Instead of rendering a
+view and sending out the HTTP protocol, they are just sending it out through the
+Email protocols instead. Due to this, it makes sense to just have your
+controller tell the Mailer to send an email when a user is successfully created.
Setting this up is painfully simple.
-First off, we need to create a simple `User` scaffold:
+First, let's create a simple `User` scaffold:
```bash
-$ rails generate scaffold user name:string email:string login:string
+$ rails generate scaffold user name email login
$ 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 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:
+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
+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:
```ruby
class UsersController < ApplicationController
@@ -141,67 +178,55 @@ class UsersController < ApplicationController
end
```
-This provides a much simpler implementation that does not require the registering of observers and the like.
-
-The method `welcome_email` returns a `Mail::Message` object which can then just be told `deliver` to send itself out.
-
-NOTE: In previous versions of Rails, you would call `deliver_welcome_email` or `create_welcome_email`. This has been deprecated in Rails 3.0 in favour of just calling the method name itself.
-
-WARNING: Sending out an email should only take a fraction of a second. If you are planning on sending out many emails, or you have a slow domain resolution service, you might want to investigate using a background process like Delayed Job.
+The method `welcome_email` returns a `Mail::Message` object which can then just
+be told `deliver` to send itself out.
### Auto encoding header values
-Action Mailer now handles the auto encoding of multibyte characters inside of headers and bodies.
-
-If you are using UTF-8 as your character set, you do not have to do anything special, just go ahead and send in UTF-8 data to the address fields, subject, keywords, filenames or body of the email and Action Mailer will auto encode it into quoted printable for you in the case of a header field or Base64 encode any body parts that are non US-ASCII.
+Action Mailer handles the auto encoding of multibyte characters inside of
+headers and bodies.
-For more complex examples such as defining alternate character sets or self-encoding text first, please refer to the Mail library.
+For more complex examples such as defining alternate character sets or
+self-encoding text first, please refer to the
+[Mail](https://github.com/mikel/mail) library.
### Complete List of Action Mailer Methods
-There are just three methods that you need to send pretty much any email message:
-
-* `headers` - Specifies any header on the email you want. You can pass a hash of header field names and value pairs, or you can call `headers[:field_name] = 'value'`.
-* `attachments` - Allows you to add attachments to your email. For example, `attachments['file-name.jpg'] = File.read('file-name.jpg')`.
-* `mail` - Sends the actual email itself. You can pass in headers as a hash to the mail method as a parameter, mail will then create an email, either plain text, or multipart, depending on what email templates you have defined.
-
-#### Custom Headers
-
-Defining custom headers are simple, you can do it one of three ways:
-
-* Defining a header field as a parameter to the `mail` method:
-
- ```ruby
- mail('X-Spam' => value)
- ```
-
-* Passing in a key value assignment to the `headers` method:
-
- ```ruby
- headers['X-Spam'] = value
- ```
-
-* Passing a hash of key value pairs to the `headers` method:
-
- ```ruby
- headers {'X-Spam' => value, 'X-Special' => another_value}
- ```
+There are just three methods that you need to send pretty much any email
+message:
-TIP: All `X-Value` headers per the RFC2822 can appear more than once. If you want to delete an `X-Value` header, you need to assign it a value of `nil`.
+* `headers` - Specifies any header on the email you want. You can pass a hash of
+ header field names and value pairs, or you can call `headers[:field_name] =
+ 'value'`.
+* `attachments` - Allows you to add attachments to your email. For example,
+ `attachments['file-name.jpg'] = File.read('file-name.jpg')`.
+* `mail` - Sends the actual email itself. You can pass in headers as a hash to
+ the mail method as a parameter, mail will then create an email, either plain
+ text, or multipart, depending on what email templates you have defined.
#### Adding Attachments
-Adding attachments has been simplified in Action Mailer 3.0.
+Action Mailer makes it very easy to add attachments.
-* Pass the file name and content and Action Mailer and the Mail gem will automatically guess the mime_type, set the encoding and create the attachment.
+* Pass the file name and content and Action Mailer and the
+ [Mail gem](https://github.com/mikel/mail) will automatically guess the
+ mime_type, set the encoding and create the attachment.
```ruby
attachments['filename.jpg'] = File.read('/path/to/filename.jpg')
```
-NOTE: Mail will automatically Base64 encode an attachment. If you want something different, pre-encode your content and pass in the encoded content and encoding in a `Hash` to the `attachments` method.
+ When the `mail` method will be triggered, it will send a multipart email with
+ an attachment, properly nested with the top level being `multipart/mixed` and
+ the first part being a `multipart/alternative` containing the plain text and
+ HTML email messages.
+
+NOTE: Mail will automatically Base64 encode an attachment. If you want something
+different, encode your content and pass in the encoded content and encoding in a
+`Hash` to the `attachments` method.
-* Pass the file name and specify headers and content and Action Mailer and Mail will use the settings you pass in.
+* Pass the file name and specify headers and content and Action Mailer and Mail
+ will use the settings you pass in.
```ruby
encoded_content = SpecialEncode(File.read('/path/to/filename.jpg'))
@@ -210,13 +235,14 @@ NOTE: Mail will automatically Base64 encode an attachment. If you want something
content: encoded_content }
```
-NOTE: If you specify an encoding, Mail will assume that your content is already encoded and not try to Base64 encode it.
+NOTE: If you specify an encoding, Mail will assume that your content is already
+encoded and not try to Base64 encode it.
#### Making Inline Attachments
Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in pre 3.0 versions, much simpler and trivial as they should be.
-* Firstly, to tell Mail to turn an attachment into an inline attachment, you just call `#inline` on the attachments method within your Mailer:
+* First, to tell Mail to turn an attachment into an inline attachment, you just call `#inline` on the attachments method within your Mailer:
```ruby
def welcome
@@ -224,7 +250,9 @@ Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in p
end
```
-* Then in your view, you can just reference `attachments[]` as a hash and specify which attachment you want to show, calling `url` on it and then passing the result into the `image_tag` method:
+* Then in your view, you can just reference `attachments` as a hash and specify
+ which attachment you want to show, calling `url` on it and then passing the
+ result into the `image_tag` method:
```html+erb
<p>Hello there, this is our image</p>
@@ -232,7 +260,8 @@ Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in p
<%= image_tag attachments['image.jpg'].url %>
```
-* As this is a standard call to `image_tag` you can pass in an options hash after the attachment URL as you could for any other image:
+* As this is a standard call to `image_tag` you can pass in an options hash
+ after the attachment URL as you could for any other image:
```html+erb
<p>Hello there, this is our image</p>
@@ -243,7 +272,10 @@ Action Mailer 3.0 makes inline attachments, which involved a lot of hacking in p
#### Sending Email To Multiple Recipients
-It is possible to send email to one or more recipients in one email (e.g., informing all admins of a new signup) by setting the list of emails to the `:to` key. The list of emails can be an array of email addresses or a single string with the addresses separated by commas.
+It is possible to send email to one or more recipients in one email (e.g.,
+informing all admins of a new signup) by setting the list of emails to the `:to`
+key. The list of emails can be an array of email addresses or a single string
+with the addresses separated by commas.
```ruby
class AdminMailer < ActionMailer::Base
@@ -257,12 +289,14 @@ class AdminMailer < ActionMailer::Base
end
```
-The same format can be used to set carbon copy (Cc:) and blind carbon copy (Bcc:) recipients, by using the `:cc` and `:bcc` keys respectively.
+The same format can be used to set carbon copy (Cc:) and blind carbon copy
+(Bcc:) recipients, by using the `:cc` and `:bcc` keys respectively.
#### Sending Email With Name
-Sometimes you wish to show the name of the person instead of just their email address when they receive the email. The trick to doing that is
-to format the email address in the format `"Name <email>"`.
+Sometimes you wish to show the name of the person instead of just their email
+address when they receive the email. The trick to doing that is to format the
+email address in the format `"Full Name <email>"`.
```ruby
def welcome_email(user)
@@ -274,7 +308,11 @@ end
### Mailer Views
-Mailer views are located in the `app/views/name_of_mailer_class` directory. The specific mailer view is known to the class because its name is the same as the mailer method. In our example from above, our mailer view for the `welcome_email` method will be in `app/views/user_mailer/welcome_email.html.erb` for the HTML version and `welcome_email.text.erb` for the plain text version.
+Mailer views are located in the `app/views/name_of_mailer_class` directory. The
+specific mailer view is known to the class because its name is the same as the
+mailer method. In our example from above, our mailer view for the
+`welcome_email` method will be in `app/views/user_mailer/welcome_email.html.erb`
+for the HTML version and `welcome_email.text.erb` for the plain text version.
To change the default mailer view for your action you do something like:
@@ -285,7 +323,7 @@ class UserMailer < ActionMailer::Base
def welcome_email(user)
@user = user
@url = 'http://example.com/login'
- mail(to: user.email,
+ mail(to: @user.email,
subject: 'Welcome to My Awesome Site',
template_path: 'notifications',
template_name: 'another')
@@ -293,9 +331,12 @@ class UserMailer < ActionMailer::Base
end
```
-In this case it will look for templates at `app/views/notifications` with name `another`.
+In this case it will look for templates at `app/views/notifications` with name
+`another`. You can also specify an array of paths for `template_path`, and they
+will be searched in order.
-If you want more flexibility you can also pass a block and render specific templates or even render inline or text without using a template file:
+If you want more flexibility you can also pass a block and render specific
+templates or even render inline or text without using a template file:
```ruby
class UserMailer < ActionMailer::Base
@@ -304,23 +345,28 @@ class UserMailer < ActionMailer::Base
def welcome_email(user)
@user = user
@url = 'http://example.com/login'
- mail(to: user.email,
+ mail(to: @user.email,
subject: 'Welcome to My Awesome Site') do |format|
format.html { render 'another_template' }
format.text { render text: 'Render text' }
end
end
-
end
```
-This will render the template 'another_template.html.erb' for the HTML part and use the rendered text for the text part. The render command is the same one used inside of Action Controller, so you can use all the same options, such as `:text`, `:inline` etc.
+This will render the template 'another_template.html.erb' for the HTML part and
+use the rendered text for the text part. The render command is the same one used
+inside of Action Controller, so you can use all the same options, such as
+`:text`, `:inline` etc.
### Action Mailer Layouts
-Just like controller views, you can also have mailer layouts. The layout name needs to be the same as your mailer, such as `user_mailer.html.erb` and `user_mailer.text.erb` to be automatically recognized by your mailer as a layout.
+Just like controller views, you can also have mailer layouts. The layout name
+needs to be the same as your mailer, such as `user_mailer.html.erb` and
+`user_mailer.text.erb` to be automatically recognized by your mailer as a
+layout.
-In order to use a different file just use:
+In order to use a different file, call `layout` in your mailer:
```ruby
class UserMailer < ActionMailer::Base
@@ -328,9 +374,11 @@ class UserMailer < ActionMailer::Base
end
```
-Just like with controller views, use `yield` to render the view inside the layout.
+Just like with controller views, use `yield` to render the view inside the
+layout.
-You can also pass in a `layout: 'layout_name'` option to the render call inside the format block to specify different layouts for different actions:
+You can also pass in a `layout: 'layout_name'` option to the render call inside
+the format block to specify different layouts for different formats:
```ruby
class UserMailer < ActionMailer::Base
@@ -343,87 +391,106 @@ class UserMailer < ActionMailer::Base
end
```
-Will render the HTML part using the `my_layout.html.erb` file and the text part with the usual `user_mailer.text.erb` file if it exists.
+Will render the HTML part using the `my_layout.html.erb` file and the text part
+with the usual `user_mailer.text.erb` file if it exists.
### Generating URLs in Action Mailer Views
-URLs can be generated in mailer views using `url_for` or named routes.
+Unlike controllers, the mailer instance doesn't have any context about the
+incoming request so you'll need to provide the `:host` parameter yourself.
-Unlike controllers, the mailer instance doesn't have any context about the incoming request so you'll need to provide the `:host`, `:controller`, and `:action`:
+As the `:host` usually is consistent across the application you can configure it
+globally in `config/application.rb`:
-```erb
-<%= url_for(host: 'example.com',
- controller: 'welcome',
- action: 'greeting') %>
+```ruby
+config.action_mailer.default_url_options = { host: 'example.com' }
```
-When using named routes you only need to supply the `:host`:
+#### generating URLs with `url_for`
+
+You need to pass the `only_path: false` option when using `url_for`. This will
+ensure that absolute URLs are generated because the `url_for` view helper will,
+by default, generate relative URLs when a `:host` option isn't explicitly
+provided.
```erb
-<%= user_url(@user, host: 'example.com') %>
+<%= url_for(controller: 'welcome',
+ action: 'greeting',
+ only_path: false) %>
```
-Email clients have no web context and so paths have no base URL to form complete web addresses. Thus, when using named routes only the "_url" variant makes sense.
+If you did not configure the `:host` option globally make sure to pass it to
+`url_for`.
-It is also possible to set a default host that will be used in all mailers by setting the `:host` option as a configuration option in `config/application.rb`:
-```ruby
-config.action_mailer.default_url_options = { host: 'example.com' }
+```erb
+<%= url_for(host: 'example.com',
+ controller: 'welcome',
+ action: 'greeting') %>
```
-If you use this setting, you should pass the `only_path: false` option when using `url_for`. This will ensure that absolute URLs are generated because the `url_for` view helper will, by default, generate relative URLs when a `:host` option isn't explicitly provided.
+NOTE: When you explicitly pass the `:host` Rails will always generate absolute
+URLs, so there is no need to pass `only_path: false`.
-### Sending Multipart Emails
+#### generating URLs with named routes
-Action Mailer will automatically send multipart emails if you have different templates for the same action. So, for our UserMailer example, if you have `welcome_email.text.erb` and `welcome_email.html.erb` in `app/views/user_mailer`, Action Mailer will automatically send a multipart email with the HTML and text versions setup as different parts.
+Email clients have no web context and so paths have no base URL to form complete
+web addresses. Thus, you should always use the "_url" variant of named route
+helpers.
-The order of the parts getting inserted is determined by the `:parts_order` inside of the `ActionMailer::Base.default` method. If you want to explicitly alter the order, you can either change the `:parts_order` or explicitly render the parts in a different order:
+If you did not configure the `:host` option globally make sure to pass it to the
+url helper.
-```ruby
-class UserMailer < ActionMailer::Base
- def welcome_email(user)
- @user = user
- @url = user_url(@user)
- mail(to: user.email,
- subject: 'Welcome to My Awesome Site') do |format|
- format.html
- format.text
- end
- end
-end
+```erb
+<%= user_url(@user, host: 'example.com') %>
```
-Will put the HTML part first, and the plain text part second.
+### Sending Multipart Emails
+
+Action Mailer will automatically send multipart emails if you have different
+templates for the same action. So, for our UserMailer example, if you have
+`welcome_email.text.erb` and `welcome_email.html.erb` in
+`app/views/user_mailer`, Action Mailer will automatically send a multipart email
+with the HTML and text versions setup as different parts.
+
+The order of the parts getting inserted is determined by the `:parts_order`
+inside of the `ActionMailer::Base.default` method.
-### Sending Emails with Attachments
+### Sending Emails with Dynamic Delivery Options
-Attachments can be added by using the `attachments` method:
+If you wish to override the default delivery options (e.g. SMTP credentials)
+while delivering emails, you can do this using `delivery_method_options` in the
+mailer action.
```ruby
class UserMailer < ActionMailer::Base
- def welcome_email(user)
+ def welcome_email(user, company)
@user = user
@url = user_url(@user)
- attachments['terms.pdf'] = File.read('/path/terms.pdf')
- mail(to: user.email,
- subject: 'Please see the Terms and Conditions attached')
+ delivery_options = { user_name: company.smtp_user,
+ password: company.smtp_password,
+ address: company.smtp_host }
+ mail(to: @user.email,
+ subject: "Please see the Terms and Conditions attached",
+ delivery_method_options: delivery_options)
end
end
```
-The above will send a multipart email with an attachment, properly nested with the top level being `multipart/mixed` and the first part being a `multipart/alternative` containing the plain text and HTML email messages.
+### Sending Emails without Template Rendering
-#### Sending Emails with Dynamic Delivery Options
-
-If you wish to override the default delivery options (e.g. SMTP credentials) while delivering emails, you can do this using `delivery_method_options` in the mailer action.
+There may be cases in which you want to skip the template rendering step and
+supply the email body as a string. You can achieve this using the `:body`
+option. In such cases don't forget to add the `:content_type` option. Rails
+will default to `text/plain` otherwise.
```ruby
class UserMailer < ActionMailer::Base
- def welcome_email(user,company)
- @user = user
- @url = user_url(@user)
- delivery_options = { user_name: company.smtp_user, password: company.smtp_password, address: company.smtp_host }
- mail(to: user.email, subject: "Please see the Terms and Conditions attached", delivery_method_options: delivery_options)
+ def welcome_email(user, email_body)
+ mail(to: user.email,
+ body: email_body,
+ content_type: "text/html",
+ subject: "Already rendered!")
end
end
```
@@ -431,18 +498,26 @@ end
Receiving Emails
----------------
-Receiving and parsing emails with Action Mailer can be a rather complex endeavor. Before your email reaches your Rails app, you would have had to configure your system to somehow forward emails to your app, which needs to be listening for that. So, to receive emails in your Rails app you'll need to:
+Receiving and parsing emails with Action Mailer can be a rather complex
+endeavor. Before your email reaches your Rails app, you would have had to
+configure your system to somehow forward emails to your app, which needs to be
+listening for that. So, to receive emails in your Rails app you'll need to:
* Implement a `receive` method in your mailer.
-* Configure your email server to forward emails from the address(es) you would like your app to receive to `/path/to/app/script/rails runner 'UserMailer.receive(STDIN.read)'`.
+* Configure your email server to forward emails from the address(es) you would
+ like your app to receive to `/path/to/app/bin/rails runner
+ 'UserMailer.receive(STDIN.read)'`.
-Once a method called `receive` is defined in any mailer, Action Mailer will parse the raw incoming email into an email object, decode it, instantiate a new mailer, and pass the email object to the mailer `receive` instance method. Here's an example:
+Once a method called `receive` is defined in any mailer, Action Mailer will
+parse the raw incoming email into an email object, decode it, instantiate a new
+mailer, and pass the email object to the mailer `receive` instance
+method. Here's an example:
```ruby
class UserMailer < ActionMailer::Base
def receive(email)
- page = Page.find_by_address(email.to.first)
+ page = Page.find_by(address: email.to.first)
page.emails.create(
subject: email.subject,
body: email.body
@@ -460,32 +535,95 @@ class UserMailer < ActionMailer::Base
end
```
+Action Mailer Callbacks
+---------------------------
+
+Action Mailer allows for you to specify a `before_action`, `after_action` and
+`around_action`.
+
+* Filters can be specified with a block or a symbol to a method in the mailer
+ class similar to controllers.
+
+* You could use a `before_action` to populate the mail object with defaults,
+ delivery_method_options or insert default headers and attachments.
+
+* You could use an `after_action` to do similar setup as a `before_action` but
+ using instance variables set in your mailer action.
+
+```ruby
+class UserMailer < ActionMailer::Base
+ after_action :set_delivery_options,
+ :prevent_delivery_to_guests,
+ :set_business_headers
+
+ def feedback_message(business, user)
+ @business = business
+ @user = user
+ mail
+ end
+
+ def campaign_message(business, user)
+ @business = business
+ @user = user
+ end
+
+ private
+
+ def set_delivery_options
+ # You have access to the mail instance,
+ # @business and @user instance variables here
+ if @business && @business.has_smtp_settings?
+ mail.delivery_method.settings.merge!(@business.smtp_settings)
+ end
+ end
+
+ def prevent_delivery_to_guests
+ if @user && @user.guest?
+ mail.perform_deliveries = false
+ end
+ end
+
+ def set_business_headers
+ if @business
+ headers["X-SMTPAPI-CATEGORY"] = @business.code
+ end
+ end
+end
+```
+
+* Mailer Filters abort further processing if body is set to a non-nil value.
+
Using Action Mailer Helpers
---------------------------
-Action Mailer now just inherits from Abstract Controller, so you have access to the same generic helpers as you do in Action Controller.
+Action Mailer now just inherits from `AbstractController`, so you have access to
+the same generic helpers as you do in Action Controller.
Action Mailer Configuration
---------------------------
-The following configuration options are best made in one of the environment files (environment.rb, production.rb, etc...)
+The following configuration options are best made in one of the environment
+files (environment.rb, production.rb, etc...)
| Configuration | Description |
|---------------|-------------|
-|`template_root`|Determines the base from which template references will be made.|
|`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 `:smtp` (default), `:sendmail`, `:file` and `:test`.|
|`perform_deliveries`|Determines whether deliveries are actually carried out when the `deliver` method is invoked on the Mail message. By default they are, but this can be turned off to help functional testing.|
|`deliveries`|Keeps an array of all the emails sent out through the Action Mailer with delivery_method :test. Most useful for unit and functional testing.|
|`default_options`|Allows you to set default values for the `mail` method options (`:from`, `:reply_to`, etc.).|
-|`async`|Setting this flag will turn on asynchronous message sending, message rendering and delivery will be pushed to `Rails.queue` for processing.|
+
+For a complete writeup of possible configurations see the
+[Action Mailer section](configuring.html#configuring-action-mailer) in
+our Configuring Rails Applications guide.
### Example Action Mailer Configuration
-An example would be adding the following to your appropriate `config/environments/$RAILS_ENV.rb` file:
+An example would be adding the following to your appropriate
+`config/environments/$RAILS_ENV.rb` file:
```ruby
config.action_mailer.delivery_method = :sendmail
@@ -496,19 +634,20 @@ config.action_mailer.delivery_method = :sendmail
# }
config.action_mailer.perform_deliveries = true
config.action_mailer.raise_delivery_errors = true
-config.action_mailer.default_options = {from: 'no-replay@example.org'}
+config.action_mailer.default_options = {from: 'no-reply@example.com'}
```
-### Action Mailer Configuration for GMail
+### Action Mailer Configuration for Gmail
-As Action Mailer now uses the Mail gem, this becomes as simple as adding to your `config/environments/$RAILS_ENV.rb` file:
+As Action Mailer now uses the Mail gem, this becomes as simple as adding to your
+`config/environments/$RAILS_ENV.rb` file:
```ruby
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: 'smtp.gmail.com',
port: 587,
- domain: 'baci.lindsaar.net',
+ domain: 'example.com',
user_name: '<username>',
password: '<password>',
authentication: 'plain',
@@ -518,49 +657,33 @@ config.action_mailer.smtp_settings = {
Mailer Testing
--------------
-By default Action Mailer does not send emails in the test environment. They are just added to the `ActionMailer::Base.deliveries` array.
+You can find detailed instructions on how to test your mailers in the
+[testing guide](testing.html#testing-your-mailers).
-Testing mailers normally involves two things: One is that the mail was queued, and the other one that the email is correct. With that in mind, we could test our example mailer from above like so:
+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
+right before they are handed to the delivery agents.
```ruby
-class UserMailerTest < ActionMailer::TestCase
- def test_welcome_email
- user = users(:some_user_in_your_fixtures)
-
- # Send the email, then test that it got queued
- email = UserMailer.welcome_email(user).deliver
- assert !ActionMailer::Base.deliveries.empty?
-
- # Test the body of the sent email contains what we expect it to
- assert_equal [user.email], email.to
- assert_equal 'Welcome to My Awesome Site', email.subject
- assert_match "<h1>Welcome to example.com, #{user.name}</h1>", email.body.to_s
- assert_match 'you have joined to example.com community', email.body.to_s
+class SandboxEmailInterceptor
+ def self.delivering_email(message)
+ message.to = ['sandbox@example.com']
end
end
```
-In the test we send the email and store the returned object in the `email` variable. We then ensure that it was sent (the first assert), then, in the second batch of assertions, we ensure that the email does indeed contain what we expect.
-
-Asynchronous
-------------
-
-Rails provides a Synchronous Queue by default. If you want to use an Asynchronous one you will need to configure an async Queue provider like Resque. Queue providers are supposed to have a Railtie where they configure it's own async queue.
-
-### Custom Queues
-
-If you need a different queue than `Rails.queue` for your mailer you can use `ActionMailer::Base.queue=`:
-
-```ruby
-class WelcomeMailer < ActionMailer::Base
- self.queue = MyQueue.new
-end
-```
-
-or adding to your `config/environments/$RAILS_ENV.rb`:
+Before the interceptor can do its job you need to register it with the Action
+Mailer framework. You can do this in an initializer file
+`config/initializers/sandbox_email_interceptor.rb`
```ruby
-config.action_mailer.queue = MyQueue.new
+ActionMailer::Base.register_interceptor(SandboxEmailInterceptor) if Rails.env.staging?
```
-Your custom queue should expect a job that responds to `#run`.
+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)
+for more information about custom Rails environments.
diff --git a/guides/source/action_view_overview.md b/guides/source/action_view_overview.md
index cec7e5335b..d19dd11181 100644
--- a/guides/source/action_view_overview.md
+++ b/guides/source/action_view_overview.md
@@ -1,13 +1,13 @@
Action View Overview
====================
-In this guide you will learn:
+After reading this guide, you will know:
-* What Action View is, and how to use it with Rails
-* How to use Action View outside of Rails
-* How best to use templates, partials, and layouts
-* What helpers are provided by Action View, and how to make your own
-* How to use localized views
+* What Action View is and how to use it with Rails.
+* How best to use templates, partials, and layouts.
+* What helpers are provided by Action View and how to make your own.
+* How to use localized views.
+* How to use Action View outside of Rails.
--------------------------------------------------------------------------------
@@ -18,7 +18,7 @@ Action View and Action Controller are the two major components of Action Pack. I
Action View templates are written using embedded Ruby in tags mingled with HTML. To avoid cluttering the templates with boilerplate code, a number of helper classes provide common behavior for forms, dates, and strings. It's also easy to add new helpers to your application as it evolves.
-NOTE. Some features of Action View are tied to Active Record, but that doesn't mean that Action View depends on Active Record. Action View is an independent package that can be used with any sort of backend.
+NOTE: Some features of Action View are tied to Active Record, but that doesn't mean Action View depends on Active Record. Action View is an independent package that can be used with any sort of Ruby libraries.
Using Action View with Rails
----------------------------
@@ -44,82 +44,14 @@ $ rails generate scaffold post
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.
-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 this three components.
+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.
-Using Action View outside of Rails
-----------------------------------
-
-Action View works well with Action Record, but it can also be used with other Ruby tools. We can demonstrate this by creating a small [Rack](http://rack.rubyforge.org/) application that includes Action View functionality. This may be useful, for example, if you'd like access to Action View's helpers in a Rack application.
-
-Let's start by ensuring that you have the Action Pack and Rack gems installed:
-
-```bash
-$ gem install actionpack
-$ gem install rack
-```
-
-Now we'll create a simple "Hello World" application that uses the `titleize` method provided by Active Support.
-
-**hello_world.rb:**
-
-```ruby
-require 'active_support/core_ext/string/inflections'
-require 'rack'
-
-def hello_world(env)
- [200, {"Content-Type" => "text/html"}, "hello world".titleize]
-end
-
-Rack::Handler::Mongrel.run method(:hello_world), :Port => 4567
-```
-
-We can see this all come together by starting up the application and then visiting `http://localhost:4567/`
-
-```bash
-$ ruby hello_world.rb
-```
-
-TODO needs a screenshot? I have one - not sure where to put it.
-
-Notice how 'hello world' has been converted into 'Hello World' by the `titleize` helper method.
-
-Action View can also be used with [Sinatra](http://www.sinatrarb.com/) in the same way.
-
-Let's start by ensuring that you have the Action Pack and Sinatra gems installed:
-
-```bash
-$ gem install actionpack
-$ gem install sinatra
-```
-
-Now we'll create the same "Hello World" application in Sinatra.
-
-**hello_world.rb:**
-
-```ruby
-require 'action_view'
-require 'sinatra'
-
-get '/' do
- erb 'hello world'.titleize
-end
-```
-
-Then, we can run the application:
-
-```bash
-$ ruby hello_world.rb
-```
-
-Once the application is running, you can see Sinatra and Action View working together by visiting `http://localhost:4567/`
-
-TODO needs a screenshot? I have one - not sure where to put it.
Templates, Partials and Layouts
-------------------------------
As mentioned before, the final HTML output is a composition of three Rails elements: `Templates`, `Partials` and `Layouts`.
-Find below a brief overview of each one of them.
+Below is a brief overview of each one of them.
### Templates
@@ -129,18 +61,18 @@ Rails supports multiple template systems and uses a file extension to distinguis
#### ERB
-Within an ERB template Ruby code can be included using both `<% %>` and `<%= %>` tags. The `<% %>` are used to execute Ruby code that does not return anything, such as conditions, loops or blocks, and the `<%= %>` tags are used when you want output.
+Within an ERB template, Ruby code can be included using both `<% %>` and `<%= %>` tags. The `<% %>` tags are used to execute Ruby code that does not return anything, such as conditions, loops or blocks, and the `<%= %>` tags are used when you want output.
Consider the following loop for names:
```html+erb
-<b>Names of all the people</b>
+<h1>Names of all the people</h1>
<% @people.each do |person| %>
- Name: <%= person.name %><br/>
+ Name: <%= person.name %><br>
<% end %>
```
-The loop is setup in regular embedding tags `<% %>` and the name is written using the output embedding tag `<%= %>`. Note that this is not just a usage suggestion, for Regular output functions like print or puts won't work with ERB templates. So this would be wrong:
+The loop is set up in regular embedding tags (`<% %>`) and the name is written using the output embedding tags (`<%= %>`). Note that this is not just a usage suggestion, for regular output functions like `print` or `puts` won't work with ERB templates. So this would be wrong:
```html+erb
<%# WRONG %>
@@ -158,11 +90,11 @@ Here are some basic examples:
```ruby
xml.em("emphasized")
xml.em { xml.b("emph & bold") }
-xml.a("A Link", "href"=>"http://rubyonrails.org")
-xml.target("name"=>"compile", "option"=>"fast")
+xml.a("A Link", "href" => "http://rubyonrails.org")
+xml.target("name" => "compile", "option" => "fast")
```
-will produce
+which would produce:
```html
<em>emphasized</em>
@@ -189,7 +121,7 @@ would produce something like:
</div>
```
-A full-length RSS example actually used on Basecamp:
+Below is a full-length RSS example actually used on Basecamp:
```ruby
xml.rss("version" => "2.0", "xmlns:dc" => "http://purl.org/dc/elements/1.1/") do
@@ -220,7 +152,7 @@ By default, Rails will compile each template to a method in order to render it.
### Partials
-Partial templates – usually just called "partials" – are another device for breaking the rendering process into more manageable chunks. With a partial, you can move the code for rendering a particular piece of a response to its own file.
+Partial templates - usually just called "partials" - are another device for breaking the rendering process into more manageable chunks. With partials, you can extract pieces of code from your templates to separate files and also reuse them throughout your templates.
#### Naming Partials
@@ -230,7 +162,7 @@ To render a partial as part of a view, you use the `render` method within the vi
<%= render "menu" %>
```
-This will render a file named `_menu.html.erb` at that point within the view is being rendered. Note the leading underscore character: partials are named with a leading underscore to distinguish them from regular views, even though they are referred to without the underscore. This holds true even when you're pulling in a partial from another folder:
+This will render a file named `_menu.html.erb` at that point within the view that is being rendered. Note the leading underscore character: partials are named with a leading underscore to distinguish them from regular views, even though they are referred to without the underscore. This holds true even when you're pulling in a partial from another folder:
```erb
<%= render "shared/menu" %>
@@ -240,7 +172,7 @@ That code will pull in the partial from `app/views/shared/_menu.html.erb`.
#### Using Partials to simplify Views
-One way to use partials is to treat them as the equivalent of subroutines: as a way to move details out of a view so that you can grasp what's going on more easily. For example, you might have a view that looked like this:
+One way to use partials is to treat them as the equivalent of subroutines; a way to move details out of a view so that you can grasp what's going on more easily. For example, you might have a view that looks like this:
```html+erb
<%= render "shared/ad_banner" %>
@@ -249,7 +181,7 @@ One way to use partials is to treat them as the equivalent of subroutines: as a
<p>Here are a few of our fine products:</p>
<% @products.each do |product| %>
- <%= render :partial => "product", :locals => { :product => product } %>
+ <%= render partial: "product", locals: {product: product} %>
<% end %>
<%= render "shared/footer" %>
@@ -257,100 +189,108 @@ One way to use partials is to treat them as the equivalent of subroutines: as a
Here, the `_ad_banner.html.erb` and `_footer.html.erb` partials could contain content that is shared among many pages in your application. You don't need to see the details of these sections when you're concentrating on a particular page.
-#### The :as and :object options
+#### The `as` and `object` options
-By default `ActionView::Partials::PartialRenderer` has its object in a local variable with the same name as the template. So, given
+By default `ActionView::Partials::PartialRenderer` has its object in a local variable with the same name as the template. So, given:
```erb
-<%= render :partial => "product" %>
+<%= render partial: "product" %>
```
within product we'll get `@product` in the local variable `product`, as if we had written:
```erb
-<%= render :partial => "product", :locals => { :product => @product } %>
+<%= render partial: "product", locals: {product: @product} %>
```
-With the `:as` option we can specify a different name for said local variable. For example, if we wanted it to be `item` instead of product+ we'd do:
+With the `as` option we can specify a different name for the local variable. For example, if we wanted it to be `item` instead of `product` we would do:
```erb
-<%= render :partial => "product", :as => 'item' %>
+<%= render partial: "product", as: "item" %>
```
-The `:object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere, in a different ivar or in a local variable for instance.
+The `object` option can be used to directly specify which object is rendered into the partial; useful when the template's object is elsewhere (eg. in a different instance variable or in a local variable).
For example, instead of:
```erb
-<%= render :partial => "product", :locals => { :product => @item } %>
+<%= render partial: "product", locals: {product: @item} %>
```
-you'd do:
+we would do:
```erb
-<%= render :partial => "product", :object => @item %>
+<%= render partial: "product", object: @item %>
```
-The `:object` and `:as` options can be used together.
+The `object` and `as` options can also be used together:
+
+```erb
+<%= render partial: "product", object: @item, as: "item" %>
+```
#### Rendering Collections
-The example of partial use describes a familiar pattern where a template needs to iterate over an array and render a sub template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial by the same name as the elements contained within.
-So the three-lined example for rendering all the products can be rewritten with a single line:
+It is very common that a template needs to iterate over a collection and render a sub-template for each of the elements. This pattern has been implemented as a single method that accepts an array and renders a partial for each one of the elements in the array.
+
+So this example for rendering all the products:
+
+```erb
+<% @products.each do |product| %>
+ <%= render partial: "product", locals: { product: product } %>
+<% end %>
+```
+
+can be rewritten in a single line:
```erb
-<%= render :partial => "product", :collection => @products %>
+<%= render partial: "product", collection: @products %>
```
-When a partial is called with a pluralized collection, then the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product` , and within the `_product` partial, you can refer to `product` to get the instance that is being rendered.
+When a partial is called like this (eg. with a collection), the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product`, and within it you can refer to `product` to get the instance that is being rendered.
-You can use a shorthand syntax for rendering collections. Assuming @products is a collection of `Product` instances, you can simply write the following to produce the same result:
+You can use a shorthand syntax for rendering collections. Assuming `@products` is a collection of `Product` instances, you can simply write the following to produce the same result:
```erb
<%= render @products %>
```
-Rails determines the name of the partial to use by looking at the model name in the collection. In fact, you can even create a heterogeneous collection and render it this way, and Rails will choose the proper partial for each member of the collection.
+Rails determines the name of the partial to use by looking at the model name in the collection, `Product` in this case. In fact, you can even create a heterogeneous collection and render it this way, and Rails will choose the proper partial for each member of the collection.
#### Spacer Templates
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:
```erb
-<%= render @products, :spacer_template => "product_ruler" %>
+<%= render @products, spacer_template: "product_ruler" %>
```
-Rails will render the `_product_ruler` partial (with no data passed in to it) between each pair of `_product` partials.
+Rails will render the `_product_ruler` partial (with no data passed to it) between each pair of `_product` partials.
### Layouts
-TODO...
-
-Using Templates, Partials and Layouts in "The Rails Way"
---------------------------------------------------------
-
-TODO...
+Layouts can be used to render a common view template around the results of Rails controller actions. Typically, every Rails application has a couple of overall layouts that most pages are rendered within. For example, a site might have a layout for a logged in user, and a layout for the marketing or sales side of the site. The logged in user layout might include top-level navigation that should be present across many controller actions. The sales layout for a SaaS app might include top-level navigation for things like "Pricing" and "Contact Us." You would expect each layout to have a different look and feel. You can read more details about Layouts in the [Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
Partial Layouts
---------------
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 where it should be wrapped in a `div` for display purposes. First, we'll create a new `Post`:
+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`:
```ruby
-Post.create(:body => 'Partial Layouts are cool!')
+Post.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 `_post` partial wrapped in the `box` layout:
**posts/show.html.erb**
```erb
-<%= render :partial => 'post', :layout => 'box', :locals => {:post => @post} %>
+<%= render partial: 'post', layout: 'box', locals: {post: @post} %>
```
-The `box` layout simply wraps the `post` partial in a `div`:
+The `box` layout simply wraps the `_post` partial in a `div`:
**posts/_box.html.erb**
@@ -360,7 +300,7 @@ 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 `_post` partial wraps the post's `body` in a `div` with the `id` of the post using the `div_for` helper:
**posts/_post.html.erb**
@@ -370,7 +310,7 @@ The `post` partial wraps the post's `body` in a `div` with the `id` of the post
<% end %>
```
-This example would output the following:
+this would output the following:
```html
<div class='box'>
@@ -382,84 +322,31 @@ This example would output the following:
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.
-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 `_post` partial, we could do this instead:
**posts/show.html.erb**
```html+erb
-<% render(:layout => 'box', :locals => {:post => @post}) do %>
+<% render(layout: 'box', locals: {post: @post}) do %>
<%= div_for(post) do %>
<p><%= post.body %></p>
<% end %>
<% end %>
```
-If we're using the same `box` partial from above, his would produce the same output as the previous example.
+Supposing we use the same `_box` partial from above, this would produce the same output as the previous example.
View Paths
----------
TODO...
-Overview of all the helpers provided by Action View
----------------------------------------------------
-
-The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the API Documentation, which covers all of the helpers in more detail, but this should serve as a good starting point.
-
-### ActiveRecordHelper
-
-The Active Record Helper makes it easier to create forms for records kept in instance variables. You may also want to review the [Rails Form helpers guide](form_helpers.html).
-
-#### error_message_on
-
-Returns a string containing the error message attached to the method on the object if one exists.
-
-```ruby
-error_message_on "post", "title"
-```
-
-#### error_messages_for
-
-Returns a string with a DIV containing all of the error messages for the objects located as instance variables by the names given.
-
-```ruby
-error_messages_for "post"
-```
-
-#### form
-
-Returns a form with inputs for all attributes of the specified Active Record object. For example, let's say we have a `@post` with attributes named `title` of type `String` and `body` of type `Text`. Calling `form` would produce a form to creating a new post with inputs for those attributes.
-
-```ruby
-form("post")
-```
-
-```html
-<form action='/posts/create' method='post'>
- <p>
- <label for="post_title">Title</label><br />
- <input id="post_title" name="post[title]" type="text" value="Hello World" />
- </p>
- <p>
- <label for="post_body">Body</label><br />
- <textarea id="post_body" name="post[body]"></textarea>
- </p>
- <input name="commit" type="submit" value="Create" />
-</form>
-```
-
-Typically, `form_for` is used instead of `form` because it doesn't automatically include all of the model's attributes.
+Overview of helpers provided by Action View
+-------------------------------------------
-#### input
+WIP: Not all the helpers are listed here. For a full list see the [API documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html)
-Returns a default input tag for the type of object returned by the method.
-
-For example, if `@post` has an attribute `title` mapped to a `String` column that holds "Hello World":
-
-```ruby
-input("post", "title") # =>
- <input id="post_title" name="post[title]" type="text" value="Hello World" />
-```
+The following is only a brief overview summary of the helpers available in Action View. It's recommended that you review the [API Documentation](http://api.rubyonrails.org/classes/ActionView/Helpers.html), which covers all of the helpers in more detail, but this should serve as a good starting point.
### RecordTagHelper
@@ -488,7 +375,7 @@ 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 %>
+<%= content_tag_for(:tr, @post, class: "frontpage") do %>
<td><%= @post.title %></td>
<% end %>
```
@@ -525,7 +412,7 @@ 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 %>
+<%= div_for(@post, class: "frontpage") do %>
<td><%= @post.title %></td>
<% end %>
```
@@ -554,7 +441,7 @@ image_tag("rails.png") # => <img src="http://assets.example.com/images/rails.png
Register one or more JavaScript files to be included when symbol is passed to javascript_include_tag. This method is typically intended to be called from plugin initialization to register JavaScript files that the plugin installed in `vendor/assets/javascripts`.
```ruby
-ActionView::Helpers::AssetTagHelper.register_javascript_expansion :monkey => ["head", "body", "tail"]
+ActionView::Helpers::AssetTagHelper.register_javascript_expansion monkey: ["head", "body", "tail"]
javascript_include_tag :monkey # =>
<script src="/assets/head.js"></script>
@@ -567,7 +454,7 @@ javascript_include_tag :monkey # =>
Register one or more stylesheet files to be included when symbol is passed to `stylesheet_link_tag`. This method is typically intended to be called from plugin initialization to register stylesheet files that the plugin installed in `vendor/assets/stylesheets`.
```ruby
-ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion :monkey => ["head", "body", "tail"]
+ActionView::Helpers::AssetTagHelper.register_stylesheet_expansion monkey: ["head", "body", "tail"]
stylesheet_link_tag :monkey # =>
<link href="/assets/head.css" media="screen" rel="stylesheet" />
@@ -580,7 +467,7 @@ stylesheet_link_tag :monkey # =>
Returns a link tag that browsers and news readers can use to auto-detect an RSS or Atom feed.
```ruby
-auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {:title => "RSS Feed"}) # =>
+auto_discovery_link_tag(:rss, "http://www.example.com/feed.rss", {title: "RSS Feed"}) # =>
<link rel="alternate" type="application/rss+xml" title="RSS Feed" href="http://www.example.com/feed" />
```
@@ -600,7 +487,7 @@ image_path("edit.png") # => /assets/edit-2d1a2db63fc738690021fedb5a65b68e.png
#### image_url
-Computes the url to an image asset in the `app/asset/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
+Computes the url to an image asset in the `app/assets/images` directory. This will call `image_path` internally and merge with your current host or your asset host.
```ruby
image_url("edit.png") # => http://www.example.com/assets/edit.png
@@ -637,7 +524,7 @@ javascript_include_tag :all
You can also cache multiple JavaScript files into one file, which requires less HTTP connections to download and can better be compressed by gzip (leading to faster transfers). Caching will only happen if `ActionController::Base.perform_caching` is set to true (which is the case by default for the Rails production environment, but not for the development environment).
```ruby
-javascript_include_tag :all, :cache => true # =>
+javascript_include_tag :all, cache: true # =>
<script src="/javascripts/all.js"></script>
```
@@ -674,7 +561,7 @@ stylesheet_link_tag :all
You can also cache multiple stylesheets into one file, which requires less HTTP connections and can better be compressed by gzip (leading to faster transfers). Caching will only happen if ActionController::Base.perform_caching is set to true (which is the case by default for the Rails production environment, but not for the development environment).
```ruby
-stylesheet_link_tag :all, :cache => true
+stylesheet_link_tag :all, cache: true
# => <link href="/assets/all.css" media="screen" rel="stylesheet" />
```
@@ -729,7 +616,7 @@ atom_feed do |feed|
@posts.each do |post|
feed.entry(post) do |entry|
entry.title(post.title)
- entry.content(post.body, :type => 'html')
+ entry.content(post.body, type: 'html')
entry.author do |author|
author.name(post.author_name)
@@ -844,7 +731,7 @@ Reports the approximate distance in time between two Time or Date objects or int
```ruby
distance_of_time_in_words(Time.now, Time.now + 15.seconds) # => less than a minute
-distance_of_time_in_words(Time.now, Time.now + 15.seconds, :include_seconds => true) # => less than 20 seconds
+distance_of_time_in_words(Time.now, Time.now + 15.seconds, include_seconds: true) # => less than 20 seconds
```
#### select_date
@@ -888,8 +775,8 @@ select_day(5)
Returns a select tag with options for each of the hours 0 through 23 with the current hour selected.
```ruby
-# Generates a select field for minutes that defaults to the minutes for the time provided
-select_minute(Time.now + 6.hours)
+# Generates a select field for hours that defaults to the hours for the time provided
+select_hour(Time.now + 6.hours)
```
#### select_minute
@@ -937,7 +824,7 @@ Returns a select tag with options for each of the five years on each side of the
select_year(Date.today)
# Generates a select field from 1900 to 2009 that defaults to the current year
-select_year(Date.today, :start_year => 1900, :end_year => 2009)
+select_year(Date.today, start_year: 1900, end_year: 2009)
```
#### time_ago_in_words
@@ -945,7 +832,7 @@ select_year(Date.today, :start_year => 1900, :end_year => 2009)
Like `distance_of_time_in_words`, but where `to_time` is fixed to `Time.now`.
```ruby
-time_ago_in_words(3.minutes.from_now) # => 3 minutes
+time_ago_in_words(3.minutes.from_now) # => 3 minutes
```
#### time_select
@@ -987,7 +874,7 @@ The core method of this helper, form_for, gives you the ability to create a form
```html+erb
# Note: a @person variable will have been created in the controller (e.g. @person = Person.new)
-<%= form_for @person, :url => { :action => "create" } do |f| %>
+<%= form_for @person, url: {action: "create"} do |f| %>
<%= f.text_field :first_name %>
<%= f.text_field :last_name %>
<%= submit_tag 'Create' %>
@@ -997,7 +884,7 @@ The core method of this helper, form_for, gives you the ability to create a form
The HTML generated for this would be:
```html
-<form action="/persons/create" method="post">
+<form action="/people/create" method="post">
<input id="person_first_name" name="person[first_name]" type="text" />
<input id="person_last_name" name="person[last_name]" type="text" />
<input name="commit" type="submit" value="Create" />
@@ -1007,7 +894,7 @@ The HTML generated for this would be:
The params object created when this form is submitted would look like:
```ruby
-{"action"=>"create", "controller"=>"persons", "person"=>{"first_name"=>"William", "last_name"=>"Smith"}}
+{"action" => "create", "controller" => "people", "person" => {"first_name" => "William", "last_name" => "Smith"}}
```
The params hash has a nested person value, which can therefore be accessed with params[:person] in the controller.
@@ -1028,7 +915,7 @@ check_box("post", "validated")
Creates a scope around a specific model object like form_for, but doesn't create the form tags themselves. This makes fields_for suitable for specifying additional model objects in the same form:
```html+erb
-<%= form_for @person, :url => { :action => "update" } do |person_form| %>
+<%= form_for @person, url: {action: "update"} do |person_form| %>
First name: <%= person_form.text_field :first_name %>
Last name : <%= person_form.text_field :last_name %>
@@ -1054,9 +941,9 @@ Creates a form and a scope around a specific model object that is used as a base
```html+erb
<%= form_for @post do |f| %>
<%= f.label :title, 'Title' %>:
- <%= f.text_field :title %><br />
+ <%= f.text_field :title %><br>
<%= f.label :body, 'Body' %>:
- <%= f.text_area :body %><br />
+ <%= f.text_area :body %><br>
<% end %>
```
@@ -1104,7 +991,7 @@ radio_button("post", "category", "java")
Returns a textarea opening and closing tag set tailored for accessing a specified attribute.
```ruby
-text_area(:comment, :text, :size => "20x30")
+text_area(:comment, :text, size: "20x30")
# => <textarea cols="20" rows="30" id="comment_text" name="comment[text]">
# #{@comment.text}
# </textarea>
@@ -1119,6 +1006,24 @@ text_field(:post, :title)
# => <input type="text" id="post_title" name="post[title]" value="#{@post.title}" />
```
+#### email_field
+
+Returns an input tag of the "email" type tailored for accessing a specified attribute.
+
+```ruby
+email_field(:user, :email)
+# => <input type="email" id="user_email" name="user[email]" value="#{@user.email}" />
+```
+
+#### url_field
+
+Returns an input tag of the "url" type tailored for accessing a specified attribute.
+
+```ruby
+url_field(:user, :url)
+# => <input type="url" id="user_url" name="user[url]" value="#{@user.url}" />
+```
+
### FormOptionsHelper
Provides a number of methods for turning different kinds of containers into a set of option tags.
@@ -1145,7 +1050,7 @@ end
Sample usage (selecting the associated Author for an instance of Post, `@post`):
```ruby
-collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {:prompt => true})
+collection_select(:post, :author_id, Author.all, :id, :name_with_initial, {prompt: true})
```
If `@post.author_id` is 1, this would return:
@@ -1203,7 +1108,7 @@ Example object structure for use with this method:
```ruby
class Post < ActiveRecord::Base
- has_and_belongs_to_many :author
+ has_and_belongs_to_many :authors
end
class Author < ActiveRecord::Base
@@ -1317,7 +1222,7 @@ 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("post", "person_id", Person.all.collect {|p| [ p.name, p.id ] }, {include_blank: true})
```
If `@post.person_id` is 1, this would become:
@@ -1343,6 +1248,14 @@ Return select and option tags for the given object and method, using `time_zone_
time_zone_select( "user", "time_zone")
```
+#### date_field
+
+Returns an input tag of the "date" type tailored for accessing a specified attribute.
+
+```ruby
+date_field("user", "dob")
+```
+
### FormTagHelper
Provides a number of methods for creating form tags that doesn't rely on an Active Record object assigned to the template like FormHelper does. Instead, you provide the names and values manually.
@@ -1371,10 +1284,8 @@ Creates a field set for grouping HTML form elements.
Creates a file upload field.
-Prior to Rails 3.1, if you are using file uploads, then you will need to set the multipart option for the form tag. Rails 3.1+ does this automatically.
-
```html+erb
-<%= form_tag { :action => "post" }, { :multipart => true } do %>
+<%= form_tag {action: "post"}, {multipart: true} do %>
<label for="file">File to Upload</label> <%= file_field_tag "file" %>
<%= submit_tag %>
<% end %>
@@ -1479,6 +1390,33 @@ text_field_tag 'name'
# => <input id="name" name="name" type="text" />
```
+#### email_field_tag
+
+Creates a standard input field of email type.
+
+```ruby
+email_field_tag 'email'
+# => <input id="email" name="email" type="email" />
+```
+
+#### url_field_tag
+
+Creates a standard input field of url type.
+
+```ruby
+url_field_tag 'url'
+# => <input id="url" name="url" type="url" />
+```
+
+#### date_field_tag
+
+Creates a standard input field of date type.
+
+```ruby
+date_field_tag "dob"
+# => <input id="dob" name="dob" type="date" />
+```
+
### JavaScriptHelper
Provides functionality for working with JavaScript in your views.
@@ -1554,7 +1492,7 @@ number_to_human_size(1234567) # => 1.2 MB
Formats a number as a percentage string.
```ruby
-number_to_percentage(100, :precision => 0) # => 100%
+number_to_percentage(100, precision: 0) # => 100%
```
#### number_to_phone
@@ -1582,6 +1520,72 @@ number_with_precision(111.2345) # => 111.235
number_with_precision(111.2345, 2) # => 111.23
```
+### SanitizeHelper
+
+The SanitizeHelper module provides a set of methods for scrubbing text of undesired HTML elements.
+
+#### sanitize
+
+This sanitize helper will html encode all tags and strip all attributes that aren't specifically allowed.
+
+```ruby
+sanitize @article.body
+```
+
+If either the :attributes or :tags options are passed, only the mentioned tags and attributes are allowed and nothing else.
+
+```ruby
+sanitize @article.body, tags: %w(table tr td), attributes: %w(id class style)
+```
+
+To change defaults for multiple uses, for example adding table tags to the default:
+
+```ruby
+class Application < Rails::Application
+ config.action_view.sanitized_allowed_tags = 'table', 'tr', 'td'
+end
+```
+
+#### sanitize_css(style)
+
+Sanitizes a block of CSS code.
+
+#### strip_links(html)
+Strips all link tags from text leaving just the link text.
+
+```ruby
+strip_links("<a href="http://rubyonrails.org">Ruby on Rails</a>")
+# => Ruby on Rails
+```
+
+```ruby
+strip_links("emails to <a href="mailto:me@email.com">me@email.com</a>.")
+# => emails to me@email.com.
+```
+
+```ruby
+strip_links('Blog: <a href="http://myblog.com/">Visit</a>.')
+# => Blog: Visit.
+```
+
+#### strip_tags(html)
+
+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
+strip_tags("Strip <i>these</i> tags!")
+# => Strip these tags!
+```
+
+```ruby
+strip_tags("<b>Bold</b> no more! <a href='more.html'>See more</a>")
+# => Bold no more! See more
+```
+
+NB: The output may still contain unescaped '<', '>', '&' characters and confuse browsers.
+
+
Localized Views
---------------
@@ -1594,7 +1598,7 @@ You can use the same technique to localize the rescue files in your public direc
Since Rails doesn't restrict the symbols that you use to set I18n.locale, you can leverage this system to display different content depending on anything you like. For example, suppose you have some "expert" users that should see different pages from "normal" users. You could add the following to `app/controllers/application.rb`:
```ruby
-before_filter :set_expert_locale
+before_action :set_expert_locale
def set_expert_locale
I18n.locale = :expert if current_user.expert?
diff --git a/guides/source/active_model_basics.md b/guides/source/active_model_basics.md
index bfb088ed03..0019d08328 100644
--- a/guides/source/active_model_basics.md
+++ b/guides/source/active_model_basics.md
@@ -1,20 +1,20 @@
Active Model Basics
===================
-This guide should provide you with all you need to get started using model classes. Active Model allows for Action Pack helpers to interact with non-ActiveRecord models. Active Model also helps building custom ORMs for use outside of the Rails framework.
+This guide should provide you with all you need to get started using model classes. Active Model allows for Action Pack helpers to interact with non-Active Record models. Active Model also helps building custom ORMs for use outside of the Rails framework.
---------------------------------------------------------------------------------
+After reading this guide, you will know:
-WARNING. This guide is based on Rails 3.0. Some of the code shown here will not work in earlier versions of Rails.
+--------------------------------------------------------------------------------
Introduction
------------
-Active Model is a library containing various modules used in developing frameworks that need to interact with the Rails Action Pack library. Active Model provides a known set of interfaces for usage in classes. Some of modules are explained below.
+Active Model is a library containing various modules used in developing frameworks that need to interact with the Rails Action Pack library. Active Model provides a known set of interfaces for usage in classes. Some of modules are explained below.
### AttributeMethods
-The AttributeMethods module can add custom prefixes and suffixes on methods of a class. It is used by defining the prefixes and suffixes, which methods on the object will use them.
+The AttributeMethods module can add custom prefixes and suffixes on methods of a class. It is used by defining the prefixes and suffixes and which methods on the object will use them.
```ruby
class Person
@@ -26,28 +26,26 @@ class Person
attr_accessor :age
-private
- def reset_attribute(attribute)
- send("#{attribute}=", 0)
- end
+ private
+ def reset_attribute(attribute)
+ send("#{attribute}=", 0)
+ end
- def attribute_highest?(attribute)
- send(attribute) > 100 ? true : false
- end
-
+ def attribute_highest?(attribute)
+ send(attribute) > 100
+ end
end
person = Person.new
person.age = 110
person.age_highest? # true
person.reset_age # 0
-person.age_highest? # false
-
+person.age_highest? # false
```
### Callbacks
-Callbacks gives Active Record style callbacks. This provides the ability to define the callbacks and those will run at appropriate time. After defining a callbacks you can wrap with before, after and around custom methods.
+Callbacks gives Active Record style callbacks. This provides an ability to define callbacks which run at appropriate times. After defining callbacks, you can wrap them with before, after and around custom methods.
```ruby
class Person
@@ -59,19 +57,19 @@ class Person
def update
run_callbacks(:update) do
- # This will call when we are trying to call update on object.
+ # This method is called when update is called on an object.
end
end
def reset_me
- # This method will call when you are calling update on object as a before_update callback as defined.
+ # This method is called when update is called on an object as a before_update callback is defined.
end
end
```
### Conversion
-If a class defines `persisted?` and `id` methods then you can include `Conversion` module in that class and you can able to call Rails conversion methods to objects of that class.
+If a class defines `persisted?` and `id` methods, then you can include the `Conversion` module in that class and call the Rails conversion methods on objects of that class.
```ruby
class Person
@@ -87,14 +85,14 @@ class Person
end
person = Person.new
-person.to_model == person #=> true
-person.to_key #=> nil
-person.to_param #=> nil
+person.to_model == person # => true
+person.to_key # => nil
+person.to_param # => nil
```
### Dirty
-An object becomes dirty when it has gone through one or more changes to its attributes and has not been saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Let's consider a Person class with attributes first_name and last_name
+An object becomes dirty when it has gone through one or more changes to its attributes and has not been saved. This gives the ability to check whether an object has been changed or not. It also has attribute based accessor methods. Let's consider a Person class with attributes `first_name` and `last_name`:
```ruby
require 'active_model'
@@ -122,9 +120,9 @@ class Person
end
def save
- @previously_changed = changes
+ # do save work...
+ changes_applied
end
-
end
```
@@ -132,21 +130,22 @@ end
```ruby
person = Person.new
-person.first_name = "First Name"
+person.changed? # => false
-person.first_name #=> "First Name"
-person.first_name = "First Name Changed"
+person.first_name = "First Name"
+person.first_name # => "First Name"
-person.changed? #=> true
+# returns if any attribute has changed.
+person.changed? # => true
-#returns an list of fields arry which all has been changed before saved.
-person.changed #=> ["first_name"]
+# returns a list of attributes that have changed before saving.
+person.changed # => ["first_name"]
-#returns a hash of the fields that have changed with their original values.
-person.changed_attributes #=> {"first_name" => "First Name Changed"}
+# returns a hash of the attributes that have changed with their original values.
+person.changed_attributes # => {"first_name"=>nil}
-#returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field.
-person.changes #=> {"first_name" => ["First Name","First Name Changed"]}
+# returns a hash of changes, with the attribute names as the keys, and the values will be an array of the old and new value for that field.
+person.changes # => {"first_name"=>[nil, "First Name"]}
```
#### Attribute based accessor methods
@@ -154,28 +153,24 @@ person.changes #=> {"first_name" => ["First Name","First Name Changed"]}
Track whether the particular attribute has been changed or not.
```ruby
-#attr_name_changed?
-person.first_name #=> "First Name"
-
-#assign some other value to first_name attribute
-person.first_name = "First Name 1"
-
-person.first_name_changed? #=> true
+# attr_name_changed?
+person.first_name # => "First Name"
+person.first_name_changed? # => true
```
Track what was the previous value of the attribute.
```ruby
-#attr_name_was accessor
-person.first_name_was #=> "First Name"
+# attr_name_was accessor
+person.first_name_was # => "First Name"
```
Track both previous and current value of the changed attribute. Returns an array if changed, else returns nil.
```ruby
-#attr_name_change
-person.first_name_change #=> ["First Name", "First Name 1"]
-person.last_name_change #=> nil
+# attr_name_change
+person.first_name_change # => [nil, "First Name"]
+person.last_name_change # => nil
```
### Validations
@@ -187,20 +182,19 @@ class Person
include ActiveModel::Validations
attr_accessor :name, :email, :token
-
- validates :name, :presence => true
- validates_format_of :email, :with => /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
- validates! :token, :presence => true
-
+
+ validates :name, presence: true
+ validates_format_of :email, with: /\A([^\s]+)((?:[-a-z0-9]\.)[a-z]{2,})\z/i
+ validates! :token, presence: true
end
-person = Person.new(:token => "2b1f325")
-person.valid? #=> false
-person.name = 'vishnu'
-person.email = 'me'
-person.valid? #=> false
+person = Person.new(token: "2b1f325")
+person.valid? # => false
+person.name = 'vishnu'
+person.email = 'me'
+person.valid? # => false
person.email = 'me@vishnuatrai.com'
-person.valid? #=> true
+person.valid? # => true
person.token = nil
-person.valid? #=> raises ActiveModel::StrictValidationFailed
+person.valid? # => raises ActiveModel::StrictValidationFailed
```
diff --git a/guides/source/active_record_basics.md b/guides/source/active_record_basics.md
index 5bc100f326..a184f0753d 100644
--- a/guides/source/active_record_basics.md
+++ b/guides/source/active_record_basics.md
@@ -1,90 +1,146 @@
Active Record Basics
====================
-This guide is an introduction to Active Record. After reading this guide we hope that you'll learn:
+This guide is an introduction to Active Record.
-* What Object Relational Mapping and Active Record are and how they are used in Rails
-* How Active Record fits into the Model-View-Controller paradigm
-* How to use Active Record models to manipulate data stored in a relational database
-* Active Record schema naming conventions
-* The concepts of database migrations, validations and callbacks
+After reading this guide, you will know:
+
+* What Object Relational Mapping and Active Record are and how they are used in
+ Rails.
+* How Active Record fits into the Model-View-Controller paradigm.
+* How to use Active Record models to manipulate data stored in a relational
+ database.
+* Active Record schema naming conventions.
+* The concepts of database migrations, validations and callbacks.
--------------------------------------------------------------------------------
What is Active Record?
----------------------
-Active Record is the M in [MVC](getting_started.html#the-mvc-architecture) - the model - which is the layer of the system responsible for representing business data and logic. Active Record facilitates the creation and use of business objects whose data requires persistent storage to a database. It is an implementation of the Active Record pattern which itself is a description of an Object Relational Mapping system.
+Active Record is the M in [MVC](getting_started.html#the-mvc-architecture) - the
+model - which is the layer of the system responsible for representing business
+data and logic. Active Record facilitates the creation and use of business
+objects whose data requires persistent storage to a database. It is an
+implementation of the Active Record pattern which itself is a description of an
+Object Relational Mapping system.
### The Active Record Pattern
-Active Record was described by Martin Fowler in his book _Patterns of Enterprise Application Architecture_. In Active Record, objects carry both persistent data and behavior which operates on that data. Active Record takes the opinion that ensuring data access logic is part of the object will educate users of that object on how to write to and read from the database.
+[Active Record was described by Martin Fowler](http://www.martinfowler.com/eaaCatalog/activeRecord.html)
+in his book _Patterns of Enterprise Application Architecture_. In
+Active Record, objects carry both persistent data and behavior which
+operates on that data. Active Record takes the opinion that ensuring
+data access logic is part of the object will educate users of that
+object on how to write to and read from the database.
### Object Relational Mapping
-Object-Relational Mapping, commonly referred to as its abbreviation ORM, is a technique that connects the rich objects of an application to tables in a relational database management system. Using ORM, the properties and relationships of the objects in an application can be easily stored and retrieved from a database without writing SQL statements directly and with less overall database access code.
+Object-Relational Mapping, commonly referred to as its abbreviation ORM, is
+a technique that connects the rich objects of an application to tables in
+a relational database management system. Using ORM, the properties and
+relationships of the objects in an application can be easily stored and
+retrieved from a database without writing SQL statements directly and with less
+overall database access code.
### Active Record as an ORM Framework
-Active Record gives us several mechanisms, the most important being the ability to:
+Active Record gives us several mechanisms, the most important being the ability
+to:
-* Represent models and their data
-* Represent associations between these models
-* Represent inheritance hierarchies through related models
-* Validate models before they get persisted to the database
+* Represent models and their data.
+* Represent associations between these models.
+* Represent inheritance hierarchies through related models.
+* Validate models before they get persisted to the database.
* Perform database operations in an object-oriented fashion.
Convention over Configuration in Active Record
----------------------------------------------
-When writing applications using other programming languages or frameworks, it may be necessary to write a lot of configuration code. This is particularly true for ORM frameworks in general. However, if you follow the conventions adopted by Rails, you'll need to write very little configuration (in some case no configuration at all) when creating Active Record models. The idea is that if you configure your applications in the very same way most of the times then this should be the default way. In this cases, explicit configuration would be needed only in those cases where you can't follow the conventions for any reason.
+When writing applications using other programming languages or frameworks, it
+may be necessary to write a lot of configuration code. This is particularly true
+for ORM frameworks in general. However, if you follow the conventions adopted by
+Rails, you'll need to write very little configuration (in some case no
+configuration at all) when creating Active Record models. The idea is that if
+you configure your applications in the very same way most of the time then this
+should be the default way. Thus, explicit configuration would be needed
+only in those cases where you can't follow the standard convention.
### Naming Conventions
-By default, Active Record uses some naming conventions to find out how the mapping between models and database tables should be created. Rails will pluralize your class names to find the respective database table. So, for a class `Book`, you should have a database table called **books**. The Rails pluralization mechanisms are very powerful, being capable to pluralize (and singularize) both regular and irregular words. When using class names composed of two or more words, the model class name should follow the Ruby conventions, using the CamelCase form, while the table name must contain the words separated by underscores. Examples:
+By default, Active Record uses some naming conventions to find out how the
+mapping between models and database tables should be created. Rails will
+pluralize your class names to find the respective database table. So, for
+a class `Book`, you should have a database table called **books**. The Rails
+pluralization mechanisms are very powerful, being capable to pluralize (and
+singularize) both regular and irregular words. When using class names composed
+of two or more words, the model class name should follow the Ruby conventions,
+using the CamelCase form, while the table name must contain the words separated
+by underscores. Examples:
-* Database Table - Plural with underscores separating words (e.g., `book_clubs`)
-* Model Class - Singular with the first letter of each word capitalized (e.g., `BookClub`)
+* Database Table - Plural with underscores separating words (e.g., `book_clubs`).
+* Model Class - Singular with the first letter of each word capitalized (e.g.,
+`BookClub`).
| Model / Class | Table / Schema |
| ------------- | -------------- |
| `Post` | `posts` |
| `LineItem` | `line_items` |
-| `Deer` | `deer` |
+| `Deer` | `deers` |
| `Mouse` | `mice` |
| `Person` | `people` |
### Schema Conventions
-Active Record uses naming conventions for the columns in database tables, depending on the purpose of these columns.
-
-* **Foreign keys** - These fields should be named following the pattern `singularized_table_name_id` (e.g., `item_id`, `order_id`). These are the fields that Active Record will look for when you create associations between your models.
-* **Primary keys** - By default, Active Record will use an integer column named `id` as the table's primary key. When using [Rails Migrations](migrations.html) to create your tables, this column will be automatically created.
-
-There are also some optional column names that will create additional features to Active Record instances:
-
-* `created_at` - Automatically gets set to the current date and time when the record is first created.
-* `created_on` - Automatically gets set to the current date when the record is first created.
-* `updated_at` - Automatically gets set to the current date and time whenever the record is updated.
-* `updated_on` - Automatically gets set to the current date whenever the record is updated.
-* `lock_version` - Adds [optimistic locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to a model.
-* `type` - Specifies that the model uses [Single Table Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html)
-* `(table_name)_count` - Used to cache the number of belonging objects on associations. For example, a `comments_count` column in a `Post` class that has many instances of `Comment` will cache the number of existent comments for each post.
+Active Record uses naming conventions for the columns in database tables,
+depending on the purpose of these columns.
+
+* **Foreign keys** - These fields should be named following the pattern
+ `singularized_table_name_id` (e.g., `item_id`, `order_id`). These are the
+ fields that Active Record will look for when you create associations between
+ your models.
+* **Primary keys** - By default, Active Record will use an integer column named
+ `id` as the table's primary key. When using [Active Record
+ Migrations](migrations.html) to create your tables, this column will be
+ automatically created.
+
+There are also some optional column names that will add additional features
+to Active Record instances:
+
+* `created_at` - Automatically gets set to the current date and time when the
+ record is first created.
+* `updated_at` - Automatically gets set to the current date and time whenever
+ the record is updated.
+* `lock_version` - Adds [optimistic
+ locking](http://api.rubyonrails.org/classes/ActiveRecord/Locking.html) to
+ a model.
+* `type` - Specifies that the model uses [Single Table
+ Inheritance](http://api.rubyonrails.org/classes/ActiveRecord/Base.html#label-Single+table+inheritance).
+* `(association_name)_type` - Stores the type for
+ [polymorphic associations](association_basics.html#polymorphic-associations).
+* `(table_name)_count` - Used to cache the number of belonging objects on
+ associations. For example, a `comments_count` column in a `Post` class that
+ has many instances of `Comment` will cache the number of existent comments
+ for each post.
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.
Creating Active Record Models
-----------------------------
-It is very easy to create Active Record models. All you have to do is to subclass the `ActiveRecord::Base` class and you're good to go:
+It is very easy to create Active Record models. All you have to do is to
+subclass the `ActiveRecord::Base` class and you're good to go:
```ruby
class Product < ActiveRecord::Base
end
```
-This will create a `Product` model, mapped to a `products` table at the database. By doing this you'll also have the ability to map the columns of each row in that table with the attributes of the instances of your model. Suppose that the `products` table was created using an SQL sentence like:
+This will create a `Product` model, mapped to a `products` table at the
+database. By doing this you'll also have the ability to map the columns of each
+row in that table with the attributes of the instances of your model. Suppose
+that the `products` table was created using an SQL sentence like:
```sql
CREATE TABLE products (
@@ -94,7 +150,8 @@ CREATE TABLE products (
);
```
-Following the table schema above, you would be able to write code like the following:
+Following the table schema above, you would be able to write code like the
+following:
```ruby
p = Product.new
@@ -105,9 +162,12 @@ puts p.name # "Some Book"
Overriding the Naming Conventions
---------------------------------
-What if you need to follow a different naming convention or need to use your Rails application with a legacy database? No problem, you can easily override the default conventions.
+What if you need to follow a different naming convention or need to use your
+Rails application with a legacy database? No problem, you can easily override
+the default conventions.
-You can use the `ActiveRecord::Base.table_name=` method to specify the table name that should be used:
+You can use the `ActiveRecord::Base.table_name=` method to specify the table
+name that should be used:
```ruby
class Product < ActiveRecord::Base
@@ -115,114 +175,199 @@ class Product < ActiveRecord::Base
end
```
-If you do so, you will have to define manually the class name that is hosting the fixtures (class_name.yml) using the `set_fixture_class` method in your test definition:
+If you do so, you will have to define manually the class name that is hosting
+the fixtures (class_name.yml) using the `set_fixture_class` method in your test
+definition:
```ruby
class FunnyJoke < ActiveSupport::TestCase
- set_fixture_class :funny_jokes => 'Joke'
+ set_fixture_class funny_jokes: Joke
fixtures :funny_jokes
...
end
```
-It's also possible to override the column that should be used as the table's primary key using the `ActiveRecord::Base.set_primary_key` method:
+It's also possible to override the column that should be used as the table's
+primary key using the `ActiveRecord::Base.primary_key=` method:
```ruby
class Product < ActiveRecord::Base
- set_primary_key "product_id"
+ self.primary_key = "product_id"
end
```
CRUD: Reading and Writing Data
------------------------------
-CRUD is an acronym for the four verbs we use to operate on data: **C**reate, **R**ead, **U**pdate and **D**elete. Active Record automatically creates methods to allow an application to read and manipulate data stored within its tables.
+CRUD is an acronym for the four verbs we use to operate on data: **C**reate,
+**R**ead, **U**pdate and **D**elete. Active Record automatically creates methods
+to allow an application to read and manipulate data stored within its tables.
### Create
-Active Record objects can be created from a hash, a block or have their attributes manually set after creation. The `new` method will return a new object while `create` will return the object and save it to the database.
+Active Record objects can be created from a hash, a block or have their
+attributes manually set after creation. The `new` method will return a new
+object while `create` will return the object and save it to the database.
-For example, given a model `User` with attributes of `name` and `occupation`, the `create` method call will create and save a new record into the database:
+For example, given a model `User` with attributes of `name` and `occupation`,
+the `create` method call will create and save a new record into the database:
```ruby
- user = User.create(:name => "David", :occupation => "Code Artist")
+user = User.create(name: "David", occupation: "Code Artist")
```
-Using the `new` method, an object can be created without being saved:
+Using the `new` method, an object can be instantiated without being saved:
```ruby
- user = User.new
- user.name = "David"
- user.occupation = "Code Artist"
+user = User.new
+user.name = "David"
+user.occupation = "Code Artist"
```
A call to `user.save` will commit the record to the database.
-Finally, if a block is provided, both `create` and `new` will yield the new object to that block for initialization:
+Finally, if a block is provided, both `create` and `new` will yield the new
+object to that block for initialization:
```ruby
- user = User.new do |u|
- u.name = "David"
- u.occupation = "Code Artist"
- end
+user = User.new do |u|
+ u.name = "David"
+ u.occupation = "Code Artist"
+end
```
### Read
-Active Record provides a rich API for accessing data within a database. Below are a few examples of different data access methods provided by Active Record.
+Active Record provides a rich API for accessing data within a database. Below
+are a few examples of different data access methods provided by Active Record.
```ruby
- # return array with all records
- users = User.all
+# return a collection with all users
+users = User.all
```
```ruby
- # return the first record
- user = User.first
+# return the first user
+user = User.first
```
```ruby
- # return the first user named David
- david = User.find_by_name('David')
+# return the first user named David
+david = User.find_by(name: 'David')
```
```ruby
- # find all users named David who are Code Artists and sort by created_at in reverse chronological order
- users = User.where(:name => 'David', :occupation => 'Code Artist').order('created_at DESC')
+# find all users named David who are Code Artists and sort by created_at in reverse chronological order
+users = User.where(name: 'David', occupation: 'Code Artist').order('created_at DESC')
```
-You can learn more about querying an Active Record model in the [Active Record Query Interface](active_record_querying.html) guide.
+You can learn more about querying an Active Record model in the [Active Record
+Query Interface](active_record_querying.html) guide.
### Update
-Once an Active Record object has been retrieved, its attributes can be modified and it can be saved to the database.
+Once an Active Record object has been retrieved, its attributes can be modified
+and it can be saved to the database.
+
+```ruby
+user = User.find_by(name: 'David')
+user.name = 'Dave'
+user.save
+```
+
+A shorthand for this is to use a hash mapping attribute names to the desired
+value, like so:
+
+```ruby
+user = User.find_by(name: 'David')
+user.update(name: 'Dave')
+```
+
+This is most useful when updating several attributes at once. If, on the other
+hand, you'd like to update several records in bulk, you may find the
+`update_all` class method useful:
```ruby
- user = User.find_by_name('David')
- user.name = 'Dave'
- user.save
+User.update_all "max_login_attempts = 3, must_change_password = 'true'"
```
### Delete
-Likewise, once retrieved an Active Record object can be destroyed which removes it from the database.
+Likewise, once retrieved an Active Record object can be destroyed which removes
+it from the database.
```ruby
- user = User.find_by_name('David')
- user.destroy
+user = User.find_by(name: 'David')
+user.destroy
```
Validations
-----------
-Active Record allows you to validate the state of a model before it gets written 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. You can learn more about validations in the [Active Record Validations and Callbacks guide](active_record_validations_callbacks.html#validations-overview).
+Active Record allows you to validate the state of a model before it gets written
+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
+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
+they raise the exception `ActiveRecord::RecordInvalid` if validation fails.
+A quick example to illustrate:
+
+```ruby
+class User < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+User.create # => false
+User.create! # => ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+```
+
+You can learn more about validations in the [Active Record Validations
+guide](active_record_validations.html).
Callbacks
---------
-Active Record callbacks allow you to attach code to certain events in the life-cycle of your models. This enables you to add behavior to your models by transparently executing code when those events occur, like when you create a new record, update it, destroy it and so on. You can learn more about callbacks in the [Active Record Validations and Callbacks guide](active_record_validations_callbacks.html#callbacks-overview).
+Active Record callbacks allow you to attach code to certain events in the
+life-cycle of your models. This enables you to add behavior to your models by
+transparently executing code when those events occur, like when you create a new
+record, update it, destroy it and so on. You can learn more about callbacks in
+the [Active Record Callbacks guide](active_record_callbacks.html).
Migrations
----------
-Rails provides a domain-specific language for managing a database schema called migrations. Migrations are stored in files which are executed against any database that Active Record support using rake. Rails keeps track of which files have been committed to the database and provides rollback features. You can learn more about migrations in the [Active Record Migrations guide](migrations.html)
+Rails provides a domain-specific language for managing a database schema called
+migrations. Migrations are stored in files which are executed against any
+database that Active Record supports using `rake`. Here's a migration that
+creates a table:
+
+```ruby
+class CreatePublications < ActiveRecord::Migration
+ def change
+ create_table :publications do |t|
+ t.string :title
+ t.text :description
+ t.references :publication_type
+ t.integer :publisher_id
+ t.string :publisher_type
+ t.boolean :single_issue
+
+ t.timestamps
+ end
+ add_index :publications, :publication_type_id
+ end
+end
+```
+
+Rails keeps track of which files have been committed to the database and
+provides rollback features. To actually create the table, you'd run `rake db:migrate`
+and to roll it back, `rake db:rollback`.
+
+Note that the above code is database-agnostic: it will run in MySQL,
+PostgreSQL, Oracle and others. You can learn more about migrations in the
+[Active Record Migrations guide](migrations.html).
diff --git a/guides/source/active_record_callbacks.md b/guides/source/active_record_callbacks.md
new file mode 100644
index 0000000000..5cc6ca5798
--- /dev/null
+++ b/guides/source/active_record_callbacks.md
@@ -0,0 +1,361 @@
+Active Record Callbacks
+=======================
+
+This guide teaches you how to hook into the life cycle of your Active Record
+objects.
+
+After reading this guide, you will know:
+
+* The life cycle of Active Record objects.
+* How to create callback methods that respond to events in the object life cycle.
+* How to create special classes that encapsulate common behavior for your callbacks.
+
+--------------------------------------------------------------------------------
+
+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.
+
+Callbacks allow you to trigger logic before or after an alteration of an object's state.
+
+Callbacks Overview
+------------------
+
+Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database.
+
+### Callback Registration
+
+In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks:
+
+```ruby
+class User < ActiveRecord::Base
+ validates :login, :email, presence: true
+
+ before_validation :ensure_login_has_a_value
+
+ protected
+ def ensure_login_has_a_value
+ if login.nil?
+ self.login = email unless email.blank?
+ end
+ end
+end
+```
+
+The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line:
+
+```ruby
+class User < ActiveRecord::Base
+ validates :login, :email, presence: true
+
+ before_create do
+ self.name = login.capitalize if name.blank?
+ end
+end
+```
+
+Callbacks can also be registered to only fire on certain lifecycle events:
+
+```ruby
+class User < ActiveRecord::Base
+ before_validation :normalize_name, on: :create
+
+ # :on takes an array as well
+ after_validation :set_location, on: [ :create, :update ]
+
+ protected
+ def normalize_name
+ self.name = self.name.downcase.titleize
+ end
+
+ def set_location
+ self.location = LocationService.query(self)
+ end
+end
+```
+
+It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
+
+Available Callbacks
+-------------------
+
+Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations:
+
+### Creating an Object
+
+* `before_validation`
+* `after_validation`
+* `before_save`
+* `around_save`
+* `before_create`
+* `around_create`
+* `after_create`
+* `after_save`
+
+### Updating an Object
+
+* `before_validation`
+* `after_validation`
+* `before_save`
+* `around_save`
+* `before_update`
+* `around_update`
+* `after_update`
+* `after_save`
+
+### Destroying an Object
+
+* `before_destroy`
+* `around_destroy`
+* `after_destroy`
+
+WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed.
+
+### `after_initialize` and `after_find`
+
+The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method.
+
+The `after_find` callback will be called whenever Active Record loads a record from the database. `after_find` is called before `after_initialize` if both are defined.
+
+The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks.
+
+```ruby
+class User < ActiveRecord::Base
+ after_initialize do |user|
+ puts "You have initialized an object!"
+ end
+
+ after_find do |user|
+ puts "You have found an object!"
+ end
+end
+
+>> User.new
+You have initialized an object!
+=> #<User id: nil>
+
+>> User.first
+You have found an object!
+You have initialized an object!
+=> #<User id: 1>
+```
+
+Running Callbacks
+-----------------
+
+The following methods trigger callbacks:
+
+* `create`
+* `create!`
+* `decrement!`
+* `destroy`
+* `destroy!`
+* `destroy_all`
+* `increment!`
+* `save`
+* `save!`
+* `save(validate: false)`
+* `toggle!`
+* `update_attribute`
+* `update`
+* `update!`
+* `valid?`
+
+Additionally, the `after_find` callback is triggered by the following finder methods:
+
+* `all`
+* `first`
+* `find`
+* `find_by`
+* `find_by_*`
+* `find_by_*!`
+* `find_by_sql`
+* `last`
+
+The `after_initialize` callback is triggered every time a new object of the class is initialized.
+
+NOTE: The `find_by_*` and `find_by_*!` methods are dynamic finders generated automatically for every attribute. Learn more about them at the [Dynamic finders section](active_record_querying.html#dynamic-finders)
+
+Skipping Callbacks
+------------------
+
+Just as with validations, it is also possible to skip callbacks by using the following methods:
+
+* `decrement`
+* `decrement_counter`
+* `delete`
+* `delete_all`
+* `increment`
+* `increment_counter`
+* `toggle`
+* `touch`
+* `update_column`
+* `update_columns`
+* `update_all`
+* `update_counters`
+
+These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data.
+
+Halting Execution
+-----------------
+
+As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed.
+
+The whole callback chain is wrapped in a transaction. If any _before_ callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; _after_ callbacks can only accomplish that by raising an exception.
+
+WARNING. Any exception that is not `ActiveRecord::Rollback` will be re-raised by Rails after the callback chain is halted. Raising an exception other than `ActiveRecord::Rollback` may break code that does not expect methods like `save` and `update_attributes` (which normally try to return `true` or `false`) to raise an exception.
+
+Relational Callbacks
+--------------------
+
+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:
+
+```ruby
+class User < ActiveRecord::Base
+ has_many :posts, dependent: :destroy
+end
+
+class Post < ActiveRecord::Base
+ after_destroy :log_destroy_action
+
+ def log_destroy_action
+ puts 'Post destroyed'
+ end
+end
+
+>> user = User.first
+=> #<User id: 1>
+>> user.posts.create!
+=> #<Post id: 1, user_id: 1>
+>> user.destroy
+Post destroyed
+=> #<User id: 1>
+```
+
+Conditional Callbacks
+---------------------
+
+As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
+
+### Using `:if` and `:unless` with a `Symbol`
+
+You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed.
+
+```ruby
+class Order < ActiveRecord::Base
+ before_save :normalize_card_number, if: :paid_with_card?
+end
+```
+
+### Using `:if` and `:unless` with a String
+
+You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition:
+
+```ruby
+class Order < ActiveRecord::Base
+ before_save :normalize_card_number, if: "paid_with_card?"
+end
+```
+
+### Using `:if` and `:unless` with a `Proc`
+
+Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners:
+
+```ruby
+class Order < ActiveRecord::Base
+ before_save :normalize_card_number,
+ if: Proc.new { |order| order.paid_with_card? }
+end
+```
+
+### Multiple Conditions for Callbacks
+
+When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration:
+
+```ruby
+class Comment < ActiveRecord::Base
+ after_create :send_email_to_author, if: :author_wants_emails?,
+ unless: Proc.new { |comment| comment.post.ignore_comments? }
+end
+```
+
+Callback Classes
+----------------
+
+Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them.
+
+Here's an example where we create a class with an `after_destroy` callback for a `PictureFile` model:
+
+```ruby
+class PictureFileCallbacks
+ def after_destroy(picture_file)
+ if File.exists?(picture_file.filepath)
+ File.delete(picture_file.filepath)
+ end
+ end
+end
+```
+
+When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model:
+
+```ruby
+class PictureFile < ActiveRecord::Base
+ after_destroy PictureFileCallbacks.new
+end
+```
+
+Note that we needed to instantiate a new `PictureFileCallbacks` object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods:
+
+```ruby
+class PictureFileCallbacks
+ def self.after_destroy(picture_file)
+ if File.exists?(picture_file.filepath)
+ File.delete(picture_file.filepath)
+ end
+ end
+end
+```
+
+If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object.
+
+```ruby
+class PictureFile < ActiveRecord::Base
+ after_destroy PictureFileCallbacks
+end
+```
+
+You can declare as many callbacks as you want inside your callback classes.
+
+Transaction Callbacks
+---------------------
+
+There are two additional callbacks that are triggered by the completion of a database transaction: `after_commit` and `after_rollback`. These callbacks are very similar to the `after_save` callback except that they don't execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction.
+
+Consider, for example, the previous example where the `PictureFile` model needs to delete a file after the corresponding record is destroyed. If anything raises an exception after the `after_destroy` callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that `picture_file_2` in the code below is not valid and the `save!` method raises an error.
+
+```ruby
+PictureFile.transaction do
+ picture_file_1.destroy
+ picture_file_2.save!
+end
+```
+
+By using the `after_commit` callback we can account for this case.
+
+```ruby
+class PictureFile < ActiveRecord::Base
+ after_commit :delete_picture_file_from_disk, on: [:destroy]
+
+ def delete_picture_file_from_disk
+ if File.exist?(filepath)
+ File.delete(filepath)
+ end
+ end
+end
+```
+
+NOTE: the `:on` option specifies when a callback will be fired. If you
+don't supply the `:on` option the callback will fire for every action.
+
+The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
diff --git a/guides/source/active_record_querying.md b/guides/source/active_record_querying.md
index 66e6390f67..57e8e080f4 100644
--- a/guides/source/active_record_querying.md
+++ b/guides/source/active_record_querying.md
@@ -1,15 +1,17 @@
Active Record Query Interface
=============================
-This guide covers different ways to retrieve data from the database using Active Record. By referring to this guide, you will be able to:
+This guide covers different ways to retrieve data from the database using Active Record.
-* Find records using a variety of methods and conditions
-* Specify the order, retrieved attributes, grouping, and other properties of the found records
-* Use eager loading to reduce the number of database queries needed for data retrieval
-* Use dynamic finders methods
-* Check for the existence of particular records
-* Perform various calculations on Active Record models
-* Run EXPLAIN on relations
+After reading this guide, you will know:
+
+* How to find records using a variety of methods and conditions.
+* How to specify the order, retrieved attributes, grouping, and other properties of the found records.
+* How to use eager loading to reduce the number of database queries needed for data retrieval.
+* How to use dynamic finders methods.
+* How to check for the existence of particular records.
+* How to perform various calculations on Active Record models.
+* How to run EXPLAIN on relations.
--------------------------------------------------------------------------------
@@ -35,7 +37,7 @@ end
```ruby
class Order < ActiveRecord::Base
- belongs_to :client, :counter_cache => true
+ belongs_to :client, counter_cache: true
end
```
@@ -56,6 +58,7 @@ The methods are:
* `bind`
* `create_with`
+* `distinct`
* `eager_load`
* `extending`
* `from`
@@ -88,7 +91,7 @@ The primary operation of `Model.find(options)` can be summarized as:
### Retrieving a Single Object
-Active Record provides five different ways of retrieving a single object.
+Active Record provides several different ways of retrieving a single object.
#### Using a Primary Key
@@ -297,7 +300,7 @@ Client.first(2)
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients LIMIT 2
+SELECT * FROM clients ORDER BY id ASC LIMIT 2
```
#### last
@@ -313,7 +316,7 @@ Client.last(2)
The SQL equivalent of the above is:
```sql
-SELECT * FROM clients ORDER By id DESC LIMIT 2
+SELECT * FROM clients ORDER BY id DESC LIMIT 2
```
### Retrieving Multiple Objects in Batches
@@ -356,7 +359,7 @@ Two additional options, `:batch_size` and `:start`, are available as well.
The `:batch_size` option allows you to specify the number of records to be retrieved in each batch, before being passed individually to the block. For example, to retrieve records in batches of 5000:
```ruby
-User.find_each(:batch_size => 5000) do |user|
+User.find_each(batch_size: 5000) do |user|
NewsLetter.weekly_deliver(user)
end
```
@@ -368,7 +371,7 @@ By default, records are fetched in ascending order of the primary key, which mus
For example, to send newsletters only to users with the primary key starting from 2000, and to retrieve them in batches of 5000:
```ruby
-User.find_each(:start => 2000, :batch_size => 5000) do |user|
+User.find_each(start: 2000, batch_size: 5000) do |user|
NewsLetter.weekly_deliver(user)
end
```
@@ -381,7 +384,7 @@ The `find_in_batches` method is similar to `find_each`, since both retrieve batc
```ruby
# Give add_invoices an array of 1000 invoices at a time
-Invoice.find_in_batches(:include => :invoice_lines) do |invoices|
+Invoice.find_in_batches(include: :invoice_lines) do |invoices|
export.add_invoices(invoices)
end
```
@@ -443,7 +446,7 @@ Similar to the `(?)` replacement style of params, you can also specify keys/valu
```ruby
Client.where("created_at >= :start_date AND created_at <= :end_date",
- {:start_date => params[:start_date], :end_date => params[:end_date]})
+ {start_date: params[:start_date], end_date: params[:end_date]})
```
This makes for clearer readability if you have a large number of variable conditions.
@@ -457,7 +460,7 @@ NOTE: Only equality, range and subset checking are possible with Hash conditions
#### Equality Conditions
```ruby
-Client.where(:locked => true)
+Client.where(locked: true)
```
The field name can also be a string:
@@ -466,19 +469,19 @@ The field name can also be a string:
Client.where('locked' => true)
```
-In the case of a belongs_to relationship, an association key can be used to specify the model if an ActiveRecord object is used as the value. This method works with polymorphic relationships as well.
+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})
+Post.where(author: author)
+Author.joins(:posts).where(posts: {author: author})
```
-NOTE: The values cannot be symbols. For example, you cannot do `Client.where(:status => :active)`.
+NOTE: The values cannot be symbols. For example, you cannot do `Client.where(status: :active)`.
#### Range Conditions
```ruby
-Client.where(:created_at => (Time.now.midnight - 1.day)..Time.now.midnight)
+Client.where(created_at: (Time.now.midnight - 1.day)..Time.now.midnight)
```
This will find all clients created yesterday by using a `BETWEEN` SQL statement:
@@ -494,7 +497,7 @@ This demonstrates a shorter syntax for the examples in [Array Conditions](#array
If you want to find records using the `IN` expression you can pass an array to the conditions hash:
```ruby
-Client.where(:orders_count => [1,3,5])
+Client.where(orders_count: [1,3,5])
```
This code will generate SQL like this:
@@ -503,6 +506,16 @@ This code will generate SQL like this:
SELECT * FROM clients WHERE (clients.orders_count IN (1,3,5))
```
+### NOT Conditions
+
+`NOT` SQL queries can be built by `where.not`.
+
+```ruby
+Post.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.
+
Ordering
--------
@@ -511,12 +524,18 @@ To retrieve records from the database in a specific order, you can use the `orde
For example, if you're getting a set of records and want to order them in ascending order by the `created_at` field in your table:
```ruby
+Client.order(:created_at)
+# OR
Client.order("created_at")
```
You could specify `ASC` or `DESC` as well:
```ruby
+Client.order(created_at: :desc)
+# OR
+Client.order(created_at: :asc)
+# OR
Client.order("created_at DESC")
# OR
Client.order("created_at ASC")
@@ -525,16 +544,20 @@ Client.order("created_at ASC")
Or ordering by multiple fields:
```ruby
+Client.order(orders_count: :asc, created_at: :desc)
+# OR
+Client.order(:orders_count, created_at: :desc)
+# OR
Client.order("orders_count ASC, created_at DESC")
# OR
Client.order("orders_count ASC", "created_at DESC")
```
-If you want to call `order` multiple times e.g. in different context, new order will prepend previous one
+If you want to call `order` multiple times e.g. in different context, new order will append previous one
```ruby
Client.order("orders_count ASC").order("created_at DESC")
-# SELECT * FROM clients ORDER BY created_at DESC, orders_count ASC
+# SELECT * FROM clients ORDER BY orders_count ASC, created_at DESC
```
Selecting Specific Fields
@@ -564,10 +587,10 @@ ActiveModel::MissingAttributeError: missing attribute: <attribute>
Where `<attribute>` is the attribute you asked for. The `id` method will not raise the `ActiveRecord::MissingAttributeError`, so just be careful when working with associations because they need the `id` method to function properly.
-If you would like to only grab a single record per unique value in a certain field, you can use `uniq`:
+If you would like to only grab a single record per unique value in a certain field, you can use `distinct`:
```ruby
-Client.select(:name).uniq
+Client.select(:name).distinct
```
This would generate SQL like:
@@ -579,10 +602,10 @@ SELECT DISTINCT name FROM clients
You can also remove the uniqueness constraint:
```ruby
-query = Client.select(:name).uniq
+query = Client.select(:name).distinct
# => Returns unique names
-query.uniq(false)
+query.distinct(false)
# => Returns all names, even if there are duplicates
```
@@ -674,6 +697,31 @@ The SQL that would be executed:
```sql
SELECT * FROM posts WHERE id > 10 LIMIT 20
+
+# Original query without `except`
+SELECT * FROM posts WHERE id > 10 ORDER BY id asc LIMIT 20
+
+```
+
+### `unscope`
+
+The `except` method does not work when the relation is merged. For example:
+
+```ruby
+Post.comments.except(:order)
+```
+
+will still have an order if the order comes from a default scope on Comment. In order to remove all ordering, even from relations which are merged in, use unscope as follows:
+
+```ruby
+Post.order('id DESC').limit(20).unscope(:order) = Post.limit(20)
+Post.order('id DESC').limit(20).unscope(:order, :limit) = Post.all
+```
+
+You can additionally unscope specific where clauses. For example:
+
+```ruby
+Post.where(id: 10).limit(1).unscope({ where: :id }, :limit).order('id DESC') = Post.order('id DESC')
```
### `only`
@@ -688,6 +736,10 @@ The SQL that would be executed:
```sql
SELECT * FROM posts WHERE id > 10 ORDER BY id DESC
+
+# Original query without `only`
+SELECT "posts".* FROM "posts" WHERE (id > 10) ORDER BY id desc LIMIT 20
+
```
### `reorder`
@@ -698,7 +750,7 @@ The `reorder` method overrides the default scope order. For example:
class Post < ActiveRecord::Base
..
..
- has_many :comments, :order => 'posted_at DESC'
+ has_many :comments, -> { order('posted_at DESC') }
end
Post.find(10).comments.reorder('name')
@@ -755,12 +807,12 @@ Post.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])
+@posts = current_user.visible_posts.where(name: params[:name])
def visible_posts
case role
when 'Country Manager'
- Post.where(:country => country)
+ Post.where(country: country)
when 'Reviewer'
Post.published
when 'Bad User'
@@ -891,7 +943,7 @@ 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.
-For example, consider the following `Category`, `Post`, `Comments` and `Guest` models:
+For example, consider the following `Category`, `Post`, `Comment`, `Guest` and `Tag` models:
```ruby
class Category < ActiveRecord::Base
@@ -933,7 +985,7 @@ SELECT categories.* FROM categories
INNER JOIN posts ON posts.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).select("distinct(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`.
#### Joining Multiple Associations
@@ -954,7 +1006,7 @@ Or, in English: "return all posts that have a category and at least one comment"
#### Joining Nested Associations (Single Level)
```ruby
-Post.joins(:comments => :guest)
+Post.joins(comments: :guest)
```
This produces:
@@ -970,7 +1022,7 @@ Or, in English: "return all posts that have a comment made by a guest."
#### Joining Nested Associations (Multiple Level)
```ruby
-Category.joins(:posts => [{:comments => :guest}, :tags])
+Category.joins(posts: [{comments: :guest}, :tags])
```
This produces:
@@ -985,7 +1037,7 @@ SELECT categories.* FROM categories
### Specifying Conditions on the Joined Tables
-You can specify conditions on the joined tables using the regular [Array](array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables:
+You can specify conditions on the joined tables using the regular [Array](#array-conditions) and [String](#pure-string-conditions) conditions. [Hash conditions](#hash-conditions) provides a special syntax for specifying conditions for the joined tables:
```ruby
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
@@ -996,7 +1048,7 @@ An alternative and cleaner syntax is to nest the hash conditions:
```ruby
time_range = (Time.now.midnight - 1.day)..Time.now.midnight
-Client.joins(:orders).where(:orders => {:created_at => time_range})
+Client.joins(:orders).where(orders: {created_at: time_range})
```
This will find all clients who have orders that were created yesterday, again using a `BETWEEN` SQL expression.
@@ -1057,7 +1109,7 @@ This loads all the posts and the associated category and comments for each post.
#### Nested Associations Hash
```ruby
-Category.includes(:posts => [{:comments => :guest}, :tags]).find(1)
+Category.includes(posts: [{comments: :guest}, :tags]).find(1)
```
This will find the category with id 1 and eager load all of the associated posts, the associated posts' tags and comments, and every comment's guest association.
@@ -1087,7 +1139,7 @@ Scopes
Scoping allows you to specify commonly-used queries which can be referenced as method calls on the association objects or models. With these scopes, you can use every method previously covered such as `where`, `joins` and `includes`. All scope methods will return an `ActiveRecord::Relation` object which will allow for further methods (such as other scopes) to be called on it.
-To define a simple scope, we use the `scope` method inside the class, passing the query that we'd like run when this scope is called:
+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
@@ -1109,7 +1161,7 @@ Scopes are also chainable within scopes:
```ruby
class Post < ActiveRecord::Base
- scope :published, -> { where(:published => true) }
+ scope :published, -> { where(published: true) }
scope :published_and_commented, -> { published.where("comments_count > 0") }
end
```
@@ -1137,7 +1189,7 @@ class Post < ActiveRecord::Base
end
```
-This may then be called using this:
+Call the scope as if it were a class method:
```ruby
Post.created_before(Time.zone.now)
@@ -1159,6 +1211,60 @@ Using a class method is the preferred way to accept arguments for scopes. These
category.posts.created_before(time)
```
+### Merging of scopes
+
+Just like `where` clauses scopes are merged using `AND` conditions.
+
+```ruby
+class User < ActiveRecord::Base
+ scope :active, -> { where state: 'active' }
+ scope :inactive, -> { where state: 'inactive' }
+end
+
+User.active.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` .
+
+```ruby
+User.active.where(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 .
+
+```ruby
+User.active.merge(User.inactive)
+# => SELECT "users".* FROM "users" WHERE "users"."state" = 'inactive'
+```
+
+One important caveat is that `default_scope` will be overridden by
+`scope` and `where` conditions.
+
+```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'
+```
+
+As you can see above the `default_scope` is being overridden by 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
@@ -1188,7 +1294,7 @@ class Client < ActiveRecord::Base
end
```
-### Removing all scoping
+### Removing All Scoping
If we wish to remove scoping for any reason we can use the `unscoped` method. This is
especially useful if a `default_scope` is specified in the model and should not be
@@ -1205,37 +1311,38 @@ recommended that you use the block form of `unscoped`:
```ruby
Client.unscoped {
- Client.created_before(Time.zome.now)
+ Client.created_before(Time.zone.now)
}
```
Dynamic Finders
---------------
-For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` and `find_all_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and `find_all_by_locked` methods.
+NOTE: Dynamic finders have been deprecated in Rails 4.0 and will be
+removed in Rails 4.1. The best practice is to use Active Record scopes
+instead. You can find the deprecation gem at
+https://github.com/rails/activerecord-deprecated_finders
-You can also use `find_last_by_*` methods which will find the last record matching your argument.
+For every field (also known as an attribute) you define in your table, Active Record provides a finder method. If you have a field called `first_name` on your `Client` model for example, you get `find_by_first_name` for free from Active Record. If you have a `locked` field on the `Client` model, you also get `find_by_locked` and methods.
You can specify an exclamation point (`!`) on the end of the dynamic finders to get them to raise an `ActiveRecord::RecordNotFound` error if they do not return any records, like `Client.find_by_name!("Ryan")`
If you want to find both by name and locked, you can chain these finders together by simply typing "`and`" between the fields. For example, `Client.find_by_first_name_and_locked("Ryan", true)`.
-WARNING: Up to and including Rails 3.1, when the number of arguments passed to a dynamic finder method is lesser than the number of fields, say `Client.find_by_name_and_locked("Ryan")`, the behavior is to pass `nil` as the missing argument. This is **unintentional** and this behavior will be changed in Rails 3.2 to throw an `ArgumentError`.
-
-Find or build a new object
+Find or Build a New Object
--------------------------
-It's common that you need to find a record or create it if it doesn't exist. You can do that with the `first_or_create` and `first_or_create!` methods.
+It's common that you need to find a record or create it if it doesn't exist. You can do that with the `find_or_create_by` and `find_or_create_by!` methods.
-### `first_or_create`
+### `find_or_create_by`
-The `first_or_create` method checks whether `first` returns `nil` or not. If it does return `nil`, then `create` is called. This is very powerful when coupled with the `where` method. Let's see an example.
+The `find_or_create_by` method checks whether a record with the attributes exists. If it doesn't, then `create` is called. Let's see an example.
-Suppose you want to find a client named 'Andy', and if there's none, create one and additionally set his `locked` attribute to false. You can do so by running:
+Suppose you want to find a client named 'Andy', and if there's none, create one. You can do so by running:
```ruby
-Client.where(:first_name => 'Andy').first_or_create(:locked => false)
-# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+Client.find_or_create_by(first_name: 'Andy')
+# => #<Client id: 1, first_name: "Andy", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
```
The SQL generated by this method looks like this:
@@ -1243,48 +1350,62 @@ The SQL generated by this method looks like this:
```sql
SELECT * FROM clients WHERE (clients.first_name = 'Andy') LIMIT 1
BEGIN
-INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 0, NULL, '2011-08-30 05:22:57')
+INSERT INTO clients (created_at, first_name, locked, orders_count, updated_at) VALUES ('2011-08-30 05:22:57', 'Andy', 1, NULL, '2011-08-30 05:22:57')
COMMIT
```
-`first_or_create` returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and returned.
+`find_or_create_by` returns either the record that already exists or the new record. In our case, we didn't already have a client named Andy so the record is created and returned.
The new record might not be saved to the database; that depends on whether validations passed or not (just like `create`).
-It's also worth noting that `first_or_create` takes into account the arguments of the `where` method. In the example above we didn't explicitly pass a `:first_name => 'Andy'` argument to `first_or_create`. However, that was used when creating the new record because it was already passed before to the `where` method.
+Suppose we want to set the 'locked' attribute to `false` if we're
+creating a new record, but we don't want to include it in the query. So
+we want to find the client named "Andy", or if that client doesn't
+exist, create a client named "Andy" which is not locked.
+
+We can achieve this in two ways. The first is to use `create_with`:
+
+```ruby
+Client.create_with(locked: false).find_or_create_by(first_name: 'Andy')
+```
-You can do the same with the `find_or_create_by` method:
+The second way is using a block:
```ruby
-Client.find_or_create_by_first_name(:first_name => "Andy", :locked => false)
+Client.find_or_create_by(first_name: 'Andy') do |c|
+ c.locked = false
+end
```
-This method still works, but it's encouraged to use `first_or_create` because it's more explicit on which arguments are used to _find_ the record and which are used to _create_, resulting in less confusion overall.
+The block will only be executed if the client is being created. The
+second time we run this code, the block will be ignored.
-### `first_or_create!`
+### `find_or_create_by!`
-You can also use `first_or_create!` to raise an exception if the new record is invalid. Validations are not covered on this guide, but let's assume for a moment that you temporarily add
+You can also use `find_or_create_by!` to raise an exception if the new record is invalid. Validations are not covered on this guide, but let's assume for a moment that you temporarily add
```ruby
-validates :orders_count, :presence => true
+validates :orders_count, presence: true
```
to your `Client` model. If you try to create a new `Client` without passing an `orders_count`, the record will be invalid and an exception will be raised:
```ruby
-Client.where(:first_name => 'Andy').first_or_create!(:locked => false)
+Client.find_or_create_by!(first_name: 'Andy')
# => ActiveRecord::RecordInvalid: Validation failed: Orders count can't be blank
```
-As with `first_or_create` there is a `find_or_create_by!` method but the `first_or_create!` method is preferred for clarity.
-
-### `first_or_initialize`
+### `find_or_initialize_by`
-The `first_or_initialize` method will work just like `first_or_create` but it will not call `create` but `new`. This means that a new model instance will be created in memory but won't be saved to the database. Continuing with the `first_or_create` example, we now want the client named 'Nick':
+The `find_or_initialize_by` method will work just like
+`find_or_create_by` but it will call `new` instead of `create`. This
+means that a new model instance will be created in memory but won't be
+saved to the database. Continuing with the `find_or_create_by` example, we
+now want the client named 'Nick':
```ruby
-nick = Client.where(:first_name => 'Nick').first_or_initialize(:locked => false)
-# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: false, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
+nick = Client.find_or_initialize_by(first_name: 'Nick')
+# => <Client id: nil, first_name: "Nick", orders_count: 0, locked: true, created_at: "2011-08-30 06:09:27", updated_at: "2011-08-30 06:09:27">
nick.persisted?
# => false
@@ -1332,11 +1453,11 @@ Client.connection.select_all("SELECT * FROM clients WHERE id = '1'")
`pluck` can be used to query a single or multiple columns from the underlying table of a model. It accepts a list of column names as argument and returns an array of values of the specified columns with the corresponding data type.
```ruby
-Client.where(:active => true).pluck(:id)
+Client.where(active: true).pluck(:id)
# SELECT id FROM clients WHERE active = 1
# => [1, 2, 3]
-Client.uniq.pluck(:role)
+Client.distinct.pluck(:role)
# SELECT DISTINCT role FROM clients
# => ['admin', 'member', 'guest']
@@ -1345,15 +1466,17 @@ Client.pluck(:id, :name)
# => [[1, 'David'], [2, 'Jeremy'], [3, 'Jose']]
```
-`pluck` makes it possible to replace code like
+`pluck` makes it possible to replace code like:
```ruby
Client.select(:id).map { |c| c.id }
# or
-Client.select(:id).map { |c| [c.id, c.name] }
+Client.select(:id).map(&:id)
+# or
+Client.select(:id, :name).map { |c| [c.id, c.name] }
```
-with
+with:
```ruby
Client.pluck(:id)
@@ -1361,6 +1484,37 @@ Client.pluck(:id)
Client.pluck(:id, :name)
```
+Unlike `select`, `pluck` directly converts a database result into a Ruby `Array`,
+without constructing `ActiveRecord` objects. This can mean better performance for
+a large or often-running query. However, any model method overrides will
+not be available. For example:
+
+```ruby
+class Client < ActiveRecord::Base
+ def name
+ "I am #{super}"
+ end
+end
+
+Client.select(:name).map &:name
+# => ["I am David", "I am Jeremy", "I am Jose"]
+
+Client.pluck(:name)
+# => ["David", "Jeremy", "Jose"]
+```
+
+Furthermore, unlike `select` and other `Relation` scopes, `pluck` triggers an immediate
+query, and thus cannot be chained with any further scopes, although it can work with
+scopes already constructed earlier:
+
+```ruby
+Client.pluck(:name).limit(1)
+# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
+
+Client.limit(1).pluck(:name)
+# => ["David"]
+```
+
### `ids`
`ids` can be used to pluck all the IDs for the relation using the table's primary key.
@@ -1382,27 +1536,31 @@ Person.ids
Existence of Objects
--------------------
-If you simply want to check for the existence of the object there's a method called `exists?`. This method will query the database using the same query as `find`, but instead of returning an object or collection of objects it will return either `true` or `false`.
+If you simply want to check for the existence of the object there's a method called `exists?`.
+This method will query the database using the same query as `find`, but instead of returning an
+object or collection of objects it will return either `true` or `false`.
```ruby
Client.exists?(1)
```
-The `exists?` method also takes multiple ids, but the catch is that it will return true if any one of those records exists.
+The `exists?` method also takes multiple values, but the catch is that it will return `true` if any
+one of those records exists.
```ruby
-Client.exists?(1,2,3)
+Client.exists?(id: [1,2,3])
# or
-Client.exists?([1,2,3])
+Client.exists?(name: ['John', 'Sergei'])
```
It's even possible to use `exists?` without any arguments on a model or a relation.
```ruby
-Client.where(:first_name => 'Ryan').exists?
+Client.where(first_name: 'Ryan').exists?
```
-The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false` otherwise.
+The above returns `true` if there is at least one client with the `first_name` 'Ryan' and `false`
+otherwise.
```ruby
Client.exists?
@@ -1422,8 +1580,8 @@ Post.recent.any?
Post.recent.many?
# via a relation
-Post.where(:published => true).any?
-Post.where(:published => true).many?
+Post.where(published: true).any?
+Post.where(published: true).many?
# via an association
Post.first.categories.any?
@@ -1445,14 +1603,14 @@ Client.count
Or on a relation:
```ruby
-Client.where(:first_name => 'Ryan').count
+Client.where(first_name: 'Ryan').count
# SELECT count(*) AS count_all FROM clients WHERE (first_name = 'Ryan')
```
You can also use various finder methods on a relation for performing complex calculations:
```ruby
-Client.includes("orders").where(:first_name => 'Ryan', :orders => {:status => 'received'}).count
+Client.includes("orders").where(first_name: 'Ryan', orders: {status: 'received'}).count
```
Which will execute:
@@ -1517,7 +1675,7 @@ 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(:posts).explain
```
may yield
@@ -1556,7 +1714,7 @@ 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(:posts).explain
```
yields
@@ -1581,45 +1739,6 @@ EXPLAIN for: SELECT `posts`.* FROM `posts` WHERE `posts`.`user_id` IN (1)
under MySQL.
-### Automatic EXPLAIN
-
-Active Record is able to run EXPLAIN automatically on slow queries and log its
-output. This feature is controlled by the configuration parameter
-
-```ruby
-config.active_record.auto_explain_threshold_in_seconds
-```
-
-If set to a number, any query exceeding those many seconds will have its EXPLAIN
-automatically triggered and logged. In the case of relations, the threshold is
-compared to the total time needed to fetch records. So, a relation is seen as a
-unit of work, no matter whether the implementation of eager loading involves
-several queries under the hood.
-
-A threshold of `nil` disables automatic EXPLAINs.
-
-The default threshold in development mode is 0.5 seconds, and `nil` in test and
-production modes.
-
-INFO. Automatic EXPLAIN gets disabled if Active Record has no logger, regardless
-of the value of the threshold.
-
-#### Disabling Automatic EXPLAIN
-
-Automatic EXPLAIN can be selectively silenced with `ActiveRecord::Base.silence_auto_explain`:
-
-```ruby
-ActiveRecord::Base.silence_auto_explain do
- # no automatic EXPLAIN is triggered here
-end
-```
-
-That may be useful for queries you know are slow but fine, like a heavyweight
-report of an admin interface.
-
-As its name suggests, `silence_auto_explain` only silences automatic EXPLAINs.
-Explicit calls to `ActiveRecord::Relation#explain` run.
-
### Interpreting EXPLAIN
Interpretation of the output of EXPLAIN is beyond the scope of this guide. The
diff --git a/guides/source/active_record_validations.md b/guides/source/active_record_validations.md
new file mode 100644
index 0000000000..0df52a655f
--- /dev/null
+++ b/guides/source/active_record_validations.md
@@ -0,0 +1,1167 @@
+Active Record Validations
+=========================
+
+This guide teaches you how to validate the state of objects before they go into
+the database using Active Record's validations feature.
+
+After reading this guide, you will know:
+
+* How to use the built-in Active Record validation helpers.
+* How to create your own custom validation methods.
+* How to work with the error messages generated by the validation process.
+
+--------------------------------------------------------------------------------
+
+Validations Overview
+--------------------
+
+Here's an example of a very simple validation:
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+```
+
+As you can see, our validation lets us know that our `Person` is not valid
+without a `name` attribute. The second `Person` will not be persisted to the
+database.
+
+Before we dig into more details, let's talk about how validations fit into the
+big picture of your application.
+
+### Why Use Validations?
+
+Validations are used to ensure that only valid data is saved into your
+database. For example, it may be important to your application to ensure that
+every user provides a valid email address and mailing address. Model-level
+validations are the best way to ensure that only valid data is saved into your
+database. They are database agnostic, cannot be bypassed by end users, and are
+convenient to test and maintain. Rails makes them easy to use, provides
+built-in helpers for common needs, and allows you to create your own validation
+methods as well.
+
+There are several other ways to validate data before it is saved into your
+database, including native database constraints, client-side validations,
+controller-level validations. Here's a summary of the pros and cons:
+
+* Database constraints and/or stored procedures make the validation mechanisms
+ database-dependent and can make testing and maintenance more difficult.
+ However, if your database is used by other applications, it may be a good
+ idea to use some constraints at the database level. Additionally,
+ database-level validations can safely handle some things (such as uniqueness
+ in heavily-used tables) that can be difficult to implement otherwise.
+* Client-side validations can be useful, but are generally unreliable if used
+ alone. If they are implemented using JavaScript, they may be bypassed if
+ JavaScript is turned off in the user's browser. However, if combined with
+ other techniques, client-side validation can be a convenient way to provide
+ users with immediate feedback as they use your site.
+* Controller-level validations can be tempting to use, but often become
+ unwieldy and difficult to test and maintain. Whenever possible, it's a good
+ idea to keep your controllers skinny, as it will make your application a
+ pleasure to work with in the long run.
+
+Choose these in certain, specific cases. It's the opinion of the Rails team
+that model-level validations are the most appropriate in most circumstances.
+
+### When Does Validation Happen?
+
+There are two kinds of Active Record objects: those that correspond to a row
+inside your database and those that do not. When you create a fresh object, for
+example using the `new` method, that object does not belong to the database
+yet. Once you call `save` upon that object it will be saved into the
+appropriate database table. Active Record uses the `new_record?` instance
+method to determine whether an object is already in the database or not.
+Consider the following simple Active Record class:
+
+```ruby
+class Person < ActiveRecord::Base
+end
+```
+
+We can see how it works by looking at some `rails console` output:
+
+```ruby
+$ rails console
+>> p = Person.new(name: "John Doe")
+=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
+>> p.new_record?
+=> true
+>> p.save
+=> true
+>> p.new_record?
+=> false
+```
+
+Creating and saving a new record will send an SQL `INSERT` operation to the
+database. Updating an existing record will send an SQL `UPDATE` operation
+instead. Validations are typically run before these commands are sent to the
+database. If any validations fail, the object will be marked as invalid and
+Active Record will not perform the `INSERT` or `UPDATE` operation. This avoids
+storing an invalid object in the database. You can choose to have specific
+validations run when an object is created, saved, or updated.
+
+CAUTION: There are many ways to change the state of an object in the database.
+Some methods will trigger validations, but some will not. This means that it's
+possible to save an object in the database in an invalid state if you aren't
+careful.
+
+The following methods trigger validations, and will save the object to the
+database only if the object is valid:
+
+* `create`
+* `create!`
+* `save`
+* `save!`
+* `update`
+* `update!`
+
+The bang versions (e.g. `save!`) raise an exception if the record is invalid.
+The non-bang versions don't, `save` and `update` return `false`,
+`create` just returns the object.
+
+### Skipping Validations
+
+The following methods skip validations, and will save the object to the
+database regardless of its validity. They should be used with caution.
+
+* `decrement!`
+* `decrement_counter`
+* `increment!`
+* `increment_counter`
+* `toggle!`
+* `touch`
+* `update_all`
+* `update_attribute`
+* `update_column`
+* `update_columns`
+* `update_counters`
+
+Note that `save` also has the ability to skip validations if passed `validate:
+false` as argument. This technique should be used with caution.
+
+* `save(validate: false)`
+
+### `valid?` and `invalid?`
+
+To verify whether or not an object is valid, Rails uses the `valid?` method.
+You can also use this method on your own. `valid?` triggers your validations
+and returns true if no errors were found in the object, and false otherwise.
+As you saw above:
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+Person.create(name: "John Doe").valid? # => true
+Person.create(name: nil).valid? # => false
+```
+
+After Active Record has performed validations, any errors found can be accessed
+through the `errors.messages` instance method, which returns a collection of errors.
+By definition, an object is valid if this collection is empty after running
+validations.
+
+Note that an object instantiated with `new` will not report errors even if it's
+technically invalid, because validations are not run when using `new`.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+>> p = Person.new
+#=> #<Person id: nil, name: nil>
+>> p.errors.messages
+#=> {}
+
+>> p.valid?
+#=> false
+>> p.errors.messages
+#=> {name:["can't be blank"]}
+
+>> p = Person.create
+#=> #<Person id: nil, name: nil>
+>> p.errors.messages
+#=> {name:["can't be blank"]}
+
+>> p.save
+#=> false
+
+>> p.save!
+#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+
+>> Person.create!
+#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
+```
+
+`invalid?` is simply the inverse of `valid?`. It triggers your validations,
+returning true if any errors were found in the object, and false otherwise.
+
+### `errors[]`
+
+To verify whether or not a particular attribute of an object is valid, you can
+use `errors[:attribute]`. It returns an array of all the errors for
+`:attribute`. If there are no errors on the specified attribute, an empty array
+is returned.
+
+This method is only useful _after_ validations have been run, because it only
+inspects the errors collection and does not trigger validations itself. It's
+different from the `ActiveRecord::Base#invalid?` method explained above because
+it doesn't verify the validity of the object as a whole. It only checks to see
+whether there are errors found on an individual attribute of the object.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true
+end
+
+>> Person.new.errors[:name].any? # => false
+>> Person.create.errors[:name].any? # => true
+```
+
+We'll cover validation errors in greater depth in the [Working with Validation
+Errors](#working-with-validation-errors) section. For now, let's turn to the
+built-in validation helpers that Rails provides by default.
+
+Validation Helpers
+------------------
+
+Active Record offers many pre-defined validation helpers that you can use
+directly inside your class definitions. These helpers provide common validation
+rules. Every time a validation fails, an error message is added to the object's
+`errors` collection, and this message is associated with the attribute being
+validated.
+
+Each helper accepts an arbitrary number of attribute names, so with a single
+line of code you can add the same kind of validation to several attributes.
+
+All of them accept the `:on` and `:message` options, which define when the
+validation should be run and what message should be added to the `errors`
+collection if it fails, respectively. The `:on` option takes one of the values
+`:create` or `:update`. There is a default error
+message for each one of the validation helpers. These messages are used when
+the `:message` option isn't specified. Let's take a look at each one of the
+available helpers.
+
+### `acceptance`
+
+This method validates that a checkbox on the user interface was checked when a
+form was submitted. This is typically used when the user needs to agree to your
+application's terms of service, confirm reading some text, or any similar
+concept. This validation is very specific to web applications and this
+'acceptance' does not need to be recorded anywhere in your database (if you
+don't have a field for it, the helper will just create a virtual attribute).
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :terms_of_service, acceptance: true
+end
+```
+
+The default error message for this helper is _"must be accepted"_.
+
+It can receive an `:accept` option, which determines the value that will be
+considered acceptance. It defaults to "1" and can be easily changed.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :terms_of_service, acceptance: { accept: 'yes' }
+end
+```
+
+### `validates_associated`
+
+You should use this helper when your model has associations with other models
+and they also need to be validated. When you try to save your object, `valid?`
+will be called upon each one of the associated objects.
+
+```ruby
+class Library < ActiveRecord::Base
+ has_many :books
+ validates_associated :books
+end
+```
+
+This validation will work with all of the association types.
+
+CAUTION: Don't use `validates_associated` on both ends of your associations.
+They would call each other in an infinite loop.
+
+The default error message for `validates_associated` is _"is invalid"_. Note
+that each associated object will contain its own `errors` collection; errors do
+not bubble up to the calling model.
+
+### `confirmation`
+
+You should use this helper when you have two text fields that should receive
+exactly the same content. For example, you may want to confirm an email address
+or a password. This validation creates a virtual attribute whose name is the
+name of the field that has to be confirmed with "_confirmation" appended.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :email, confirmation: true
+end
+```
+
+In your view template you could use something like
+
+```erb
+<%= text_field :person, :email %>
+<%= text_field :person, :email_confirmation %>
+```
+
+This check is performed only if `email_confirmation` is not `nil`. To require
+confirmation, make sure to add a presence check for the confirmation attribute
+(we'll take a look at `presence` later on this guide):
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :email, confirmation: true
+ validates :email_confirmation, presence: true
+end
+```
+
+The default error message for this helper is _"doesn't match confirmation"_.
+
+### `exclusion`
+
+This helper validates that the attributes' values are not included in a given
+set. In fact, this set can be any enumerable object.
+
+```ruby
+class Account < ActiveRecord::Base
+ validates :subdomain, exclusion: { in: %w(www us ca jp),
+ message: "Subdomain %{value} is reserved." }
+end
+```
+
+The `exclusion` helper has an option `:in` that receives the set of values that
+will not be accepted for the validated attributes. The `:in` option has an
+alias called `:within` that you can use for the same purpose, if you'd like to.
+This example uses the `:message` option to show how you can include the
+attribute's value.
+
+The default error message is _"is reserved"_.
+
+### `format`
+
+This helper validates the attributes' values by testing whether they match a
+given regular expression, which is specified using the `:with` option.
+
+```ruby
+class Product < ActiveRecord::Base
+ validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
+ message: "only allows letters" }
+end
+```
+
+The default error message is _"is invalid"_.
+
+### `inclusion`
+
+This helper validates that the attributes' values are included in a given set.
+In fact, this set can be any enumerable object.
+
+```ruby
+class Coffee < ActiveRecord::Base
+ validates :size, inclusion: { in: %w(small medium large),
+ message: "%{value} is not a valid size" }
+end
+```
+
+The `inclusion` helper has an option `:in` that receives the set of values that
+will be accepted. The `:in` option has an alias called `:within` that you can
+use for the same purpose, if you'd like to. The previous example uses the
+`:message` option to show how you can include the attribute's value.
+
+The default error message for this helper is _"is not included in the list"_.
+
+### `length`
+
+This helper validates the length of the attributes' values. It provides a
+variety of options, so you can specify length constraints in different ways:
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, length: { minimum: 2 }
+ validates :bio, length: { maximum: 500 }
+ validates :password, length: { in: 6..20 }
+ validates :registration_number, length: { is: 6 }
+end
+```
+
+The possible length constraint options are:
+
+* `:minimum` - The attribute cannot have less than the specified length.
+* `:maximum` - The attribute cannot have more than the specified length.
+* `:in` (or `:within`) - The attribute length must be included in a given
+ interval. The value for this option must be a range.
+* `:is` - The attribute length must be equal to the given value.
+
+The default error messages depend on the type of length validation being
+performed. You can personalize these messages using the `:wrong_length`,
+`:too_long`, and `:too_short` options and `%{count}` as a placeholder for the
+number corresponding to the length constraint being used. You can still use the
+`:message` option to specify an error message.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :bio, length: { maximum: 1000,
+ too_long: "%{count} characters is the maximum allowed" }
+end
+```
+
+This helper counts characters by default, but you can split the value in a
+different way using the `:tokenizer` option:
+
+```ruby
+class Essay < ActiveRecord::Base
+ validates :content, length: {
+ minimum: 300,
+ maximum: 400,
+ tokenizer: lambda { |str| str.scan(/\w+/) },
+ too_short: "must have at least %{count} words",
+ too_long: "must have at most %{count} words"
+ }
+end
+```
+
+Note that the default error messages are plural (e.g., "is too short (minimum
+is %{count} characters)"). For this reason, when `:minimum` is 1 you should
+provide a personalized message or use `presence: true` instead. When
+`:in` or `:within` have a lower limit of 1, you should either provide a
+personalized message or call `presence` prior to `length`.
+
+### `numericality`
+
+This helper validates that your attributes have only numeric values. By
+default, it will match an optional sign followed by an integral or floating
+point number. To specify that only integral numbers are allowed set
+`:only_integer` to true.
+
+If you set `:only_integer` to `true`, then it will use the
+
+```ruby
+/\A[+-]?\d+\Z/
+```
+
+regular expression to validate the attribute's value. Otherwise, it will try to
+convert the value to a number using `Float`.
+
+WARNING. Note that the regular expression above allows a trailing newline
+character.
+
+```ruby
+class Player < ActiveRecord::Base
+ validates :points, numericality: true
+ validates :games_played, numericality: { only_integer: true }
+end
+```
+
+Besides `:only_integer`, this helper also accepts the following options to add
+constraints to acceptable values:
+
+* `:greater_than` - Specifies the value must be greater than the supplied
+ value. The default error message for this option is _"must be greater than
+ %{count}"_.
+* `:greater_than_or_equal_to` - Specifies the value must be greater than or
+ equal to the supplied value. The default error message for this option is
+ _"must be greater than or equal to %{count}"_.
+* `:equal_to` - Specifies the value must be equal to the supplied value. The
+ default error message for this option is _"must be equal to %{count}"_.
+* `:less_than` - Specifies the value must be less than the supplied value. The
+ default error message for this option is _"must be less than %{count}"_.
+* `:less_than_or_equal_to` - Specifies the value must be less than or equal the
+ supplied value. The default error message for this option is _"must be less
+ than or equal to %{count}"_.
+* `:odd` - Specifies the value must be an odd number if set to true. The
+ default error message for this option is _"must be odd"_.
+* `:even` - Specifies the value must be an even number if set to true. The
+ default error message for this option is _"must be even"_.
+
+The default error message is _"is not a number"_.
+
+### `presence`
+
+This helper validates that the specified attributes are not empty. It uses the
+`blank?` method to check if the value is either `nil` or a blank string, that
+is, a string that is either empty or consists of whitespace.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, :login, :email, presence: true
+end
+```
+
+If you want to be sure that an association is present, you'll need to test
+whether the associated object itself is present, and not the foreign key used
+to map the association.
+
+```ruby
+class LineItem < ActiveRecord::Base
+ belongs_to :order
+ validates :order, presence: true
+end
+```
+
+In order to validate associated records whose presence is required, you must
+specify the `:inverse_of` option for the association:
+
+```ruby
+class Order < ActiveRecord::Base
+ has_many :line_items, inverse_of: :order
+end
+```
+
+If you validate the presence of an object associated via a `has_one` or
+`has_many` relationship, it will check that the object is neither `blank?` nor
+`marked_for_destruction?`.
+
+Since `false.blank?` is true, if you want to validate the presence of a boolean
+field you should use `validates :field_name, inclusion: { in: [true, false] }`.
+
+The default error message is _"can't be blank"_.
+
+### `absence`
+
+This helper validates that the specified attributes are absent. It uses the
+`present?` method to check if the value is not either nil or a blank string, that
+is, a string that is either empty or consists of whitespace.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, :login, :email, absence: true
+end
+```
+
+If you want to be sure that an association is absent, you'll need to test
+whether the associated object itself is absent, and not the foreign key used
+to map the association.
+
+```ruby
+class LineItem < ActiveRecord::Base
+ belongs_to :order
+ validates :order, absence: true
+end
+```
+
+In order to validate associated records whose absence is required, you must
+specify the `:inverse_of` option for the association:
+
+```ruby
+class Order < ActiveRecord::Base
+ has_many :line_items, inverse_of: :order
+end
+```
+
+If you validate the absence of an object associated via a `has_one` or
+`has_many` relationship, it will check that the object is neither `present?` nor
+`marked_for_destruction?`.
+
+Since `false.present?` is false, if you want to validate the absence of a boolean
+field you should use `validates :field_name, exclusion: { in: [true, false] }`.
+
+The default error message is _"must be blank"_.
+
+### `uniqueness`
+
+This helper validates that the attribute's value is unique right before the
+object gets saved. It does not create a uniqueness constraint in the database,
+so it may happen that two different database connections create two records
+with the same value for a column that you intend to be unique. To avoid that,
+you must create a unique index in your database.
+
+```ruby
+class Account < ActiveRecord::Base
+ validates :email, uniqueness: true
+end
+```
+
+The validation happens by performing an SQL query into the model's table,
+searching for an existing record with the same value in that attribute.
+
+There is a `:scope` option that you can use to specify other attributes that
+are used to limit the uniqueness check:
+
+```ruby
+class Holiday < ActiveRecord::Base
+ validates :name, uniqueness: { scope: :year,
+ message: "should happen once per year" }
+end
+```
+
+There is also a `:case_sensitive` option that you can use to define whether the
+uniqueness constraint will be case sensitive or not. This option defaults to
+true.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, uniqueness: { case_sensitive: false }
+end
+```
+
+WARNING. Note that some databases are configured to perform case-insensitive
+searches anyway.
+
+The default error message is _"has already been taken"_.
+
+### `validates_with`
+
+This helper passes the record to a separate class for validation.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator
+end
+
+class GoodnessValidator < ActiveModel::Validator
+ def validate(record)
+ if record.first_name == "Evil"
+ record.errors[:base] << "This person is evil"
+ end
+ end
+end
+```
+
+NOTE: Errors added to `record.errors[:base]` relate to the state of the record
+as a whole, and not to a specific attribute.
+
+The `validates_with` helper takes a class, or a list of classes to use for
+validation. There is no default error message for `validates_with`. You must
+manually add errors to the record's errors collection in the validator class.
+
+To implement the validate method, you must have a `record` parameter defined,
+which is the record to be validated.
+
+Like all other validations, `validates_with` takes the `:if`, `:unless` and
+`:on` options. If you pass any other options, it will send those options to the
+validator class as `options`:
+
+```ruby
+class Person < ActiveRecord::Base
+ validates_with GoodnessValidator, fields: [:first_name, :last_name]
+end
+
+class GoodnessValidator < ActiveModel::Validator
+ def validate(record)
+ if options[:fields].any?{|field| record.send(field) == "Evil" }
+ record.errors[:base] << "This person is evil"
+ end
+ end
+end
+```
+
+Note that the validator will be initialized *only once* for the whole application
+life cycle, and not on each validation run, so be careful about using instance
+variables inside it.
+
+If your validator is complex enough that you want instance variables, you can
+easily use a plain old Ruby object instead:
+
+```ruby
+class Person < ActiveRecord::Base
+ validate do |person|
+ GoodnessValidator.new(person).validate
+ end
+end
+
+class GoodnessValidator
+ def initialize(person)
+ @person = person
+ end
+
+ def validate
+ if some_complex_condition_involving_ivars_and_private_methods?
+ @person.errors[:base] << "This person is evil"
+ end
+ end
+
+ # ...
+end
+```
+
+### `validates_each`
+
+This helper validates attributes against a block. It doesn't have a predefined
+validation function. You should create one using a block, and every attribute
+passed to `validates_each` will be tested against it. In the following example,
+we don't want names and surnames to begin with lower case.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates_each :name, :surname do |record, attr, value|
+ record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
+ end
+end
+```
+
+The block receives the record, the attribute's name and the attribute's value.
+You can do anything you like to check for valid data within the block. If your
+validation fails, you should add an error message to the model, therefore
+making it invalid.
+
+Common Validation Options
+-------------------------
+
+These are common validation options:
+
+### `:allow_nil`
+
+The `:allow_nil` option skips the validation when the value being validated is
+`nil`.
+
+```ruby
+class Coffee < ActiveRecord::Base
+ validates :size, inclusion: { in: %w(small medium large),
+ message: "%{value} is not a valid size" }, allow_nil: true
+end
+```
+
+### `:allow_blank`
+
+The `:allow_blank` option is similar to the `:allow_nil` option. This option
+will let validation pass if the attribute's value is `blank?`, like `nil` or an
+empty string for example.
+
+```ruby
+class Topic < ActiveRecord::Base
+ validates :title, length: { is: 5 }, allow_blank: true
+end
+
+Topic.create(title: "").valid? # => true
+Topic.create(title: nil).valid? # => true
+```
+
+### `:message`
+
+As you've already seen, the `:message` option lets you specify the message that
+will be added to the `errors` collection when validation fails. When this
+option is not used, Active Record will use the respective default error message
+for each validation helper.
+
+### `:on`
+
+The `:on` option lets you specify when the validation should happen. The
+default behavior for all the built-in validation helpers is to be run on save
+(both when you're creating a new record and when you're updating it). If you
+want to change it, you can use `on: :create` to run the validation only when a
+new record is created or `on: :update` to run the validation only when a record
+is updated.
+
+```ruby
+class Person < ActiveRecord::Base
+ # it will be possible to update email with a duplicated value
+ validates :email, uniqueness: true, on: :create
+
+ # it will be possible to create the record with a non-numerical age
+ validates :age, numericality: true, on: :update
+
+ # the default (validates on both create and update)
+ validates :name, presence: true
+end
+```
+
+Strict Validations
+------------------
+
+You can also specify validations to be strict and raise
+`ActiveModel::StrictValidationFailed` when the object is invalid.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: { strict: true }
+end
+
+Person.new.valid? # => ActiveModel::StrictValidationFailed: Name can't be blank
+```
+
+There is also an ability to pass custom exception to `:strict` option.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :token, presence: true, uniqueness: true, strict: TokenGenerationException
+end
+
+Person.new.valid? # => TokenGenerationException: Token can't be blank
+```
+
+Conditional Validation
+----------------------
+
+Sometimes it will make sense to validate an object only when a given predicate
+is satisfied. You can do that by using the `:if` and `:unless` options, which
+can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if`
+option when you want to specify when the validation **should** happen. If you
+want to specify when the validation **should not** happen, then you may use the
+`:unless` option.
+
+### Using a Symbol with `:if` and `:unless`
+
+You can associate the `:if` and `:unless` options with a symbol corresponding
+to the name of a method that will get called right before validation happens.
+This is the most commonly used option.
+
+```ruby
+class Order < ActiveRecord::Base
+ validates :card_number, presence: true, if: :paid_with_card?
+
+ def paid_with_card?
+ payment_type == "card"
+ end
+end
+```
+
+### Using a String with `:if` and `:unless`
+
+You can also use a string that will be evaluated using `eval` and needs to
+contain valid Ruby code. You should use this option only when the string
+represents a really short condition.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :surname, presence: true, if: "name.nil?"
+end
+```
+
+### Using a Proc with `:if` and `:unless`
+
+Finally, it's possible to associate `:if` and `:unless` with a `Proc` object
+which will be called. Using a `Proc` object gives you the ability to write an
+inline condition instead of a separate method. This option is best suited for
+one-liners.
+
+```ruby
+class Account < ActiveRecord::Base
+ validates :password, confirmation: true,
+ unless: Proc.new { |a| a.password.blank? }
+end
+```
+
+### Grouping Conditional validations
+
+Sometimes it is useful to have multiple validations use one condition, it can
+be easily achieved using `with_options`.
+
+```ruby
+class User < ActiveRecord::Base
+ with_options if: :is_admin? do |admin|
+ admin.validates :password, length: { minimum: 10 }
+ admin.validates :email, presence: true
+ end
+end
+```
+
+All validations inside of `with_options` block will have automatically passed
+the condition `if: :is_admin?`
+
+### Combining Validation Conditions
+
+On the other hand, when multiple conditions define whether or not a validation
+should happen, an `Array` can be used. Moreover, you can apply both `:if` and
+`:unless` to the same validation.
+
+```ruby
+class Computer < ActiveRecord::Base
+ validates :mouse, presence: true,
+ if: ["market.retail?", :desktop?]
+ unless: Proc.new { |c| c.trackpad.present? }
+end
+```
+
+The validation only runs when all the `:if` conditions and none of the
+`:unless` conditions are evaluated to `true`.
+
+Performing Custom Validations
+-----------------------------
+
+When the built-in validation helpers are not enough for your needs, you can
+write your own validators or validation methods as you prefer.
+
+### Custom Validators
+
+Custom validators are classes that extend `ActiveModel::Validator`. These
+classes must implement a `validate` method which takes a record as an argument
+and performs the validation on it. The custom validator is called using the
+`validates_with` method.
+
+```ruby
+class MyValidator < ActiveModel::Validator
+ def validate(record)
+ unless record.name.starts_with? 'X'
+ record.errors[:name] << 'Need a name starting with X please!'
+ end
+ end
+end
+
+class Person
+ include ActiveModel::Validations
+ validates_with MyValidator
+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
+instance.
+
+```ruby
+class EmailValidator < ActiveModel::EachValidator
+ def validate_each(record, attribute, value)
+ unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
+ record.errors[attribute] << (options[:message] || "is not an email")
+ end
+ end
+end
+
+class Person < ActiveRecord::Base
+ validates :email, presence: true, email: true
+end
+```
+
+As shown in the example, you can also combine standard validations with your
+own custom validators.
+
+### Custom Methods
+
+You can also create methods that verify the state of your models and add
+messages to the `errors` collection when they are invalid. You must then
+register these methods by using the `validate` class method, passing in the
+symbols for the validation methods' names.
+
+You can pass more than one symbol for each class method and the respective
+validations will be run in the same order as they were registered.
+
+```ruby
+class Invoice < ActiveRecord::Base
+ validate :expiration_date_cannot_be_in_the_past,
+ :discount_cannot_be_greater_than_total_value
+
+ def expiration_date_cannot_be_in_the_past
+ if expiration_date.present? && expiration_date < Date.today
+ errors.add(:expiration_date, "can't be in the past")
+ end
+ end
+
+ def discount_cannot_be_greater_than_total_value
+ if discount > total_value
+ errors.add(:discount, "can't be greater than total value")
+ end
+ end
+end
+```
+
+By default such validations will run every time you call `valid?`. It is also
+possible to control when to run these custom validations by giving an `:on`
+option to the `validate` method, with either: `:create` or `:update`.
+
+```ruby
+class Invoice < ActiveRecord::Base
+ validate :active_customer, on: :create
+
+ def active_customer
+ errors.add(:customer_id, "is not active") unless customer.active?
+ end
+end
+```
+
+Working with Validation Errors
+------------------------------
+
+In addition to the `valid?` and `invalid?` methods covered earlier, Rails provides a number of methods for working with the `errors` collection and inquiring about the validity of objects.
+
+The following is a list of the most commonly used methods. Please refer to the `ActiveModel::Errors` documentation for a list of all the available methods.
+
+### `errors`
+
+Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.messages
+ # => {:name=>["can't be blank", "is too short (minimum is 3 characters)"]}
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors.messages # => {}
+```
+
+### `errors[]`
+
+`errors[]` is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new(name: "John Doe")
+person.valid? # => true
+person.errors[:name] # => []
+
+person = Person.new(name: "JD")
+person.valid? # => false
+person.errors[:name] # => ["is too short (minimum is 3 characters)"]
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+```
+
+### `errors.add`
+
+The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself.
+
+```ruby
+class Person < ActiveRecord::Base
+ def a_method_used_for_validation_purposes
+ errors.add(:name, "cannot contain the characters !@#%*()_-+=")
+ end
+end
+
+person = Person.create(name: "!@#")
+
+person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+person.errors.full_messages
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+```
+
+Another way to do this is using `[]=` setter
+
+```ruby
+ class Person < ActiveRecord::Base
+ def a_method_used_for_validation_purposes
+ errors[:name] = "cannot contain the characters !@#%*()_-+="
+ end
+ end
+
+ person = Person.create(name: "!@#")
+
+ person.errors[:name]
+ # => ["cannot contain the characters !@#%*()_-+="]
+
+ person.errors.to_a
+ # => ["Name cannot contain the characters !@#%*()_-+="]
+```
+
+### `errors[:base]`
+
+You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can simply add a string to it and it will be used as an error message.
+
+```ruby
+class Person < ActiveRecord::Base
+ def a_method_used_for_validation_purposes
+ errors[:base] << "This person is invalid because ..."
+ end
+end
+```
+
+### `errors.clear`
+
+The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors[:name]
+ # => ["can't be blank", "is too short (minimum is 3 characters)"]
+
+person.errors.clear
+person.errors.empty? # => true
+
+p.save # => false
+
+p.errors[:name]
+# => ["can't be blank", "is too short (minimum is 3 characters)"]
+```
+
+### `errors.size`
+
+The `size` method returns the total number of error messages for the object.
+
+```ruby
+class Person < ActiveRecord::Base
+ validates :name, presence: true, length: { minimum: 3 }
+end
+
+person = Person.new
+person.valid? # => false
+person.errors.size # => 2
+
+person = Person.new(name: "Andrea", email: "andrea@example.com")
+person.valid? # => true
+person.errors.size # => 0
+```
+
+Displaying Validation Errors in Views
+-------------------------------------
+
+Once you've created a model and added validations, if that model is created via
+a web form, you probably want to display an error message when one of the
+validations fail.
+
+Because every application handles this kind of thing differently, Rails does
+not include any view helpers to help you generate these messages directly.
+However, due to the rich number of methods Rails gives you to interact with
+validations in general, it's fairly easy to build your own. In addition, when
+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:
+
+```ruby
+<% 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 %>
+```
+
+Furthermore, if you use the Rails form helpers to generate your forms, when
+a validation error occurs on a field, it will generate an extra `<div>` around
+the entry.
+
+```
+<div class="field_with_errors">
+ <input id="post_title" name="post[title]" size="30" type="text" value="">
+</div>
+```
+
+You can then style this div however you'd like. The default scaffold that
+Rails generates, for example, adds this CSS rule:
+
+```
+.field_with_errors {
+ padding: 2px;
+ background-color: red;
+ display: table;
+}
+```
+
+This means that any field with an error ends up with a 2 pixel red border.
diff --git a/guides/source/active_record_validations_callbacks.md b/guides/source/active_record_validations_callbacks.md
deleted file mode 100644
index 333cbdd90b..0000000000
--- a/guides/source/active_record_validations_callbacks.md
+++ /dev/null
@@ -1,1365 +0,0 @@
-Active Record Validations and Callbacks
-=======================================
-
-This guide teaches you how to hook into the life cycle of your Active Record objects. You will learn how to validate the state of objects before they go into the database, and how to perform custom operations at certain points in the object life cycle.
-
-After reading this guide and trying out the presented concepts, we hope that you'll be able to:
-
-* Understand the life cycle of Active Record objects
-* Use the built-in Active Record validation helpers
-* Create your own custom validation methods
-* Work with the error messages generated by the validation process
-* Create callback methods that respond to events in the object life cycle
-* Create special classes that encapsulate common behavior for your callbacks
-* Create Observers that respond to life cycle events outside of the original class
-
---------------------------------------------------------------------------------
-
-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.
-
-Validations allow you to ensure that only valid data is stored in your database. Callbacks and observers allow you to trigger logic before or after an alteration of an object's state.
-
-Validations Overview
---------------------
-
-Before you dive into the detail of validations in Rails, you should understand a bit about how validations fit into the big picture.
-
-### Why Use Validations?
-
-Validations are used to ensure that only valid data is saved into your database. For example, it may be important to your application to ensure that every user provides a valid email address and mailing address.
-
-There are several ways to validate data before it is saved into your database, including native database constraints, client-side validations, controller-level validations, and model-level validations:
-
-* Database constraints and/or stored procedures make the validation mechanisms database-dependent and can make testing and maintenance more difficult. However, if your database is used by other applications, it may be a good idea to use some constraints at the database level. Additionally, database-level validations can safely handle some things (such as uniqueness in heavily-used tables) that can be difficult to implement otherwise.
-* Client-side validations can be useful, but are generally unreliable if used alone. If they are implemented using JavaScript, they may be bypassed if JavaScript is turned off in the user's browser. However, if combined with other techniques, client-side validation can be a convenient way to provide users with immediate feedback as they use your site.
-* Controller-level validations can be tempting to use, but often become unwieldy and difficult to test and maintain. Whenever possible, it's a good idea to [keep your controllers skinny](http://weblog.jamisbuck.org/2006/10/18/skinny-controller-fat-model), as it will make your application a pleasure to work with in the long run.
-* Model-level validations are the best way to ensure that only valid data is saved into your database. They are database agnostic, cannot be bypassed by end users, and are convenient to test and maintain. Rails makes them easy to use, provides built-in helpers for common needs, and allows you to create your own validation methods as well.
-
-### When Does Validation Happen?
-
-There are two kinds of Active Record objects: those that correspond to a row inside your database and those that do not. When you create a fresh object, for example using the `new` method, that object does not belong to the database yet. Once you call `save` upon that object it will be saved into the appropriate database table. Active Record uses the `new_record?` instance method to determine whether an object is already in the database or not. Consider the following simple Active Record class:
-
-```ruby
-class Person < ActiveRecord::Base
-end
-```
-
-We can see how it works by looking at some `rails console` output:
-
-```ruby
-$ rails console
->> p = Person.new(:name => "John Doe")
-=> #<Person id: nil, name: "John Doe", created_at: nil, updated_at: nil>
->> p.new_record?
-=> true
->> p.save
-=> true
->> p.new_record?
-=> false
-```
-
-TIP: All lines starting with a dollar sign `$` are intended to be run on the command line.
-
-Creating and saving a new record will send an SQL `INSERT` operation to the database. Updating an existing record will send an SQL `UPDATE` operation instead. Validations are typically run before these commands are sent to the database. If any validations fail, the object will be marked as invalid and Active Record will not perform the `INSERT` or `UPDATE` operation. This helps to avoid storing an invalid object in the database. You can choose to have specific validations run when an object is created, saved, or updated.
-
-CAUTION: There are many ways to change the state of an object in the database. Some methods will trigger validations, but some will not. This means that it's possible to save an object in the database in an invalid state if you aren't careful.
-
-The following methods trigger validations, and will save the object to the database only if the object is valid:
-
-* `create`
-* `create!`
-* `save`
-* `save!`
-* `update`
-* `update_attributes`
-* `update_attributes!`
-
-The bang versions (e.g. `save!`) raise an exception if the record is invalid. The non-bang versions don't: `save` and `update_attributes` return `false`, `create` and `update` just return the objects.
-
-### Skipping Validations
-
-The following methods skip validations, and will save the object to the database regardless of its validity. They should be used with caution.
-
-* `decrement!`
-* `decrement_counter`
-* `increment!`
-* `increment_counter`
-* `toggle!`
-* `touch`
-* `update_all`
-* `update_attribute`
-* `update_column`
-* `update_columns`
-* `update_counters`
-
-Note that `save` also has the ability to skip validations if passed `:validate => false` as argument. This technique should be used with caution.
-
-* `save(:validate => false)`
-
-### `valid?` and `invalid?`
-
-To verify whether or not an object is valid, Rails uses the `valid?` method. You can also use this method on your own. `valid?` triggers your validations and returns true if no errors were found in the object, and false otherwise.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true
-end
-
-Person.create(:name => "John Doe").valid? # => true
-Person.create(:name => nil).valid? # => false
-```
-
-After Active Record has performed validations, any errors found can be accessed through the `errors` instance method, which returns a collection of errors. By definition, an object is valid if this collection is empty after running validations.
-
-Note that an object instantiated with `new` will not report errors even if it's technically invalid, because validations are not run when using `new`.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true
-end
-
->> p = Person.new
-#=> #<Person id: nil, name: nil>
->> p.errors
-#=> {}
-
->> p.valid?
-#=> false
->> p.errors
-#=> {:name=>["can't be blank"]}
-
->> p = Person.create
-#=> #<Person id: nil, name: nil>
->> p.errors
-#=> {:name=>["can't be blank"]}
-
->> p.save
-#=> false
-
->> p.save!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
-
->> Person.create!
-#=> ActiveRecord::RecordInvalid: Validation failed: Name can't be blank
-```
-
-`invalid?` is simply the inverse of `valid?`. It triggers your validations, returning true if any errors were found in the object, and false otherwise.
-
-### `errors[]`
-
-To verify whether or not a particular attribute of an object is valid, you can use `errors[:attribute]`. It returns an array of all the errors for `:attribute`. If there are no errors on the specified attribute, an empty array is returned.
-
-This method is only useful _after_ validations have been run, because it only inspects the errors collection and does not trigger validations itself. It's different from the `ActiveRecord::Base#invalid?` method explained above because it doesn't verify the validity of the object as a whole. It only checks to see whether there are errors found on an individual attribute of the object.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true
-end
-
->> Person.new.errors[:name].any? # => false
->> Person.create.errors[:name].any? # => true
-```
-
-We'll cover validation errors in greater depth in the [Working with Validation Errors](#working-with-validation-errors) section. For now, let's turn to the built-in validation helpers that Rails provides by default.
-
-Validation Helpers
-------------------
-
-Active Record offers many pre-defined validation helpers that you can use directly inside your class definitions. These helpers provide common validation rules. Every time a validation fails, an error message is added to the object's `errors` collection, and this message is associated with the attribute being validated.
-
-Each helper accepts an arbitrary number of attribute names, so with a single line of code you can add the same kind of validation to several attributes.
-
-All of them accept the `:on` and `:message` options, which define when the validation should be run and what message should be added to the `errors` collection if it fails, respectively. The `:on` option takes one of the values `:save` (the default), `:create` or `:update`. There is a default error message for each one of the validation helpers. These messages are used when the `:message` option isn't specified. Let's take a look at each one of the available helpers.
-
-### `acceptance`
-
-Validates that a checkbox on the user interface was checked when a form was submitted. This is typically used when the user needs to agree to your application's terms of service, confirm reading some text, or any similar concept. This validation is very specific to web applications and this 'acceptance' does not need to be recorded anywhere in your database (if you don't have a field for it, the helper will just create a virtual attribute).
-
-```ruby
-class Person < ActiveRecord::Base
- validates :terms_of_service, :acceptance => true
-end
-```
-
-The default error message for this helper is "_must be accepted_".
-
-It can receive an `:accept` option, which determines the value that will be considered acceptance. It defaults to "1" and can be easily changed.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :terms_of_service, :acceptance => { :accept => 'yes' }
-end
-```
-
-### `validates_associated`
-
-You should use this helper when your model has associations with other models and they also need to be validated. When you try to save your object, `valid?` will be called upon each one of the associated objects.
-
-```ruby
-class Library < ActiveRecord::Base
- has_many :books
- validates_associated :books
-end
-```
-
-This validation will work with all of the association types.
-
-CAUTION: Don't use `validates_associated` on both ends of your associations. They would call each other in an infinite loop.
-
-The default error message for `validates_associated` is "_is invalid_". Note that each associated object will contain its own `errors` collection; errors do not bubble up to the calling model.
-
-### `confirmation`
-
-You should use this helper when you have two text fields that should receive exactly the same content. For example, you may want to confirm an email address or a password. This validation creates a virtual attribute whose name is the name of the field that has to be confirmed with "_confirmation" appended.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :email, :confirmation => true
-end
-```
-
-In your view template you could use something like
-
-```erb
-<%= text_field :person, :email %>
-<%= text_field :person, :email_confirmation %>
-```
-
-This check is performed only if `email_confirmation` is not `nil`. To require confirmation, make sure to add a presence check for the confirmation attribute (we'll take a look at `presence` later on this guide):
-
-```ruby
-class Person < ActiveRecord::Base
- validates :email, :confirmation => true
- validates :email_confirmation, :presence => true
-end
-```
-
-The default error message for this helper is "_doesn't match confirmation_".
-
-### `exclusion`
-
-This helper validates that the attributes' values are not included in a given set. In fact, this set can be any enumerable object.
-
-```ruby
-class Account < ActiveRecord::Base
- validates :subdomain, :exclusion => { :in => %w(www us ca jp),
- :message => "Subdomain %{value} is reserved." }
-end
-```
-
-The `exclusion` helper has an option `:in` that receives the set of values that will not be accepted for the validated attributes. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. This example uses the `:message` option to show how you can include the attribute's value.
-
-The default error message is "_is reserved_".
-
-### `format`
-
-This helper validates the attributes' values by testing whether they match a given regular expression, which is specified using the `:with` option.
-
-```ruby
-class Product < ActiveRecord::Base
- validates :legacy_code, :format => { :with => /\A[a-zA-Z]+\z/,
- :message => "Only letters allowed" }
-end
-```
-
-The default error message is "_is invalid_".
-
-### `inclusion`
-
-This helper validates that the attributes' values are included in a given set. In fact, this set can be any enumerable object.
-
-```ruby
-class Coffee < ActiveRecord::Base
- validates :size, :inclusion => { :in => %w(small medium large),
- :message => "%{value} is not a valid size" }
-end
-```
-
-The `inclusion` helper has an option `:in` that receives the set of values that will be accepted. The `:in` option has an alias called `:within` that you can use for the same purpose, if you'd like to. The previous example uses the `:message` option to show how you can include the attribute's value.
-
-The default error message for this helper is "_is not included in the list_".
-
-### `length`
-
-This helper validates the length of the attributes' values. It provides a variety of options, so you can specify length constraints in different ways:
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :length => { :minimum => 2 }
- validates :bio, :length => { :maximum => 500 }
- validates :password, :length => { :in => 6..20 }
- validates :registration_number, :length => { :is => 6 }
-end
-```
-
-The possible length constraint options are:
-
-* `:minimum` - The attribute cannot have less than the specified length.
-* `:maximum` - The attribute cannot have more than the specified length.
-* `:in` (or `:within`) - The attribute length must be included in a given interval. The value for this option must be a range.
-* `:is` - The attribute length must be equal to the given value.
-
-The default error messages depend on the type of length validation being performed. You can personalize these messages using the `:wrong_length`, `:too_long`, and `:too_short` options and `%{count}` as a placeholder for the number corresponding to the length constraint being used. You can still use the `:message` option to specify an error message.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :bio, :length => { :maximum => 1000,
- :too_long => "%{count} characters is the maximum allowed" }
-end
-```
-
-This helper counts characters by default, but you can split the value in a different way using the `:tokenizer` option:
-
-```ruby
-class Essay < ActiveRecord::Base
- validates :content, :length => {
- :minimum => 300,
- :maximum => 400,
- :tokenizer => lambda { |str| str.scan(/\w+/) },
- :too_short => "must have at least %{count} words",
- :too_long => "must have at most %{count} words"
- }
-end
-```
-
-Note that the default error messages are plural (e.g., "is too short (minimum is %{count} characters)"). For this reason, when `:minimum` is 1 you should provide a personalized message or use `validates_presence_of` instead. When `:in` or `:within` have a lower limit of 1, you should either provide a personalized message or call `presence` prior to `length`.
-
-The `size` helper is an alias for `length`.
-
-### `numericality`
-
-This helper validates that your attributes have only numeric values. By default, it will match an optional sign followed by an integral or floating point number. To specify that only integral numbers are allowed set `:only_integer` to true.
-
-If you set `:only_integer` to `true`, then it will use the
-
-```ruby
-/\A[+-]?\d+\Z/
-```
-
-regular expression to validate the attribute's value. Otherwise, it will try to convert the value to a number using `Float`.
-
-WARNING. Note that the regular expression above allows a trailing newline character.
-
-```ruby
-class Player < ActiveRecord::Base
- validates :points, :numericality => true
- validates :games_played, :numericality => { :only_integer => true }
-end
-```
-
-Besides `:only_integer`, this helper also accepts the following options to add constraints to acceptable values:
-
-* `:greater_than` - Specifies the value must be greater than the supplied value. The default error message for this option is "_must be greater than %{count}_".
-* `:greater_than_or_equal_to` - Specifies the value must be greater than or equal to the supplied value. The default error message for this option is "_must be greater than or equal to %{count}_".
-* `:equal_to` - Specifies the value must be equal to the supplied value. The default error message for this option is "_must be equal to %{count}_".
-* `:less_than` - Specifies the value must be less than the supplied value. The default error message for this option is "_must be less than %{count}_".
-* `:less_than_or_equal_to` - Specifies the value must be less than or equal the supplied value. The default error message for this option is "_must be less than or equal to %{count}_".
-* `:odd` - Specifies the value must be an odd number if set to true. The default error message for this option is "_must be odd_".
-* `:even` - Specifies the value must be an even number if set to true. The default error message for this option is "_must be even_".
-
-The default error message is "_is not a number_".
-
-### `presence`
-
-This helper validates that the specified attributes are not empty. It uses the `blank?` method to check if the value is either `nil` or a blank string, that is, a string that is either empty or consists of whitespace.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :login, :email, :presence => true
-end
-```
-
-If you want to be sure that an association is present, you'll need to test whether the foreign key used to map the association is present, and not the associated object itself.
-
-```ruby
-class LineItem < ActiveRecord::Base
- belongs_to :order
- validates :order_id, :presence => true
-end
-```
-
-If you validate the presence of an object associated via a `has_one` or `has_many` relationship, it will check that the object is neither `blank?` nor `marked_for_destruction?`.
-
-Since `false.blank?` is true, if you want to validate the presence of a boolean field you should use `validates :field_name, :inclusion => { :in => [true, false] }`.
-
-The default error message is "_can't be empty_".
-
-### `uniqueness`
-
-This helper validates that the attribute's value is unique right before the object gets saved. It does not create a uniqueness constraint in the database, so it may happen that two different database connections create two records with the same value for a column that you intend to be unique. To avoid that, you must create a unique index in your database.
-
-```ruby
-class Account < ActiveRecord::Base
- validates :email, :uniqueness => true
-end
-```
-
-The validation happens by performing an SQL query into the model's table, searching for an existing record with the same value in that attribute.
-
-There is a `:scope` option that you can use to specify other attributes that are used to limit the uniqueness check:
-
-```ruby
-class Holiday < ActiveRecord::Base
- validates :name, :uniqueness => { :scope => :year,
- :message => "should happen once per year" }
-end
-```
-
-There is also a `:case_sensitive` option that you can use to define whether the uniqueness constraint will be case sensitive or not. This option defaults to true.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :uniqueness => { :case_sensitive => false }
-end
-```
-
-WARNING. Note that some databases are configured to perform case-insensitive searches anyway.
-
-The default error message is "_has already been taken_".
-
-### `validates_with`
-
-This helper passes the record to a separate class for validation.
-
-```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator
-end
-
-class GoodnessValidator < ActiveModel::Validator
- def validate(record)
- if record.first_name == "Evil"
- record.errors[:base] << "This person is evil"
- end
- end
-end
-```
-
-NOTE: Errors added to `record.errors[:base]` relate to the state of the record as a whole, and not to a specific attribute.
-
-The `validates_with` helper takes a class, or a list of classes to use for validation. There is no default error message for `validates_with`. You must manually add errors to the record's errors collection in the validator class.
-
-To implement the validate method, you must have a `record` parameter defined, which is the record to be validated.
-
-Like all other validations, `validates_with` takes the `:if`, `:unless` and `:on` options. If you pass any other options, it will send those options to the validator class as `options`:
-
-```ruby
-class Person < ActiveRecord::Base
- validates_with GoodnessValidator, :fields => [:first_name, :last_name]
-end
-
-class GoodnessValidator < ActiveModel::Validator
- def validate(record)
- if options[:fields].any?{|field| record.send(field) == "Evil" }
- record.errors[:base] << "This person is evil"
- end
- end
-end
-```
-
-### `validates_each`
-
-This helper validates attributes against a block. It doesn't have a predefined validation function. You should create one using a block, and every attribute passed to `validates_each` will be tested against it. In the following example, we don't want names and surnames to begin with lower case.
-
-```ruby
-class Person < ActiveRecord::Base
- validates_each :name, :surname do |record, attr, value|
- record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
- end
-end
-```
-
-The block receives the record, the attribute's name and the attribute's value. You can do anything you like to check for valid data within the block. If your validation fails, you should add an error message to the model, therefore making it invalid.
-
-Common Validation Options
--------------------------
-
-These are common validation options:
-
-### `:allow_nil`
-
-The `:allow_nil` option skips the validation when the value being validated is `nil`.
-
-```ruby
-class Coffee < ActiveRecord::Base
- validates :size, :inclusion => { :in => %w(small medium large),
- :message => "%{value} is not a valid size" }, :allow_nil => true
-end
-```
-
-TIP: `:allow_nil` is ignored by the presence validator.
-
-### `:allow_blank`
-
-The `:allow_blank` option is similar to the `:allow_nil` option. This option will let validation pass if the attribute's value is `blank?`, like `nil` or an empty string for example.
-
-```ruby
-class Topic < ActiveRecord::Base
- validates :title, :length => { :is => 5 }, :allow_blank => true
-end
-
-Topic.create("title" => "").valid? # => true
-Topic.create("title" => nil).valid? # => true
-```
-
-TIP: `:allow_blank` is ignored by the presence validator.
-
-### `:message`
-
-As you've already seen, the `:message` option lets you specify the message that will be added to the `errors` collection when validation fails. When this option is not used, Active Record will use the respective default error message for each validation helper.
-
-### `:on`
-
-The `:on` option lets you specify when the validation should happen. The default behavior for all the built-in validation helpers is to be run on save (both when you're creating a new record and when you're updating it). If you want to change it, you can use `:on => :create` to run the validation only when a new record is created or `:on => :update` to run the validation only when a record is updated.
-
-```ruby
-class Person < ActiveRecord::Base
- # it will be possible to update email with a duplicated value
- validates :email, :uniqueness => true, :on => :create
-
- # it will be possible to create the record with a non-numerical age
- validates :age, :numericality => true, :on => :update
-
- # the default (validates on both create and update)
- validates :name, :presence => true, :on => :save
-end
-```
-
-Strict Validations
-------------------
-
-You can also specify validations to be strict and raise `ActiveModel::StrictValidationFailed` when the object is invalid.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => { :strict => true }
-end
-
-Person.new.valid? #=> ActiveModel::StrictValidationFailed: Name can't be blank
-```
-
-There is also an ability to pass custom exception to `:strict` option
-
-```ruby
-class Person < ActiveRecord::Base
- validates :token, :presence => true, :uniqueness => true, :strict => TokenGenerationException
-end
-
-Person.new.valid? #=> TokenGenerationException: Token can't be blank
-```
-
-Conditional Validation
-----------------------
-
-Sometimes it will make sense to validate an object just when a given predicate is satisfied. You can do that by using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify when the validation **should** happen. If you want to specify when the validation **should not** happen, then you may use the `:unless` option.
-
-### Using a Symbol with `:if` and `:unless`
-
-You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a method that will get called right before validation happens. This is the most commonly used option.
-
-```ruby
-class Order < ActiveRecord::Base
- validates :card_number, :presence => true, :if => :paid_with_card?
-
- def paid_with_card?
- payment_type == "card"
- end
-end
-```
-
-### Using a String with `:if` and `:unless`
-
-You can also use a string that will be evaluated using `eval` and needs to contain valid Ruby code. You should use this option only when the string represents a really short condition.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :surname, :presence => true, :if => "name.nil?"
-end
-```
-
-### Using a Proc with `:if` and `:unless`
-
-Finally, it's possible to associate `:if` and `:unless` with a `Proc` object which will be called. Using a `Proc` object gives you the ability to write an inline condition instead of a separate method. This option is best suited for one-liners.
-
-```ruby
-class Account < ActiveRecord::Base
- validates :password, :confirmation => true,
- :unless => Proc.new { |a| a.password.blank? }
-end
-```
-
-### Grouping conditional validations
-
-Sometimes it is useful to have multiple validations use one condition, it can be easily achieved using `with_options`.
-
-```ruby
-class User < ActiveRecord::Base
- with_options :if => :is_admin? do |admin|
- admin.validates :password, :length => { :minimum => 10 }
- admin.validates :email, :presence => true
- end
-end
-```
-
-All validations inside of `with_options` block will have automatically passed the condition `:if => :is_admin?`
-
-### Combining validation conditions
-
-On the other hand, when multiple conditions define whether or not a validation should happen, an `Array` can be used. Moreover, you can apply both `:if` and `:unless` to the same validation.
-
-```ruby
-class Computer < ActiveRecord::Base
- validates :mouse, :presence => true,
- :if => ["market.retail?", :desktop?]
- :unless => Proc.new { |c| c.trackpad.present? }
-end
-```
-
-The validation only runs when all the `:if` conditions and none of the `:unless` conditions are evaluated to `true`.
-
-Performing Custom Validations
------------------------------
-
-When the built-in validation helpers are not enough for your needs, you can write your own validators or validation methods as you prefer.
-
-### Custom Validators
-
-Custom validators are classes that extend `ActiveModel::Validator`. These classes must implement a `validate` method which takes a record as an argument and performs the validation on it. The custom validator is called using the `validates_with` method.
-
-```ruby
-class MyValidator < ActiveModel::Validator
- def validate(record)
- unless record.name.starts_with? 'X'
- record.errors[:name] << 'Need a name starting with X please!'
- end
- end
-end
-
-class Person
- include ActiveModel::Validations
- validates_with MyValidator
-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 instance.
-
-```ruby
-class EmailValidator < ActiveModel::EachValidator
- def validate_each(record, attribute, value)
- unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
- record.errors[attribute] << (options[:message] || "is not an email")
- end
- end
-end
-
-class Person < ActiveRecord::Base
- validates :email, :presence => true, :email => true
-end
-```
-
-As shown in the example, you can also combine standard validations with your own custom validators.
-
-### Custom Methods
-
-You can also create methods that verify the state of your models and add messages to the `errors` collection when they are invalid. You must then register these methods by using the `validate` class method, passing in the symbols for the validation methods' names.
-
-You can pass more than one symbol for each class method and the respective validations will be run in the same order as they were registered.
-
-```ruby
-class Invoice < ActiveRecord::Base
- validate :expiration_date_cannot_be_in_the_past,
- :discount_cannot_be_greater_than_total_value
-
- def expiration_date_cannot_be_in_the_past
- if !expiration_date.blank? and expiration_date < Date.today
- errors.add(:expiration_date, "can't be in the past")
- end
- end
-
- def discount_cannot_be_greater_than_total_value
- if discount > total_value
- errors.add(:discount, "can't be greater than total value")
- end
- end
-end
-```
-
-By default such validations will run every time you call `valid?`. It is also possible to control when to run these custom validations by giving an `:on` option to the `validate` method, with either: `:create` or `:update`.
-
-```ruby
-class Invoice < ActiveRecord::Base
- validate :active_customer, :on => :create
-
- def active_customer
- errors.add(:customer_id, "is not active") unless customer.active?
- end
-end
-```
-
-You can even create your own validation helpers and reuse them in several different models. For example, an application that manages surveys may find it useful to express that a certain field corresponds to a set of choices:
-
-```ruby
-ActiveRecord::Base.class_eval do
- def self.validates_as_choice(attr_name, n, options={})
- validates attr_name, :inclusion => { { :in => 1..n }.merge!(options) }
- end
-end
-```
-
-Simply reopen `ActiveRecord::Base` and define a class method like that. You'd typically put this code somewhere in `config/initializers`. You can use this helper like this:
-
-```ruby
-class Movie < ActiveRecord::Base
- validates_as_choice :rating, 5
-end
-```
-
-Working with Validation Errors
-------------------------------
-
-In addition to the `valid?` and `invalid?` methods covered earlier, Rails provides a number of methods for working with the `errors` collection and inquiring about the validity of objects.
-
-The following is a list of the most commonly used methods. Please refer to the `ActiveModel::Errors` documentation for a list of all the available methods.
-
-### `errors`
-
-Returns an instance of the class `ActiveModel::Errors` containing all errors. Each key is the attribute name and the value is an array of strings with all errors.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true, :length => { :minimum => 3 }
-end
-
-person = Person.new
-person.valid? # => false
-person.errors
- # => {:name => ["can't be blank", "is too short (minimum is 3 characters)"]}
-
-person = Person.new(:name => "John Doe")
-person.valid? # => true
-person.errors # => []
-```
-
-### `errors[]`
-
-`errors[]` is used when you want to check the error messages for a specific attribute. It returns an array of strings with all error messages for the given attribute, each string with one error message. If there are no errors related to the attribute, it returns an empty array.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true, :length => { :minimum => 3 }
-end
-
-person = Person.new(:name => "John Doe")
-person.valid? # => true
-person.errors[:name] # => []
-
-person = Person.new(:name => "JD")
-person.valid? # => false
-person.errors[:name] # => ["is too short (minimum is 3 characters)"]
-
-person = Person.new
-person.valid? # => false
-person.errors[:name]
- # => ["can't be blank", "is too short (minimum is 3 characters)"]
-```
-
-### `errors.add`
-
-The `add` method lets you manually add messages that are related to particular attributes. You can use the `errors.full_messages` or `errors.to_a` methods to view the messages in the form they might be displayed to a user. Those particular messages get the attribute name prepended (and capitalized). `add` receives the name of the attribute you want to add the message to, and the message itself.
-
-```ruby
-class Person < ActiveRecord::Base
- def a_method_used_for_validation_purposes
- errors.add(:name, "cannot contain the characters !@#%*()_-+=")
- end
-end
-
-person = Person.create(:name => "!@#")
-
-person.errors[:name]
- # => ["cannot contain the characters !@#%*()_-+="]
-
-person.errors.full_messages
- # => ["Name cannot contain the characters !@#%*()_-+="]
-```
-
-Another way to do this is using `[]=` setter
-
-```ruby
- class Person < ActiveRecord::Base
- def a_method_used_for_validation_purposes
- errors[:name] = "cannot contain the characters !@#%*()_-+="
- end
- end
-
- person = Person.create(:name => "!@#")
-
- person.errors[:name]
- # => ["cannot contain the characters !@#%*()_-+="]
-
- person.errors.to_a
- # => ["Name cannot contain the characters !@#%*()_-+="]
-```
-
-### `errors[:base]`
-
-You can add error messages that are related to the object's state as a whole, instead of being related to a specific attribute. You can use this method when you want to say that the object is invalid, no matter the values of its attributes. Since `errors[:base]` is an array, you can simply add a string to it and it will be used as an error message.
-
-```ruby
-class Person < ActiveRecord::Base
- def a_method_used_for_validation_purposes
- errors[:base] << "This person is invalid because ..."
- end
-end
-```
-
-### `errors.clear`
-
-The `clear` method is used when you intentionally want to clear all the messages in the `errors` collection. Of course, calling `errors.clear` upon an invalid object won't actually make it valid: the `errors` collection will now be empty, but the next time you call `valid?` or any method that tries to save this object to the database, the validations will run again. If any of the validations fail, the `errors` collection will be filled again.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true, :length => { :minimum => 3 }
-end
-
-person = Person.new
-person.valid? # => false
-person.errors[:name]
- # => ["can't be blank", "is too short (minimum is 3 characters)"]
-
-person.errors.clear
-person.errors.empty? # => true
-
-p.save # => false
-
-p.errors[:name]
-# => ["can't be blank", "is too short (minimum is 3 characters)"]
-```
-
-### `errors.size`
-
-The `size` method returns the total number of error messages for the object.
-
-```ruby
-class Person < ActiveRecord::Base
- validates :name, :presence => true, :length => { :minimum => 3 }
-end
-
-person = Person.new
-person.valid? # => false
-person.errors.size # => 2
-
-person = Person.new(:name => "Andrea", :email => "andrea@example.com")
-person.valid? # => true
-person.errors.size # => 0
-```
-
-Displaying Validation Errors in the View
-----------------------------------------
-
-[DynamicForm](https://github.com/joelmoss/dynamic_form) provides helpers to display the error messages of your models in your view templates.
-
-You can install it as a gem by adding this line to your Gemfile:
-
-```ruby
-gem "dynamic_form"
-```
-
-Now you will have access to the two helper methods `error_messages` and `error_messages_for` in your view templates.
-
-### `error_messages` and `error_messages_for`
-
-When creating a form with the `form_for` helper, you can use the `error_messages` method on the form builder to render all failed validation messages for the current model instance.
-
-```ruby
-class Product < ActiveRecord::Base
- validates :description, :value, :presence => true
- validates :value, :numericality => true, :allow_nil => true
-end
-```
-
-```erb
-<%= form_for(@product) do |f| %>
- <%= f.error_messages %>
- <p>
- <%= f.label :description %><br />
- <%= f.text_field :description %>
- </p>
- <p>
- <%= f.label :value %><br />
- <%= f.text_field :value %>
- </p>
- <p>
- <%= f.submit "Create" %>
- </p>
-<% end %>
-```
-
-If you submit the form with empty fields, the result will be similar to the one shown below:
-
-![Error messages](images/error_messages.png)
-
-NOTE: The appearance of the generated HTML will be different from the one shown, unless you have used scaffolding. See [Customizing the Error Messages CSS](#customizing-the-error-messages-css).
-
-You can also use the `error_messages_for` helper to display the error messages of a model assigned to a view template. It is very similar to the previous example and will achieve exactly the same result.
-
-```erb
-<%= error_messages_for :product %>
-```
-
-The displayed text for each error message will always be formed by the capitalized name of the attribute that holds the error, followed by the error message itself.
-
-Both the `form.error_messages` and the `error_messages_for` helpers accept options that let you customize the `div` element that holds the messages, change the header text, change the message below the header, and specify the tag used for the header element. For example,
-
-```erb
-<%= f.error_messages :header_message => "Invalid product!",
- :message => "You'll need to fix the following fields:",
- :header_tag => :h3 %>
-```
-
-results in:
-
-![Customized error messages](images/customized_error_messages.png)
-
-If you pass `nil` in any of these options, the corresponding section of the `div` will be discarded.
-
-### Customizing the Error Messages CSS
-
-The selectors used to customize the style of error messages are:
-
-* `.field_with_errors` - Style for the form fields and labels with errors.
-* `#error_explanation` - Style for the `div` element with the error messages.
-* `#error_explanation h2` - Style for the header of the `div` element.
-* `#error_explanation p` - Style for the paragraph holding the message that appears right below the header of the `div` element.
-* `#error_explanation ul li` - Style for the list items with individual error messages.
-
-If scaffolding was used, file `app/assets/stylesheets/scaffolds.css.scss` will have been generated automatically. This file defines the red-based styles you saw in the examples above.
-
-The name of the class and the id can be changed with the `:class` and `:id` options, accepted by both helpers.
-
-### Customizing the Error Messages HTML
-
-By default, form fields with errors are displayed enclosed by a `div` element with the `field_with_errors` CSS class. However, it's possible to override that.
-
-The way form fields with errors are treated is defined by `ActionView::Base.field_error_proc`. This is a `Proc` that receives two parameters:
-
-* A string with the HTML tag
-* An instance of `ActionView::Helpers::InstanceTag`.
-
-Below is a simple example where we change the Rails behavior to always display the error messages in front of each of the form fields in error. The error messages will be enclosed by a `span` element with a `validation-error` CSS class. There will be no `div` element enclosing the `input` element, so we get rid of that red border around the text field. You can use the `validation-error` CSS class to style it anyway you want.
-
-```ruby
-ActionView::Base.field_error_proc = Proc.new do |html_tag, instance|
- errors = Array(instance.error_message).join(',')
- %(#{html_tag}<span class="validation-error">&nbsp;#{errors}</span>).html_safe
-end
-```
-
-The result looks like the following:
-
-![Validation error messages](images/validation_error_messages.png)
-
-Callbacks Overview
-------------------
-
-Callbacks are methods that get called at certain moments of an object's life cycle. With callbacks it is possible to write code that will run whenever an Active Record object is created, saved, updated, deleted, validated, or loaded from the database.
-
-### Callback Registration
-
-In order to use the available callbacks, you need to register them. You can implement the callbacks as ordinary methods and use a macro-style class method to register them as callbacks:
-
-```ruby
-class User < ActiveRecord::Base
- validates :login, :email, :presence => true
-
- before_validation :ensure_login_has_a_value
-
- protected
- def ensure_login_has_a_value
- if login.nil?
- self.login = email unless email.blank?
- end
- end
-end
-```
-
-The macro-style class methods can also receive a block. Consider using this style if the code inside your block is so short that it fits in a single line:
-
-```ruby
-class User < ActiveRecord::Base
- validates :login, :email, :presence => true
-
- before_create do |user|
- user.name = user.login.capitalize if user.name.blank?
- end
-end
-```
-
-Callbacks can also be registered to only fire on certain lifecycle events:
-<ruby>
-class User < ActiveRecord::Base
- before_validation :normalize_name, :on => :create
-
- # :on takes an array as well
- after_validation :set_location, :on => [ :create, :update ]
-
- protected
- def normalize_name
- self.name = self.name.downcase.titleize
- end
-
- def set_location
- self.location = LocationService.query(self)
- end
-end
-</ruby>
-
-It is considered good practice to declare callback methods as protected or private. If left public, they can be called from outside of the model and violate the principle of object encapsulation.
-
-Available Callbacks
--------------------
-
-Here is a list with all the available Active Record callbacks, listed in the same order in which they will get called during the respective operations:
-
-### Creating an Object
-
-* `before_validation`
-* `after_validation`
-* `before_save`
-* `around_save`
-* `before_create`
-* `around_create`
-* `after_create`
-* `after_save`
-
-### Updating an Object
-
-* `before_validation`
-* `after_validation`
-* `before_save`
-* `around_save`
-* `before_update`
-* `around_update`
-* `after_update`
-* `after_save`
-
-### Destroying an Object
-
-* `before_destroy`
-* `around_destroy`
-* `after_destroy`
-
-WARNING. `after_save` runs both on create and update, but always _after_ the more specific callbacks `after_create` and `after_update`, no matter the order in which the macro calls were executed.
-
-### `after_initialize` and `after_find`
-
-The `after_initialize` callback will be called whenever an Active Record object is instantiated, either by directly using `new` or when a record is loaded from the database. It can be useful to avoid the need to directly override your Active Record `initialize` method.
-
-The `after_find` callback will be called whenever Active Record loads a record from the database. `after_find` is called before `after_initialize` if both are defined.
-
-The `after_initialize` and `after_find` callbacks have no `before_*` counterparts, but they can be registered just like the other Active Record callbacks.
-
-```ruby
-class User < ActiveRecord::Base
- after_initialize do |user|
- puts "You have initialized an object!"
- end
-
- after_find do |user|
- puts "You have found an object!"
- end
-end
-
->> User.new
-You have initialized an object!
-=> #<User id: nil>
-
->> User.first
-You have found an object!
-You have initialized an object!
-=> #<User id: 1>
-```
-
-Running Callbacks
------------------
-
-The following methods trigger callbacks:
-
-* `create`
-* `create!`
-* `decrement!`
-* `destroy`
-* `destroy_all`
-* `increment!`
-* `save`
-* `save!`
-* `save(:validate => false)`
-* `toggle!`
-* `update`
-* `update_attribute`
-* `update_attributes`
-* `update_attributes!`
-* `valid?`
-
-Additionally, the `after_find` callback is triggered by the following finder methods:
-
-* `all`
-* `first`
-* `find`
-* `find_all_by_<em>attribute</em>`
-* `find_by_<em>attribute</em>`
-* `find_by_<em>attribute</em>!`
-* `find_by_sql`
-* `last`
-
-The `after_initialize` callback is triggered every time a new object of the class is initialized.
-
-Skipping Callbacks
-------------------
-
-Just as with validations, it is also possible to skip callbacks. These methods should be used with caution, however, because important business rules and application logic may be kept in callbacks. Bypassing them without understanding the potential implications may lead to invalid data.
-
-* `decrement`
-* `decrement_counter`
-* `delete`
-* `delete_all`
-* `increment`
-* `increment_counter`
-* `toggle`
-* `touch`
-* `update_column`
-* `update_columns`
-* `update_all`
-* `update_counters`
-
-Halting Execution
------------------
-
-As you start registering new callbacks for your models, they will be queued for execution. This queue will include all your model's validations, the registered callbacks, and the database operation to be executed.
-
-The whole callback chain is wrapped in a transaction. If any <em>before</em> callback method returns exactly `false` or raises an exception, the execution chain gets halted and a ROLLBACK is issued; <em>after</em> callbacks can only accomplish that by raising an exception.
-
-WARNING. Raising an arbitrary exception may break code that expects `save` and its friends not to fail like that. The `ActiveRecord::Rollback` exception is thought precisely to tell Active Record a rollback is going on. That one is internally captured but not reraised.
-
-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:
-
-```ruby
-class User < ActiveRecord::Base
- has_many :posts, :dependent => :destroy
-end
-
-class Post < ActiveRecord::Base
- after_destroy :log_destroy_action
-
- def log_destroy_action
- puts 'Post destroyed'
- end
-end
-
->> user = User.first
-=> #<User id: 1>
->> user.posts.create!
-=> #<Post id: 1, user_id: 1>
->> user.destroy
-Post destroyed
-=> #<User id: 1>
-```
-
-Conditional Callbacks
----------------------
-
-As with validations, we can also make the calling of a callback method conditional on the satisfaction of a given predicate. We can do this using the `:if` and `:unless` options, which can take a symbol, a string, a `Proc` or an `Array`. You may use the `:if` option when you want to specify under which conditions the callback **should** be called. If you want to specify the conditions under which the callback **should not** be called, then you may use the `:unless` option.
-
-### Using `:if` and `:unless` with a `Symbol`
-
-You can associate the `:if` and `:unless` options with a symbol corresponding to the name of a predicate method that will get called right before the callback. When using the `:if` option, the callback won't be executed if the predicate method returns false; when using the `:unless` option, the callback won't be executed if the predicate method returns true. This is the most common option. Using this form of registration it is also possible to register several different predicates that should be called to check if the callback should be executed.
-
-```ruby
-class Order < ActiveRecord::Base
- before_save :normalize_card_number, :if => :paid_with_card?
-end
-```
-
-### Using `:if` and `:unless` with a String
-
-You can also use a string that will be evaluated using `eval` and hence needs to contain valid Ruby code. You should use this option only when the string represents a really short condition:
-
-```ruby
-class Order < ActiveRecord::Base
- before_save :normalize_card_number, :if => "paid_with_card?"
-end
-```
-
-### Using `:if` and `:unless` with a `Proc`
-
-Finally, it is possible to associate `:if` and `:unless` with a `Proc` object. This option is best suited when writing short validation methods, usually one-liners:
-
-```ruby
-class Order < ActiveRecord::Base
- before_save :normalize_card_number,
- :if => Proc.new { |order| order.paid_with_card? }
-end
-```
-
-### Multiple Conditions for Callbacks
-
-When writing conditional callbacks, it is possible to mix both `:if` and `:unless` in the same callback declaration:
-
-```ruby
-class Comment < ActiveRecord::Base
- after_create :send_email_to_author, :if => :author_wants_emails?,
- :unless => Proc.new { |comment| comment.post.ignore_comments? }
-end
-```
-
-Callback Classes
-----------------
-
-Sometimes the callback methods that you'll write will be useful enough to be reused by other models. Active Record makes it possible to create classes that encapsulate the callback methods, so it becomes very easy to reuse them.
-
-Here's an example where we create a class with an `after_destroy` callback for a `PictureFile` model:
-
-```ruby
-class PictureFileCallbacks
- def after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
- File.delete(picture_file.filepath)
- end
- end
-end
-```
-
-When declared inside a class, as above, the callback methods will receive the model object as a parameter. We can now use the callback class in the model:
-
-```ruby
-class PictureFile < ActiveRecord::Base
- after_destroy PictureFileCallbacks.new
-end
-```
-
-Note that we needed to instantiate a new `PictureFileCallbacks` object, since we declared our callback as an instance method. This is particularly useful if the callbacks make use of the state of the instantiated object. Often, however, it will make more sense to declare the callbacks as class methods:
-
-```ruby
-class PictureFileCallbacks
- def self.after_destroy(picture_file)
- if File.exists?(picture_file.filepath)
- File.delete(picture_file.filepath)
- end
- end
-end
-```
-
-If the callback method is declared this way, it won't be necessary to instantiate a `PictureFileCallbacks` object.
-
-```ruby
-class PictureFile < ActiveRecord::Base
- after_destroy PictureFileCallbacks
-end
-```
-
-You can declare as many callbacks as you want inside your callback classes.
-
-Observers
----------
-
-Observers are similar to callbacks, but with important differences. Whereas callbacks can pollute a model with code that isn't directly related to its purpose, observers allow you to add the same functionality without changing the code of the model. For example, it could be argued that a `User` model should not include code to send registration confirmation emails. Whenever you use callbacks with code that isn't directly related to your model, you may want to consider creating an observer instead.
-
-### Creating Observers
-
-For example, imagine a `User` model where we want to send an email every time a new user is created. Because sending emails is not directly related to our model's purpose, we should create an observer to contain the code implementing this functionality.
-
-```bash
-$ rails generate observer User
-```
-
-generates `app/models/user_observer.rb` containing the observer class `UserObserver`:
-
-```ruby
-class UserObserver < ActiveRecord::Observer
-end
-```
-
-You may now add methods to be called at the desired occasions:
-
-```ruby
-class UserObserver < ActiveRecord::Observer
- def after_create(model)
- # code to send confirmation email...
- end
-end
-```
-
-As with callback classes, the observer's methods receive the observed model as a parameter.
-
-### Registering Observers
-
-Observers are conventionally placed inside of your `app/models` directory and registered in your application's `config/application.rb` file. For example, the `UserObserver` above would be saved as `app/models/user_observer.rb` and registered in `config/application.rb` this way:
-
-```ruby
-# Activate observers that should always be running.
-config.active_record.observers = :user_observer
-```
-
-As usual, settings in `config/environments` take precedence over those in `config/application.rb`. So, if you prefer that an observer doesn't run in all environments, you can simply register it in a specific environment instead.
-
-### Sharing Observers
-
-By default, Rails will simply strip "Observer" from an observer's name to find the model it should observe. However, observers can also be used to add behavior to more than one model, and thus it is possible to explicitly specify the models that our observer should observe:
-
-```ruby
-class MailerObserver < ActiveRecord::Observer
- observe :registration, :user
-
- def after_create(model)
- # code to send confirmation email...
- end
-end
-```
-
-In this example, the `after_create` method will be called whenever a `Registration` or `User` is created. Note that this new `MailerObserver` would also need to be registered in `config/application.rb` in order to take effect:
-
-```ruby
-# Activate observers that should always be running.
-config.active_record.observers = :mailer_observer
-```
-
-Transaction Callbacks
----------------------
-
-There are two additional callbacks that are triggered by the completion of a database transaction: `after_commit` and `after_rollback`. These callbacks are very similar to the `after_save` callback except that they don't execute until after database changes have either been committed or rolled back. They are most useful when your active record models need to interact with external systems which are not part of the database transaction.
-
-Consider, for example, the previous example where the `PictureFile` model needs to delete a file after the corresponding record is destroyed. If anything raises an exception after the `after_destroy` callback is called and the transaction rolls back, the file will have been deleted and the model will be left in an inconsistent state. For example, suppose that `picture_file_2` in the code below is not valid and the `save!` method raises an error.
-
-```ruby
-PictureFile.transaction do
- picture_file_1.destroy
- picture_file_2.save!
-end
-```
-
-By using the `after_commit` callback we can account for this case.
-
-```ruby
-class PictureFile < ActiveRecord::Base
- attr_accessor :delete_file
-
- after_destroy do |picture_file|
- picture_file.delete_file = picture_file.filepath
- end
-
- after_commit do |picture_file|
- if picture_file.delete_file && File.exist?(picture_file.delete_file)
- File.delete(picture_file.delete_file)
- picture_file.delete_file = nil
- end
- end
-end
-```
-
-The `after_commit` and `after_rollback` callbacks are guaranteed to be called for all models created, updated, or destroyed within a transaction block. If any exceptions are raised within one of these callbacks, they will be ignored so that they don't interfere with the other callbacks. As such, if your callback code could raise an exception, you'll need to rescue it and handle it appropriately within the callback.
diff --git a/guides/source/active_support_core_extensions.md b/guides/source/active_support_core_extensions.md
index f990b4f79f..0370e40012 100644
--- a/guides/source/active_support_core_extensions.md
+++ b/guides/source/active_support_core_extensions.md
@@ -5,7 +5,12 @@ Active Support is the Ruby on Rails component responsible for providing Ruby lan
It offers a richer bottom-line at the language level, targeted both at the development of Rails applications, and at the development of Ruby on Rails itself.
-By referring to this guide you will learn the extensions to the Ruby core classes and modules provided by Active Support.
+After reading this guide, you will know:
+
+* What Core Extensions are.
+* How to load all extensions.
+* How to cherry-pick just the extensions you want.
+* What extensions Active Support provides.
--------------------------------------------------------------------------------
@@ -14,7 +19,7 @@ How to Load Core Extensions
### Stand-Alone Active Support
-In order to have a near zero default footprint, Active Support does not load anything by default. It is broken in small pieces so that you may load just what you need, and also has some convenience entry points to load related extensions in one shot, even everything.
+In order to have a near-zero default footprint, Active Support does not load anything by default. It is broken in small pieces so that you can load just what you need, and also has some convenience entry points to load related extensions in one shot, even everything.
Thus, after a simple require like:
@@ -85,18 +90,19 @@ The following values are considered to be blank in a Rails application:
* empty arrays and hashes, and
-* any other object that responds to `empty?` and it is empty.
+* any other object that responds to `empty?` and is empty.
INFO: The predicate for strings uses the Unicode-aware character class `[:space:]`, so for example U+2029 (paragraph separator) is considered to be whitespace.
-WARNING: Note that numbers are not mentioned, in particular 0 and 0.0 are **not** blank.
+WARNING: Note that numbers are not mentioned. In particular, 0 and 0.0 are **not** blank.
-For example, this method from `ActionDispatch::Session::AbstractStore` uses `blank?` for checking whether a session key is present:
+For example, this method from `ActionController::HttpAuthentication::Token::ControllerMethods` uses `blank?` for checking whether a token is present:
```ruby
-def ensure_session_key!
- if @key.blank?
- raise ArgumentError, 'A key is required...'
+def authenticate(controller, &login_procedure)
+ token, options = token_and_options(controller.request)
+ unless token.blank?
+ login_procedure.call(token, options)
end
end
```
@@ -147,19 +153,21 @@ Some numbers which are not singletons are not duplicable either:
Active Support provides `duplicable?` to programmatically query an object about this property:
```ruby
+"foo".duplicable? # => true
"".duplicable? # => true
+0.0.duplicable? # => false
false.duplicable? # => false
```
-By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, and class and module objects.
+By definition all objects are `duplicable?` except `nil`, `false`, `true`, symbols, numbers, class, and module objects.
-WARNING. Any class can disallow duplication removing `dup` and `clone` or raising exceptions from them, only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case.
+WARNING: Any class can disallow duplication by removing `dup` and `clone` or raising exceptions from them. Thus only `rescue` can tell whether a given arbitrary object is duplicable. `duplicable?` depends on the hard-coded list above, but it is much faster than `rescue`. Use it only if you know the hard-coded list is enough in your use case.
NOTE: Defined in `active_support/core_ext/object/duplicable.rb`.
### `deep_dup`
-The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, ruby does not `dup` them. If you have an array with a string, for example, it will look like this:
+The `deep_dup` method returns deep copy of a given object. Normally, when you `dup` an object that contains other objects, Ruby does not `dup` them, so it creates a shallow copy of the object. If you have an array with a string, for example, it will look like this:
```ruby
array = ['string']
@@ -167,18 +175,18 @@ duplicate = array.dup
duplicate.push 'another-string'
-# object was duplicated, so element was added only to duplicate
+# the object was duplicated, so the element was added only to the duplicate
array #=> ['string']
duplicate #=> ['string', 'another-string']
duplicate.first.gsub!('string', 'foo')
-# first element was not duplicated, it will be changed for both arrays
+# first element was not duplicated, it will be changed in both arrays
array #=> ['foo']
duplicate #=> ['foo', 'another-string']
```
-As you can see, after duplicating `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside array is still the same object.
+As you can see, after duplicating the `Array` instance, we got another object, therefore we can modify it and the original object will stay unchanged. This is not true for array's elements, however. Since `dup` does not make deep copy, the string inside the array is still the same object.
If you need a deep copy of an object, you should use `deep_dup`. Here is an example:
@@ -192,12 +200,12 @@ array #=> ['string']
duplicate #=> ['foo']
```
-If object is not duplicable, `deep_dup` will just return this object:
+If the object is not duplicable, `deep_dup` will just return it:
```ruby
number = 1
-dup = number.deep_dup
-number.object_id == dup.object_id # => true
+duplicate = number.deep_dup
+number.object_id == duplicate.object_id # => true
```
NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`.
@@ -358,13 +366,13 @@ Arrays return the result of applying `to_query` to each element with `_key_[]` a
Hashes also respond to `to_query` but with a different signature. If no argument is passed a call generates a sorted series of key/value assignments calling `to_query(key)` on its values. Then it joins the result with "&":
```ruby
-{:c => 3, :b => 2, :a => 1}.to_query # => "a=1&b=2&c=3"
+{c: 3, b: 2, a: 1}.to_query # => "a=1&b=2&c=3"
```
The method `Hash#to_query` accepts an optional namespace for the keys:
```ruby
-{:id => 89, :name => "John Smith"}.to_query('user')
+{id: 89, name: "John Smith"}.to_query('user')
# => "user%5Bid%5D=89&user%5Bname%5D=John+Smith"
```
@@ -378,10 +386,10 @@ Given a default options hash, `with_options` yields a proxy object to a block. W
```ruby
class Account < ActiveRecord::Base
- has_many :customers, :dependent => :destroy
- has_many :products, :dependent => :destroy
- has_many :invoices, :dependent => :destroy
- has_many :expenses, :dependent => :destroy
+ has_many :customers, dependent: :destroy
+ has_many :products, dependent: :destroy
+ has_many :invoices, dependent: :destroy
+ has_many :expenses, dependent: :destroy
end
```
@@ -389,7 +397,7 @@ this way:
```ruby
class Account < ActiveRecord::Base
- with_options :dependent => :destroy do |assoc|
+ with_options dependent: :destroy do |assoc|
assoc.has_many :customers
assoc.has_many :products
assoc.has_many :invoices
@@ -401,9 +409,9 @@ end
That idiom may convey _grouping_ to the reader as well. For example, say you want to send a newsletter whose language depends on the user. Somewhere in the mailer you could group locale-dependent bits like this:
```ruby
-I18n.with_options :locale => user.locale, :scope => "newsletter" do |i18n|
+I18n.with_options locale: user.locale, scope: "newsletter" do |i18n|
subject i18n.t :subject
- body i18n.t :body, :user_name => user.name
+ body i18n.t :body, user_name: user.name
end
```
@@ -411,13 +419,20 @@ TIP: Since `with_options` forwards calls to its receiver they can be nested. Eac
NOTE: Defined in `active_support/core_ext/object/with_options.rb`.
+### JSON support
+
+Active Support provides a better implementation of `to_json` than the +json+ gem ordinarily provides for Ruby objects. This is because some classes, like +Hash+, +OrderedHash+, and +Process::Status+ need special handling in order to provide a proper JSON representation.
+
+NOTE: Defined in `active_support/core_ext/object/json.rb`.
+
### Instance Variables
Active Support provides several methods to ease access to instance variables.
-#### `instance_variable_names`
+#### `instance_values`
-Ruby 1.8 and 1.9 have a method called `instance_variables` that returns the names of the defined instance variables. But they behave differently, in 1.8 it returns strings whereas in 1.9 it returns symbols. Active Support defines `instance_variable_names` as a portable way to obtain them as strings:
+The method `instance_values` returns a hash that maps instance variable names without "@" to their
+corresponding values. Keys are strings:
```ruby
class C
@@ -426,17 +441,14 @@ class C
end
end
-C.new(0, 1).instance_variable_names # => ["@y", "@x"]
+C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
```
-WARNING: The order in which the names are returned is unspecified, and it indeed depends on the version of the interpreter.
-
NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`.
-#### `instance_values`
+#### `instance_variable_names`
-The method `instance_values` returns a hash that maps instance variable names without "@" to their
-corresponding values. Keys are strings:
+The method `instance_variable_names` returns an array. Each name includes the "@" sign.
```ruby
class C
@@ -445,7 +457,7 @@ class C
end
end
-C.new(0, 1).instance_values # => {"x" => 0, "y" => 1}
+C.new(0, 1).instance_variable_names # => ["@x", "@y"]
```
NOTE: Defined in `active_support/core_ext/object/instance_variables.rb`.
@@ -487,12 +499,11 @@ NOTE: Defined in `active_support/core_ext/kernel/reporting.rb`.
### `in?`
-The predicate `in?` tests if an object is included in another object or a list of objects. An `ArgumentError` exception will be raised if a single argument is passed and it does not respond to `include?`.
+The predicate `in?` tests if an object is included in another object. An `ArgumentError` exception will be raised if the argument passed does not respond to `include?`.
Examples of `in?`:
```ruby
-1.in?(1,2) # => true
1.in?([1,2]) # => true
"lo".in?("hello") # => true
25.in?(30..50) # => false
@@ -892,7 +903,7 @@ That is what `delegate` does for you:
class User < ActiveRecord::Base
has_one :profile
- delegate :name, :to => :profile
+ delegate :name, to: :profile
end
```
@@ -903,17 +914,17 @@ The method must be public in the target.
The `delegate` macro accepts several methods:
```ruby
-delegate :name, :age, :address, :twitter, :to => :profile
+delegate :name, :age, :address, :twitter, to: :profile
```
When interpolated into a string, the `:to` option should become an expression that evaluates to the object the method is delegated to. Typically a string or symbol. Such an expression is evaluated in the context of the receiver:
```ruby
# delegates to the Rails constant
-delegate :logger, :to => :Rails
+delegate :logger, to: :Rails
# delegates to the receiver's class
-delegate :table_name, :to => 'self.class'
+delegate :table_name, to: :class
```
WARNING: If the `:prefix` option is `true` this is less generic, see below.
@@ -921,7 +932,7 @@ WARNING: If the `:prefix` option is `true` this is less generic, see below.
By default, if the delegation raises `NoMethodError` and the target is `nil` the exception is propagated. You can ask that `nil` is returned instead with the `:allow_nil` option:
```ruby
-delegate :name, :to => :profile, :allow_nil => true
+delegate :name, to: :profile, allow_nil: true
```
With `:allow_nil` the call `user.name` returns `nil` if the user has no profile.
@@ -929,7 +940,7 @@ With `:allow_nil` the call `user.name` returns `nil` if the user has no profile.
The option `:prefix` adds a prefix to the name of the generated method. This may be handy for example to get a better name:
```ruby
-delegate :street, :to => :address, :prefix => true
+delegate :street, to: :address, prefix: true
```
The previous example generates `address_street` rather than `street`.
@@ -939,7 +950,7 @@ WARNING: Since in this case the name of the generated method is composed of the
A custom prefix may also be configured:
```ruby
-delegate :size, :to => :attachment, :prefix => :avatar
+delegate :size, to: :attachment, prefix: :avatar
```
In the previous example the macro generates `avatar_size` rather than `size`.
@@ -1003,10 +1014,10 @@ For example `ActionMailer::Base` defines:
```ruby
class_attribute :default_params
self.default_params = {
- :mime_version => "1.0",
- :charset => "UTF-8",
- :content_type => "text/plain",
- :parts_order => [ "text/plain", "text/enriched", "text/html" ]
+ mime_version: "1.0",
+ charset: "UTF-8",
+ content_type: "text/plain",
+ parts_order: [ "text/plain", "text/enriched", "text/html" ]
}.freeze
```
@@ -1028,7 +1039,7 @@ The generation of the writer instance method can be prevented by setting the opt
```ruby
module ActiveRecord
class Base
- class_attribute :table_name_prefix, :instance_writer => false
+ class_attribute :table_name_prefix, instance_writer: false
self.table_name_prefix = ""
end
end
@@ -1040,7 +1051,7 @@ The generation of the reader instance method can be prevented by setting the opt
```ruby
class A
- class_attribute :x, :instance_reader => false
+ class_attribute :x, instance_reader: false
end
A.new.x = 1 # NoMethodError
@@ -1050,6 +1061,8 @@ For convenience `class_attribute` also defines an instance predicate which is th
When `:instance_reader` is `false`, the instance predicate returns a `NoMethodError` just like the reader method.
+If you do not want the instance predicate, pass `instance_predicate: false` and it will not be defined.
+
NOTE: Defined in `active_support/core_ext/class/attribute.rb`
#### `cattr_reader`, `cattr_writer`, and `cattr_accessor`
@@ -1083,11 +1096,11 @@ The generation of the reader instance method can be prevented by setting `:insta
module A
class B
# No first_name instance reader is generated.
- cattr_accessor :first_name, :instance_reader => false
+ cattr_accessor :first_name, instance_reader: false
# No last_name= instance writer is generated.
- cattr_accessor :last_name, :instance_writer => false
+ cattr_accessor :last_name, instance_writer: false
# No surname instance reader or surname= writer is generated.
- cattr_accessor :surname, :instance_accessor => false
+ cattr_accessor :surname, instance_accessor: false
end
end
```
@@ -1118,8 +1131,6 @@ C.subclasses # => [B, D]
The order in which these classes are returned is unspecified.
-WARNING: This method is redefined in some Rails core classes but should be all compatible in Rails 3.1.
-
NOTE: Defined in `active_support/core_ext/class/subclasses.rb`.
#### `descendants`
@@ -1155,7 +1166,7 @@ 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 since Rails 3. 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 <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.
Strings are considered to be <i>unsafe</i> by default:
@@ -1192,10 +1203,10 @@ Safe arguments are directly appended:
"".html_safe + "<".html_safe # => "<"
```
-These methods should not be used in ordinary views. In Rails 3 unsafe values are automatically escaped:
+These methods should not be used in ordinary views. Unsafe values are automatically escaped:
```erb
-<%= @review.title %> <%# fine in Rails 3, escaped if needed %>
+<%= @review.title %> <%# fine, escaped if needed %>
```
To insert something verbatim use the `raw` helper rather than calling `html_safe`:
@@ -1236,6 +1247,18 @@ Calling `to_s` on a safe string returns a safe string, but coercion with `to_str
Calling `dup` or `clone` on safe strings yields safe strings.
+### `remove`
+
+The method `remove` will remove all occurrences of the pattern:
+
+```ruby
+"Hello World".remove(/Hello /) => "World"
+```
+
+There's also the destructive version `String#remove!`.
+
+NOTE: Defined in `active_support/core_ext/string/filters.rb`.
+
### `squish`
The method `squish` strips leading and trailing whitespace, and substitutes runs of whitespace with a single space each:
@@ -1246,6 +1269,8 @@ The method `squish` strips leading and trailing whitespace, and substitutes runs
There's also the destructive version `String#squish!`.
+Note that it handles both ASCII and Unicode whitespace like mongolian vowel separator (U+180E).
+
NOTE: Defined in `active_support/core_ext/string/filters.rb`.
### `truncate`
@@ -1260,7 +1285,7 @@ The method `truncate` returns a copy of its receiver truncated after a given `le
Ellipsis can be customized with the `:omission` option:
```ruby
-"Oh dear! Oh dear! I shall be late!".truncate(20, :omission => '&hellip;')
+"Oh dear! Oh dear! I shall be late!".truncate(20, omission: '&hellip;')
# => "Oh dear! Oh &hellip;"
```
@@ -1271,14 +1296,14 @@ Pass a `:separator` to truncate the string at a natural break:
```ruby
"Oh dear! Oh dear! I shall be late!".truncate(18)
# => "Oh dear! Oh dea..."
-"Oh dear! Oh dear! I shall be late!".truncate(18, :separator => ' ')
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: ' ')
# => "Oh dear! Oh..."
```
The option `:separator` can be a regexp:
```ruby
-"Oh dear! Oh dear! I shall be late!".truncate(18, :separator => /\s/)
+"Oh dear! Oh dear! I shall be late!".truncate(18, separator: /\s/)
# => "Oh dear! Oh..."
```
@@ -1355,7 +1380,7 @@ The second argument, `indent_string`, specifies which indent string to use. The
"foo".indent(2, "\t") # => "\t\tfoo"
```
-While `indent_string` is tipically one space or tab, it may be any string.
+While `indent_string` is typically one space or tab, it may be any string.
The third argument, `indent_empty_lines`, is a flag that says whether empty lines should be indented. Default is false.
@@ -1433,7 +1458,7 @@ The method `pluralize` returns the plural of its receiver:
As the previous example shows, Active Support knows some irregular plurals and uncountable nouns. Built-in rules can be extended in `config/initializers/inflections.rb`. That file is generated by the `rails` command and has instructions in comments.
-`pluralize` can also take an optional `count` parameter. If `count == 1` the singular form will be returned. For any other value of `count` the plural form will be returned:
+`pluralize` can also take an optional `count` parameter. If `count == 1` the singular form will be returned. For any other value of `count` the plural form will be returned:
```ruby
"dude".pluralize(0) # => "dudes"
@@ -1444,11 +1469,10 @@ As the previous example shows, Active Support knows some irregular plurals and u
Active Record uses this method to compute the default table name that corresponds to a model:
```ruby
-# active_record/base.rb
+# active_record/model_schema.rb
def undecorated_table_name(class_name = base_class.name)
table_name = class_name.to_s.demodulize.underscore
- table_name = table_name.pluralize if pluralize_table_names
- table_name
+ pluralize_table_names ? table_name.pluralize : table_name
end
```
@@ -1757,7 +1781,7 @@ def full_messages
each do |attribute, messages|
...
attr_name = attribute.to_s.gsub('.', '_').humanize
- attr_name = @base.class.human_attribute_name(attribute, :default => attr_name)
+ attr_name = @base.class.human_attribute_name(attribute, default: attr_name)
...
end
@@ -1861,13 +1885,13 @@ These methods use Time#advance for precise date calculations when using from_now
as well as adding or subtracting their results from a Time object. For example:
```ruby
-# equivalent to Time.current.advance(:months => 1)
+# equivalent to Time.current.advance(months: 1)
1.month.from_now
-# equivalent to Time.current.advance(:years => 2)
+# equivalent to Time.current.advance(years: 2)
2.years.from_now
-# equivalent to Time.current.advance(:months => 4, :years => 5)
+# equivalent to Time.current.advance(months: 4, years: 5)
(4.months + 5.years).from_now
```
@@ -1900,22 +1924,22 @@ Produce a string representation of a number as a telephone number:
# => 555-1234
1235551234.to_s(:phone)
# => 123-555-1234
-1235551234.to_s(:phone, :area_code => true)
+1235551234.to_s(:phone, area_code: true)
# => (123) 555-1234
-1235551234.to_s(:phone, :delimiter => " ")
+1235551234.to_s(:phone, delimiter: " ")
# => 123 555 1234
-1235551234.to_s(:phone, :area_code => true, :extension => 555)
+1235551234.to_s(:phone, area_code: true, extension: 555)
# => (123) 555-1234 x 555
-1235551234.to_s(:phone, :country_code => 1)
+1235551234.to_s(:phone, country_code: 1)
# => +1-123-555-1234
```
Produce a string representation of a number as currency:
```ruby
-1234567890.50.to_s(:currency) # => $1,234,567,890.50
-1234567890.506.to_s(:currency) # => $1,234,567,890.51
-1234567890.506.to_s(:currency, :precision => 3) # => $1,234,567,890.506
+1234567890.50.to_s(:currency) # => $1,234,567,890.50
+1234567890.506.to_s(:currency) # => $1,234,567,890.51
+1234567890.506.to_s(:currency, precision: 3) # => $1,234,567,890.506
```
Produce a string representation of a number as a percentage:
@@ -1923,32 +1947,32 @@ Produce a string representation of a number as a percentage:
```ruby
100.to_s(:percentage)
# => 100.000%
-100.to_s(:percentage, :precision => 0)
+100.to_s(:percentage, precision: 0)
# => 100%
-1000.to_s(:percentage, :delimiter => '.', :separator => ',')
+1000.to_s(:percentage, delimiter: '.', separator: ',')
# => 1.000,000%
-302.24398923423.to_s(:percentage, :precision => 5)
+302.24398923423.to_s(:percentage, precision: 5)
# => 302.24399%
```
Produce a string representation of a number in delimited form:
```ruby
-12345678.to_s(:delimited) # => 12,345,678
-12345678.05.to_s(:delimited) # => 12,345,678.05
-12345678.to_s(:delimited, :delimiter => ".") # => 12.345.678
-12345678.to_s(:delimited, :delimiter => ",") # => 12,345,678
-12345678.05.to_s(:delimited, :separator => " ") # => 12,345,678 05
+12345678.to_s(:delimited) # => 12,345,678
+12345678.05.to_s(:delimited) # => 12,345,678.05
+12345678.to_s(:delimited, delimiter: ".") # => 12.345.678
+12345678.to_s(:delimited, delimiter: ",") # => 12,345,678
+12345678.05.to_s(:delimited, separator: " ") # => 12,345,678 05
```
Produce a string representation of a number rounded to a precision:
```ruby
-111.2345.to_s(:rounded) # => 111.235
-111.2345.to_s(:rounded, :precision => 2) # => 111.23
-13.to_s(:rounded, :precision => 5) # => 13.00000
-389.32314.to_s(:rounded, :precision => 0) # => 389
-111.2345.to_s(:rounded, :significant => true) # => 111
+111.2345.to_s(:rounded) # => 111.235
+111.2345.to_s(:rounded, precision: 2) # => 111.23
+13.to_s(:rounded, precision: 5) # => 13.00000
+389.32314.to_s(:rounded, precision: 0) # => 389
+111.2345.to_s(:rounded, significant: true) # => 111
```
Produce a string representation of a number as a human-readable number of bytes:
@@ -1974,7 +1998,7 @@ Produce a string representation of a number in human-readable words:
1234567890123456.to_s(:human) # => "1.23 Quadrillion"
```
-NOTE: Defined in `active_support/core_ext/numeric/formatting.rb`.
+NOTE: Defined in `active_support/core_ext/numeric/conversions.rb`.
Extensions to `Integer`
-----------------------
@@ -2022,8 +2046,33 @@ NOTE: Defined in `active_support/core_ext/integer/inflections.rb`.
Extensions to `BigDecimal`
--------------------------
+### `to_s`
-...
+The method `to_s` is aliased to `to_formatted_s`. This provides a convenient way to display a BigDecimal value in floating-point notation:
+
+```ruby
+BigDecimal.new(5.00, 6).to_s # => "5.0"
+```
+
+### `to_formatted_s`
+
+Te method `to_formatted_s` provides a default specifier of "F". This means that a simple call to `to_formatted_s` or `to_s` will result in floating point representation instead of engineering notation:
+
+```ruby
+BigDecimal.new(5.00, 6).to_formatted_s # => "5.0"
+```
+
+and that symbol specifiers are also supported:
+
+```ruby
+BigDecimal.new(5.00, 6).to_formatted_s(:db) # => "5.0"
+```
+
+Engineering notation is still supported:
+
+```ruby
+BigDecimal.new(5.00, 6).to_formatted_s("e") # => "0.5E1"
+```
Extensions to `Enumerable`
--------------------------
@@ -2042,7 +2091,7 @@ Addition only assumes the elements respond to `+`:
```ruby
[[1, 2], [2, 3], [3, 4]].sum # => [1, 2, 2, 3, 3, 4]
%w(foo bar baz).sum # => "foobarbaz"
-{:a => 1, :b => 2, :c => 3}.sum # => [:b, 2, :c, 3, :a, 1]
+{a: 1, b: 2, c: 3}.sum # => [:b, 2, :c, 3, :a, 1]
```
The sum of an empty collection is zero by default, but this is customizable:
@@ -2065,14 +2114,6 @@ The sum of an empty receiver can be customized in this form as well:
[].sum(1) {|n| n**3} # => 1
```
-The method `ActiveRecord::Observer#observed_subclasses` for example is implemented this way:
-
-```ruby
-def observed_subclasses
- observed_classes.sum([]) { |klass| klass.send(:subclasses) }
-end
-```
-
NOTE: Defined in `active_support/core_ext/enumerable.rb`.
### `index_by`
@@ -2176,7 +2217,7 @@ NOTE: Defined in `active_support/core_ext/array/prepend_and_append.rb`.
When the last argument in a method call is a hash, except perhaps for a `&block` argument, Ruby allows you to omit the brackets:
```ruby
-User.exists?(:email => params[:email])
+User.exists?(email: params[:email])
```
That syntactic sugar is used a lot in Rails to avoid positional arguments where there would be too many, offering instead interfaces that emulate named parameters. In particular it is very idiomatic to use a trailing hash for options.
@@ -2218,7 +2259,7 @@ This method accepts three options:
* `:words_connector`: What is used to join the elements of arrays with 3 or more elements, except for the last two. Default is ", ".
* `:last_word_connector`: What is used to join the last items of an array with 3 or more elements. Default is ", and ".
-The defaults for these options can be localised, their keys are:
+The defaults for these options can be localized, their keys are:
| Option | I18n key |
| ---------------------- | ----------------------------------- |
@@ -2234,7 +2275,9 @@ NOTE: Defined in `active_support/core_ext/array/conversions.rb`.
The method `to_formatted_s` acts like `to_s` by default.
-If the array contains items that respond to `id`, however, it may be passed the symbol `:db` as argument. That's typically used with collections of ARs. Returned strings are:
+If the array contains items that respond to `id`, however, the symbol
+`:db` may be passed as argument. That's typically used with
+collections of Active Record objects. Returned strings are:
```ruby
[].to_formatted_s(:db) # => "null"
@@ -2305,7 +2348,7 @@ If there's any element that does not belong to the type of the first one the roo
If the receiver is an array of hashes the root element is by default also "objects":
```ruby
-[{:a => 1, :b => 2}, {:c => 3}].to_xml
+[{a: 1, b: 2}, {c: 3}].to_xml
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <objects type="array">
@@ -2326,7 +2369,7 @@ The name of children nodes is by default the name of the root node singularized.
The default XML builder is a fresh instance of `Builder::XmlMarkup`. You can configure your own builder via the `:builder` option. The method also accepts options like `:dasherize` and friends, they are forwarded to the builder:
```ruby
-Contributor.limit(2).order(:rank).to_xml(:skip_types => true)
+Contributor.limit(2).order(:rank).to_xml(skip_types: true)
# =>
# <?xml version="1.0" encoding="UTF-8"?>
# <contributors>
@@ -2372,8 +2415,8 @@ This method is similar in purpose to `Kernel#Array`, but there are some differen
The last point is particularly worth comparing for some enumerables:
```ruby
-Array.wrap(:foo => :bar) # => [{:foo => :bar}]
-Array(:foo => :bar) # => [[:foo, :bar]]
+Array.wrap(foo: :bar) # => [{:foo=>:bar}]
+Array(foo: :bar) # => [[:foo, :bar]]
```
There's also a related idiom that uses the splat operator:
@@ -2390,7 +2433,8 @@ NOTE: Defined in `active_support/core_ext/array/wrap.rb`.
### Duplicating
-The method `Array.deep_dup` duplicates itself and all objects inside recursively with ActiveSupport method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside.
+The method `Array.deep_dup` duplicates itself and all objects inside
+recursively with Active Support method `Object#deep_dup`. It works like `Array#map` with sending `deep_dup` method to each object inside.
```ruby
array = [1, [2, 3]]
@@ -2399,7 +2443,7 @@ dup[1][2] = 4
array[1][2] == nil # => true
```
-NOTE: Defined in `active_support/core_ext/array/deep_dup.rb`.
+NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`.
### Grouping
@@ -2416,9 +2460,9 @@ or yields them in turn if a block is passed:
```html+erb
<% sample.in_groups_of(3) do |a, b, c| %>
<tr>
- <td><%=h a %></td>
- <td><%=h b %></td>
- <td><%=h c %></td>
+ <td><%= a %></td>
+ <td><%= b %></td>
+ <td><%= c %></td>
</tr>
<% end %>
```
@@ -2556,8 +2600,8 @@ NOTE: Defined in `active_support/core_ext/hash/conversions.rb`.
Ruby has a built-in method `Hash#merge` that merges two hashes:
```ruby
-{:a => 1, :b => 1}.merge(:a => 0, :c => 2)
-# => {:a => 0, :b => 1, :c => 2}
+{a: 1, b: 1}.merge(a: 0, c: 2)
+# => {:a=>0, :b=>1, :c=>2}
```
Active Support defines a few more ways of merging hashes that may be convenient.
@@ -2567,19 +2611,19 @@ Active Support defines a few more ways of merging hashes that may be convenient.
In case of collision the key in the hash of the argument wins in `merge`. You can support option hashes with default values in a compact way with this idiom:
```ruby
-options = {:length => 30, :omission => "..."}.merge(options)
+options = {length: 30, omission: "..."}.merge(options)
```
Active Support defines `reverse_merge` in case you prefer this alternative notation:
```ruby
-options = options.reverse_merge(:length => 30, :omission => "...")
+options = options.reverse_merge(length: 30, omission: "...")
```
And a bang version `reverse_merge!` that performs the merge in place:
```ruby
-options.reverse_merge!(:length => 30, :omission => "...")
+options.reverse_merge!(length: 30, omission: "...")
```
WARNING. Take into account that `reverse_merge!` may change the hash in the caller, which may or may not be a good idea.
@@ -2601,8 +2645,8 @@ As you can see in the previous example if a key is found in both hashes the valu
Active Support defines `Hash#deep_merge`. In a deep merge, if a key is found in both hashes and their values are hashes in turn, then their _merge_ becomes the value in the resulting hash:
```ruby
-{:a => {:b => 1}}.deep_merge(:a => {:c => 2})
-# => {:a => {:b => 1, :c => 2}}
+{a: {b: 1}}.deep_merge(a: {c: 2})
+# => {:a=>{:b=>1, :c=>2}}
```
The method `deep_merge!` performs a deep merge in place.
@@ -2611,10 +2655,11 @@ NOTE: Defined in `active_support/core_ext/hash/deep_merge.rb`.
### Deep duplicating
-The method `Hash.deep_dup` duplicates itself and all keys and values inside recursively with ActiveSupport method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside.
+The method `Hash.deep_dup` duplicates itself and all keys and values
+inside recursively with Active Support method `Object#deep_dup`. It works like `Enumerator#each_with_object` with sending `deep_dup` method to each pair inside.
```ruby
-hash = { :a => 1, :b => { :c => 2, :d => [3, 4] } }
+hash = { a: 1, b: { c: 2, d: [3, 4] } }
dup = hash.deep_dup
dup[:b][:e] = 5
@@ -2624,45 +2669,7 @@ hash[:b][:e] == nil # => true
hash[:b][:d] == [3, 4] # => true
```
-NOTE: Defined in `active_support/core_ext/hash/deep_dup.rb`.
-
-### Diffing
-
-The method `diff` returns a hash that represents a diff of the receiver and the argument with the following logic:
-
-* Pairs `key`, `value` that exist in both hashes do not belong to the diff hash.
-
-* If both hashes have `key`, but with different values, the pair in the receiver wins.
-
-* The rest is just merged.
-
-```ruby
-{:a => 1}.diff(:a => 1)
-# => {}, first rule
-
-{:a => 1}.diff(:a => 2)
-# => {:a => 1}, second rule
-
-{:a => 1}.diff(:b => 2)
-# => {:a => 1, :b => 2}, third rule
-
-{:a => 1, :b => 2, :c => 3}.diff(:b => 1, :c => 3, :d => 4)
-# => {:a => 1, :b => 2, :d => 4}, all rules
-
-{}.diff({}) # => {}
-{:a => 1}.diff({}) # => {:a => 1}
-{}.diff(:a => 1) # => {:a => 1}
-```
-
-An important property of this diff hash is that you can retrieve the original hash by applying `diff` twice:
-
-```ruby
-hash.diff(hash2).diff(hash2) == hash
-```
-
-Diffing hashes may be useful for error messages related to expected option hashes for example.
-
-NOTE: Defined in `active_support/core_ext/hash/diff.rb`.
+NOTE: Defined in `active_support/core_ext/object/deep_dup.rb`.
### Working with Keys
@@ -2671,21 +2678,14 @@ NOTE: Defined in `active_support/core_ext/hash/diff.rb`.
The method `except` returns a hash with the keys in the argument list removed, if present:
```ruby
-{:a => 1, :b => 2}.except(:a) # => {:b => 2}
+{a: 1, b: 2}.except(:a) # => {:b=>2}
```
If the receiver responds to `convert_key`, the method is called on each of the arguments. This allows `except` to play nice with hashes with indifferent access for instance:
```ruby
-{:a => 1}.with_indifferent_access.except(:a) # => {}
-{:a => 1}.with_indifferent_access.except("a") # => {}
-```
-
-The method `except` may come in handy for example when you want to protect some parameter that can't be globally protected with `attr_protected`:
-
-```ruby
-params[:account] = params[:account].except(:plan_id) unless admin?
-@account.update_attributes(params[:account])
+{a: 1}.with_indifferent_access.except(:a) # => {}
+{a: 1}.with_indifferent_access.except("a") # => {}
```
There's also the bang variant `except!` that removes keys in the very receiver.
@@ -2697,14 +2697,14 @@ NOTE: Defined in `active_support/core_ext/hash/except.rb`.
The method `transform_keys` accepts a block and returns a hash that has applied the block operations to each of the keys in the receiver:
```ruby
-{nil => nil, 1 => 1, :a => :a}.transform_keys{ |key| key.to_s.upcase }
+{nil => nil, 1 => 1, a: :a}.transform_keys { |key| key.to_s.upcase }
# => {"" => nil, "A" => :a, "1" => 1}
```
The result in case of collision is undefined:
```ruby
-{"a" => 1, :a => 2}.transform_keys{ |key| key.to_s.upcase }
+{"a" => 1, a: 2}.transform_keys { |key| key.to_s.upcase }
# => {"A" => 2}, in my test, can't rely on this result though
```
@@ -2712,11 +2712,11 @@ This method may be useful for example to build specialized conversions. For inst
```ruby
def stringify_keys
- transform_keys{ |key| key.to_s }
+ transform_keys { |key| key.to_s }
end
...
def symbolize_keys
- transform_keys{ |key| key.to_sym rescue key }
+ transform_keys { |key| key.to_sym rescue key }
end
```
@@ -2725,7 +2725,7 @@ There's also the bang variant `transform_keys!` that applies the block operation
Besides that, one can use `deep_transform_keys` and `deep_transform_keys!` to perform the block operation on all the keys in the given hash and all the hashes nested into it. An example of the result is:
```ruby
-{nil => nil, 1 => 1, :nested => {:a => 3, 5 => 5}}.deep_transform_keys{ |key| key.to_s.upcase }
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_transform_keys { |key| key.to_s.upcase }
# => {""=>nil, "1"=>1, "NESTED"=>{"A"=>3, "5"=>5}}
```
@@ -2736,14 +2736,14 @@ NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
The method `stringify_keys` returns a hash that has a stringified version of the keys in the receiver. It does so by sending `to_s` to them:
```ruby
-{nil => nil, 1 => 1, :a => :a}.stringify_keys
+{nil => nil, 1 => 1, a: :a}.stringify_keys
# => {"" => nil, "a" => :a, "1" => 1}
```
The result in case of collision is undefined:
```ruby
-{"a" => 1, :a => 2}.stringify_keys
+{"a" => 1, a: 2}.stringify_keys
# => {"a" => 2}, in my test, can't rely on this result though
```
@@ -2764,7 +2764,7 @@ There's also the bang variant `stringify_keys!` that stringifies keys in the ver
Besides that, one can use `deep_stringify_keys` and `deep_stringify_keys!` to stringify all the keys in the given hash and all the hashes nested into it. An example of the result is:
```ruby
-{nil => nil, 1 => 1, :nested => {:a => 3, 5 => 5}}.deep_stringify_keys
+{nil => nil, 1 => 1, nested: {a: 3, 5 => 5}}.deep_stringify_keys
# => {""=>nil, "1"=>1, "nested"=>{"a"=>3, "5"=>5}}
```
@@ -2776,7 +2776,7 @@ The method `symbolize_keys` returns a hash that has a symbolized version of the
```ruby
{nil => nil, 1 => 1, "a" => "a"}.symbolize_keys
-# => {1 => 1, nil => nil, :a => "a"}
+# => {1=>1, nil=>nil, :a=>"a"}
```
WARNING. Note in the previous example only one key was symbolized.
@@ -2784,8 +2784,8 @@ WARNING. Note in the previous example only one key was symbolized.
The result in case of collision is undefined:
```ruby
-{"a" => 1, :a => 2}.symbolize_keys
-# => {:a => 2}, in my test, can't rely on this result though
+{"a" => 1, a: 2}.symbolize_keys
+# => {:a=>2}, in my test, can't rely on this result though
```
This method may be useful for example to easily accept both symbols and strings as options. For instance `ActionController::UrlRewriter` defines
@@ -2806,7 +2806,7 @@ Besides that, one can use `deep_symbolize_keys` and `deep_symbolize_keys!` to sy
```ruby
{nil => nil, 1 => 1, "nested" => {"a" => 3, 5 => 5}}.deep_symbolize_keys
-# => {nil=>nil, 1=>1, :nested=>{:a=>3, 5=>5}}
+# => {nil=>nil, 1=>1, nested:{a:3, 5=>5}}
```
NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
@@ -2822,8 +2822,8 @@ NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
The method `assert_valid_keys` receives an arbitrary number of arguments, and checks whether the receiver has any key outside that white list. If it does `ArgumentError` is raised.
```ruby
-{:a => 1}.assert_valid_keys(:a) # passes
-{:a => 1}.assert_valid_keys("a") # ArgumentError
+{a: 1}.assert_valid_keys(:a) # passes
+{a: 1}.assert_valid_keys("a") # ArgumentError
```
Active Record does not accept unknown options when building associations, for example. It implements that control via `assert_valid_keys`.
@@ -2835,18 +2835,18 @@ NOTE: Defined in `active_support/core_ext/hash/keys.rb`.
Ruby has built-in support for taking slices out of strings and arrays. Active Support extends slicing to hashes:
```ruby
-{:a => 1, :b => 2, :c => 3}.slice(:a, :c)
-# => {:c => 3, :a => 1}
+{a: 1, b: 2, c: 3}.slice(:a, :c)
+# => {:c=>3, :a=>1}
-{:a => 1, :b => 2, :c => 3}.slice(:b, :X)
-# => {:b => 2} # non-existing keys are ignored
+{a: 1, b: 2, c: 3}.slice(:b, :X)
+# => {:b=>2} # non-existing keys are ignored
```
If the receiver responds to `convert_key` keys are normalized:
```ruby
-{:a => 1, :b => 2}.with_indifferent_access.slice("a")
-# => {:a => 1}
+{a: 1, b: 2}.with_indifferent_access.slice("a")
+# => {:a=>1}
```
NOTE. Slicing may come in handy for sanitizing option hashes with a white list of keys.
@@ -2854,9 +2854,9 @@ NOTE. Slicing may come in handy for sanitizing option hashes with a white list o
There's also `slice!` which in addition to perform a slice in place returns what's removed:
```ruby
-hash = {:a => 1, :b => 2}
-rest = hash.slice!(:a) # => {:b => 2}
-hash # => {:a => 1}
+hash = {a: 1, b: 2}
+rest = hash.slice!(:a) # => {:b=>2}
+hash # => {:a=>1}
```
NOTE: Defined in `active_support/core_ext/hash/slice.rb`.
@@ -2866,15 +2866,15 @@ NOTE: Defined in `active_support/core_ext/hash/slice.rb`.
The method `extract!` removes and returns the key/value pairs matching the given keys.
```ruby
-hash = {:a => 1, :b => 2}
-rest = hash.extract!(:a, :x) # => {:a => 1} # non-existing keys are ignored
-hash # => {:b => 2}
+hash = {a: 1, b: 2}
+rest = hash.extract!(:a) # => {:a=>1}
+hash # => {:b=>2}
```
The method `extract!` returns the same subclass of Hash, that the receiver is.
```ruby
-hash = {:a => 1, :b => 2}.with_indifferent_access
+hash = {a: 1, b: 2}.with_indifferent_access
rest = hash.extract!(:a).class
# => ActiveSupport::HashWithIndifferentAccess
```
@@ -2886,7 +2886,7 @@ NOTE: Defined in `active_support/core_ext/hash/slice.rb`.
The method `with_indifferent_access` returns an `ActiveSupport::HashWithIndifferentAccess` out of its receiver:
```ruby
-{:a => 1}.with_indifferent_access["a"] # => 1
+{a: 1}.with_indifferent_access["a"] # => 1
```
NOTE: Defined in `active_support/core_ext/hash/indifferent_access.rb`.
@@ -2990,7 +2990,7 @@ An unbound method is not callable as is, you need to bind it first to an object
```ruby
clear = Hash.instance_method(:clear)
-clear.bind({:a => 1}).call # => {}
+clear.bind({a: 1}).call # => {}
```
Active Support defines `Proc#bind` with an analogous purpose:
@@ -3249,8 +3249,8 @@ The most generic way to jump to other days is `advance`. This method receives a
```ruby
date = Date.new(2010, 6, 6)
-date.advance(:years => 1, :weeks => 2) # => Mon, 20 Jun 2011
-date.advance(:months => 2, :days => -2) # => Wed, 04 Aug 2010
+date.advance(years: 1, weeks: 2) # => Mon, 20 Jun 2011
+date.advance(months: 2, days: -2) # => Wed, 04 Aug 2010
```
Note in the previous example that increments may be negative.
@@ -3260,14 +3260,14 @@ To perform the computation the method first increments years, then months, then
The method `advance` advances first one month, and then one day, the result is:
```ruby
-Date.new(2010, 2, 28).advance(:months => 1, :days => 1)
+Date.new(2010, 2, 28).advance(months: 1, days: 1)
# => Sun, 29 Mar 2010
```
While if it did it the other way around the result would be different:
```ruby
-Date.new(2010, 2, 28).advance(:days => 1).advance(:months => 1)
+Date.new(2010, 2, 28).advance(days: 1).advance(months: 1)
# => Thu, 01 Apr 2010
```
@@ -3276,14 +3276,14 @@ Date.new(2010, 2, 28).advance(:days => 1).advance(:months => 1)
The method `change` allows you to get a new date which is the same as the receiver except for the given year, month, or day:
```ruby
-Date.new(2010, 12, 23).change(:year => 2011, :month => 11)
+Date.new(2010, 12, 23).change(year: 2011, month: 11)
# => Wed, 23 Nov 2011
```
This method is not tolerant to non-existing dates, if the change is invalid `ArgumentError` is raised:
```ruby
-Date.new(2010, 1, 31).change(:month => 2)
+Date.new(2010, 1, 31).change(month: 2)
# => ArgumentError: invalid date
```
@@ -3347,7 +3347,25 @@ date.end_of_hour # => Mon Jun 07 19:59:59 +0200 2010
`beginning_of_hour` is aliased to `at_beginning_of_hour`.
-INFO: `beginning_of_hour` and `end_of_hour` are implemented for `Time` and `DateTime` but **not** `Date` as it does not make sense to request the beginning or end of an hour on a `Date` instance.
+##### `beginning_of_minute`, `end_of_minute`
+
+The method `beginning_of_minute` returns a timestamp at the beginning of the minute (hh:mm:00):
+
+```ruby
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.beginning_of_minute # => Mon Jun 07 19:55:00 +0200 2010
+```
+
+The method `end_of_minute` returns a timestamp at the end of the minute (hh:mm:59):
+
+```ruby
+date = DateTime.new(2010, 6, 7, 19, 55, 25)
+date.end_of_minute # => Mon Jun 07 19:55:59 +0200 2010
+```
+
+`beginning_of_minute` is aliased to `at_beginning_of_minute`.
+
+INFO: `beginning_of_hour`, `end_of_hour`, `beginning_of_minute` and `end_of_minute` are implemented for `Time` and `DateTime` but **not** `Date` as it does not make sense to request the beginning or end of an hour or minute on a `Date` instance.
##### `ago`, `since`
@@ -3469,7 +3487,7 @@ The most generic way to jump to another datetime is `advance`. This method recei
```ruby
d = DateTime.current
# => Thu, 05 Aug 2010 11:33:31 +0000
-d.advance(:years => 1, :months => 1, :days => 1, :hours => 1, :minutes => 1, :seconds => 1)
+d.advance(years: 1, months: 1, days: 1, hours: 1, minutes: 1, seconds: 1)
# => Tue, 06 Sep 2011 12:34:32 +0000
```
@@ -3480,14 +3498,14 @@ If we first move the date bits (that have also a relative order of processing, a
```ruby
d = DateTime.new(2010, 2, 28, 23, 59, 59)
# => Sun, 28 Feb 2010 23:59:59 +0000
-d.advance(:months => 1, :seconds => 1)
+d.advance(months: 1, seconds: 1)
# => Mon, 29 Mar 2010 00:00:00 +0000
```
but if we computed them the other way around, the result would be different:
```ruby
-d.advance(:seconds => 1).advance(:months => 1)
+d.advance(seconds: 1).advance(months: 1)
# => Thu, 01 Apr 2010 00:00:00 +0000
```
@@ -3500,28 +3518,28 @@ The method `change` allows you to get a new datetime which is the same as the re
```ruby
now = DateTime.current
# => Tue, 08 Jun 2010 01:56:22 +0000
-now.change(:year => 2011, :offset => Rational(-6, 24))
+now.change(year: 2011, offset: Rational(-6, 24))
# => Wed, 08 Jun 2011 01:56:22 -0600
```
If hours are zeroed, then minutes and seconds are too (unless they have given values):
```ruby
-now.change(:hour => 0)
+now.change(hour: 0)
# => Tue, 08 Jun 2010 00:00:00 +0000
```
Similarly, if minutes are zeroed, then seconds are too (unless it has given a value):
```ruby
-now.change(:min => 0)
+now.change(min: 0)
# => Tue, 08 Jun 2010 01:00:00 +0000
```
This method is not tolerant to non-existing dates, if the change is invalid `ArgumentError` is raised:
```ruby
-DateTime.current.change(:month => 2, :day => 30)
+DateTime.current.change(month: 2, day: 30)
# => ArgumentError: invalid date
```
@@ -3602,9 +3620,9 @@ Time.zone_default
# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
# In Barcelona, 2010/03/28 02:00 +0100 becomes 2010/03/28 03:00 +0200 due to DST.
-t = Time.local_time(2010, 3, 28, 1, 59, 59)
+t = Time.local(2010, 3, 28, 1, 59, 59)
# => Sun Mar 28 01:59:59 +0100 2010
-t.advance(:seconds => 1)
+t.advance(seconds: 1)
# => Sun Mar 28 03:00:00 +0200 2010
```
@@ -3657,26 +3675,6 @@ Time.current
Analogously to `DateTime`, the predicates `past?`, and `future?` are relative to `Time.current`.
-Use the `local_time` class method to create time objects honoring the user time zone:
-
-```ruby
-Time.zone_default
-# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
-Time.local_time(2010, 8, 15)
-# => Sun Aug 15 00:00:00 +0200 2010
-```
-
-The `utc_time` class method returns a time in UTC:
-
-```ruby
-Time.zone_default
-# => #<ActiveSupport::TimeZone:0x7f73654d4f38 @utc_offset=nil, @name="Madrid", ...>
-Time.utc_time(2010, 8, 15)
-# => Sun Aug 15 00:00:00 UTC 2010
-```
-
-Both `local_time` and `utc_time` accept up to seven positional arguments: year, month, day, hour, min, sec, usec. Year is mandatory, month and day default to 1, and the rest default to 0.
-
If the time to be constructed lies beyond the range supported by `Time` in the runtime platform, usecs are discarded and a `DateTime` object is returned instead.
#### Durations
@@ -3695,7 +3693,7 @@ now - 1.week
They translate to calls to `since` or `advance`. For example here we get the correct jump in the calendar reform:
```ruby
-Time.utc_time(1582, 10, 3) + 5.days
+Time.utc(1582, 10, 3) + 5.days
# => Mon Oct 18 00:00:00 UTC 1582
```
@@ -3716,7 +3714,9 @@ File.atomic_write(joined_asset_path) do |cache|
end
```
-To accomplish this `atomic_write` creates a temporary file. That's the file the code in the block actually writes to. On completion, the temporary file is renamed, which is an atomic operation on POSIX systems. If the target file exists `atomic_write` overwrites it and keeps owners and permissions.
+To accomplish this `atomic_write` creates a temporary file. That's the file the code in the block actually writes to. On completion, the temporary file is renamed, which is an atomic operation on POSIX systems. If the target file exists `atomic_write` overwrites it and keeps owners and permissions. However there are a few cases where `atomic_write` cannot change the file ownership or permissions, this error is caught and skipped over trusting in the user/filesystem to ensure the file is accessible to the processes that need it.
+
+NOTE. Due to the chmod operation `atomic_write` performs, if the target file has an ACL set on it this ACL will be recalculated/modified.
WARNING. Note you can't append with `atomic_write`.
@@ -3724,6 +3724,25 @@ The auxiliary file is written in a standard directory for temporary files, but y
NOTE: Defined in `active_support/core_ext/file/atomic.rb`.
+Extensions to `Marshal`
+-----------------------
+
+### `load`
+
+Active Support adds constant autoloading support to `load`.
+
+For example, the file cache store deserializes this way:
+
+```ruby
+File.open(file_name) { |f| Marshal.load(f) }
+```
+
+If the cached data refers to a constant that is unknown at that point, the autoloading mechanism is triggered and if it succeeds the deserialization is retried transparently.
+
+WARNING. If the argument is an `IO` it needs to respond to `rewind` to be able to retry. Regular files respond to `rewind`.
+
+NOTE: Defined in `active_support/core_ext/marshal.rb`.
+
Extensions to `Logger`
----------------------
@@ -3785,13 +3804,13 @@ def default_helper_module!
module_path = module_name.underscore
helper module_path
rescue MissingSourceFile => e
- raise e unless e.is_missing? "#{module_path}_helper"
+ raise e unless e.is_missing? "helpers/#{module_path}_helper"
rescue NameError => e
raise e unless e.missing_name? "#{module_name}Helper"
end
```
-NOTE: Defined in `active_support/core_ext/name_error.rb`.
+NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`.
Extensions to `LoadError`
-------------------------
@@ -3814,4 +3833,4 @@ rescue NameError => e
end
```
-NOTE: Defined in `active_support/core_ext/load_error.rb`.
+NOTE: Defined in `actionpack/lib/abstract_controller/helpers.rb`.
diff --git a/guides/source/active_support_instrumentation.md b/guides/source/active_support_instrumentation.md
index 6c06cfcc4b..969596f470 100644
--- a/guides/source/active_support_instrumentation.md
+++ b/guides/source/active_support_instrumentation.md
@@ -3,19 +3,21 @@ Active Support Instrumentation
Active Support is a part of core Rails that provides Ruby language extensions, utilities and other things. One of the things it includes is an instrumentation API that can be used inside an application to measure certain actions that occur within Ruby code, such as that inside a Rails application or the framework itself. It is not limited to Rails, however. It can be used independently in other Ruby scripts if it is so desired.
-In this guide, you will learn how to use the instrumentation API inside of ActiveSupport to measure events inside of Rails and other Ruby code. We cover:
+In this guide, you will learn how to use the instrumentation API inside of Active Support to measure events inside of Rails and other Ruby code.
-* What instrumentation can provide
-* The hooks inside the Rails framework for instrumentation
-* Adding a subscriber to a hook
-* Building a custom instrumentation implementation
+After reading this guide, you will know:
+
+* What instrumentation can provide.
+* The hooks inside the Rails framework for instrumentation.
+* Adding a subscriber to a hook.
+* Building a custom instrumentation implementation.
--------------------------------------------------------------------------------
Introduction to instrumentation
-------------------------------
-The instrumentation API provided by ActiveSupport 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.
@@ -26,8 +28,8 @@ Rails framework hooks
Within the Ruby on Rails framework, there are a number of hooks provided for common events. These are detailed below.
-ActionController
-----------------
+Action Controller
+-----------------
### write_fragment.action_controller
@@ -37,7 +39,7 @@ ActionController
```ruby
{
- :key => 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -49,7 +51,7 @@ ActionController
```ruby
{
- :key => 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -61,7 +63,7 @@ ActionController
```ruby
{
- :key => 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -73,7 +75,7 @@ ActionController
```ruby
{
- :key => 'posts/1-dasboard-view'
+ key: 'posts/1-dashboard-view'
}
```
@@ -85,7 +87,7 @@ ActionController
```ruby
{
- :path => '/users/1'
+ path: '/users/1'
}
```
@@ -97,7 +99,7 @@ ActionController
```ruby
{
- :path => '/users/1'
+ path: '/users/1'
}
```
@@ -114,12 +116,12 @@ ActionController
```ruby
{
- :controller => "PostsController",
- :action => "new",
- :params => { "action" => "new", "controller" => "posts" },
- :format => :html,
- :method => "GET",
- :path => "/posts/new"
+ controller: "PostsController",
+ action: "new",
+ params: { "action" => "new", "controller" => "posts" },
+ format: :html,
+ method: "GET",
+ path: "/posts/new"
}
```
@@ -137,15 +139,15 @@ ActionController
```ruby
{
- :controller => "PostsController",
- :action => "index",
- :params => {"action" => "index", "controller" => "posts"},
- :format => :html,
- :method => "GET",
- :path => "/posts",
- :status => 200,
- :view_runtime => 46.848,
- :db_runtime => 0.157
+ controller: "PostsController",
+ action: "index",
+ params: {"action" => "index", "controller" => "posts"},
+ format: :html,
+ method: "GET",
+ path: "/posts",
+ status: 200,
+ view_runtime: 46.848,
+ db_runtime: 0.157
}
```
@@ -170,8 +172,8 @@ INFO. Additional keys may be added by the caller.
```ruby
{
- :status => 302,
- :location => "http://localhost:3000/posts/new"
+ status: 302,
+ location: "http://localhost:3000/posts/new"
}
```
@@ -183,12 +185,12 @@ INFO. Additional keys may be added by the caller.
```ruby
{
- :filter => ":halting_filter"
+ filter: ":halting_filter"
}
```
-ActionView
-----------
+Action View
+-----------
### render_template.action_view
@@ -199,8 +201,8 @@ ActionView
```ruby
{
- :identifier => "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
- :layout => "layouts/application"
+ identifier: "/Users/adam/projects/notifications/app/views/posts/index.html.erb",
+ layout: "layouts/application"
}
```
@@ -212,11 +214,11 @@ ActionView
```ruby
{
- :identifier => "/Users/adam/projects/notifications/app/views/posts/_form.html.erb",
+ identifier: "/Users/adam/projects/notifications/app/views/posts/_form.html.erb",
}
```
-ActiveRecord
+Active Record
------------
### sql.active_record
@@ -231,10 +233,10 @@ INFO. The adapters will add their own data as well.
```ruby
{
- :sql => "SELECT \"posts\".* FROM \"posts\" ",
- :name => "Post Load",
- :connection_id => 70307250813140,
- :binds => []
+ sql: "SELECT \"posts\".* FROM \"posts\" ",
+ name: "Post Load",
+ connection_id: 70307250813140,
+ binds: []
}
```
@@ -246,8 +248,8 @@ INFO. The adapters will add their own data as well.
| `:name` | Record's class |
| `:connection_id` | `self.object_id` |
-ActionMailer
-------------
+Action Mailer
+-------------
### receive.action_mailer
@@ -265,13 +267,13 @@ ActionMailer
```ruby
{
- :mailer => "Notification",
- :message_id => "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
- :subject => "Rails Guides",
- :to => ["users@rails.com", "ddh@rails.com"],
- :from => ["me@rails.com"],
- :date => Sat, 10 Mar 2012 14:18:09 +0100,
- :mail=> "..." # ommitted for beverity
+ mailer: "Notification",
+ message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+ subject: "Rails Guides",
+ to: ["users@rails.com", "ddh@rails.com"],
+ from: ["me@rails.com"],
+ date: Sat, 10 Mar 2012 14:18:09 +0100,
+ mail: "..." # omitted for brevity
}
```
@@ -291,13 +293,13 @@ ActionMailer
```ruby
{
- :mailer => "Notification",
- :message_id => "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
- :subject => "Rails Guides",
- :to => ["users@rails.com", "ddh@rails.com"],
- :from => ["me@rails.com"],
- :date => Sat, 10 Mar 2012 14:18:09 +0100,
- :mail=> "..." # ommitted for beverity
+ mailer: "Notification",
+ message_id: "4f5b5491f1774_181b23fc3d4434d38138e5@mba.local.mail",
+ subject: "Rails Guides",
+ to: ["users@rails.com", "ddh@rails.com"],
+ from: ["me@rails.com"],
+ date: Sat, 10 Mar 2012 14:18:09 +0100,
+ mail: "..." # omitted for brevity
}
```
@@ -312,8 +314,8 @@ ActiveResource
| `:request_uri` | Complete URI |
| `:result` | HTTP response object |
-ActiveSupport
--------------
+Active Support
+--------------
### cache_read.active_support
@@ -335,7 +337,7 @@ INFO. Options passed to fetch will be merged with the payload when writing to th
```ruby
{
- :key => 'name-of-complicated-computation'
+ key: 'name-of-complicated-computation'
}
```
@@ -352,7 +354,7 @@ INFO. Options passed to fetch will be merged with the payload.
```ruby
{
- :key => 'name-of-complicated-computation'
+ key: 'name-of-complicated-computation'
}
```
@@ -366,7 +368,7 @@ INFO. Cache stores my add their own keys
```ruby
{
- :key => 'name-of-complicated-computation'
+ key: 'name-of-complicated-computation'
}
```
@@ -378,7 +380,7 @@ INFO. Cache stores my add their own keys
```ruby
{
- :key => 'name-of-complicated-computation'
+ key: 'name-of-complicated-computation'
}
```
@@ -390,7 +392,7 @@ INFO. Cache stores my add their own keys
```ruby
{
- :key => 'name-of-complicated-computation'
+ key: 'name-of-complicated-computation'
}
```
@@ -426,29 +428,29 @@ end
```
Defining all those block arguments each time can be tedious. You can easily create an `ActiveSupport::Notifications::Event`
-from block args like this:
+from block arguments like this:
```ruby
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
- event = ActiveSupport::Notification::Event.new args
+ event = ActiveSupport::Notifications::Event.new *args
event.name # => "process_action.action_controller"
event.duration # => 10 (in milliseconds)
- event.payload # => { :extra => :information }
+ event.payload # => {:extra=>information}
Rails.logger.info "#{event} Received!"
end
```
-Most times you only care about the data itself. Here is a shortuct to just get the data.
+Most times you only care about the data itself. Here is a shortcut to just get the data.
```ruby
ActiveSupport::Notifications.subscribe "process_action.action_controller" do |*args|
data = args.extract_options!
- data # { :extra => :information }
+ data # { extra: :information }
```
-You may also subscribe to events matching a regular expresssion. This enables you to subscribe to
+You may also subscribe to events matching a regular expression. This enables you to subscribe to
multiple events at once. Here's you could subscribe to everything from `ActionController`.
```ruby
@@ -463,12 +465,12 @@ Creating custom events
Adding your own events is easy as well. `ActiveSupport::Notifications` will take care of
all the heavy lifting for you. Simply call `instrument` with a `name`, `payload` and a block.
The notification will be sent after the block returns. `ActiveSupport` will generate the start and end times
-as well as the unique ID. All data passed into the `insturment` call will make it into the payload.
+as well as the unique ID. All data passed into the `instrument` call will make it into the payload.
Here's an example:
```ruby
-ActiveSupport::Notifications.instrument "my.custom.event", :this => :data do
+ActiveSupport::Notifications.instrument "my.custom.event", this: :data do
# do your custom stuff here
end
```
@@ -477,7 +479,7 @@ Now you can listen to this event with:
```ruby
ActiveSupport::Notifications.subscribe "my.custom.event" do |name, started, finished, unique_id, data|
- puts data.inspect # { :this => :data }
+ puts data.inspect # {:this=>:data}
end
```
diff --git a/guides/source/ajax_on_rails.md b/guides/source/ajax_on_rails.md
deleted file mode 100644
index 97c56036e8..0000000000
--- a/guides/source/ajax_on_rails.md
+++ /dev/null
@@ -1,316 +0,0 @@
-AJAX on Rails
-=============
-
-This guide covers the built-in Ajax/JavaScript functionality of Rails (and more);
-it will enable you to create rich and dynamic AJAX applications with ease! We will
-cover the following topics:
-
-* Quick introduction to AJAX and related technologies
-* Unobtrusive JavaScript helpers with drivers for Prototype, jQuery etc
-* Testing JavaScript functionality
-
---------------------------------------------------------------------------------
-
-Hello AJAX - a Quick Intro
---------------------------
-
-AJAX is about updating parts of a web page without reloading the page. An AJAX
-call happens as a response to an event, like when the page finished loading or
-when a user clicks on an element. For example, let say you click on a link, which
-would usually take you to a new page, but instead of doing that, an asynchronous
-HTTP request is made and the response is evaluated with JavaScript. That way the
-page is not reloaded and new information can be dynamically included in the page.
-The way that happens is by inserting, removing or changing parts of the DOM. The
-DOM, or Document Object Model, is a convention to represent the HTML document as
-a set of nodes that contain other nodes. For example, a list of names is represented
-as a `ul` element node containing several `li` element nodes. An AJAX call can
-be made to obtain a new list item to include, and append it inside a `li` node to
-the `ul` node.
-
-### Asynchronous JavaScript + XML
-
-AJAX means Asynchronous JavaScript + XML. Asynchronous means that the page is not
-reloaded, the request made is separate from the regular page request. JavaScript
-is used to evaluate the response and the XML part is a bit misleading as XML is
-not required, you respond to the HTTP request with JSON or regular HTML as well.
-
-### The DOM
-
-The DOM (Document Object Model) is a convention to represent HTML (or XML)
-documents, as a set of nodes that act as objects and contain other nodes. You can
-have a `div` element that contains other `div` elements as well as `p` elements
-that contain text.
-
-### Standard HTML communication vs AJAX
-
-In regular HTML comunications, when you click on a link, the browser makes an HTTP
-`GET` request, the server responds with a new HTML document that the browsers renders
-and then replaces the previous one. The same thing happens when you click a button to
-submit a form, except that you make and HTTP `POST` request, but you also get a new
-HTML document that the browser renders and replaces the current one. In AJAX
-communications, the request is separate, and the response is evaluated in JavaScript
-instead of rendered by the browser. That way you can have more control over the content
-that gets returned, and the page is not reloaded.
-
-Built-in Rails Helpers
-----------------------
-
-Rails 4.0 ships with [jQuery](http://jquery.com) as the default JavaScript library.
-The Gemfile contains `gem 'jquery-rails'` which provides the `jquery.js` and
-`jquery_ujs.js` files via the asset pipeline.
-
-You will have to use the `require` directive to tell Sprockets to load `jquery.js`
-and `jquery.js`. For example, a new Rails application includes a default
-`app/assets/javascripts/application.js` file which contains the following lines:
-
-```
-// ...
-//= require jquery
-//= require jquery_ujs
-// ...
-```
-
-The `application.js` file acts like a manifest and is used to tell Sprockets the
-files that you wish to require. In this case, you are requiring the files `jquery.js`
-and `jquery_ujs.js` provided by the `jquery-rails` gem.
-
-If the application is not using the asset pipeline, this can be accessed as:
-
-```ruby
-javascript_include_tag :defaults
-```
-
-By default, `:defaults` loads jQuery.
-
-You can also choose to use Prototype instead of jQuery and specify the option
-using `-j` switch while generating the application.
-
-```bash
-rails new app_name -j prototype
-```
-
-This will add the `prototype-rails` gem to the Gemfile and modify the
-`app/assets/javascripts/application.js` file:
-
-```
-// ...
-//= require prototype
-//= require prototype_ujs
-// ...
-```
-
-You are ready to add some AJAX love to your Rails app!
-
-### Examples
-
-To make them working with AJAX, simply pass the `remote: true` option to
-the original non-remote method.
-
-```ruby
-button_to 'New', action: 'new', form_class: 'new-thing'
-```
-
-will produce
-
-```html
-<form method="post" action="/controller/new" class="new-thing">
- <div><input value="New" type="submit" /></div>
-</form>
-```
-
-```ruby
-button_to 'Create', action: 'create', remote: true, form: { 'data-type' => 'json' }
-```
-
-will produce
-
-```html
-<form method="post" action="/images/create" class="button_to" data-remote="true" data-type="json">
- <div><input value="Create" type="submit" /></div>
-</form>
-```
-
-```ruby
-button_to 'Delete Image', { action: 'delete', id: @image.id },
- method: :delete, data: { confirm: 'Are you sure?' }
-```
-
-will produce
-
-```html
-<form method="post" action="/images/delete/1" class="button_to">
- <div>
- <input type="hidden" name="_method" value="delete" />
- <input data-confirm='Are you sure?' value="Delete" type="submit" />
- </div>
-</form>
-```
-
-```ruby
-button_to 'Destroy', 'http://www.example.com',
- method: 'delete', remote: true, data: { disable_with: 'loading...', confirm: 'Are you sure?' }
-```
-
-will produce
-
-```html
-<form class='button_to' method='post' action='http://www.example.com' data-remote='true'>
- <div>
- <input name='_method' value='delete' type='hidden' />
- <input value='Destroy' type='submit' data-disable-with='loading...' data-confirm='Are you sure?' />
- </div>
-</form>
-```
-
-### The Quintessential AJAX Rails Helper: link_to_remote
-
-Let's start with what is probably the most often used helper: `link_to_remote`. It has an interesting feature from the documentation point of view: the options supplied to `link_to_remote` are shared by all other AJAX helpers, so learning the mechanics and options of `link_to_remote` is a great help when using other helpers.
-
-The signature of `link_to_remote` function is the same as that of the standard `link_to` helper:
-
-```ruby
-def link_to_remote(name, options = {}, html_options = nil)
-```
-
-And here is a simple example of link_to_remote in action:
-
-```ruby
-link_to_remote "Add to cart",
- :url => add_to_cart_url(product.id),
- :update => "cart"
-```
-
-* The very first parameter, a string, is the text of the link which appears on the page.
-* The second parameter, the `options` hash is the most interesting part as it has the AJAX specific stuff:
- * **:url** This is the only parameter that is always required to generate the simplest remote link (technically speaking, it is not required, you can pass an empty `options` hash to `link_to_remote` - but in this case the URL used for the POST request will be equal to your current URL which is probably not your intention). This URL points to your AJAX action handler. The URL is typically specified by Rails REST view helpers, but you can use the `url_for` format too.
- * **:update** Specifying a DOM id of the element we would like to update. The above example demonstrates the simplest way of accomplishing this - however, we are in trouble if the server responds with an error message because that will be injected into the page too! However, Rails has a solution for this situation:
-
- ```ruby
- link_to_remote "Add to cart",
- :url => add_to_cart_url(product),
- :update => { :success => "cart", :failure => "error" }
- ```
-
- If the server returns 200, the output of the above example is equivalent to our first, simple one. However, in case of error, the element with the DOM id `error` is updated rather than the `cart` element.
-
- * **position** By default (i.e. when not specifying this option, like in the examples before) the response is injected into the element with the specified DOM id, replacing the original content of the element (if there was any). You might want to alter this behavior by keeping the original content - the only question is where to place the new content? This can specified by the `position` parameter, with four possibilities:
- * `:before` Inserts the response text just before the target element. More precisely, it creates a text node from the response and inserts it as the left sibling of the target element.
- * `:after` Similar behavior to `:before`, but in this case the response is inserted after the target element.
- * `:top` Inserts the text into the target element, before its original content. If the target element was empty, this is equivalent with not specifying `:position` at all.
- * `:bottom` The counterpart of `:top`: the response is inserted after the target element's original content.
-
- A typical example of using `:bottom` is inserting a new \<li> element into an existing list:
-
- ```ruby
- link_to_remote "Add new item",
- :url => items_url,
- :update => 'item_list',
- :position => :bottom
- ```
-
- * **:method** Most typically you want to use a POST request when adding a remote
-link to your view so this is the default behavior. However, sometimes you'll want to update (PATCH/PUT) or delete/destroy (DELETE) something and you can specify this with the `:method` option. Let's see an example for a typical AJAX link for deleting an item from a list:
-
- ```ruby
- link_to_remote "Delete the item",
- :url => item_url(item),
- :method => :delete
- ```
-
- Note that if we wouldn't override the default behavior (POST), the above snippet would route to the create action rather than destroy.
-
- * **JavaScript filters** You can customize the remote call further by wrapping it with some JavaScript code. Let's say in the previous example, when deleting a link, you'd like to ask for a confirmation by showing a simple modal text box to the user. This is a typical example what you can accomplish with these options - let's see them one by one:
- * `:condition` =&gt; `code` Evaluates `code` (which should evaluate to a boolean) and proceeds if it's true, cancels the request otherwise.
- * `:before` =&gt; `code` Evaluates the `code` just before launching the request. The output of the code has no influence on the execution. Typically used show a progress indicator (see this in action in the next example).
- * `:after` =&gt; `code` Evaluates the `code` after launching the request. Note that this is different from the `:success` or `:complete` callback (covered in the next section) since those are triggered after the request is completed, while the code snippet passed to `:after` is evaluated after the remote call is made. A common example is to disable elements on the page or otherwise prevent further action while the request is completed.
- * `:submit` =&gt; `dom_id` This option does not make sense for `link_to_remote`, but we'll cover it for the sake of completeness. By default, the parent element of the form elements the user is going to submit is the current form - use this option if you want to change the default behavior. By specifying this option you can change the parent element to the element specified by the DOM id `dom_id`.
- * `:with` &gt; `code` The JavaScript code snippet in `code` is evaluated and added to the request URL as a parameter (or set of parameters). Therefore, `code` should return a valid URL query string (like "item_type=8" or "item_type=8&sort=true"). Usually you want to obtain some value(s) from the page - let's see an example:
-
- ```ruby
- link_to_remote "Update record",
- :url => record_url(record),
- :method => :patch,
- :with => "'status=' + 'encodeURIComponent($('status').value) + '&completed=' + $('completed')"
- ```
-
- This generates a remote link which adds 2 parameters to the standard URL generated by Rails, taken from the page (contained in the elements matched by the 'status' and 'completed' DOM id).
-
- * **Callbacks** Since an AJAX call is typically asynchronous, as its name suggests (this is not a rule, and you can fire a synchronous request - see the last option, `:type`) your only way of communicating with a request once it is fired is via specifying callbacks. There are six options at your disposal (in fact 508, counting all possible response types, but these six are the most frequent and therefore specified by a constant):
- * `:loading:` =&gt; `code` The request is in the process of receiving the data, but the transfer is not completed yet.
- * `:loaded:` =&gt; `code` The transfer is completed, but the data is not processed and returned yet
- * `:interactive:` =&gt; `code` One step after `:loaded`: The data is fully received and being processed
- * `:success:` =&gt; `code` The data is fully received, parsed and the server responded with "200 OK"
- * `:failure:` =&gt; `code` The data is fully received, parsed and the server responded with **anything** but "200 OK" (typically 404 or 500, but in general with any status code ranging from 100 to 509)
- * `:complete:` =&gt; `code` The combination of the previous two: The request has finished receiving and parsing the data, and returned a status code (which can be anything).
- * Any other status code ranging from 100 to 509: Additionally you might want to check for other HTTP status codes, such as 404. In this case simply use the status code as a number:
-
- ```ruby
- link_to_remote "Add new item",
- :url => items_url,
- :update => "item_list",
- 404 => "alert('Item not found!')"
- ```
-
- Let's see a typical example for the most frequent callbacks, `:success`, `:failure` and `:complete` in action:
-
- ```ruby
- link_to_remote "Add new item",
- :url => items_url,
- :update => "item_list",
- :before => "$('progress').show()",
- :complete => "$('progress').hide()",
- :success => "display_item_added(request)",
- :failure => "display_error(request)"
- ```
-
- * **:type** If you want to fire a synchronous request for some obscure reason (blocking the browser while the request is processed and doesn't return a status code), you can use the `:type` option with the value of `:synchronous`.
-
-* Finally, using the `html_options` parameter you can add HTML attributes to the generated tag. It works like the same parameter of the `link_to` helper. There are interesting side effects for the `href` and `onclick` parameters though:
- * If you specify the `href` parameter, the AJAX link will degrade gracefully, i.e. the link will point to the URL even if JavaScript is disabled in the client browser
- * `link_to_remote` gains its AJAX behavior by specifying the remote call in the onclick handler of the link. If you supply `html_options[:onclick]` you override the default behavior, so use this with care!
-
-We are finished with `link_to_remote`. I know this is quite a lot to digest for one helper function, but remember, these options are common for all the rest of the Rails view helpers, so we will take a look at the differences / additional parameters in the next sections.
-
-### AJAX Forms
-
-There are three different ways of adding AJAX forms to your view using Rails Prototype helpers. They are slightly different, but striving for the same goal: instead of submitting the form using the standard HTTP request/response cycle, it is submitted asynchronously, thus not reloading the page. These methods are the following:
-
-* `remote_form_for` (and its alias `form_remote_for`) is tied to Rails most tightly of the three since it takes a resource, model or array of resources (in case of a nested resource) as a parameter.
-* `form_remote_tag` AJAXifies the form by serializing and sending its data in the background
-* `submit_to_remote` and `button_to_remote` is more rarely used than the previous two. Rather than creating an AJAX form, you add a button/input
-
-Let's see them in action one by one!
-
-#### `remote_form_for`
-
-#### `form_remote_tag`
-
-#### `submit_to_remote`
-
-### Serving JavaScript
-
-First we'll check out how to send JavaScript to the server manually. You are practically never going to need this, but it's interesting to understand what's going on under the hood.
-
-```ruby
-def javascript_test
- render :text => "alert('Hello, world!')",
- :content_type => "text/javascript"
-end
-```
-
-(Note: if you want to test the above method, create a `link_to_remote` with a single parameter - `:url`, pointing to the `javascript_test` action)
-
-What happens here is that by specifying the Content-Type header variable, we instruct the browser to evaluate the text we are sending over (rather than displaying it as plain text, which is the default behavior).
-
-Testing JavaScript
-------------------
-
-JavaScript testing reminds me the definition of the world 'classic' by Mark Twain: "A classic is something that everybody wants to have read and nobody wants to read." It's similar with JavaScript testing: everyone would like to have it, yet it's not done by too much developers as it is tedious, complicated, there is a proliferation of tools and no consensus/accepted best practices, but we will nevertheless take a stab at it:
-
-* (Fire)Watir
-* Selenium
-* Celerity/Culerity
-* Cucumber+Webrat
-* Mention stuff like screw.unit/jsSpec
-
-Note to self: check out the RailsConf JS testing video
diff --git a/guides/source/api_documentation_guidelines.md b/guides/source/api_documentation_guidelines.md
index dcfa7eb6fb..98ead9570f 100644
--- a/guides/source/api_documentation_guidelines.md
+++ b/guides/source/api_documentation_guidelines.md
@@ -3,6 +3,11 @@ API Documentation Guidelines
This guide documents the Ruby on Rails API documentation guidelines.
+After reading this guide, you will know:
+
+* How to write effective prose for documentation purposes.
+* Style guidelines for documenting different kinds of Ruby code.
+
--------------------------------------------------------------------------------
RDoc
@@ -20,7 +25,8 @@ Write in present tense: "Returns a hash that...", rather than "Returned a hash t
Start comments in upper case. Follow regular punctuation rules:
```ruby
-# Declares an attribute reader backed by an internally-named instance variable.
+# Declares an attribute reader backed by an internally-named
+# instance variable.
def attr_internal_reader(*attrs)
...
end
@@ -51,8 +57,8 @@ Use two spaces to indent chunks of code--that is, for markup purposes, two space
Short docs do not need an explicit "Examples" label to introduce snippets; they just follow paragraphs:
```ruby
-# Converts a collection of elements into a formatted string by calling
-# `to_s` on all elements and joining them.
+# Converts a collection of elements into a formatted string by
+# calling +to_s+ on all elements and joining them.
#
# Blog.all.to_formatted_s # => "First PostSecond PostThird Post"
```
@@ -64,7 +70,7 @@ On the other hand, big chunks of structured documentation may have a separate "E
#
# Person.exists?(5)
# Person.exists?('5')
-# Person.exists?(:name => "David")
+# Person.exists?(name: "David")
# Person.exists?(['name LIKE ?', "%#{query}%"])
```
@@ -88,7 +94,7 @@ If a line is too long, the comment may be placed on the next line:
# label(:post, :title, "A short title")
# # => <label for="post_title">A short title</label>
#
-# label(:post, :title, "A short title", :class => "title_label")
+# label(:post, :title, "A short title", class: "title_label")
# # => <label for="post_title" class="title_label">A short title</label>
```
@@ -135,21 +141,23 @@ class Array
end
```
-WARNING: Using a pair of `+...+` for fixed-width font only works with **words**; that is: anything matching `\A\w+\z`. For anything else use `<tt>...</tt>`, notably symbols, setters, inline snippets, etc.
+WARNING: Using a pair of `+...+` for fixed-width font only works with **words**; that is: anything matching `\A\w+\z`. For anything else use `<tt>...</tt>`, notably symbols, setters, inline snippets, etc.
### Regular Font
When "true" and "false" are English words rather than Ruby keywords use a regular font:
```ruby
-# Runs all the validations within the specified context. Returns true if no errors are found,
-# false otherwise.
+# Runs all the validations within the specified context.
+# Returns true if no errors are found, false 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.
+# 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.
+# 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 valid?(context = nil)
...
end
@@ -161,10 +169,10 @@ Description Lists
In lists of options, parameters, etc. use a hyphen between the item and its description (reads better than a colon because normally options are symbols):
```ruby
-# * <tt>:allow_nil</tt> - Skip validation if attribute is `nil`.
+# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
```
-The description starts in upper case and ends with a full stop—it's standard English.
+The description starts in upper case and ends with a full stop-it's standard English.
Dynamically Generated Methods
-----------------------------
diff --git a/guides/source/asset_pipeline.md b/guides/source/asset_pipeline.md
index fc0092a93e..e9d3712a2a 100644
--- a/guides/source/asset_pipeline.md
+++ b/guides/source/asset_pipeline.md
@@ -1,60 +1,112 @@
-Asset Pipeline
-==============
+The Asset Pipeline
+==================
-This guide covers the asset pipeline introduced in Rails 3.1.
-By referring to this guide you will be able to:
+This guide covers the asset pipeline.
-* Understand what the asset pipeline is and what it does
-* Properly organize your application assets
-* Understand the benefits of the asset pipeline
-* Add a pre-processor to the pipeline
-* Package assets with a gem
+After reading this guide, you will know:
+
+* What the asset pipeline is and what it does.
+* How to properly organize your application assets.
+* The benefits of the asset pipeline.
+* How to add a pre-processor to the pipeline.
+* How to package assets with a gem.
--------------------------------------------------------------------------------
What is the Asset Pipeline?
---------------------------
-The asset pipeline provides a framework to concatenate and minify or compress JavaScript and CSS assets. It also adds the ability to write these assets in other languages such as CoffeeScript, Sass and ERB.
+The asset pipeline provides a framework to concatenate and minify or compress
+JavaScript and CSS assets. It also adds the ability to write these assets in
+other languages and pre-processors such as CoffeeScript, Sass and ERB.
+
+The asset pipeline is technically no longer a core feature of Rails 4, it has
+been extracted out of the framework into the
+[sprockets-rails](https://github.com/rails/sprockets-rails) gem.
+
+The asset pipeline is enabled by default.
-Prior to Rails 3.1 these features were added through third-party Ruby libraries such as Jammit and Sprockets. Rails 3.1 is integrated with Sprockets through Action Pack which depends on the `sprockets` gem, by default.
+You can disable the asset pipeline while creating a new application by
+passing the `--skip-sprockets` option.
-Making the asset pipeline a core feature of Rails means that all developers can benefit from the power of having their assets pre-processed, compressed and minified by one central library, Sprockets. This is part of Rails' "fast by default" strategy as outlined by DHH in his keynote at RailsConf 2011.
+```bash
+rails new appname --skip-sprockets
+```
-In Rails 3.1, the asset pipeline is enabled by default. It can be disabled in `config/application.rb` by putting this line inside the application class definition:
+Rails 4 automatically adds the `sass-rails`, `coffee-rails` and `uglifier`
+gems to your Gemfile, which are used by Sprockets for asset compression:
```ruby
-config.assets.enabled = false
+gem 'sass-rails'
+gem 'uglifier'
+gem 'coffee-rails'
```
-You can also disable the asset pipeline while creating a new application by passing the `--skip-sprockets` option.
+Using the `--skip-sprockets` option will prevent Rails 4 from adding
+`sass-rails` and `uglifier` to Gemfile, so if you later want to enable
+the asset pipeline you will have to add those gems to your Gemfile. Also,
+creating an application with the `--skip-sprockets` option will generate
+a slightly different `config/application.rb` file, with a require statement
+for the sprockets railtie that is commented-out. You will have to remove
+the comment operator on that line to later enable the asset pipeline:
-```bash
-rails new appname --skip-sprockets
+```ruby
+# require "sprockets/railtie"
+```
+
+To set asset compression methods, set the appropriate configuration options
+in `production.rb` - `config.assets.css_compressor` for your CSS and
+`config.assets.js_compressor` for your Javascript:
+
+```ruby
+config.assets.css_compressor = :yui
+config.assets.js_compressor = :uglify
```
-You should use the defaults for all new applications unless you have a specific reason to avoid the asset pipeline.
+NOTE: The `sass-rails` gem is automatically used for CSS compression if included
+in Gemfile and no `config.assets.css_compressor` option is set.
### Main Features
-The first feature of the pipeline is to concatenate assets. This is important in a production environment, because it can reduce the number of requests that a browser must make to render a web page. Web browsers are limited in the number of requests that they can make in parallel, so fewer requests can mean faster loading for your application.
+The first feature of the pipeline is to concatenate assets, which can reduce the
+number of requests that a browser makes to render a web page. Web browsers are
+limited in the number of requests that they can make in parallel, so fewer
+requests can mean faster loading for your application.
-Rails 2.x introduced the ability to concatenate JavaScript and CSS assets by placing `:cache => true` at the end of the `javascript_include_tag` and `stylesheet_link_tag` methods. But this technique has some limitations. For example, it cannot generate the caches in advance, and it is not able to transparently include assets provided by third-party libraries.
+Sprockets concatenates all JavaScript files into one master `.js` file and all
+CSS files into one master `.css` file. As you'll learn later in this guide, you
+can customize this strategy to group files any way you like. In production,
+Rails inserts an MD5 fingerprint into each filename so that the file is cached
+by the web browser. You can invalidate the cache by altering this fingerprint,
+which happens automatically whenever you change the file contents.
-Starting with version 3.1, Rails defaults to concatenating all JavaScript files into one master `.js` file and all CSS files into one master `.css` file. As you'll learn later in this guide, you can customize this strategy to group files any way you like. In production, Rails inserts an MD5 fingerprint into each filename so that the file is cached by the web browser. You can invalidate the cache by altering this fingerprint, which happens automatically whenever you change the file contents.
+The second feature of the asset pipeline is asset minification or compression.
+For CSS files, this is done by removing whitespace and comments. For JavaScript,
+more complex processes can be applied. You can choose from a set of built in
+options or specify your own.
-The second feature of the asset pipeline is asset minification or compression. For CSS files, this is done by removing whitespace and comments. For JavaScript, more complex processes can be applied. You can choose from a set of built in options or specify your own.
-
-The third feature of the asset pipeline is that it allows coding assets via a higher-level language, with precompilation down to the actual assets. Supported languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by default.
+The third feature of the asset pipeline is it allows coding assets via a
+higher-level language, with precompilation down to the actual assets. Supported
+languages include Sass for CSS, CoffeeScript for JavaScript, and ERB for both by
+default.
### What is Fingerprinting and Why Should I Care?
-Fingerprinting is a technique that makes the name of a file dependent on the contents of the file. When the file contents change, the filename is also changed. For content that is static or infrequently changed, this provides an easy way to tell whether two versions of a file are identical, even across different servers or deployment dates.
+Fingerprinting is a technique that makes the name of a file dependent on the
+contents of the file. When the file contents change, the filename is also
+changed. For content that is static or infrequently changed, this provides an
+easy way to tell whether two versions of a file are identical, even across
+different servers or deployment dates.
-When a filename is unique and based on its content, HTTP headers can be set to encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment, or in web browsers) to keep their own copy of the content. When the content is updated, the fingerprint will change. This will cause the remote clients to request a new copy of the content. This is generally known as _cache busting_.
+When a filename is unique and based on its content, HTTP headers can be set to
+encourage caches everywhere (whether at CDNs, at ISPs, in networking equipment,
+or in web browsers) to keep their own copy of the content. When the content is
+updated, the fingerprint will change. This will cause the remote clients to
+request a new copy of the content. This is generally known as _cache busting_.
-The technique that Rails uses for fingerprinting is to insert a hash of the content into the name, usually at the end. For example a CSS file `global.css` could be renamed with an MD5 digest of its contents:
+The technique sprockets uses for fingerprinting is to insert a hash of the
+content into the name, usually at the end. For example a CSS file `global.css`
```
global-908e25f4bf641868d8683022a5b62f54.css
@@ -62,7 +114,8 @@ global-908e25f4bf641868d8683022a5b62f54.css
This is the strategy adopted by the Rails asset pipeline.
-Rails' old strategy was to append a date-based query string to every asset linked with a built-in helper. In the source the generated code looked like this:
+Rails' old strategy was to append a date-based query string to every asset linked
+with a built-in helper. In the source the generated code looked like this:
```
/stylesheets/global.css?1309495796
@@ -70,54 +123,131 @@ Rails' old strategy was to append a date-based query string to every asset linke
The query string strategy has several disadvantages:
-1. **Not all caches will reliably cache content where the filename only differs by query parameters**<br />
- [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/), "...avoiding a querystring for cacheable resources". He found that in this case 5-20% of requests will not be cached. Query strings in particular do not work at all with some CDNs for cache invalidation.
+1. **Not all caches will reliably cache content where the filename only differs by
+query parameters**<br>
+ [Steve Souders recommends](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/),
+ "...avoiding a querystring for cacheable resources". He found that in this
+case 5-20% of requests will not be cached. Query strings in particular do not
+work at all with some CDNs for cache invalidation.
+
+2. **The file name can change between nodes in multi-server environments.**<br>
+ The default query string in Rails 2.x is based on the modification time of
+the files. When assets are deployed to a cluster, there is no guarantee that the
+timestamps will be the same, resulting in different values being used depending
+on which server handles the request.
-2. **The file name can change between nodes in multi-server environments.**<br />
- The default query string in Rails 2.x is based on the modification time of the files. When assets are deployed to a cluster, there is no guarantee that the timestamps will be the same, resulting in different values being used depending on which server handles the request.
-3. **Too much cache invalidation**<br />
- When static assets are deployed with each new release of code, the mtime of _all_ these files changes, forcing all remote clients to fetch them again, even when the content of those assets has not changed.
+3. **Too much cache invalidation**<br>
+ When static assets are deployed with each new release of code, the mtime
+(time of last modification) of _all_ these files changes, forcing all remote
+clients to fetch them again, even when the content of those assets has not changed.
-Fingerprinting fixes these problems by avoiding query strings, and by ensuring that filenames are consistent based on their content.
+Fingerprinting fixes these problems by avoiding query strings, and by ensuring
+that filenames are consistent based on their content.
-Fingerprinting is enabled by default for production and disabled for all other environments. You can enable or disable it in your configuration through the `config.assets.digest` option.
+Fingerprinting is enabled by default for production and disabled for all other
+environments. You can enable or disable it in your configuration through the
+`config.assets.digest` option.
More reading:
* [Optimize caching](http://code.google.com/speed/page-speed/docs/caching.html)
-* [Revving Filenames: don’t use querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
+* [Revving Filenames: don't use
+* querystring](http://www.stevesouders.com/blog/2008/08/23/revving-filenames-dont-use-querystring/)
How to Use the Asset Pipeline
-----------------------------
-In previous versions of Rails, all assets were located in subdirectories of `public` such as `images`, `javascripts` and `stylesheets`. With the asset pipeline, the preferred location for these assets is now the `app/assets` directory. Files in this directory are served by the Sprockets middleware included in the sprockets gem.
-
-Assets can still be placed in the `public` hierarchy. Any assets under `public` will be served as static files by the application or web server. You should use `app/assets` for files that must undergo some pre-processing before they are served.
+In previous versions of Rails, all assets were located in subdirectories of
+`public` such as `images`, `javascripts` and `stylesheets`. With the asset
+pipeline, the preferred location for these assets is now the `app/assets`
+directory. Files in this directory are served by the Sprockets middleware.
+
+Assets can still be placed in the `public` hierarchy. Any assets under `public`
+will be served as static files by the application or web server. You should use
+`app/assets` for files that must undergo some pre-processing before they are
+served.
+
+In production, Rails precompiles these files to `public/assets` by default. The
+precompiled copies are then served as static assets by the web server. The files
+in `app/assets` are never served directly in production.
+
+### Controller Specific Assets
+
+When you generate a scaffold or a controller, Rails also generates a JavaScript
+file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a
+Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`)
+for that controller. Additionally, when generating a scaffold, Rails generates
+the file scaffolds.css (or scaffolds.css.scss if `sass-rails` is in the
+`Gemfile`.)
+
+For example, if you generate a `ProjectsController`, Rails will also add a new
+file at `app/assets/javascripts/projects.js.coffee` and another at
+`app/assets/stylesheets/projects.css.scss`. By default these files will be ready
+to use by your application immediately using the `require_tree` directive. See
+[Manifest Files and Directives](#manifest-files-and-directives) for more details
+on require_tree.
+
+You can also opt to include controller specific stylesheets and JavaScript files
+only in their respective controllers using the following:
+
+`<%= javascript_include_tag params[:controller] %>` or `<%= stylesheet_link_tag
+params[:controller] %>`
+
+When doing this, ensure you are not using the `require_tree` directive, as that
+will result in your assets being included more than once.
+
+WARNING: When using asset precompilation, you will need to ensure that your
+controller assets will be precompiled when loading them on a per page basis. By
+default .coffee and .scss files will not be precompiled on their own. This will
+result in false positives during development as these files will work just fine
+since assets are compiled on the fly in development mode. When running in
+production, however, you will see 500 errors since live compilation is turned
+off by default. See [Precompiling Assets](#precompiling-assets) for more
+information on how precompiling works.
+
+NOTE: You must have an ExecJS supported runtime in order to use CoffeeScript.
+If you are using Mac OS X or Windows, you have a JavaScript runtime installed in
+your operating system. Check
+[ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all
+supported JavaScript runtimes.
+
+You can also disable generation of controller specific asset files by adding the
+following to your `config/application.rb` configuration:
-In production, Rails precompiles these files to `public/assets` by default. The precompiled copies are then served as static assets by the web server. The files in `app/assets` are never served directly in production.
-
-When you generate a scaffold or a controller, Rails also generates a JavaScript file (or CoffeeScript file if the `coffee-rails` gem is in the `Gemfile`) and a Cascading Style Sheet file (or SCSS file if `sass-rails` is in the `Gemfile`) for that controller.
-
-For example, if you generate a `ProjectsController`, Rails will also add a new file at `app/assets/javascripts/projects.js.coffee` and another at `app/assets/stylesheets/projects.css.scss`. You should put any JavaScript or CSS unique to a controller inside their respective asset files, as these files can then be loaded just for these controllers with lines such as `<%= javascript_include_tag params[:controller] %>` or `<%= stylesheet_link_tag params[:controller] %>`.
-
-NOTE: You must have an [ExecJS](https://github.com/sstephenson/execjs#readme) supported runtime in order to use CoffeeScript. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. Check [ExecJS](https://github.com/sstephenson/execjs#readme) documentation to know all supported JavaScript runtimes.
+```ruby
+ config.generators do |g|
+ g.assets false
+ end
+```
### Asset Organization
-Pipeline assets can be placed inside an application in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`.
+Pipeline assets can be placed inside an application in one of three locations:
+`app/assets`, `lib/assets` or `vendor/assets`.
+
+* `app/assets` is for assets that are owned by the application, such as custom
+images, JavaScript files or stylesheets.
-* `app/assets` is for assets that are owned by the application, such as custom images, JavaScript files or stylesheets.
+* `lib/assets` is for your own libraries' code that doesn't really fit into the
+scope of the application or those libraries which are shared across applications.
-* `lib/assets` is for your own libraries' code that doesn't really fit into the scope of the application or those libraries which are shared across applications.
+* `vendor/assets` is for assets that are owned by outside entities, such as
+code for JavaScript plugins and CSS frameworks.
-* `vendor/assets` is for assets that are owned by outside entities, such as code for JavaScript plugins and CSS frameworks.
+WARNING: If you are upgrading from Rails 3, please take into account that assets
+under `lib/assets` or `vendor/assets` are available for inclusion via the
+application manifests but no longer part of the precompile array. See
+[Precompiling Assets](#precompiling-assets) for guidance.
-#### Search paths
+#### Search Paths
-When a file is referenced from a manifest or a helper, Sprockets searches the three default asset locations for it.
+When a file is referenced from a manifest or a helper, Sprockets searches the
+three default asset locations for it.
-The default locations are: `app/assets/images` and the subdirectories `javascripts` and `stylesheets` in all three asset locations, but these subdirectories are not special. Any path under `assets/*` will be searched.
+The default locations are: the `images`, `javascripts` and `stylesheets`
+directories under the `apps/assets` folder, but these subdirectories
+are not special - any path under `assets/*` will be searched.
For example, these files:
@@ -149,72 +279,113 @@ is referenced as:
//= require sub/something
```
-You can view the search path by inspecting `Rails.application.config.assets.paths` in the Rails console.
+You can view the search path by inspecting
+`Rails.application.config.assets.paths` in the Rails console.
-Besides the standard `assets/*` paths, additional (fully qualified) paths can be added to the pipeline in `config/application.rb`. For example:
+Besides the standard `assets/*` paths, additional (fully qualified) paths can be
+added to the pipeline in `config/application.rb`. For example:
```ruby
config.assets.paths << Rails.root.join("lib", "videoplayer", "flash")
```
-Paths are traversed in the order that they occur in the search path. By default, this means the files in `app/assets` take precedence, and will mask corresponding paths in `lib` and `vendor`.
+Paths are traversed in the order they occur in the search path. By default,
+this means the files in `app/assets` take precedence, and will mask
+corresponding paths in `lib` and `vendor`.
-It is important to note that files you want to reference outside a manifest must be added to the precompile array or they will not be available in the production environment.
+It is important to note that files you want to reference outside a manifest must
+be added to the precompile array or they will not be available in the production
+environment.
-#### Using index files
+#### Using Index Files
-Sprockets uses files named `index` (with the relevant extensions) for a special purpose.
+Sprockets uses files named `index` (with the relevant extensions) for a special
+purpose.
-For example, if you have a jQuery library with many modules, which is stored in `lib/assets/library_name`, the file `lib/assets/library_name/index.js` serves as the manifest for all files in this library. This file could include a list of all the required files in order, or a simple `require_tree` directive.
+For example, if you have a jQuery library with many modules, which is stored in
+`lib/assets/library_name`, the file `lib/assets/library_name/index.js` serves as
+the manifest for all files in this library. This file could include a list of
+all the required files in order, or a simple `require_tree` directive.
-The library as a whole can be accessed in the site's application manifest like so:
+The library as a whole can be accessed in the application manifest like so:
```js
//= require library_name
```
-This simplifies maintenance and keeps things clean by allowing related code to be grouped before inclusion elsewhere.
+This simplifies maintenance and keeps things clean by allowing related code to
+be grouped before inclusion elsewhere.
### Coding Links to Assets
-Sprockets does not add any new methods to access your assets - you still use the familiar `javascript_include_tag` and `stylesheet_link_tag`.
+Sprockets does not add any new methods to access your assets - you still use the
+familiar `javascript_include_tag` and `stylesheet_link_tag`:
```erb
-<%= stylesheet_link_tag "application" %>
+<%= stylesheet_link_tag "application", media: "all" %>
<%= javascript_include_tag "application" %>
```
-In regular views you can access images in the `assets/images` directory like this:
+If using the turbolinks gem, which is included by default in Rails 4, then
+include the 'data-turbolinks-track' option which causes turbolinks to check if
+an asset has been updated and if so loads it into the page:
+
+```erb
+<%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+<%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+```
+
+In regular views you can access images in the `public/assets/images` directory
+like this:
```erb
<%= image_tag "rails.png" %>
```
-Provided that the pipeline is enabled within your application (and not disabled in the current environment context), this file is served by Sprockets. If a file exists at `public/assets/rails.png` it is served by the web server.
+Provided that the pipeline is enabled within your application (and not disabled
+in the current environment context), this file is served by Sprockets. If a file
+exists at `public/assets/rails.png` it is served by the web server.
-Alternatively, a request for a file with an MD5 hash such as `public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same way. How these hashes are generated is covered in the [In Production](#in-production) section later on in this guide.
+Alternatively, a request for a file with an MD5 hash such as
+`public/assets/rails-af27b6a414e6da00003503148be9b409.png` is treated the same
+way. How these hashes are generated is covered in the [In
+Production](#in-production) section later on in this guide.
-Sprockets will also look through the paths specified in `config.assets.paths` which includes the standard application paths and any path added by Rails engines.
+Sprockets will also look through the paths specified in `config.assets.paths`,
+which includes the standard application paths and any paths added by Rails
+engines.
-Images can also be organized into subdirectories if required, and they can be accessed by specifying the directory's name in the tag:
+Images can also be organized into subdirectories if required, and then can be
+accessed by specifying the directory's name in the tag:
```erb
<%= image_tag "icons/rails.png" %>
```
-WARNING: If you're precompiling your assets (see [In Production](#in-production) below), linking to an asset that does not exist will raise an exception in the calling page. This includes linking to a blank string. As such, be careful using `image_tag` and the other helpers with user-supplied data.
+WARNING: If you're precompiling your assets (see [In Production](#in-production)
+below), linking to an asset that does not exist will raise an exception in the
+calling page. This includes linking to a blank string. As such, be careful using
+`image_tag` and the other helpers with user-supplied data.
#### CSS and ERB
-The asset pipeline automatically evaluates ERB. This means that if you add an `erb` extension to a CSS asset (for example, `application.css.erb`), then helpers like `asset_path` are available in your CSS rules:
+The asset pipeline automatically evaluates ERB. This means if you add an
+`erb` extension to a CSS asset (for example, `application.css.erb`), then
+helpers like `asset_path` are available in your CSS rules:
```css
.class { background-image: url(<%= asset_path 'image.png' %>) }
```
-This writes the path to the particular asset being referenced. In this example, it would make sense to have an image in one of the asset load paths, such as `app/assets/images/image.png`, which would be referenced here. If this image is already available in `public/assets` as a fingerprinted file, then that path is referenced.
+This writes the path to the particular asset being referenced. In this example,
+it would make sense to have an image in one of the asset load paths, such as
+`app/assets/images/image.png`, which would be referenced here. If this image is
+already available in `public/assets` as a fingerprinted file, then that path is
+referenced.
-If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) -- a method of embedding the image data directly into the CSS file -- you can use the `asset_data_uri` helper.
+If you want to use a [data URI](http://en.wikipedia.org/wiki/Data_URI_scheme) -
+a method of embedding the image data directly into the CSS file - you can use
+the `asset_data_uri` helper.
```css
#logo { background: url(<%= asset_data_uri 'logo.png' %>) }
@@ -226,29 +397,33 @@ Note that the closing tag cannot be of the style `-%>`.
#### CSS and Sass
-When using the asset pipeline, paths to assets must be re-written and `sass-rails` provides `-url` and `-path` helpers (hyphenated in Sass, underscored in Ruby) for the following asset classes: image, font, video, audio, JavaScript and stylesheet.
+When using the asset pipeline, paths to assets must be re-written and
+`sass-rails` provides `-url` and `-path` helpers (hyphenated in Sass,
+underscored in Ruby) for the following asset classes: image, font, video, audio,
+JavaScript and stylesheet.
* `image-url("rails.png")` becomes `url(/assets/rails.png)`
* `image-path("rails.png")` becomes `"/assets/rails.png"`.
-The more generic form can also be used but the asset path and class must both be specified:
+The more generic form can also be used:
-* `asset-url("rails.png", image)` becomes `url(/assets/rails.png)`
-* `asset-path("rails.png", image)` becomes `"/assets/rails.png"`
+* `asset-url("rails.png")` becomes `url(/assets/rails.png)`
+* `asset-path("rails.png")` becomes `"/assets/rails.png"`
#### JavaScript/CoffeeScript and ERB
-If you add an `erb` extension to a JavaScript asset, making it something such as `application.js.erb`, then you can use the `asset_path` helper in your JavaScript code:
+If you add an `erb` extension to a JavaScript asset, making it something such as
+`application.js.erb`, you can then use the `asset_path` helper in your
+JavaScript code:
```js
-$('#logo').attr({
- src: "<%= asset_path('logo.png') %>"
-});
+$('#logo').attr({ src: "<%= asset_path('logo.png') %>" });
```
This writes the path to the particular asset being referenced.
-Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb` extension (e.g., `application.js.coffee.erb`):
+Similarly, you can use the `asset_path` helper in CoffeeScript files with `erb`
+extension (e.g., `application.js.coffee.erb`):
```js
$('#logo').attr src: "<%= asset_path('logo.png') %>"
@@ -256,9 +431,19 @@ $('#logo').attr src: "<%= asset_path('logo.png') %>"
### Manifest Files and Directives
-Sprockets uses manifest files to determine which assets to include and serve. These manifest files contain _directives_ -- instructions that tell Sprockets which files to require in order to build a single CSS or JavaScript file. With these directives, Sprockets loads the files specified, processes them if necessary, concatenates them into one single file and then compresses them (if `Rails.application.config.assets.compress` is true). By serving one file rather than many, the load time of pages can be greatly reduced because the browser makes fewer requests.
+Sprockets uses manifest files to determine which assets to include and serve.
+These manifest files contain _directives_ - instructions that tell Sprockets
+which files to require in order to build a single CSS or JavaScript file. With
+these directives, Sprockets loads the files specified, processes them if
+necessary, concatenates them into one single file and then compresses them (if
+`Rails.application.config.assets.compress` is true). By serving one file rather
+than many, the load time of pages can be greatly reduced because the browser
+makes fewer requests. Compression also reduces file size, enabling the
+browser to download them faster.
+
-For example, a new Rails application includes a default `app/assets/javascripts/application.js` file which contains the following lines:
+For example, a new Rails 4 application includes a default
+`app/assets/javascripts/application.js` file containing the following lines:
```js
// ...
@@ -267,32 +452,69 @@ For example, a new Rails application includes a default `app/assets/javascripts/
//= require_tree .
```
-In JavaScript files, the directives begin with `//=`. In this case, the file is using the `require` and the `require_tree` directives. The `require` directive is used to tell Sprockets the files that you wish to require. Here, you are requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere in the search path for Sprockets. You need not supply the extensions explicitly. Sprockets assumes you are requiring a `.js` file when done from within a `.js` file.
-
-NOTE. In Rails 3.1 the `jquery-rails` gem provides the `jquery.js` and `jquery_ujs.js` files via the asset pipeline. You won't see them in the application tree.
-
-The `require_tree` directive tells Sprockets to recursively include _all_ JavaScript files in the specified directory into the output. These paths must be specified relative to the manifest file. You can also use the `require_directory` directive which includes all JavaScript files only in the directory specified, without recursion.
-
-Directives are processed top to bottom, but the order in which files are included by `require_tree` is unspecified. You should not rely on any particular order among those. If you need to ensure some particular JavaScript ends up above some other in the concatenated file, require the prerequisite file first in the manifest. Note that the family of `require` directives prevents files from being included twice in the output.
+In JavaScript files, Sprockets directives begin with `//=`. In the above case,
+the file is using the `require` and the `require_tree` directives. The `require`
+directive is used to tell Sprockets the files you wish to require. Here, you are
+requiring the files `jquery.js` and `jquery_ujs.js` that are available somewhere
+in the search path for Sprockets. You need not supply the extensions explicitly.
+Sprockets assumes you are requiring a `.js` file when done from within a `.js`
+file.
+
+The `require_tree` directive tells Sprockets to recursively include _all_
+JavaScript files in the specified directory into the output. These paths must be
+specified relative to the manifest file. You can also use the
+`require_directory` directive which includes all JavaScript files only in the
+directory specified, without recursion.
+
+Directives are processed top to bottom, but the order in which files are
+included by `require_tree` is unspecified. You should not rely on any particular
+order among those. If you need to ensure some particular JavaScript ends up
+above some other in the concatenated file, require the prerequisite file first
+in the manifest. Note that the family of `require` directives prevents files
+from being included twice in the output.
+
+Rails also creates a default `app/assets/stylesheets/application.css` file
+which contains these lines:
-Rails also creates a default `app/assets/stylesheets/application.css` file which contains these lines:
-
-```js
+```css
/* ...
*= require_self
*= require_tree .
*/
```
-The directives that work in the JavaScript files also work in stylesheets (though obviously including stylesheets rather than JavaScript files). The `require_tree` directive in a CSS manifest works the same way as the JavaScript one, requiring all stylesheets from the current directory.
-
-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.
-
-NOTE. If you want to use multiple Sass files, you should generally use the [Sass `@import` rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead of these Sprockets directives. Using Sprockets directives all Sass files exist within their own scope, making variables or mixins only available within the document they were defined in.
-
-You can have as many manifest files as you need. For example the `admin.css` and `admin.js` manifest could contain the JS and CSS files that are used for the admin section of an application.
-
-The same remarks about ordering made above apply. In particular, you can specify individual files and they are compiled in the order specified. For example, you might concatenate three CSS files together this way:
+Rails 4 creates both `app/assets/javascripts/application.js` and
+`app/assets/stylesheets/application.css` regardless of whether the
+--skip-sprockets option is used when creating a new rails application. This is
+so you can easily add asset pipelining later if you like.
+
+The directives that work in JavaScript files also work in stylesheets
+(though obviously including stylesheets rather than JavaScript files). The
+`require_tree` directive in a CSS manifest works the same way as the JavaScript
+one, requiring all stylesheets from the current directory.
+
+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.
+
+NOTE. If you want to use multiple Sass files, you should generally use the [Sass
+`@import`
+rule](http://sass-lang.com/docs/yardoc/file.SASS_REFERENCE.html#import) instead
+of these Sprockets directives. Using Sprockets directives all Sass files exist
+within their own scope, making variables or mixins only available within the
+document they were defined in. You can do file globbing as well using
+`@import "*"`, and `@import "**/*"` to add the whole tree equivalent to how
+`require_tree` works. Check the [sass-rails
+documentation](https://github.com/rails/sass-rails#features) for more info and
+important caveats.
+
+You can have as many manifest files as you need. For example, the `admin.css`
+and `admin.js` manifest could contain the JS and CSS files that are used for the
+admin section of an application.
+
+The same remarks about ordering made above apply. In particular, you can specify
+individual files and they are compiled in the order specified. For example, you
+might concatenate three CSS files together this way:
```js
/* ...
@@ -302,21 +524,41 @@ The same remarks about ordering made above apply. In particular, you can specify
*/
```
-
### Preprocessing
-The file extensions used on an asset determine what preprocessing is applied. When a controller or a scaffold is generated with the default Rails gemset, a CoffeeScript file and a SCSS file are generated in place of a regular JavaScript and CSS file. The example used before was a controller called "projects", which generated an `app/assets/javascripts/projects.js.coffee` and an `app/assets/stylesheets/projects.css.scss` file.
-
-When these files are requested, they are processed by the processors provided by the `coffee-script` and `sass` gems and then sent back to the browser as JavaScript and CSS respectively.
-
-Additional layers of preprocessing can be requested by adding other extensions, where each extension is processed in a right-to-left manner. These should be used in the order the processing should be applied. For example, a stylesheet called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB, then SCSS, and finally served as CSS. The same applies to a JavaScript file -- `app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then CoffeeScript, and served as JavaScript.
+The file extensions used on an asset determine what preprocessing is applied.
+When a controller or a scaffold is generated with the default Rails gemset, a
+CoffeeScript file and a SCSS file are generated in place of a regular JavaScript
+and CSS file. The example used before was a controller called "projects", which
+generated an `app/assets/javascripts/projects.js.coffee` and an
+`app/assets/stylesheets/projects.css.scss` file.
+
+In development mode, or if the asset pipeline is disabled, when these files are
+requested they are processed by the processors provided by the `coffee-script`
+and `sass` gems and then sent back to the browser as JavaScript and CSS
+respectively. When asset pipelining is enabled, these files are preprocessed and
+placed in the `public/assets` directory for serving by either the Rails app or
+web server.
+
+Additional layers of preprocessing can be requested by adding other extensions,
+where each extension is processed in a right-to-left manner. These should be
+used in the order the processing should be applied. For example, a stylesheet
+called `app/assets/stylesheets/projects.css.scss.erb` is first processed as ERB,
+then SCSS, and finally served as CSS. The same applies to a JavaScript file -
+`app/assets/javascripts/projects.js.coffee.erb` is processed as ERB, then
+CoffeeScript, and served as JavaScript.
+
+Keep in mind the order of these preprocessors is important. For example, if
+you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee`
+then it would be processed with the CoffeeScript interpreter first, which
+wouldn't understand ERB and therefore you would run into problems.
-Keep in mind that the order of these preprocessors is important. For example, if you called your JavaScript file `app/assets/javascripts/projects.js.erb.coffee` then it would be processed with the CoffeeScript interpreter first, which wouldn't understand ERB and therefore you would run into problems.
In Development
--------------
-In development mode, assets are served as separate files in the order they are specified in the manifest file.
+In development mode, assets are served as separate files in the order they are
+specified in the manifest file.
This manifest `app/assets/javascripts/application.js`:
@@ -336,41 +578,54 @@ would generate this HTML:
The `body` param is required by Sprockets.
-### Turning Debugging off
+### Turning Debugging Off
-You can turn off debug mode by updating `config/environments/development.rb` to include:
+You can turn off debug mode by updating `config/environments/development.rb` to
+include:
```ruby
config.assets.debug = false
```
-When debug mode is off, Sprockets concatenates and runs the necessary preprocessors on all files. With debug mode turned off the manifest above would generate instead:
+When debug mode is off, Sprockets concatenates and runs the necessary
+preprocessors on all files. With debug mode turned off the manifest above would
+generate instead:
```html
<script src="/assets/application.js"></script>
```
-Assets are compiled and cached on the first request after the server is started. Sprockets sets a `must-revalidate` Cache-Control HTTP header to reduce request overhead on subsequent requests -- on these the browser gets a 304 (Not Modified) response.
+Assets are compiled and cached on the first request after the server is started.
+Sprockets sets a `must-revalidate` Cache-Control HTTP header to reduce request
+overhead on subsequent requests - on these the browser gets a 304 (Not Modified)
+response.
-If any of the files in the manifest have changed between requests, the server responds with a new compiled file.
+If any of the files in the manifest have changed between requests, the server
+responds with a new compiled file.
-Debug mode can also be enabled in the Rails helper methods:
+Debug mode can also be enabled in Rails helper methods:
```erb
-<%= stylesheet_link_tag "application", :debug => true %>
-<%= javascript_include_tag "application", :debug => true %>
+<%= stylesheet_link_tag "application", debug: true %>
+<%= javascript_include_tag "application", debug: true %>
```
-The `:debug` option is redundant if debug mode is on.
+The `:debug` option is redundant if debug mode is already on.
-You could potentially also enable compression in development mode as a sanity check, and disable it on-demand as required for debugging.
+You can also enable compression in development mode as a sanity check, and
+disable it on-demand as required for debugging.
In Production
-------------
-In the production environment Rails uses the fingerprinting scheme outlined above. By default Rails assumes that assets have been precompiled and will be served as static assets by your web server.
+In the production environment Sprockets uses the fingerprinting scheme outlined
+above. By default Rails assumes assets have been precompiled and will be
+served as static assets by your web server.
-During the precompilation phase an MD5 is generated from the contents of the compiled files, and inserted into the filenames as they are written to disc. These fingerprinted names are used by the Rails helpers in place of the manifest name.
+During the precompilation phase an MD5 is generated from the contents of the
+compiled files, and inserted into the filenames as they are written to disc.
+These fingerprinted names are used by the Rails helpers in place of the manifest
+name.
For example this:
@@ -383,97 +638,144 @@ generates something like this:
```html
<script src="/assets/application-908e25f4bf641868d8683022a5b62f54.js"></script>
-<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen" rel="stylesheet" />
+<link href="/assets/application-4dd5b109ee3439da54f5bdfd78a80473.css" media="screen"
+rel="stylesheet" />
```
-The fingerprinting behavior is controlled by the setting of `config.assets.digest` setting in Rails (which defaults to `true` for production and `false` for everything else).
+Note: with the Asset Pipeline the :cache and :concat options aren't used
+anymore, delete these options from the `javascript_include_tag` and
+`stylesheet_link_tag`.
-NOTE: Under normal circumstances the default option should not be changed. If there are no digests in the filenames, and far-future headers are set, remote clients will never know to refetch the files when their content changes.
+The fingerprinting behavior is controlled by the `config.assets.digest`
+initialization option (which defaults to `true` for production and `false` for
+everything else).
+
+NOTE: Under normal circumstances the default `config.assets.digest` option
+should not be changed. If there are no digests in the filenames, and far-future
+headers are set, remote clients will never know to refetch the files when their
+content changes.
### Precompiling Assets
-Rails comes bundled with a rake task to compile the asset manifests and other files in the pipeline to the disk.
+Rails comes bundled with a rake task to compile the asset manifests and other
+files in the pipeline.
-Compiled assets are written to the location specified in `config.assets.prefix`. By default, this is the `public/assets` directory.
+Compiled assets are written to the location specified in `config.assets.prefix`.
+By default, this is the `/assets` directory.
-You can call this task on the server during deployment to create compiled versions of your assets directly on the server. See the next section for information on compiling locally.
+You can call this task on the server during deployment to create compiled
+versions of your assets directly on the server. See the next section for
+information on compiling locally.
The rake task is:
```bash
-$ bundle exec rake assets:precompile
+$ RAILS_ENV=production bundle exec rake assets:precompile
```
-For faster asset precompiles, you can partially load your application by setting
-`config.assets.initialize_on_precompile` to false in `config/application.rb`, though in that case templates
-cannot see application objects or methods. **Heroku requires this to be false.**
-
-WARNING: If you set `config.assets.initialize_on_precompile` to false, be sure to
-test `rake assets:precompile` locally before deploying. It may expose bugs where
-your assets reference application objects or methods, since those are still
-in scope in development mode regardless of the value of this flag. Changing this flag also affects
-engines. Engines can define assets for precompilation as well. Since the complete environment is not loaded,
-engines (or other gems) will not be loaded, which can cause missing assets.
-
-Capistrano (v2.8.0 and above) includes a recipe to handle this in deployment. Add the following line to `Capfile`:
+Capistrano (v2.15.1 and above) includes a recipe to handle this in deployment.
+Add the following line to `Capfile`:
```ruby
load 'deploy/assets'
```
-This links the folder specified in `config.assets.prefix` to `shared/assets`. If you already use this shared folder you'll need to write your own deployment task.
+This links the folder specified in `config.assets.prefix` to `shared/assets`.
+If you already use this shared folder you'll need to write your own deployment
+task.
-It is important that this folder is shared between deployments so that remotely cached pages that reference the old compiled assets still work for the life of the cached page.
+It is important that this folder is shared between deployments so that remotely
+cached pages referencing the old compiled assets still work for the life of
+the cached page.
-NOTE. If you are precompiling your assets locally, you can use `bundle install --without assets` on the server to avoid installing the assets gems (the gems in the assets group in the Gemfile).
-
-The default matcher for compiling files includes `application.js`, `application.css` and all non-JS/CSS files (this will include all image assets automatically):
+The default matcher for compiling files includes `application.js`,
+`application.css` and all non-JS/CSS files (this will include all image assets
+automatically) from `app/assets` folders including your gems:
```ruby
-[ Proc.new{ |path| !%w(.js .css).include?(File.extname(path)) }, /application.(css|js)$/ ]
+[ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
+/application.(css|js)$/ ]
```
-NOTE. The matcher (and other members of the precompile array; see below) is applied to final compiled file names. This means that anything that compiles to JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and `.scss` files are **not** automatically included as they compile to JS/CSS.
+NOTE: The matcher (and other members of the precompile array; see below) is
+applied to final compiled file names. This means anything that compiles to
+JS/CSS is excluded, as well as raw JS/CSS files; for example, `.coffee` and
+`.scss` files are **not** automatically included as they compile to JS/CSS.
-If you have other manifests or individual stylesheets and JavaScript files to include, you can add them to the `precompile` array:
+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`:
```ruby
config.assets.precompile += ['admin.js', 'admin.css', 'swfObject.js']
```
-NOTE. Always specify an expected compiled filename that ends with js or css, even if you want to add Sass or CoffeeScript files to the precompile array.
+Or, you can opt to precompile all assets with something like this:
-The rake task also generates a `manifest.yml` that contains a list with all your assets and their respective fingerprints. This is used by the Rails helper methods to avoid handing the mapping requests back to Sprockets. A typical manifest file looks like:
+```ruby
+# config/application.rb
+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
+ if full_path.starts_with? app_assets_path
+ puts "including asset: " + full_path
+ true
+ else
+ puts "excluding asset: " + full_path
+ false
+ end
+ else
+ false
+ end
+end
+```
+
+NOTE. Always specify an expected compiled filename that ends with .js or .css,
+even if you want to add Sass or CoffeeScript files to the precompile array.
-```yaml
----
-rails.png: rails-bd9ad5a560b5a3a7be0808c5cd76a798.png
-jquery-ui.min.js: jquery-ui-7e33882a28fc84ad0e0e47e46cbf901c.min.js
-jquery.min.js: jquery-8a50feed8d29566738ad005e19fe1c2d.min.js
-application.js: application-3fdab497b8fb70d20cfc5495239dfc29.js
-application.css: application-8af74128f904600e41a6e39241464e03.css
+The rake task also generates a `manifest-md5hash.json` that contains a list with
+all your assets and their respective fingerprints. This is used by the Rails
+helper methods to avoid handing the mapping requests back to Sprockets. A
+typical manifest file looks like:
+
+```ruby
+{"files":{"application-723d1be6cc741a3aabb1cec24276d681.js":{"logical_path":"application.js","mtime":"2013-07-26T22:55:03-07:00","size":302506,
+"digest":"723d1be6cc741a3aabb1cec24276d681"},"application-12b3c7dd74d2e9df37e7cbb1efa76a6d.css":{"logical_path":"application.css","mtime":"2013-07-26T22:54:54-07:00","size":1560,
+"digest":"12b3c7dd74d2e9df37e7cbb1efa76a6d"},"application-1c5752789588ac18d7e1a50b1f0fd4c2.css":{"logical_path":"application.css","mtime":"2013-07-26T22:56:17-07:00","size":1591,
+"digest":"1c5752789588ac18d7e1a50b1f0fd4c2"},"favicon-a9c641bf2b81f0476e876f7c5e375969.ico":{"logical_path":"favicon.ico","mtime":"2013-07-26T23:00:10-07:00","size":1406,
+"digest":"a9c641bf2b81f0476e876f7c5e375969"},"my_image-231a680f23887d9dd70710ea5efd3c62.png":{"logical_path":"my_image.png","mtime":"2013-07-26T23:00:27-07:00","size":6646,
+"digest":"231a680f23887d9dd70710ea5efd3c62"}},"assets"{"application.js":
+"application-723d1be6cc741a3aabb1cec24276d681.js","application.css":
+"application-1c5752789588ac18d7e1a50b1f0fd4c2.css",
+"favicon.ico":"favicona9c641bf2b81f0476e876f7c5e375969.ico","my_image.png":
+"my_image-231a680f23887d9dd70710ea5efd3c62.png"}}
```
-The default location for the manifest is the root of the location specified in `config.assets.prefix` ('/assets' by default).
+The default location for the manifest is the root of the location specified in
+`config.assets.prefix` ('/assets' by default).
-NOTE: If there are missing precompiled files in production you will get an `Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError` exception indicating the name of the missing file(s).
+NOTE: If there are missing precompiled files in production you will get an
+`Sprockets::Helpers::RailsHelper::AssetPaths::AssetNotPrecompiledError`
+exception indicating the name of the missing file(s).
-#### Far-future Expires header
+#### Far-future Expires Header
-Precompiled assets exist on the filesystem and are served directly by your web server. They do not have far-future headers by default, so to get the benefit of fingerprinting you'll have to update your server configuration to add them.
+Precompiled assets exist on the filesystem and are served directly by your web
+server. They do not have far-future headers by default, so to get the benefit of
+fingerprinting you'll have to update your server configuration to add those
+headers.
For Apache:
```apache
-# The Expires* directives requires the Apache module `mod_expires` to be enabled.
-<LocationMatch "^/assets/.*$">
+# The Expires* directives requires the Apache module
+# `mod_expires` to be enabled.
+<Location /assets/>
# Use of ETag is discouraged when Last-Modified is present
- Header unset ETag
- FileETag None
+ Header unset ETag FileETag None
# RFC says only cache for 1 year
- ExpiresActive On
- ExpiresDefault "access plus 1 year"
-</LocationMatch>
+ ExpiresActive On ExpiresDefault "access plus 1 year"
+</Location>
```
For nginx:
@@ -488,9 +790,15 @@ location ~ ^/assets/ {
}
```
-#### GZip compression
+#### GZip Compression
-When files are precompiled, Sprockets also creates a [gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web servers are typically configured to use a moderate compression ratio as a compromise, but since precompilation happens once, Sprockets uses the maximum compression ratio, thus reducing the size of the data transfer to the minimum. On the other hand, web servers can be configured to serve compressed content directly from disk, rather than deflating non-compressed files themselves.
+When files are precompiled, Sprockets also creates a
+[gzipped](http://en.wikipedia.org/wiki/Gzip) (.gz) version of your assets. Web
+servers are typically configured to use a moderate compression ratio as a
+compromise, but since precompilation happens once, Sprockets uses the maximum
+compression ratio, thus reducing the size of the data transfer to the minimum.
+On the other hand, web servers can be configured to serve compressed content
+directly from disk, rather than deflating non-compressed files themselves.
Nginx is able to do this automatically enabling `gzip_static`:
@@ -503,25 +811,32 @@ location ~ ^/(assets)/ {
}
```
-This directive is available if the core module that provides this feature was compiled with the web server. Ubuntu packages, even `nginx-light` have the module compiled. Otherwise, you may need to perform a manual compilation:
+This directive is available if the core module that provides this feature was
+compiled with the web server. Ubuntu/Debian packages, even `nginx-light`, have
+the module compiled. Otherwise, you may need to perform a manual compilation:
```bash
./configure --with-http_gzip_static_module
```
-If you're compiling nginx with Phusion Passenger you'll need to pass that option when prompted.
+If you're compiling nginx with Phusion Passenger you'll need to pass that option
+when prompted.
-A robust configuration for Apache is possible but tricky; please Google around. (Or help update this Guide if you have a good example configuration for Apache.)
+A robust configuration for Apache is possible but tricky; please Google around.
+(Or help update this Guide if you have a good configuration example for Apache.)
### Local Precompilation
-There are several reasons why you might want to precompile your assets locally. Among them are:
+There are several reasons why you might want to precompile your assets locally.
+Among them are:
* You may not have write access to your production file system.
-* You may be deploying to more than one server, and want to avoid the duplication of work.
+* You may be deploying to more than one server, and want to avoid
+duplication of work.
* You may be doing frequent deploys that do not include asset changes.
-Local compilation allows you to commit the compiled files into source control, and deploy as normal.
+Local compilation allows you to commit the compiled files into source control,
+and deploy as normal.
There are two caveats:
@@ -534,23 +849,23 @@ In `config/environments/development.rb`, place the following line:
config.assets.prefix = "/dev-assets"
```
-You will also need this in application.rb:
+The `prefix` change makes Sprockets use a different URL for serving assets in
+development mode, and pass all requests to Sprockets. The prefix is still set to
+`/assets` in the production environment. Without this change, the application
+would serve the precompiled assets from `/assets` in development, and you would
+not see any local changes until you compile assets again.
-```ruby
-config.assets.initialize_on_precompile = false
-```
-
-The `prefix` change makes Rails use a different URL for serving assets in development mode, and pass all requests to Sprockets. The prefix is still set to `/assets` in the production environment. Without this change, the application would serve the precompiled assets from `public/assets` in development, and you would not see any local changes until you compile assets again.
-
-The `initialize_on_precompile` change tells the precompile task to run without invoking Rails. This is because the precompile task runs in production mode by default, and will attempt to connect to your specified production database. Please note that you cannot have code in pipeline files that relies on Rails resources (such as the database) when compiling locally with this option.
-
-You will also need to ensure that any compressors or minifiers are available on your development system.
+You will also need to ensure any necessary compressors or minifiers are
+available on your development system.
-In practice, this will allow you to precompile locally, have those files in your working tree, and commit those files to source control when needed. Development mode will work as expected.
+In practice, this will allow you to precompile locally, have those files in your
+working tree, and commit those files to source control when needed. Development
+mode will work as expected.
### Live Compilation
-In some circumstances you may wish to use live compilation. In this mode all requests for assets in the pipeline are handled by Sprockets directly.
+In some circumstances you may wish to use live compilation. In this mode all
+requests for assets in the pipeline are handled by Sprockets directly.
To enable this option set:
@@ -558,13 +873,21 @@ To enable this option set:
config.assets.compile = true
```
-On the first request the assets are compiled and cached as outlined in development above, and the manifest names used in the helpers are altered to include the MD5 hash.
+On the first request the assets are compiled and cached as outlined in
+development above, and the manifest names used in the helpers are altered to
+include the MD5 hash.
-Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This signals all caches between your server and the client browser that this content (the file served) can be cached for 1 year. The effect of this is to reduce the number of requests for this asset from your server; the asset has a good chance of being in the local browser cache or some intermediate cache.
+Sprockets also sets the `Cache-Control` HTTP header to `max-age=31536000`. This
+signals all caches between your server and the client browser that this content
+(the file served) can be cached for 1 year. The effect of this is to reduce the
+number of requests for this asset from your server; the asset has a good chance
+of being in the local browser cache or some intermediate cache.
-This mode uses more memory, performs more poorly than the default and is not recommended.
+This mode uses more memory, performs more poorly than the default and is not
+recommended.
-If you are deploying a production application to a system without any pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
+If you are deploying a production application to a system without any
+pre-existing JavaScript runtimes, you may want to add one to your Gemfile:
```ruby
group :production do
@@ -572,26 +895,45 @@ group :production do
end
```
+### CDNs
+
+If your assets are being served by a CDN, ensure they don't stick around in your
+cache forever. This can cause problems. If you use
+`config.action_controller.perform_caching = true`, Rack::Cache will use
+`Rails.cache` to store assets. This can cause your cache to fill up quickly.
+
+Every cache is different, so evaluate how your CDN handles caching and make sure
+that it plays nicely with the pipeline. You may find quirks related to your
+specific set up, you may not. The defaults nginx uses, for example, should give
+you no problems when used as an HTTP cache.
+
Customizing the Pipeline
------------------------
### CSS Compression
-There is currently one option for compressing CSS, YUI. The [YUI CSS compressor](http://developer.yahoo.com/yui/compressor/css.html) provides minification.
+There is currently one option for compressing CSS, YUI. The [YUI CSS
+compressor](http://yui.github.io/yuicompressor/css.html) provides
+minification.
-The following line enables YUI compression, and requires the `yui-compressor` gem.
+The following line enables YUI compression, and requires the `yui-compressor`
+gem.
```ruby
config.assets.css_compressor = :yui
```
-The `config.assets.compress` must be set to `true` to enable CSS compression.
-
### JavaScript Compression
-Possible options for JavaScript compression are `:closure`, `:uglifier` and `:yui`. These require the use of the `closure-compiler`, `uglifier` or `yui-compressor` gems, respectively.
+Possible options for JavaScript compression are `:closure`, `:uglifier` and
+`:yui`. These require the use of the `closure-compiler`, `uglifier` or
+`yui-compressor` gems, respectively.
-The default Gemfile includes [uglifier](https://github.com/lautis/uglifier). This gem wraps [UglifierJS](https://github.com/mishoo/UglifyJS) (written for NodeJS) in Ruby. It compresses your code by removing white space. It also includes other optimizations such as changing your `if` and `else` statements to ternary operators where possible.
+The default Gemfile includes [uglifier](https://github.com/lautis/uglifier).
+This gem wraps [UglifyJS](https://github.com/mishoo/UglifyJS) (written for
+NodeJS) in Ruby. It compresses your code by removing white space and comments,
+shortening local variable names, and performing other micro-optimizations such
+as changing `if` and `else` statements to ternary operators where possible.
The following line invokes `uglifier` for JavaScript compression.
@@ -599,13 +941,21 @@ The following line invokes `uglifier` for JavaScript compression.
config.assets.js_compressor = :uglifier
```
-Note that `config.assets.compress` must be set to `true` to enable JavaScript compression
+NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme)
+supported runtime in order to use `uglifier`. If you are using Mac OS X or
+Windows you have a JavaScript runtime installed in your operating system.
-NOTE: You will need an [ExecJS](https://github.com/sstephenson/execjs#readme) supported runtime in order to use `uglifier`. If you are using Mac OS X or Windows you have a JavaScript runtime installed in your operating system. Check the [ExecJS](https://github.com/sstephenson/execjs#readme) documentation for information on all of the supported JavaScript runtimes.
+NOTE: The `config.assets.compress` initialization option is no longer used in
+Rails 4 to enable either CSS or JavaScript compression. Setting it will have no
+effect on the application. Instead, setting `config.assets.css_compressor` and
+`config.assets.js_compressor` will control compression of CSS and JavaScript
+assets.
### Using Your Own Compressor
-The compressor config settings for CSS and JavaScript also take any object. This object must have a `compress` method that takes a string as the sole argument and it must return a string.
+The compressor config settings for CSS and JavaScript also take any object.
+This object must have a `compress` method that takes a string as the sole
+argument and it must return a string.
```ruby
class Transformer
@@ -615,7 +965,7 @@ class Transformer
end
```
-To enable this, pass a `new` object to the config option in `application.rb`:
+To enable this, pass a new object to the config option in `application.rb`:
```ruby
config.assets.css_compressor = Transformer.new
@@ -632,34 +982,47 @@ This can be changed to something else:
config.assets.prefix = "/some_other_path"
```
-This is a handy option if you are updating an existing project (pre Rails 3.1) that already uses this path or you wish to use this path for a new resource.
+This is a handy option if you are updating an older project that didn't use the
+asset pipeline and already uses this path or you wish to use this path for
+a new resource.
### X-Sendfile Headers
-The X-Sendfile header is a directive to the web server to ignore the response from the application, and instead serve a specified file from disk. This option is off by default, but can be enabled if your server supports it. When enabled, this passes responsibility for serving the file to the web server, which is faster.
+The X-Sendfile header is a directive to the web server to ignore the response
+from the application, and instead serve a specified file from disk. This option
+is off by default, but can be enabled if your server supports it. When enabled,
+this passes responsibility for serving the file to the web server, which is
+faster.
-Apache and nginx support this option, which can be enabled in `config/environments/production.rb`.
+Apache and nginx support this option, which can be enabled in
+`config/environments/production.rb`:
```ruby
# config.action_dispatch.x_sendfile_header = "X-Sendfile" # for apache
# config.action_dispatch.x_sendfile_header = 'X-Accel-Redirect' # for nginx
```
-WARNING: If you are upgrading an existing application and intend to use this option, take care to paste this configuration option only into `production.rb` and any other environments you define with production behavior (not `application.rb`).
+WARNING: If you are upgrading an existing application and intend to use this
+option, take care to paste this configuration option only into `production.rb`
+and any other environments you define with production behavior (not
+`application.rb`).
Assets Cache Store
------------------
-The default Rails cache store will be used by Sprockets to cache assets in development and production. This can be changed by setting `config.assets.cache_store`.
+The default Rails cache store will be used by Sprockets to cache assets in
+development and production. This can be changed by setting
+`config.assets.cache_store`:
```ruby
config.assets.cache_store = :memory_store
```
-The options accepted by the assets cache store are the same as the application's cache store.
+The options accepted by the assets cache store are the same as the application's
+cache store.
```ruby
-config.assets.cache_store = :memory_store, { :size => 32.megabytes }
+config.assets.cache_store = :memory_store, { size: 32.megabytes }
```
Adding Assets to Your Gems
@@ -667,41 +1030,74 @@ Adding Assets to Your Gems
Assets can also come from external sources in the form of gems.
-A good example of this is the `jquery-rails` gem which comes with Rails as the standard JavaScript library gem. This gem contains an engine class which inherits from `Rails::Engine`. By doing this, Rails is informed that the directory for this gem may contain assets and the `app/assets`, `lib/assets` and `vendor/assets` directories of this engine are added to the search path of Sprockets.
+A good example of this is the `jquery-rails` gem which comes with Rails as the
+standard JavaScript library gem. This gem contains an engine class which
+inherits from `Rails::Engine`. By doing this, Rails is informed that the
+directory for this gem may contain assets and the `app/assets`, `lib/assets` and
+`vendor/assets` directories of this engine are added to the search path of
+Sprockets.
Making Your Library or Gem a Pre-Processor
------------------------------------------
-TODO: Registering gems on [Tilt](https://github.com/rtomayko/tilt) enabling Sprockets to find them.
+As Sprockets uses [Tilt](https://github.com/rtomayko/tilt) as a generic
+interface to different templating engines, your gem should just implement the
+Tilt template protocol. Normally, you would subclass `Tilt::Template` and
+reimplement the `prepare` method, which initializes your template, and the
+`evaluate` method, which returns the processed source. The original source is
+stored in `data`. Have a look at
+[`Tilt::Template`](https://github.com/rtomayko/tilt/blob/master/lib/tilt/template.rb)
+sources to learn more.
+
+```ruby
+module BangBang
+ class Template < ::Tilt::Template
+ def prepare
+ # Do any initialization here
+ end
+
+ # Adds a "!" to original template.
+ def evaluate(scope, locals, &block)
+ "#{data}!"
+ end
+ end
+end
+```
+
+Now that you have a `Template` class, it's time to associate it with an
+extension for template files:
+
+```ruby
+Sprockets.register_engine '.bang', BangBang::Template
+```
Upgrading from Old Versions of Rails
------------------------------------
-There are a few issues when upgrading. The first is moving the files from `public/` to the new locations. See [Asset Organization](#asset-organization) above for guidance on the correct locations for different file types.
+There are a few issues when upgrading from Rails 3.0 or Rails 2.x. The first is
+moving the files from `public/` to the new locations. See [Asset
+Organization](#asset-organization) above for guidance on the correct locations
+for different file types.
-Next will be avoiding duplicate JavaScript files. Since jQuery is the default JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js` into `app/assets` and it will be included automatically.
+Next will be avoiding duplicate JavaScript files. Since jQuery is the default
+JavaScript library from Rails 3.1 onwards, you don't need to copy `jquery.js`
+into `app/assets` and it will be included automatically.
-The third is updating the various environment files with the correct default options. The following changes reflect the defaults in version 3.1.0.
+The third is updating the various environment files with the correct default
+options.
In `application.rb`:
```ruby
-# Enable the asset pipeline
-config.assets.enabled = true
-
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
-# Change the path that assets are served from
-# config.assets.prefix = "/assets"
+# Change the path that assets are served from config.assets.prefix = "/assets"
```
In `development.rb`:
```ruby
-# Do not compress assets
-config.assets.compress = false
-
# Expands the lines which load the assets
config.assets.debug = true
```
@@ -709,52 +1105,28 @@ config.assets.debug = true
And in `production.rb`:
```ruby
-# Compress JavaScripts and CSS
-config.assets.compress = true
-
-# Choose the compressors to use
-# config.assets.js_compressor = :uglifier
-# config.assets.css_compressor = :yui
+# Choose the compressors to use (if any) config.assets.js_compressor =
+# :uglifier config.assets.css_compressor = :yui
# Don't fallback to assets pipeline if a precompiled asset is missed
config.assets.compile = false
-# Generate digests for assets URLs.
+# Generate digests for assets URLs. This is planned for deprecation.
config.assets.digest = true
-# Precompile additional assets (application.js, application.css, and all non-JS/CSS are already added)
-# config.assets.precompile += %w( search.js )
+# Precompile additional assets (application.js, application.css, and all
+# non-JS/CSS are already added) config.assets.precompile += %w( search.js )
```
-You should not need to change `test.rb`. The defaults in the test environment are: `config.assets.compile` is true and `config.assets.compress`, `config.assets.debug` and `config.assets.digest` are false.
+Rails 4 no longer sets default config values for Sprockets in `test.rb`, so
+`test.rb` now requies Sprockets configuration. The old defaults in the test
+environment are: `config.assets.compile = true`, `config.assets.compress =
+false`, `config.assets.debug = false` and `config.assets.digest = false`.
The following should also be added to `Gemfile`:
```ruby
-# Gems used only for assets and not required
-# in production environments by default.
-group :assets do
- gem 'sass-rails', "~> 3.2.3"
- gem 'coffee-rails', "~> 3.2.1"
- gem 'uglifier'
-end
-```
-
-If you use the `assets` group with Bundler, please make sure that your `config/application.rb` has the following Bundler require statement:
-
-```ruby
-if defined?(Bundler)
- # If you precompile assets before deploying to production, use this line
- Bundler.require *Rails.groups(:assets => %w(development test))
- # If you want your assets lazily compiled in production, use this line
- # Bundler.require(:default, :assets, Rails.env)
-end
-```
-
-Instead of the old Rails 3.0 version:
-
-```ruby
-# If you have a Gemfile, require the gems listed there, including any gems
-# you've limited to :test, :development, or :production.
-Bundler.require(:default, Rails.env) if defined?(Bundler)
+gem 'sass-rails', "~> 3.2.3"
+gem 'coffee-rails', "~> 3.2.1"
+gem 'uglifier'
```
diff --git a/guides/source/association_basics.md b/guides/source/association_basics.md
index 42c7c07745..c0482f6106 100644
--- a/guides/source/association_basics.md
+++ b/guides/source/association_basics.md
@@ -1,11 +1,13 @@
-A Guide to Active Record Associations
-=====================================
+Active Record Associations
+==========================
-This guide covers the association features of Active Record. By referring to this guide, you will be able to:
+This guide covers the association features of Active Record.
-* Declare associations between Active Record models
-* Understand the various types of Active Record associations
-* Use the methods added to your models by creating associations
+After reading this guide, you will know:
+
+* How to declare associations between Active Record models.
+* How to understand the various types of Active Record associations.
+* How to use the methods added to your models by creating associations.
--------------------------------------------------------------------------------
@@ -25,25 +27,24 @@ end
Now, suppose we wanted to add a new order for an existing customer. We'd need to do something like this:
```ruby
-@order = Order.create(:order_date => Time.now,
- :customer_id => @customer.id)
+@order = Order.create(order_date: Time.now, customer_id: @customer.id)
```
Or consider deleting a customer, and ensuring that all of its orders get deleted as well:
```ruby
-@orders = Order.where(:customer_id => @customer.id)
+@orders = Order.where(customer_id: @customer.id)
@orders.each do |order|
order.destroy
end
@customer.destroy
```
-With Active Record associations, we can streamline these -- and other -- operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
+With Active Record associations, we can streamline these - and other - operations by declaratively telling Rails that there is a connection between the two models. Here's the revised code for setting up customers and orders:
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :dependent => :destroy
+ has_many :orders, dependent: :destroy
end
class Order < ActiveRecord::Base
@@ -54,10 +55,10 @@ end
With this change, creating a new order for a particular customer is easier:
```ruby
-@order = @customer.orders.create(:order_date => Time.now)
+@order = @customer.orders.create(order_date: Time.now)
```
-Deleting a customer and all of its orders is _much_ easier:
+Deleting a customer and all of its orders is *much* easier:
```ruby
@customer.destroy
@@ -68,7 +69,7 @@ To learn more about the different types of associations, read the next section o
The Types of Associations
-------------------------
-In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key–Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
+In Rails, an _association_ is a connection between two Active Record models. Associations are implemented using macro-style calls, so that you can declaratively add features to your models. For example, by declaring that one model `belongs_to` another, you instruct Rails to maintain Primary Key-Foreign Key information between instances of the two models, and you also get a number of utility methods added to your model. Rails supports six types of associations:
* `belongs_to`
* `has_one`
@@ -93,6 +94,25 @@ end
NOTE: `belongs_to` associations _must_ use the singular term. If you used the pluralized form in the above example for the `customer` association in the `Order` model, you would be told that there was an "uninitialized constant Order::Customers". This is because Rails automatically infers the class name from the association name. If the association name is wrongly pluralized, then the inferred class will be wrongly pluralized too.
+The corresponding migration might look like this:
+
+```ruby
+class CreateOrders < ActiveRecord::Migration
+ def change
+ create_table :customers do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :orders do |t|
+ t.belongs_to :customer
+ t.datetime :order_date
+ t.timestamps
+ end
+ end
+end
+```
+
### The `has_one` Association
A `has_one` association also sets up a one-to-one connection with another model, but with somewhat different semantics (and consequences). This association indicates that each instance of a model contains or possesses one instance of another model. For example, if each supplier in your application has only one account, you'd declare the supplier model like this:
@@ -105,6 +125,25 @@ end
![has_one Association Diagram](images/has_one.png)
+The corresponding migration might look like this:
+
+```ruby
+class CreateSuppliers < ActiveRecord::Migration
+ def change
+ create_table :suppliers do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :accounts do |t|
+ t.belongs_to :supplier
+ t.string :account_number
+ t.timestamps
+ end
+ end
+end
+```
+
### The `has_many` Association
A `has_many` association indicates a one-to-many connection with another model. You'll often find this association on the "other side" of a `belongs_to` association. This association indicates that each instance of the model has zero or more instances of another model. For example, in an application containing customers and orders, the customer model could be declared like this:
@@ -119,6 +158,25 @@ NOTE: The name of the other model is pluralized when declaring a `has_many` asso
![has_many Association Diagram](images/has_many.png)
+The corresponding migration might look like this:
+
+```ruby
+class CreateCustomers < ActiveRecord::Migration
+ def change
+ create_table :customers do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :orders do |t|
+ t.belongs_to :customer
+ t.datetime :order_date
+ t.timestamps
+ end
+ end
+end
+```
+
### The `has_many :through` Association
A `has_many :through` association is often used to set up a many-to-many connection with another model. This association indicates that the declaring model can be matched with zero or more instances of another model by proceeding _through_ a third model. For example, consider a medical practice where patients make appointments to see physicians. The relevant association declarations could look like this:
@@ -126,7 +184,7 @@ A `has_many :through` association is often used to set up a many-to-many connect
```ruby
class Physician < ActiveRecord::Base
has_many :appointments
- has_many :patients, :through => :appointments
+ has_many :patients, through: :appointments
end
class Appointment < ActiveRecord::Base
@@ -136,12 +194,37 @@ end
class Patient < ActiveRecord::Base
has_many :appointments
- has_many :physicians, :through => :appointments
+ has_many :physicians, through: :appointments
end
```
![has_many :through Association Diagram](images/has_many_through.png)
+The corresponding migration might look like this:
+
+```ruby
+class CreateAppointments < ActiveRecord::Migration
+ def change
+ create_table :physicians do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :patients do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :appointments do |t|
+ t.belongs_to :physician
+ t.belongs_to :patient
+ t.datetime :appointment_date
+ t.timestamps
+ end
+ end
+end
+```
+
The collection of join models can be managed via the API. For example, if you assign
```ruby
@@ -157,7 +240,7 @@ The `has_many :through` association is also useful for setting up "shortcuts" th
```ruby
class Document < ActiveRecord::Base
has_many :sections
- has_many :paragraphs, :through => :sections
+ has_many :paragraphs, through: :sections
end
class Section < ActiveRecord::Base
@@ -170,7 +253,7 @@ class Paragraph < ActiveRecord::Base
end
```
-With `:through => :sections` specified, Rails will now understand:
+With `through: :sections` specified, Rails will now understand:
```ruby
@document.paragraphs
@@ -178,12 +261,15 @@ With `:through => :sections` specified, Rails will now understand:
### The `has_one :through` Association
-A `has_one :through` association sets up a one-to-one connection with another model. This association indicates that the declaring model can be matched with one instance of another model by proceeding _through_ a third model. For example, if each supplier has one account, and each account is associated with one account history, then the customer model could look like this:
+A `has_one :through` association sets up a one-to-one connection with another model. This association indicates
+that the declaring model can be matched with one instance of another model by proceeding _through_ a third model.
+For example, if each supplier has one account, and each account is associated with one account history, then the
+supplier model could look like this:
```ruby
class Supplier < ActiveRecord::Base
has_one :account
- has_one :account_history, :through => :account
+ has_one :account_history, through: :account
end
class Account < ActiveRecord::Base
@@ -198,6 +284,31 @@ end
![has_one :through Association Diagram](images/has_one_through.png)
+The corresponding migration might look like this:
+
+```ruby
+class CreateAccountHistories < ActiveRecord::Migration
+ def change
+ create_table :suppliers do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :accounts do |t|
+ t.belongs_to :supplier
+ t.string :account_number
+ t.timestamps
+ end
+
+ create_table :account_histories do |t|
+ t.belongs_to :account
+ t.integer :credit_rating
+ t.timestamps
+ end
+ end
+end
+```
+
### The `has_and_belongs_to_many` Association
A `has_and_belongs_to_many` association creates a direct many-to-many connection with another model, with no intervening model. For example, if your application includes assemblies and parts, with each assembly having many parts and each part appearing in many assemblies, you could declare the models this way:
@@ -214,6 +325,29 @@ end
![has_and_belongs_to_many Association Diagram](images/habtm.png)
+The corresponding migration might look like this:
+
+```ruby
+class CreateAssembliesAndParts < ActiveRecord::Migration
+ def change
+ create_table :assemblies do |t|
+ t.string :name
+ t.timestamps
+ end
+
+ create_table :parts do |t|
+ t.string :part_number
+ t.timestamps
+ end
+
+ create_table :assemblies_parts, id: false do |t|
+ t.belongs_to :assembly
+ t.belongs_to :part
+ end
+ end
+end
+```
+
### Choosing Between `belongs_to` and `has_one`
If you want to set up a one-to-one relationship between two models, you'll need to add `belongs_to` to one, and `has_one` to the other. How do you know which is which?
@@ -270,7 +404,7 @@ The second way to declare a many-to-many relationship is to use `has_many :throu
```ruby
class Assembly < ActiveRecord::Base
has_many :manifests
- has_many :parts, :through => :manifests
+ has_many :parts, through: :manifests
end
class Manifest < ActiveRecord::Base
@@ -280,7 +414,7 @@ end
class Part < ActiveRecord::Base
has_many :manifests
- has_many :assemblies, :through => :manifests
+ has_many :assemblies, through: :manifests
end
```
@@ -294,15 +428,15 @@ A slightly more advanced twist on associations is the _polymorphic association_.
```ruby
class Picture < ActiveRecord::Base
- belongs_to :imageable, :polymorphic => true
+ belongs_to :imageable, polymorphic: true
end
class Employee < ActiveRecord::Base
- has_many :pictures, :as => :imageable
+ has_many :pictures, as: :imageable
end
class Product < ActiveRecord::Base
- has_many :pictures, :as => :imageable
+ has_many :pictures, as: :imageable
end
```
@@ -332,7 +466,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
t.timestamps
end
end
@@ -347,9 +481,10 @@ In designing a data model, you will sometimes find a model that should have a re
```ruby
class Employee < ActiveRecord::Base
- has_many :subordinates, :class_name => "Employee",
- :foreign_key => "manager_id"
- belongs_to :manager, :class_name => "Employee"
+ has_many :subordinates, class_name: "Employee",
+ foreign_key: "manager_id"
+
+ belongs_to :manager, class_name: "Employee"
end
```
@@ -440,9 +575,9 @@ end
These need to be backed up by a migration to create the `assemblies_parts` table. This table should be created without a primary key:
```ruby
-class CreateAssemblyPartJoinTable < ActiveRecord::Migration
+class CreateAssembliesPartsJoinTable < ActiveRecord::Migration
def change
- create_table :assemblies_parts, :id => false do |t|
+ create_table :assemblies_parts, id: false do |t|
t.integer :assembly_id
t.integer :part_id
end
@@ -450,7 +585,7 @@ class CreateAssemblyPartJoinTable < ActiveRecord::Migration
end
```
-We pass `:id => false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled models IDs, or exceptions about conflicting IDs chances are you forgot that bit.
+We pass `id: false` to `create_table` because that table does not represent a model. That's required for the association to work properly. If you observe any strange behavior in a `has_and_belongs_to_many` association like mangled models IDs, or exceptions about conflicting IDs, chances are you forgot that bit.
### Controlling Association Scope
@@ -495,14 +630,14 @@ module MyApplication
module Business
class Supplier < ActiveRecord::Base
has_one :account,
- :class_name => "MyApplication::Billing::Account"
+ class_name: "MyApplication::Billing::Account"
end
end
module Billing
class Account < ActiveRecord::Base
belongs_to :supplier,
- :class_name => "MyApplication::Business::Supplier"
+ class_name: "MyApplication::Business::Supplier"
end
end
end
@@ -536,11 +671,11 @@ This happens because c and o.customer are two different in-memory representation
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :inverse_of => :customer
+ has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
- belongs_to :customer, :inverse_of => :orders
+ belongs_to :customer, inverse_of: :orders
end
```
@@ -561,6 +696,17 @@ There are a few limitations to `inverse_of` support:
* They do not work with `:as` associations.
* For `belongs_to` associations, `has_many` inverse associations are ignored.
+Every association will attempt to automatically find the inverse association
+and set the `:inverse_of` option heuristically (based on the association name).
+Most associations with standard names will be supported. However, associations
+that contain the following options will not have their inverses set
+automatically:
+
+* :conditions
+* :through
+* :polymorphic
+* :foreign_key
+
Detailed Association Reference
------------------------------
@@ -572,12 +718,13 @@ The `belongs_to` association creates a one-to-one match with another model. In d
#### Methods Added by `belongs_to`
-When you declare a `belongs_to` association, the declaring class automatically gains four methods related to the association:
+When you declare a `belongs_to` association, the declaring class automatically gains five methods related to the association:
* `association(force_reload = false)`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
+* `create_association!(attributes = {})`
In all of these methods, `association` is replaced with the symbol passed as the first argument to `belongs_to`. For example, given the declaration:
@@ -594,6 +741,7 @@ customer
customer=
build_customer
create_customer
+create_customer!
```
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
@@ -621,8 +769,8 @@ The `association=` method assigns an associated object to this object. Behind th
The `build_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through this object's foreign key will be set, but the associated object will _not_ yet be saved.
```ruby
-@customer = @order.build_customer(:customer_number => 123,
- :customer_name => "John Doe")
+@customer = @order.build_customer(customer_number: 123,
+ customer_name: "John Doe")
```
##### `create_association(attributes = {})`
@@ -630,19 +778,23 @@ The `build_association` method returns a new object of the associated type. This
The `create_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through this object's foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@customer = @order.create_customer(:customer_number => 123,
- :customer_name => "John Doe")
+@customer = @order.create_customer(customer_number: 123,
+ customer_name: "John Doe")
```
+##### `create_association!(attributes = {})`
+
+Does the same as `create_association` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+
#### Options for `belongs_to`
-While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this assocation uses two such options:
+While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `belongs_to` association reference. Such customizations can easily be accomplished by passing options and scope blocks when you create the association. For example, this association uses two such options:
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :dependent => :destroy,
- :counter_cache => true
+ belongs_to :customer, dependent: :destroy,
+ counter_cache: true
end
```
@@ -668,7 +820,7 @@ If the name of the other model cannot be derived from the association name, you
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :class_name => "Patron"
+ belongs_to :customer, class_name: "Patron"
end
```
@@ -689,7 +841,7 @@ With these declarations, asking for the value of `@customer.orders.size` require
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :counter_cache => true
+ belongs_to :customer, counter_cache: true
end
class Customer < ActiveRecord::Base
has_many :orders
@@ -702,7 +854,7 @@ Although the `:counter_cache` option is specified on the model that includes the
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :counter_cache => :count_of_orders
+ belongs_to :customer, counter_cache: :count_of_orders
end
class Customer < ActiveRecord::Base
has_many :orders
@@ -713,7 +865,7 @@ Counter cache columns are added to the containing model's list of read-only attr
##### `:dependent`
-If you set the `:dependent` option to `:destroy`, then deleting this object will call the `destroy` method on the associated object to delete that object. If you set the `:dependent` option to `:delete`, then deleting this object will delete the associated object _without_ calling its `destroy` method.
+If you set the `:dependent` option to `:destroy`, then deleting this object will call the `destroy` method on the associated object to delete that object. If you set the `:dependent` option to `:delete`, then deleting this object will delete the associated object _without_ calling its `destroy` method. If you set the `:dependent` option to `:restrict`, then attempting to delete this object will result in a `ActiveRecord::DeleteRestrictionError` if there are any associated objects.
WARNING: You should not specify this option on a `belongs_to` association that is connected with a `has_many` association on the other class. Doing so can lead to orphaned records in your database.
@@ -723,8 +875,8 @@ By convention, Rails assumes that the column used to hold the foreign key on thi
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :class_name => "Patron",
- :foreign_key => "patron_id"
+ belongs_to :customer, class_name: "Patron",
+ foreign_key: "patron_id"
end
```
@@ -736,11 +888,11 @@ The `:inverse_of` option specifies the name of the `has_many` or `has_one` assoc
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :inverse_of => :customer
+ has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
- belongs_to :customer, :inverse_of => :orders
+ belongs_to :customer, inverse_of: :orders
end
```
@@ -754,7 +906,7 @@ If you set the `:touch` option to `:true`, then the `updated_at` or `updated_on`
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :touch => true
+ belongs_to :customer, touch: true
end
class Customer < ActiveRecord::Base
@@ -766,7 +918,7 @@ In this case, saving or destroying an order will update the timestamp on the ass
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, :touch => :orders_updated_at
+ belongs_to :customer, touch: :orders_updated_at
end
```
@@ -780,8 +932,8 @@ There may be times when you wish to customize the query used by `belongs_to`. Su
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, -> { where :active => true },
- :dependent => :destroy
+ belongs_to :customer, -> { where active: true },
+ dependent: :destroy
end
```
@@ -798,13 +950,13 @@ The `where` method lets you specify the conditions that the associated object mu
```ruby
class Order < ActiveRecord::Base
- belongs_to :customer, -> { where :active => true }
+ belongs_to :customer, -> { where active: true }
end
```
##### `includes`
-You can use the `includes` method let you specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
+You can use the `includes` method to specify second-order associations that should be eager-loaded when this association is used. For example, consider these models:
```ruby
class LineItem < ActiveRecord::Base
@@ -870,12 +1022,13 @@ The `has_one` association creates a one-to-one match with another model. In data
#### Methods Added by `has_one`
-When you declare a `has_one` association, the declaring class automatically gains four methods related to the association:
+When you declare a `has_one` association, the declaring class automatically gains five methods related to the association:
* `association(force_reload = false)`
* `association=(associate)`
* `build_association(attributes = {})`
* `create_association(attributes = {})`
+* `create_association!(attributes = {})`
In all of these methods, `association` is replaced with the symbol passed as the first argument to `has_one`. For example, given the declaration:
@@ -892,6 +1045,7 @@ account
account=
build_account
create_account
+create_account!
```
NOTE: When initializing a new `has_one` or `belongs_to` association you must use the `build_` prefix to build the association, rather than the `association.build` method that would be used for `has_many` or `has_and_belongs_to_many` associations. To create one, use the `create_` prefix.
@@ -919,7 +1073,7 @@ The `association=` method assigns an associated object to this object. Behind th
The `build_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through its foreign key will be set, but the associated object will _not_ yet be saved.
```ruby
-@account = @supplier.build_account(:terms => "Net 30")
+@account = @supplier.build_account(terms: "Net 30")
```
##### `create_association(attributes = {})`
@@ -927,16 +1081,20 @@ The `build_association` method returns a new object of the associated type. This
The `create_association` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be set, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@account = @supplier.create_account(:terms => "Net 30")
+@account = @supplier.create_account(terms: "Net 30")
```
+##### `create_association!(attributes = {})`
+
+Does the same as `create_association` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+
#### Options for `has_one`
-While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_one` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options:
+While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_one` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
class Supplier < ActiveRecord::Base
- has_one :account, :class_name => "Billing", :dependent => :nullify
+ has_one :account, class_name: "Billing", dependent: :nullify
end
```
@@ -968,7 +1126,7 @@ If the name of the other model cannot be derived from the association name, you
```ruby
class Supplier < ActiveRecord::Base
- has_one :account, :class_name => "Billing"
+ has_one :account, class_name: "Billing"
end
```
@@ -977,18 +1135,24 @@ end
Controls what happens to the associated object when its owner is destroyed:
* `:destroy` causes the associated object to also be destroyed
-* `:delete` causes the asssociated object to be deleted directly from the database (so callbacks will not execute)
+* `:delete` causes the associated object to be deleted directly from the database (so callbacks will not execute)
* `:nullify` causes the foreign key to be set to `NULL`. Callbacks are not executed.
* `:restrict_with_exception` causes an exception to be raised if there is an associated record
* `:restrict_with_error` causes an error to be added to the owner if there is an associated object
+It's necessary not to set or leave `:nullify` option for those associations
+that have `NOT NULL` database constraints. If you don't set `dependent` to
+destroy such associations you won't be able to change the associated object
+because initial associated object foreign key will be set to unallowed `NULL`
+value.
+
##### `:foreign_key`
By convention, Rails assumes that the column used to hold the foreign key on the other model is the name of this model with the suffix `_id` added. The `:foreign_key` option lets you set the name of the foreign key directly:
```ruby
class Supplier < ActiveRecord::Base
- has_one :account, :foreign_key => "supp_id"
+ has_one :account, foreign_key: "supp_id"
end
```
@@ -1000,11 +1164,11 @@ The `:inverse_of` option specifies the name of the `belongs_to` association that
```ruby
class Supplier < ActiveRecord::Base
- has_one :account, :inverse_of => :supplier
+ has_one :account, inverse_of: :supplier
end
class Account < ActiveRecord::Base
- belongs_to :supplier, :inverse_of => :account
+ belongs_to :supplier, inverse_of: :account
end
```
@@ -1034,7 +1198,7 @@ There may be times when you wish to customize the query used by `has_one`. Such
```ruby
class Supplier < ActiveRecord::Base
- has_one :account, -> { where :active => true }
+ has_one :account, -> { where active: true }
end
```
@@ -1125,7 +1289,7 @@ The `has_many` association creates a one-to-many relationship with another model
#### Methods Added by `has_many`
-When you declare a `has_many` association, the declaring class automatically gains 13 methods related to the association:
+When you declare a `has_many` association, the declaring class automatically gains 16 methods related to the association:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
@@ -1142,8 +1306,9 @@ When you declare a `has_many` association, the declaring class automatically gai
* `collection.exists?(...)`
* `collection.build(attributes = {}, ...)`
* `collection.create(attributes = {})`
+* `collection.create!(attributes = {})`
-In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol.. For example, given the declaration:
+In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration:
```ruby
class Customer < ActiveRecord::Base
@@ -1169,6 +1334,7 @@ orders.where(...)
orders.exists?(...)
orders.build(attributes = {}, ...)
orders.create(attributes = {})
+orders.create!(attributes = {})
```
##### `collection(force_reload = false)`
@@ -1195,7 +1361,7 @@ The `collection.delete` method removes one or more objects from the collection b
@customer.orders.delete(@order1)
```
-WARNING: Additionally, objects will be destroyed if they're associated with `:dependent => :destroy`, and deleted if they're associated with `:dependent => :delete_all`.
+WARNING: Additionally, objects will be destroyed if they're associated with `dependent: :destroy`, and deleted if they're associated with `dependent: :delete_all`.
##### `collection.destroy(object, ...)`
@@ -1225,7 +1391,7 @@ The `collection_singular_ids=` method makes the collection contain only the obje
##### `collection.clear`
-The `collection.clear` method removes every object from the collection. This destroys the associated objects if they are associated with `:dependent => :destroy`, deletes them directly from the database if `:dependent => :delete_all`, and otherwise sets their foreign keys to `NULL`.
+The `collection.clear` method removes every object from the collection. This destroys the associated objects if they are associated with `dependent: :destroy`, deletes them directly from the database if `dependent: :delete_all`, and otherwise sets their foreign keys to `NULL`.
##### `collection.empty?`
@@ -1258,7 +1424,7 @@ The `collection.find` method finds objects within the collection. It uses the sa
The `collection.where` method finds objects within the collection based on the conditions supplied but the objects are loaded lazily meaning that the database is queried only when the object(s) are accessed.
```ruby
-@open_orders = @customer.orders.where(:open => true) # No query yet
+@open_orders = @customer.orders.where(open: true) # No query yet
@open_order = @open_orders.first # Now the database will be queried
```
@@ -1271,8 +1437,8 @@ The `collection.exists?` method checks whether an object meeting the supplied co
The `collection.build` method returns one or more new objects of the associated type. These objects will be instantiated from the passed attributes, and the link through their foreign key will be created, but the associated objects will _not_ yet be saved.
```ruby
-@order = @customer.orders.build(:order_date => Time.now,
- :order_number => "A12345")
+@order = @customer.orders.build(order_date: Time.now,
+ order_number: "A12345")
```
##### `collection.create(attributes = {})`
@@ -1280,17 +1446,21 @@ The `collection.build` method returns one or more new objects of the associated
The `collection.create` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through its foreign key will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@order = @customer.orders.create(:order_date => Time.now,
- :order_number => "A12345")
+@order = @customer.orders.create(order_date: Time.now,
+ order_number: "A12345")
```
+##### `collection.create!(attributes = {})`
+
+Does the same as `collection.create` above, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+
#### Options for `has_many`
-While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options:
+While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :dependent => :delete_all, :validate => :false
+ has_many :orders, dependent: :delete_all, validate: :false
end
```
@@ -1322,7 +1492,7 @@ If the name of the other model cannot be derived from the association name, you
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :class_name => "Transaction"
+ has_many :orders, class_name: "Transaction"
end
```
@@ -1331,7 +1501,7 @@ end
Controls what happens to the associated objects when their owner is destroyed:
* `:destroy` causes all the associated objects to also be destroyed
-* `:delete_all` causes all the asssociated objects to be deleted directly from the database (so callbacks will not execute)
+* `:delete_all` causes all the associated objects to be deleted directly from the database (so callbacks will not execute)
* `:nullify` causes the foreign keys to be set to `NULL`. Callbacks are not executed.
* `:restrict_with_exception` causes an exception to be raised if there are any associated records
* `:restrict_with_error` causes an error to be added to the owner if there are any associated objects
@@ -1344,7 +1514,7 @@ By convention, Rails assumes that the column used to hold the foreign key on the
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :foreign_key => "cust_id"
+ has_many :orders, foreign_key: "cust_id"
end
```
@@ -1356,11 +1526,11 @@ The `:inverse_of` option specifies the name of the `belongs_to` association that
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :inverse_of => :customer
+ has_many :orders, inverse_of: :customer
end
class Order < ActiveRecord::Base
- belongs_to :customer, :inverse_of => :orders
+ belongs_to :customer, inverse_of: :orders
end
```
@@ -1368,6 +1538,20 @@ end
By convention, Rails assumes that the column used to hold the primary key of the association is `id`. You can override this and explicitly specify the primary key with the `:primary_key` option.
+Let's say that `users` table has `id` as the primary_key but it also has
+`guid` column. And the requirement is that `todos` table should hold
+`guid` column value and not `id` value. This can be achieved like this
+
+```ruby
+class User < ActiveRecord::Base
+ has_many :todos, primary_key: :guid
+end
+```
+
+Now if we execute `@user.todos.create` then `@todo` record will have
+`user_id` value as the `guid` value of `@user`.
+
+
##### `:source`
The `:source` option specifies the source association name for a `has_many :through` association. You only need to use this option if the name of the source association cannot be automatically inferred from the association name.
@@ -1390,7 +1574,7 @@ There may be times when you wish to customize the query used by `has_many`. Such
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, -> { where :processed => true }
+ has_many :orders, -> { where processed: true }
end
```
@@ -1414,7 +1598,7 @@ The `where` method lets you specify the conditions that the associated object mu
```ruby
class Customer < ActiveRecord::Base
has_many :confirmed_orders, -> { where "confirmed = 1" },
- :class_name => "Order"
+ class_name: "Order"
end
```
@@ -1422,8 +1606,8 @@ You can also set conditions via a hash:
```ruby
class Customer < ActiveRecord::Base
- has_many :confirmed_orders, -> { where :confirmed => true },
- :class_name => "Order"
+ has_many :confirmed_orders, -> { where confirmed: true },
+ class_name: "Order"
end
```
@@ -1440,7 +1624,7 @@ The `group` method supplies an attribute name to group the result set by, using
```ruby
class Customer < ActiveRecord::Base
has_many :line_items, -> { group 'orders.id' },
- :through => :orders
+ through: :orders
end
```
@@ -1488,7 +1672,7 @@ The `limit` method lets you restrict the total number of objects that will be fe
class Customer < ActiveRecord::Base
has_many :recent_orders,
-> { order('order_date desc').limit(100) },
- :class_name => "Order",
+ class_name: "Order",
end
```
@@ -1516,43 +1700,67 @@ The `select` method lets you override the SQL `SELECT` clause that is used to re
WARNING: If you specify your own `select`, be sure to include the primary key and foreign key columns of the associated model. If you do not, Rails will throw an error.
-##### `uniq`
+##### `distinct`
-Use the `uniq` method to keep the collection free of duplicates. This is mostly useful together with the `:through` option.
+Use the `distinct` method to keep the collection free of duplicates. This is
+mostly useful together with the `:through` option.
```ruby
class Person < ActiveRecord::Base
has_many :readings
- has_many :posts, :through => :readings
+ has_many :posts, through: :readings
end
-person = Person.create(:name => 'john')
-post = Post.create(:name => 'a1')
+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>]
```
-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.posts` brings out both of
+them even though these records are pointing to the same post.
-Now let's set `uniq`:
+Now let's set `distinct`:
```ruby
class Person
has_many :readings
- has_many :posts, -> { uniq }, :through => :readings
+ has_many :posts, -> { distinct }, through: :readings
end
-person = Person.create(:name => 'honda')
-post = Post.create(:name => 'a1')
+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>]
```
-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.posts` shows
+only one post 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
+add the following in a migration:
+
+```ruby
+add_index :person_posts, :post, 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
+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)
+```
#### When are Objects Saved?
@@ -1570,7 +1778,7 @@ The `has_and_belongs_to_many` association creates a many-to-many relationship wi
#### Methods Added by `has_and_belongs_to_many`
-When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 13 methods related to the association:
+When you declare a `has_and_belongs_to_many` association, the declaring class automatically gains 16 methods related to the association:
* `collection(force_reload = false)`
* `collection<<(object, ...)`
@@ -1587,6 +1795,7 @@ When you declare a `has_and_belongs_to_many` association, the declaring class au
* `collection.exists?(...)`
* `collection.build(attributes = {})`
* `collection.create(attributes = {})`
+* `collection.create!(attributes = {})`
In all of these methods, `collection` is replaced with the symbol passed as the first argument to `has_and_belongs_to_many`, and `collection_singular` is replaced with the singularized version of that symbol. For example, given the declaration:
@@ -1614,6 +1823,7 @@ assemblies.where(...)
assemblies.exists?(...)
assemblies.build(attributes = {}, ...)
assemblies.create(attributes = {})
+assemblies.create!(attributes = {})
```
##### Additional Column Methods
@@ -1722,8 +1932,7 @@ The `collection.exists?` method checks whether an object meeting the supplied co
The `collection.build` method returns a new object of the associated type. This object will be instantiated from the passed attributes, and the link through the join table will be created, but the associated object will _not_ yet be saved.
```ruby
-@assembly = @part.assemblies.build(
- {:assembly_name => "Transmission housing"})
+@assembly = @part.assemblies.build({assembly_name: "Transmission housing"})
```
##### `collection.create(attributes = {})`
@@ -1731,18 +1940,21 @@ The `collection.build` method returns a new object of the associated type. This
The `collection.create` method returns a new object of the associated type. This object will be instantiated from the passed attributes, the link through the join table will be created, and, once it passes all of the validations specified on the associated model, the associated object _will_ be saved.
```ruby
-@assembly = @part.assemblies.create(
- {:assembly_name => "Transmission housing"})
+@assembly = @part.assemblies.create({assembly_name: "Transmission housing"})
```
+##### `collection.create!(attributes = {})`
+
+Does the same as `collection.create`, but raises `ActiveRecord::RecordInvalid` if the record is invalid.
+
#### Options for `has_and_belongs_to_many`
-While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this assocation uses two such options:
+While Rails uses intelligent defaults that will work well in most situations, there may be times when you want to customize the behavior of the `has_and_belongs_to_many` association reference. Such customizations can easily be accomplished by passing options when you create the association. For example, this association uses two such options:
```ruby
class Parts < ActiveRecord::Base
- has_and_belongs_to_many :assemblies, :uniq => true,
- :read_only => true
+ has_and_belongs_to_many :assemblies, autosave: true,
+ readonly: true
end
```
@@ -1754,6 +1966,7 @@ The `has_and_belongs_to_many` association supports these options:
* `:foreign_key`
* `:join_table`
* `:validate`
+* `:readonly`
##### `:association_foreign_key`
@@ -1763,9 +1976,10 @@ TIP: The `:foreign_key` and `:association_foreign_key` options are useful when s
```ruby
class User < ActiveRecord::Base
- has_and_belongs_to_many :friends, :class_name => "User",
- :foreign_key => "this_user_id",
- :association_foreign_key => "other_user_id"
+ has_and_belongs_to_many :friends,
+ class_name: "User",
+ foreign_key: "this_user_id",
+ association_foreign_key: "other_user_id"
end
```
@@ -1779,7 +1993,7 @@ If the name of the other model cannot be derived from the association name, you
```ruby
class Parts < ActiveRecord::Base
- has_and_belongs_to_many :assemblies, :class_name => "Gadget"
+ has_and_belongs_to_many :assemblies, class_name: "Gadget"
end
```
@@ -1789,9 +2003,10 @@ By convention, Rails assumes that the column in the join table used to hold the
```ruby
class User < ActiveRecord::Base
- has_and_belongs_to_many :friends, :class_name => "User",
- :foreign_key => "this_user_id",
- :association_foreign_key => "other_user_id"
+ has_and_belongs_to_many :friends,
+ class_name: "User",
+ foreign_key: "this_user_id",
+ association_foreign_key: "other_user_id"
end
```
@@ -1809,7 +2024,7 @@ There may be times when you wish to customize the query used by `has_and_belongs
```ruby
class Parts < ActiveRecord::Base
- has_and_belongs_to_many :assemblies, -> { where :active => true }
+ has_and_belongs_to_many :assemblies, -> { where active: true }
end
```
@@ -1842,7 +2057,7 @@ You can also set conditions via a hash:
```ruby
class Parts < ActiveRecord::Base
has_and_belongs_to_many :assemblies,
- -> { where :factory => 'Seattle' }
+ -> { where factory: 'Seattle' }
end
```
@@ -1929,7 +2144,7 @@ You define association callbacks by adding options to the association declaratio
```ruby
class Customer < ActiveRecord::Base
- has_many :orders, :before_add => :check_credit_limit
+ has_many :orders, before_add: :check_credit_limit
def check_credit_limit(order)
...
@@ -1944,7 +2159,7 @@ You can stack callbacks on a single event by passing them as an array:
```ruby
class Customer < ActiveRecord::Base
has_many :orders,
- :before_add => [:check_credit_limit, :calculate_shipping_charges]
+ before_add: [:check_credit_limit, :calculate_shipping_charges]
def check_credit_limit(order)
...
@@ -1966,7 +2181,7 @@ You're not limited to the functionality that Rails automatically builds into ass
class Customer < ActiveRecord::Base
has_many :orders do
def find_by_order_prefix(order_number)
- find_by_region_id(order_number[0..2])
+ find_by(region_id: order_number[0..2])
end
end
end
diff --git a/guides/source/caching_with_rails.md b/guides/source/caching_with_rails.md
index e4d2ecaba1..b0ab88bf59 100644
--- a/guides/source/caching_with_rails.md
+++ b/guides/source/caching_with_rails.md
@@ -3,10 +3,10 @@ Caching with Rails: An overview
This guide will teach you what you need to know about avoiding that expensive round-trip to your database and returning what you need to return to the web clients in the shortest time possible.
-After reading this guide, you should be able to use and configure:
+After reading this guide, you will know:
-* Page, action, and fragment caching.
-* Sweepers.
+* Page and action caching (moved to separate gems as of Rails 4).
+* Fragment caching.
* Alternative cache stores.
* Conditional GET support.
@@ -30,103 +30,13 @@ config.action_controller.perform_caching = true
Page caching is a Rails mechanism which allows the request for a generated page to be fulfilled by the webserver (i.e. Apache or nginx), without ever having to go through the Rails stack at all. Obviously, this is super-fast. Unfortunately, it can't be applied to every situation (such as pages that need authentication) and since the webserver is literally just serving a file from the filesystem, cache expiration is an issue that needs to be dealt with.
-To enable page caching, you need to use the `caches_page` method.
-
-```ruby
-class ProductsController < ActionController
-
- caches_page :index
-
- def index
- @products = Product.all
- end
-end
-```
-
-Let's say you have a controller called `ProductsController` and an `index` action that lists all the products. The first time anyone requests `/products`, Rails will generate a file called `products.html` and the webserver will then look for that file before it passes the next request for `/products` to your Rails application.
-
-By default, the page cache directory is set to `Rails.public_path` (which is usually set to the `public` folder) and this can be configured by changing the configuration setting `config.action_controller.page_cache_directory`. Changing the default from `public` helps avoid naming conflicts, since you may want to put other static html in `public`, but changing this will require web server reconfiguration to let the web server know where to serve the cached files from.
-
-The Page Caching mechanism will automatically add a `.html` extension to requests for pages that do not have an extension to make it easy for the webserver to find those pages and this can be configured by changing the configuration setting `config.action_controller.default_static_extension`.
-
-In order to expire this page when a new product is added we could extend our example controller like this:
-
-```ruby
-class ProductsController < ActionController
-
- caches_page :index
-
- def index
- @products = Product.all
- end
-
- def create
- expire_page :action => :index
- end
-
-end
-```
-
-If you want a more complicated expiration scheme, you can use cache sweepers to expire cached objects when things change. This is covered in the section on Sweepers.
-
-By default, page caching automatically gzips files (for example, to `products.html.gz` if user requests `/products`) to reduce the size of data transmitted (web servers are typically configured to use a moderate compression ratio as a compromise, but since precompilation happens once, compression ratio is maximum).
-
-Nginx is able to serve compressed content directly from disk by enabling `gzip_static`:
-
-```nginx
-location / {
- gzip_static on; # to serve pre-gzipped version
-}
-```
-
-You can disable gzipping by setting `:gzip` option to false (for example, if action returns image):
-
-```ruby
-caches_page :image, :gzip => false
-```
-
-Or, you can set custom gzip compression level (level names are taken from `Zlib` constants):
-
-```ruby
-caches_page :image, :gzip => :best_speed
-```
-
-NOTE: Page caching ignores all parameters. For example `/products?page=1` will be written out to the filesystem as `products.html` with no reference to the `page` parameter. Thus, if someone requests `/products?page=2` later, they will get the cached first page. A workaround for this limitation is to include the parameters in the page's path, e.g. `/productions/page/1`.
-
-INFO: Page caching runs in an after filter. Thus, invalid requests won't generate spurious cache entries as long as you halt them. Typically, a redirection in some before filter that checks request preconditions does the job.
+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.
### 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.
-Clearing the cache works in a similar way to Page Caching, except you use `expire_action` instead of `expire_page`.
-
-Let's say you only wanted authenticated users to call actions on `ProductsController`.
-
-```ruby
-class ProductsController < ActionController
-
- before_filter :authenticate
- caches_action :index
-
- def index
- @products = Product.all
- end
-
- def create
- expire_action :action => :index
- end
-
-end
-```
-
-You can also use `:if` (or `:unless`) to pass a Proc that specifies when the action should be cached. Also, you can use `:layout => false` to cache without layout so that dynamic information in the layout such as logged in user info or the number of items in the cart can be left uncached. This feature is available as of Rails 2.2.
-
-You can modify the default action cache path by passing a `:cache_path` option. This will be passed directly to `ActionCachePath.path_for`. This is handy for actions with multiple possible routes that should be cached differently. If a block is given, it is called with the current controller instance.
-
-Finally, if you are using memcached or Ehcache, you can also pass `:expires_in`. In fact, all parameters not used by `caches_action` are sent to the underlying cache store.
-
-INFO: Action caching runs in an after filter. Thus, invalid requests won't generate spurious cache entries as long as you halt them. Typically, a redirection in some before filter that checks request preconditions does the job.
+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.
### Fragment Caching
@@ -152,14 +62,14 @@ As an example, if you wanted to show all the orders placed on your website in re
The cache block in our example will bind to the action that called it and is written out to the same place as the Action Cache, which means that if you want to cache multiple fragments per action, you should provide an `action_suffix` to the cache call:
```html+erb
-<% cache(:action => 'recent', :action_suffix => 'all_products') do %>
+<% cache(action: 'recent', action_suffix: 'all_products') do %>
All available products:
```
and you can expire it using the `expire_fragment` method, like so:
```ruby
-expire_fragment(:controller => 'products', :action => 'recent', :action_suffix => 'all_products')
+expire_fragment(controller: 'products', action: 'recent', action_suffix: 'all_products')
```
If you don't want the cache block to bind to the action that called it, you can also use globally keyed fragments by calling the `cache` method with a key:
@@ -175,102 +85,60 @@ This fragment is then available to all actions in the `ProductsController` using
```ruby
expire_fragment('all_available_products')
```
-
-### Sweepers
-
-Cache sweeping is a mechanism which allows you to get around having a ton of `expire_{page,action,fragment}` calls in your code. It does this by moving all the work required to expire cached content into an `ActionController::Caching::Sweeper` subclass. This class is an observer and looks for changes to an Active Record object via callbacks, and when a change occurs it expires the caches associated with that object in an around or after filter.
-
-TIP: Sweepers rely on the use of Active Record and Active Record Observers. The object you are observing must be an Active Record model.
-
-Continuing with our Product controller example, we could rewrite it with a sweeper like this:
+If you want to avoid expiring the fragment manually, whenever an action updates a product, you can define a helper method:
```ruby
-class ProductSweeper < ActionController::Caching::Sweeper
- observe Product # This sweeper is going to keep an eye on the Product model
-
- # If our sweeper detects that a Product was created call this
- def after_create(product)
- expire_cache_for(product)
- end
-
- # If our sweeper detects that a Product was updated call this
- def after_update(product)
- expire_cache_for(product)
- end
-
- # If our sweeper detects that a Product was deleted call this
- def after_destroy(product)
- expire_cache_for(product)
- end
-
- private
- def expire_cache_for(product)
- # Expire the index page now that we added a new product
- expire_page(:controller => 'products', :action => 'index')
-
- # Expire a fragment
- expire_fragment('all_available_products')
+module ProductsHelper
+ def cache_key_for_products
+ count = Product.count
+ max_updated_at = Product.maximum(:updated_at).try(:utc).try(:to_s, :number)
+ "products/all-#{count}-#{max_updated_at}"
end
end
```
-You may notice that the actual product gets passed to the sweeper, so if we were caching the edit action for each product, we could add an expire method which specifies the page we want to expire:
+This method generates a cache key that depends on all products and can be used in the view:
-```ruby
-expire_action(:controller => 'products', :action => 'edit', :id => product.id)
+```erb
+<% cache(cache_key_for_products) do %>
+ All available products:
+<% end %>
```
-Then we add it to our controller to tell it to call the sweeper when certain actions are called. So, if we wanted to expire the cached content for the list and edit actions when the create action was called, we could do the following:
-
-```ruby
-class ProductsController < ActionController
-
- before_filter :authenticate
- caches_action :index
- cache_sweeper :product_sweeper
-
- def index
- @products = Product.all
- end
+If you want to cache a fragment under certain condition you can use `cache_if` or `cache_unless`
-end
+```erb
+<% cache_if (condition, cache_key_for_products) do %>
+ All available products:
+<% end %>
```
-Sometimes it is necessary to disambiguate the controller when you call `expire_action`, such as when there are two identically named controllers in separate namespaces:
-
-```ruby
-class ProductsController < ActionController
- caches_action :index
+You can also use an Active Record model as the cache key:
- def index
- @products = Product.all
- end
-end
+```erb
+<% Product.all.each do |p| %>
+ <% cache(p) do %>
+ <%= link_to p.name, product_url(p) %>
+ <% end %>
+<% end %>
+```
-module Admin
- class ProductsController < ActionController
- cache_sweeper :product_sweeper
+Behind the scenes, a method called `cache_key` will be invoked on the model and it returns a string like `products/23-20130109142513`. The cache key includes the model name, the id and finally the updated_at timestamp. Thus it will automatically generate a new fragment when the product is updated because the key changes.
- def new
- @product = Product.new
- end
+You can also combine the two schemes which is called "Russian Doll Caching":
- def create
- @product = Product.create(params[:product])
- end
- end
-end
-
-class ProductSweeper < ActionController::Caching::Sweeper
- observe Product
-
- def after_create(product)
- expire_action(:controller => '/products', :action => 'index')
- end
-end
+```erb
+<% cache(cache_key_for_products) do %>
+ All available products:
+ <% Product.all.each do |p| %>
+ <% cache(p) do %>
+ <%= link_to p.name, product_url(p) %>
+ <% end %>
+ <% end %>
+<% end %>
```
-Note the use of '/products' here rather than 'products'. If you wanted to expire an action cache for the `Admin::ProductsController`, you would use 'admin/products' instead.
+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.
### SQL Caching
@@ -279,7 +147,7 @@ Query caching is a Rails feature that caches the result set returned by each que
For example:
```ruby
-class ProductsController < ActionController
+class ProductsController < ApplicationController
def index
# Run a find query
@@ -294,10 +162,6 @@ class ProductsController < ActionController
end
```
-The second time the same query is run against the database, it's not actually going to hit the database. The first time the result is returned from the query it is stored in the query cache (in memory) and the second time it's pulled from memory.
-
-However, it's important to note that query caches are created at the start of an action and destroyed at the end of that action and thus persist only for the duration of the action. If you'd like to store query results in a more persistent fashion, you can in Rails by using low level caching.
-
Cache Stores
------------
@@ -340,13 +204,11 @@ There are some common options used by all cache implementations. These can be pa
This cache store keeps entries in memory in the same Ruby process. The cache store has a bounded size specified by the `:size` options to the initializer (default is 32Mb). When the cache exceeds the allotted size, a cleanup will occur and the least recently used entries will be removed.
```ruby
-config.cache_store = :memory_store, { :size => 64.megabytes }
+config.cache_store = :memory_store, { size: 64.megabytes }
```
If you're running multiple Ruby on Rails server processes (which is the case if you're using mongrel_cluster or Phusion Passenger), then your Rails server process instances won't be able to share cache data with each other. This cache store is not appropriate for large application deployments, but can work well for small, low traffic sites with only a couple of server processes or for development and test environments.
-This is the default cache store implementation.
-
### ActiveSupport::Cache::FileStore
This cache store uses the file system to store entries. The path to the directory where the store files will be stored must be specified when initializing the cache.
@@ -359,9 +221,11 @@ With this cache store, multiple server processes on the same host can share a ca
Note that the cache will grow until the disk is full unless you periodically clear out old entries.
+This is the default cache store implementation.
+
### ActiveSupport::Cache::MemCacheStore
-This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very a high performance and redundancy.
+This cache store uses Danga's `memcached` server to provide a centralized cache for your application. Rails uses the bundled `dalli` gem by default. This is currently the most popular cache store for production websites. It can be used to provide a single, shared cache cluster with very high performance and redundancy.
When initializing the cache, you need to specify the addresses for all memcached servers in your cluster. If none is specified, it will assume memcached is running on the local host on the default port, but this is not an ideal set up for larger sites.
@@ -381,7 +245,7 @@ config.cache_store = :ehcache_store
When initializing the cache, you may use the `:ehcache_config` option to specify the Ehcache config file to use (where the default is "ehcache.xml" in your Rails config directory), and the :cache_name option to provide a custom name for your cache (the default is rails_cache).
-In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including:
+In addition to the standard `:expires_in` option, the `write` method on this cache can also accept the additional `:unless_exist` option, which will cause the cache store to use Ehcache's `putIfAbsent` method instead of `put`, and therefore will not overwrite an existing entry. Additionally, the `write` method supports all of the properties exposed by the [Ehcache Element class](http://ehcache.org/apidocs/net/sf/ehcache/Element.html) , including:
| Property | Argument Type | Description |
| --------------------------- | ------------------- | ----------------------------------------------------------- |
@@ -394,8 +258,8 @@ In addition to the standard `:expires_in` option, the `write` method on this cac
These options are passed to the `write` method as Hash options using either camelCase or underscore notation, as in the following examples:
```ruby
-Rails.cache.write('key', 'value', :time_to_idle => 60.seconds, :timeToLive => 600.seconds)
-caches_action :index, :expires_in => 60.seconds, :unless_exist => true
+Rails.cache.write('key', 'value', time_to_idle: 60.seconds, timeToLive: 600.seconds)
+caches_action :index, expires_in: 60.seconds, unless_exist: true
```
For more information about Ehcache, see [http://ehcache.org/](http://ehcache.org/) .
@@ -427,7 +291,7 @@ You can use Hashes and Arrays of values as cache keys.
```ruby
# This is a legal cache key
-Rails.cache.read(:site => "mysite", :owners => [owner_1, owner_2])
+Rails.cache.read(site: "mysite", owners: [owner_1, owner_2])
```
The keys you use on `Rails.cache` will not be the same as those actually used with the storage engine. They may be modified with a namespace or altered to fit technology backend constraints. This means, for instance, that you can't save values with `Rails.cache` and then try to pull them out with the `memcache-client` gem. However, you also don't need to worry about exceeding the memcached size limit or violating syntax rules.
@@ -437,7 +301,7 @@ Conditional GET support
Conditional GETs are a feature of the HTTP specification that provide a way for web servers to tell browsers that the response to a GET request hasn't changed since the last request and can be safely pulled from the browser cache.
-They work by using the `HTTP_IF_NONE_MATCH` and `HTTP_IF_MODIFIED_SINCE` headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server’s version then the server only needs to send back an empty response with a not modified status.
+They work by using the `HTTP_IF_NONE_MATCH` and `HTTP_IF_MODIFIED_SINCE` headers to pass back and forth both a unique content identifier and the timestamp of when the content was last changed. If the browser makes a request where the content identifier (etag) or last modified since timestamp matches the server's version then the server only needs to send back an empty response with a not modified status.
It is the server's (i.e. our) responsibility to look for a last modified timestamp and the if-none-match header and determine whether or not to send back the full response. With conditional-get support in Rails this is a pretty easy task:
@@ -449,7 +313,7 @@ class ProductsController < ApplicationController
# If the request is stale according to the given timestamp and etag value
# (i.e. it needs to be processed again) then execute this block
- if stale?(:last_modified => @product.updated_at.utc, :etag => @product.cache_key)
+ if stale?(last_modified: @product.updated_at.utc, etag: @product.cache_key)
respond_to do |wants|
# ... normal response processing
end
@@ -463,18 +327,18 @@ class ProductsController < ApplicationController
end
```
-Instead of a options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
+Instead of an options hash, you can also simply pass in a model, Rails will use the `updated_at` and `cache_key` methods for setting `last_modified` and `etag`:
-<ruby>
+```ruby
class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
respond_with(@product) if stale?(@product)
end
end
-</ruby>
+```
-If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you’ve got an easy helper in fresh_when:
+If you don't have any special response processing and are using the default rendering mechanism (i.e. you're not using respond_to or calling render yourself) then you've got an easy helper in fresh_when:
```ruby
class ProductsController < ApplicationController
@@ -484,12 +348,7 @@ class ProductsController < ApplicationController
def show
@product = Product.find(params[:id])
- fresh_when :last_modified => @product.published_at.utc, :etag => @product
+ fresh_when last_modified: @product.published_at.utc, etag: @product
end
end
```
-
-Further reading
----------------
-
-* [Scaling Rails Screencasts](http://railslab.newrelic.com/scaling-rails)
diff --git a/guides/source/command_line.md b/guides/source/command_line.md
index 0338ef5ad0..1b0b93c3bc 100644
--- a/guides/source/command_line.md
+++ b/guides/source/command_line.md
@@ -1,20 +1,18 @@
-A Guide to The Rails Command Line
-=================================
+The Rails Command Line
+======================
-Rails comes with every command line tool you'll need to
+After reading this guide, you will know:
-* Create a Rails application
-* Generate models, controllers, database migrations, and unit tests
-* Start a development server
-* Experiment with objects through an interactive shell
-* Profile and benchmark your new creation
+* How to create a Rails application.
+* 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.
--------------------------------------------------------------------------------
NOTE: This tutorial assumes you have basic Rails knowledge from reading the [Getting Started with Rails Guide](getting_started.html).
-WARNING. This Guide is based on Rails 3.2. Some of the code shown here will not work in earlier versions of Rails.
-
Command Line Basics
-------------------
@@ -27,6 +25,8 @@ There are a few commands that are absolutely critical to your everyday usage of
* `rails dbconsole`
* `rails new app_name`
+All commands can run with ```-h or --help``` to list more information.
+
Let's create a simple Rails application to step through each of these commands in context.
### `rails new`
@@ -64,12 +64,12 @@ With no further work, `rails server` will run our new shiny Rails app:
$ cd commandsapp
$ rails server
=> Booting WEBrick
-=> Rails 3.2.3 application starting in development on http://0.0.0.0:3000
+=> Rails 4.0.0 application starting in development on http://0.0.0.0:3000
=> Call with -d to detach
=> Ctrl-C to shutdown server
-[2012-05-28 00:39:41] INFO WEBrick 1.3.1
-[2012-05-28 00:39:41] INFO ruby 1.9.2 (2011-02-18) [x86_64-darwin11.2.0]
-[2012-05-28 00:39:41] INFO WEBrick::HTTPServer#start: pid=69680 port=3000
+[2013-08-07 02:00:01] INFO WEBrick 1.3.1
+[2013-08-07 02:00:01] INFO ruby 2.0.0 (2013-06-27) [x86_64-darwin11.2.0]
+[2013-08-07 02:00:01] INFO WEBrick::HTTPServer#start: pid=69680 port=3000
```
With just three commands we whipped up a Rails server listening on port 3000. Go to your browser and open [http://localhost:3000](http://localhost:3000), you will see a basic Rails app running.
@@ -82,7 +82,7 @@ The server can be run on a different port using the `-p` option. The default dev
$ 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.
+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.
### `rails generate`
@@ -201,7 +201,7 @@ Usage:
...
-ActiveRecord options:
+Active Record options:
[--migration] # Indicates when to generate migration
# Default: true
@@ -220,12 +220,13 @@ We will set up a simple resource called "HighScore" that will keep track of our
```bash
$ rails generate scaffold HighScore game:string score:integer
invoke active_record
- create db/migrate/20120528060026_create_high_scores.rb
+ create db/migrate/20130717151933_create_high_scores.rb
create app/models/high_score.rb
invoke test_unit
create test/models/high_score_test.rb
create test/fixtures/high_scores.yml
- route resources :high_scores
+ invoke resource_route
+ route resources :high_scores
invoke scaffold_controller
create app/controllers/high_scores_controller.rb
invoke erb
@@ -241,18 +242,21 @@ $ rails generate scaffold HighScore game:string score:integer
create app/helpers/high_scores_helper.rb
invoke test_unit
create test/helpers/high_scores_helper_test.rb
+ invoke jbuilder
+ create app/views/high_scores/index.json.jbuilder
+ create app/views/high_scores/show.json.jbuilder
invoke assets
invoke coffee
create app/assets/javascripts/high_scores.js.coffee
invoke scss
create app/assets/stylesheets/high_scores.css.scss
invoke scss
- create app/assets/stylesheets/scaffolds.css.scss
+ identical app/assets/stylesheets/scaffolds.css.scss
```
The generator checks that there exist the directories for models, controllers, helpers, layouts, functional and unit tests, stylesheets, creates the views, controller, model and database migration for HighScore (creating the `high_scores` table and fields), takes care of the route for the **resource**, and new tests for everything.
-The migration requires that we **migrate**, that is, run some Ruby code (living in that `20120528060026_create_high_scores.rb`) to modify the schema of our database. Which database? The sqlite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while.
+The migration requires that we **migrate**, that is, run some Ruby code (living in that `20130717151933_create_high_scores.rb`) to modify the schema of our database. Which database? The sqlite3 database that Rails will create for you when we run the `rake db:migrate` command. We'll talk more about Rake in-depth in a little while.
```bash
$ rake db:migrate
@@ -288,7 +292,7 @@ If you wish to test out some code without changing any data, you can do that by
```bash
$ rails console --sandbox
-Loading development environment in sandbox (Rails 3.2.3)
+Loading development environment in sandbox (Rails 4.0.0)
Any modifications you make will be rolled back on exit
irb(main):001:0>
```
@@ -347,6 +351,9 @@ Rake is Ruby Make, a standalone Ruby utility that replaces the Unix utility 'mak
You can get a list of Rake tasks available to you, which will often depend on your current directory, by typing `rake --tasks`. Each task has a description, and should help you find the thing you need.
+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
rake about # List versions of all Rails frameworks and the environment
@@ -354,12 +361,13 @@ rake assets:clean # 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
...
-rake log:clear # Truncates all *.log files in log/ to zero bytes
+rake log:clear # Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)
rake middleware # Prints out your Rack middleware stack
...
rake tmp:clear # Clear session, cache, and socket files from tmp/ (narrow w/ tmp:sessions:clear, tmp:cache:clear, tmp:sockets:clear)
rake tmp:create # Creates tmp directories for sessions, cache, sockets, and pids
```
+INFO: You can also use ```rake -T``` to get the list of tasks.
### `about`
@@ -371,13 +379,14 @@ About your application's environment
Ruby version 1.9.3 (x86_64-linux)
RubyGems version 1.3.6
Rack version 1.3
-Rails version 4.0.0.beta
+Rails version 4.1.0
JavaScript Runtime Node.js (V8)
-Active Record version 4.0.0.beta
-Action Pack version 4.0.0.beta
-Action Mailer version 4.0.0.beta
-Active Support version 4.0.0.beta
-Middleware ActionDispatch::Static, Rack::Lock, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, ActionDispatch::Head, Rack::ConditionalGet, Rack::ETag, ActionDispatch::BestStandardsSupport
+Active Record version 4.1.0
+Action Pack version 4.1.0
+Action View version 4.1.0
+Action Mailer version 4.1.0
+Active Support version 4.1.0
+Middleware Rack::Sendfile, ActionDispatch::Static, Rack::Lock, #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x007ffd131a7c88>, Rack::Runtime, Rack::MethodOverride, ActionDispatch::RequestId, Rails::Rack::Logger, ActionDispatch::ShowExceptions, ActionDispatch::DebugExceptions, ActionDispatch::RemoteIp, ActionDispatch::Reloader, ActionDispatch::Callbacks, ActiveRecord::Migration::CheckPending, ActiveRecord::ConnectionAdapters::ConnectionManagement, ActiveRecord::QueryCache, ActionDispatch::Cookies, ActionDispatch::Session::CookieStore, ActionDispatch::Flash, ActionDispatch::ParamsParser, Rack::Head, Rack::ConditionalGet, Rack::ETag
Application root /home/foobar/commandsapp
Environment development
Database adapter sqlite3
@@ -413,7 +422,7 @@ app/controllers/admin/users_controller.rb:
* [ 20] [TODO] any other way to do this?
* [132] [FIXME] high priority for next deploy
-app/model/school.rb:
+app/models/school.rb:
* [ 13] [OPTIMIZE] refactor this code to make it faster
* [ 17] [FIXME]
```
@@ -426,7 +435,7 @@ $ rake notes:fixme
app/controllers/admin/users_controller.rb:
* [132] high priority for next deploy
-app/model/school.rb:
+app/models/school.rb:
* [ 17]
```
@@ -435,21 +444,21 @@ You can also use custom annotations in your code and list them using `rake notes
```bash
$ rake notes:custom ANNOTATION=BUG
(in /home/foobar/commandsapp)
-app/model/post.rb:
+app/models/post.rb:
* [ 23] Have to fix this one before pushing!
```
NOTE. When using specific annotations and custom annotations, the annotation name (FIXME, BUG etc) is not displayed in the output lines.
-By default, `rake notes` will look in the `app`, `config`, `lib`, `script` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`.
+By default, `rake notes` will look in the `app`, `config`, `lib`, `bin` and `test` directories. If you would like to search other directories, you can provide them as a comma separated list in an environment variable `SOURCE_ANNOTATION_DIRECTORIES`.
```bash
-$ export SOURCE_ANNOTATION_DIRECTORIES='rspec,vendor'
+$ export SOURCE_ANNOTATION_DIRECTORIES='spec,vendor'
$ rake notes
(in /home/foobar/commandsapp)
-app/model/user.rb:
+app/models/user.rb:
* [ 35] [FIXME] User should have a subscription at this point
-rspec/model/user_spec.rb:
+spec/models/user_spec.rb:
* [122] [TODO] Verify the user that has a subscription works
```
@@ -461,18 +470,19 @@ rspec/model/user_spec.rb:
INFO: A good description of unit testing in Rails is given in [A Guide to Testing Rails Applications](testing.html)
-Rails comes with a test suite called `Test::Unit`. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write.
+Rails comes with a test suite called Minitest. Rails owes its stability to the use of tests. The tasks available in the `test:` namespace helps in running the different tests you will hopefully write.
### `tmp`
The `Rails.root/tmp` directory is, like the *nix /tmp directory, the holding place for temporary files like sessions (if you're using a file store for files), process id files, and cached actions.
-The `tmp:` namespaced tasks will help you clear the `Rails.root/tmp` directory:
+The `tmp:` namespaced tasks will help you clear and create the `Rails.root/tmp` directory:
* `rake tmp:cache:clear` clears `tmp/cache`.
* `rake tmp:sessions:clear` clears `tmp/sessions`.
* `rake tmp:sockets:clear` clears `tmp/sockets`.
* `rake tmp:clear` clears all the three: cache, sessions and sockets.
+* `rake tmp:create` creates tmp directories for sessions, cache, sockets, and pids.
### Miscellaneous
@@ -482,11 +492,13 @@ The `tmp:` namespaced tasks will help you clear the `Rails.root/tmp` directory:
### Custom Rake Tasks
-Custom rake tasks have a `.rake` extension and are placed in `Rails.root/lib/tasks`.
+Custom rake tasks have a `.rake` extension and are placed in
+`Rails.root/lib/tasks`. You can create these custom rake tasks with the
+`bin/rails generate task` command.
```ruby
desc "I am short, but comprehensive description for my cool task"
-task :task_name => [:prerequisite_task, :another_task_we_depend_on] do
+task task_name: [:prerequisite_task, :another_task_we_depend_on] do
# All your magic here
# Any valid Ruby code is allowed
end
@@ -563,14 +575,20 @@ We had to create the **gitapp** directory and initialize an empty git repository
$ cat config/database.yml
# PostgreSQL. Versions 8.2 and up are supported.
#
-# Install the ruby-postgres driver:
-# gem install ruby-postgres
-# On Mac OS X:
-# gem install ruby-postgres -- --include=/usr/local/pgsql
+# Install the pg driver:
+# gem install pg
+# On OS X with Homebrew:
+# gem install pg -- --with-pg-config=/usr/local/bin/pg_config
+# On OS X with MacPorts:
+# gem install pg -- --with-pg-config=/opt/local/lib/postgresql84/bin/pg_config
# On Windows:
-# gem install ruby-postgres
+# gem install pg
# Choose the win32 build.
# Install PostgreSQL and put its /bin directory on your path.
+#
+# Configure Using Gemfile
+# gem 'pg'
+#
development:
adapter: postgresql
encoding: unicode
@@ -585,28 +603,3 @@ development:
It also generated some lines in our database.yml configuration corresponding to our choice of PostgreSQL for database.
NOTE. The only catch with using the SCM options is that you have to make your application's directory first, then initialize your SCM, then you can run the `rails new` command to generate the basis of your app.
-
-### `server` with Different Backends
-
-Many people have created a large number of different web servers in Ruby, and many of them can be used to run Rails. Since version 2.3, Rails uses Rack to serve its webpages, which means that any webserver that implements a Rack handler can be used. This includes WEBrick, Mongrel, Thin, and Phusion Passenger (to name a few!).
-
-NOTE: For more details on the Rack integration, see [Rails on Rack](rails_on_rack.html).
-
-To use a different server, just install its gem, then use its name for the first parameter to `rails server`:
-
-```bash
-$ sudo gem install mongrel
-Building native extensions. This could take a while...
-Building native extensions. This could take a while...
-Successfully installed gem_plugin-0.2.3
-Successfully installed fastthread-1.0.1
-Successfully installed cgi_multipart_eof_fix-2.5.0
-Successfully installed mongrel-1.1.5
-...
-...
-Installing RDoc documentation for mongrel-1.1.5...
-$ rails server mongrel
-=> Booting Mongrel (use 'rails server webrick' to force WEBrick)
-=> Rails 3.1.0 application starting on http://0.0.0.0:3000
-...
-```
diff --git a/guides/source/configuring.md b/guides/source/configuring.md
index 26c7976c6b..8ac34c9716 100644
--- a/guides/source/configuring.md
+++ b/guides/source/configuring.md
@@ -1,10 +1,12 @@
Configuring Rails Applications
==============================
-This guide covers the configuration and initialization features available to Rails applications. By referring to this guide, you will be able to:
+This guide covers the configuration and initialization features available to Rails applications.
-* Adjust the behavior of your Rails applications
-* Add additional code to be run at application start time
+After reading this guide, you will know:
+
+* How to adjust the behavior of your Rails applications.
+* How to add additional code to be run at application start time.
--------------------------------------------------------------------------------
@@ -28,16 +30,16 @@ Configuring Rails Components
In general, the work of configuring Rails means configuring the components of Rails, as well as configuring Rails itself. The configuration file `config/application.rb` and environment-specific configuration files (such as `config/environments/production.rb`) allow you to specify the various settings that you want to pass down to all of the components.
-For example, the default `config/application.rb` file includes this setting:
+For example, the `config/application.rb` file includes this setting:
```ruby
-config.filter_parameters += [:password]
+config.autoload_paths += %W(#{config.root}/extras)
```
This is a setting for Rails itself. If you want to pass settings to individual Rails components, you can do so via the same `config` object in `config/application.rb`:
```ruby
-config.active_record.observers = [:hotel_observer, :review_observer]
+config.active_record.schema_format = :ruby
```
Rails will use that particular setting to configure Active Record.
@@ -56,14 +58,6 @@ These configuration methods are to be called on a `Rails::Railtie` object, such
* `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_path` lets you decorate asset paths. This can be a callable, a string, or be `nil` which is the default. For example, the normal path for `blog.js` would be `/javascripts/blog.js`, let that absolute path be `path`. If `config.asset_path` is a callable, Rails calls it when generating asset paths passing `path` as argument. If `config.asset_path` is a string, it is expected to be a `sprintf` format string with a `%s` where `path` will get inserted. In either case, Rails outputs the decorated path. Shorter version of `config.action_controller.asset_path`.
-
- ```ruby
- config.asset_path = proc { |path| "/blog/public#{path}" }
- ```
-
-NOTE. The `config.asset_path` configuration is ignored if the asset pipeline is enabled, which is the default.
-
* `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.
* `config.autoload_paths` accepts an array of paths from which Rails will autoload constants. Default is all directories under `app`.
@@ -103,27 +97,27 @@ NOTE. The `config.asset_path` configuration is ignored if the asset pipeline is
* `config.file_watcher` the class used to detect file updates in the filesystem when `config.reload_classes_only_on_change` is true. Must conform to `ActiveSupport::FileUpdateChecker` API.
-* `config.filter_parameters` used for filtering out the parameters that you don't want shown in the logs, such as passwords or credit card numbers.
+* `config.filter_parameters` used for filtering out the parameters that
+you don't want shown in the logs, such as passwords or credit card
+numbers. New applications filter out passwords by adding the following `config.filter_parameters+=[:password]` in `config/initializers/filter_parameter_logging.rb`.
* `config.force_ssl` forces all requests to be under HTTPS protocol by using `ActionDispatch::SSL` middleware.
+* `config.log_formatter` defines the formatter of the Rails logger. This option defaults to an instance of `ActiveSupport::Logger::SimpleFormatter` for all modes except production, where it defaults to `Logger::Formatter`.
+
* `config.log_level` defines the verbosity of the Rails logger. This option defaults to `:debug` for all modes except production, where it defaults to `:info`.
-* `config.log_tags` accepts a list of methods that respond to `request` object. This makes it easy to tag log lines with debug information like subdomain and request id -- both very helpful in debugging multi-user production applications.
+* `config.log_tags` accepts a list of methods that 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.logger` accepts a logger conforming to the interface of Log4r or the default Ruby `Logger` class. Defaults to an instance of `ActiveSupport::BufferedLogger`, with auto flushing off in production mode.
+* `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.
* `config.middleware` allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
-* `config.queue` configures the default job queue for the application. Defaults to `ActiveSupport::Queue.new` which processes jobs in a background thread. If you change the queue, you're responsible for running the jobs as well.
-
-* `config.queue_consumer` configures a different job consumer for the default queue. Defaults to `ActiveSupport::ThreadedQueueConsumer`. The job consumer must respond to `start`.
-
* `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_token` 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_token` initialized to a random key in `config/initializers/secret_token.rb`.
+* `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`.
-* `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,13 +131,10 @@ NOTE. The `config.asset_path` configuration is ignored if the asset pipeline is
* `config.beginning_of_week` sets the default beginning of week for the application. Accepts a valid week day symbol (e.g. `:monday`).
-* `config.whiny_nils` enables or disables warnings when a certain set of methods are invoked on `nil` and it does not respond to them. Defaults to true in development and test environments.
-
### Configuring Assets
-Rails 3.1, by default, is set up to use the `sprockets` gem to manage assets within an application. This gem concatenates and compresses assets in order to make serving them much less painful.
-
-* `config.assets.enabled` a flag that controls whether the asset pipeline is enabled. It is explicitly initialized in `config/application.rb`.
+* `config.assets.enabled` a flag that controls whether the asset
+pipeline is enabled. It is set to true by default.
* `config.assets.compress` a flag that enables the compression of compiled assets. It is explicitly set to true in `config/production.rb`.
@@ -171,7 +162,7 @@ Rails 3.1, by default, is set up to use the `sprockets` gem to manage assets wit
### Configuring Generators
-Rails 3 allows you to alter what generators are used with the `config.generators` method. This method takes a block:
+Rails allows you to alter what generators are used with the `config.generators` method. This method takes a block:
```ruby
config.generators do |g|
@@ -189,7 +180,6 @@ The full set of methods that can be used in this block are as follows:
* `javascripts` turns on the hook for JavaScript files in generators. Used in Rails for when the `scaffold` generator is run. Defaults to `true`.
* `javascript_engine` configures the engine to be used (for eg. coffee) when generating assets. Defaults to `nil`.
* `orm` defines which orm to use. Defaults to `false` and will use Active Record by default.
-* `performance_tool` defines which performance tool to use. Defaults to `nil`.
* `resource_controller` defines which generator to use for generating a controller when using `rails generate resource`. Defaults to `:controller`.
* `scaffold_controller` different from `resource_controller`, defines which generator to use for generating a _scaffolded_ controller when using `rails generate scaffold`. Defaults to `:scaffold_controller`.
* `stylesheets` turns on the hook for stylesheets in generators. Used in Rails for when the `scaffold` generator is run, but this hook can be used in other generates as well. Defaults to `true`.
@@ -206,10 +196,10 @@ Every Rails application comes with a standard set of middleware which it uses in
* `Rack::Lock` wraps the app in mutex so it can only be called by a single thread at a time. Only enabled when `config.cache_classes` is `false`.
* `ActiveSupport::Cache::Strategy::LocalCache` serves as a basic memory backed cache. This cache is not thread safe and is intended only for serving as a temporary memory cache for a single thread.
* `Rack::Runtime` sets an `X-Runtime` header, containing the time (in seconds) taken to execute the request.
-* `Rails::Rack::Logger` notifies the logs that the request has began. After request is complete, flushes all the logs.
+* `Rails::Rack::Logger` notifies the logs that the request has begun. After request is complete, flushes all the logs.
* `ActionDispatch::ShowExceptions` rescues any exception returned by the application and renders nice exception pages if the request is local or if `config.consider_all_requests_local` is set to `true`. If `config.action_dispatch.show_exceptions` is set to `false`, exceptions will be raised regardless.
* `ActionDispatch::RequestId` makes a unique X-Request-Id header available to the response and enables the `ActionDispatch::Request#uuid` method.
-* `ActionDispatch::RemoteIp` checks for IP spoofing attacks. Configurable with the `config.action_dispatch.ip_spoofing_check` and `config.action_dispatch.trusted_proxies` settings.
+* `ActionDispatch::RemoteIp` checks for IP spoofing attacks and gets valid `client_ip` from request headers. Configurable with the `config.action_dispatch.ip_spoofing_check`, and `config.action_dispatch.trusted_proxies` options.
* `Rack::Sendfile` intercepts responses whose body is being served from a file and replaces it with a server specific X-Sendfile header. Configurable with `config.action_dispatch.x_sendfile_header`.
* `ActionDispatch::Callbacks` runs the prepare callbacks before serving the request.
* `ActiveRecord::ConnectionAdapters::ConnectionManagement` cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`.
@@ -220,7 +210,6 @@ Every Rails application comes with a standard set of middleware which it uses in
* `ActionDispatch::ParamsParser` parses out parameters from the request into `params`.
* `Rack::MethodOverride` allows the method to be overridden if `params[:_method]` is set. This is the middleware which supports the PATCH, PUT, and DELETE HTTP method types.
* `ActionDispatch::Head` converts HEAD requests to GET requests and serves them as so.
-* `ActionDispatch::BestStandardsSupport` enables "best standards support" so that IE8 renders some elements correctly.
Besides these usual middleware, you can add your own by using the `config.middleware.use` method:
@@ -243,13 +232,13 @@ config.middleware.insert_after ActionDispatch::Head, Magical::Unicorns
Middlewares can also be completely swapped out and replaced with others:
```ruby
-config.middleware.swap ActionDispatch::BestStandardsSupport, Magical::Unicorns
+config.middleware.swap ActionController::Failsafe, Lifo::Failsafe
```
They can also be removed from the stack completely:
```ruby
-config.middleware.delete ActionDispatch::BestStandardsSupport
+config.middleware.delete "Rack::MethodOverride"
```
### Configuring i18n
@@ -272,6 +261,8 @@ config.middleware.delete ActionDispatch::BestStandardsSupport
* `config.active_record.table_name_suffix` lets you set a global string to be appended to table names. If you set this to `_northwest`, then the Customer class will look for `customers_northwest` as its table. The default is an empty string.
+* `config.active_record.schema_migrations_table_name` lets you set a string to be used as the name of the schema migrations table.
+
* `config.active_record.pluralize_table_names` specifies whether Rails will look for singular or plural table names in the database. If set to true (the default), then the Customer class will use the `customers` table. If set to false, then the Customer class will use the `customer` table.
* `config.active_record.default_timezone` determines whether to use `Time.local` (if set to `:local`) or `Time.utc` (if set to `:utc`) when pulling dates and times from the database. The default is `:utc` for Rails, although Active Record defaults to `:local` when used outside of Rails.
@@ -282,7 +273,13 @@ config.middleware.delete ActionDispatch::BestStandardsSupport
* `config.active_record.lock_optimistically` controls whether Active Record will use optimistic locking and is true by default.
-* `config.active_record.auto_explain_threshold_in_seconds` configures the threshold for automatic EXPLAINs (`nil` disables this feature). Queries exceeding the threshold get their query plan logged. Default is 0.5 in development mode.
+* `config.active_record.cache_timestamp_format` controls the format of the timestamp value in the cache key. Default is `:number`.
+
+* `config.active_record.record_timestamps` is a boolean value which controls whether or not timestamping of `create` and `update` operations on a model occur. The default value is `true`.
+
+* `config.active_record.partial_writes` is a boolean value and controls whether or not partial writes are used (i.e. whether updates only set attributes that are dirty). Note that when using partial writes, you should also use optimistic locking `config.active_record.lock_optimistically` since concurrent updates may write attributes based on a possibly stale read state. The default value is `true`.
+
+* `config.active_record.attribute_types_cached_by_default` sets the attribute types that `ActiveRecord::AttributeMethods` will cache by default on reads. The default is `[:datetime, :timestamp, :time, :date]`.
The MySQL adapter adds one additional configuration option:
@@ -298,8 +295,6 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.asset_host` sets the host for the assets. Useful when CDNs are used for hosting assets rather than the application server itself.
-* `config.action_controller.asset_path` takes a block which configures where assets can be found. Shorter version of `config.action_controller.asset_path`.
-
* `config.action_controller.perform_caching` configures whether the application should perform caching or not. Set to false in development mode, true in production.
* `config.action_controller.default_static_extension` configures the extension used for cached pages. Defaults to `.html`.
@@ -312,10 +307,12 @@ The schema dumper adds one additional configuration option:
* `config.action_controller.allow_forgery_protection` enables or disables CSRF protection. By default this is `false` in test mode and `true` in all other modes.
-* `config.action_controller.relative_url_root` can be used to tell Rails that you are deploying to a subdirectory. The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
+* `config.action_controller.relative_url_root` can be used to tell Rails that you are [deploying to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root). The default is `ENV['RAILS_RELATIVE_URL_ROOT']`.
* `config.action_controller.permit_all_parameters` sets all the parameters for mass assignment to be permitted by default. The default value is `false`.
+* `config.action_controller.action_on_unpermitted_parameters` enables logging or raising an exception if parameters that are not explicitly permitted are found. Set to `:log` or `:raise` to enable. The default value is `:log` in development and test environments, and `false` in all other environments.
+
### Configuring Action Dispatch
* `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.
@@ -356,34 +353,6 @@ The schema dumper adds one additional configuration option:
* `config.action_view.erb_trim_mode` gives the trim mode to be used by ERB. It defaults to `'-'`. See the [ERB documentation](http://www.ruby-doc.org/stdlib/libdoc/erb/rdoc/) for more information.
-* `config.action_view.javascript_expansions` is a hash containing expansions that can be used for the JavaScript include tag. By default, this is defined as:
-
- ```ruby
- config.action_view.javascript_expansions = { :defaults => %w(jquery jquery_ujs) }
- ```
-
- However, you may add to this by defining others:
-
- ```ruby
- config.action_view.javascript_expansions[:prototype] = [
- 'prototype', 'effects', 'dragdrop', 'controls'
- ]
- ```
-
- And can reference in the view with the following code:
-
- ```ruby
- <%= javascript_include_tag :prototype %>
- ```
-
-* `config.action_view.stylesheet_expansions` works in much the same way as `javascript_expansions`, but has no default key. Keys defined for this hash can be referenced in the view like such:
-
- ```ruby
- <%= stylesheet_link_tag :special %>
- ```
-
-* `config.action_view.cache_asset_ids` With the cache enabled, the asset tag helper methods will make fewer expensive file system calls (the default implementation checks the file system timestamp). However this prevents you from modifying any asset files while the server is running.
-
* `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:
@@ -439,11 +408,6 @@ There are a number of settings available on `config.action_mailer`:
config.action_mailer.interceptors = ["MailInterceptor"]
```
-* `config.action_mailer.queue` registers the queue that will be used to deliver the mail.
-```ruby
-config.action_mailer.queue = SomeQueue.new
-```
-
### Configuring Active Support
There are a few configuration options available in Active Support:
@@ -454,7 +418,7 @@ There are a few configuration options available in Active Support:
* `config.active_support.use_standard_json_time_format` enables or disables serializing dates to ISO 8601 format. Defaults to `true`.
-* `ActiveSupport::BufferedLogger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
+* `ActiveSupport::Logger.silencer` is set to `false` to disable the ability to silence logging in a block. The default is `true`.
* `ActiveSupport::Cache::Store.logger` specifies the logger to use within cache store operations.
@@ -468,13 +432,13 @@ There are a few configuration options available in Active Support:
### Configuring a Database
-Just about every Rails application will interact with a database. The database to use is specified in a configuration file called `config/database.yml`. If you open this file in a new Rails application, you'll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:
+Just about every Rails application will interact with a database. The database to use is specified in a configuration file called `config/database.yml`. If you open this file in a new Rails application, you'll see a default database configured to use SQLite3. The file contains sections for three different environments in which Rails can run by default:
* The `development` environment is used on your development/local computer as you interact manually with the application.
* The `test` environment is used when running automated tests.
* The `production` environment is used when you deploy your application for the world to use.
-TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below.
+TIP: You don't have to update the database configurations manually. If you look at the options of the application generator, you will see that one of the options is named `--database`. This option allows you to choose an adapter from a list of the most used relational databases. You can even run the generator repeatedly: `cd .. && rails new blog --database=mysql`. When you confirm the overwriting of the `config/database.yml` file, your application will be configured for MySQL instead of SQLite. Detailed examples of the common database connections are below.
#### Configuring an SQLite3 Database
@@ -568,6 +532,52 @@ development:
Change the username and password in the `development` section as appropriate.
+### Creating Rails Environments
+
+By default Rails ships with three environments: "development", "test", and "production". While these are sufficient for most use cases, there are circumstances when you want more environments.
+
+Imagine you have a server which mirrors the production environment but is only used for testing. Such a server is commonly called a "staging server". To define an environment called "staging" for this server, just create a file called `config/environments/staging.rb`. Please use the contents of any existing file in `config/environments` as a starting point and make the necessary changes from there.
+
+That environment is no different than the default ones, start a server with `rails server -e staging`, a console with `rails console staging`, `Rails.env.staging?` works, etc.
+
+
+### Deploy to a subdirectory (relative url root)
+
+By default Rails expects that your application is running at the root
+(eg. `/`). This section explains how to run your application inside a directory.
+
+Let's assume we want to deploy our application to "/app1". Rails needs to know
+this directory to generate the appropriate routes:
+
+```ruby
+config.relative_url_root = "/app1"
+```
+
+alternatively you can set the `RAILS_RELATIVE_URL_ROOT` environment
+variable.
+
+Rails will now prepend "/app1" when generating links.
+
+#### Using Passenger
+
+Passenger makes it easiy to run your application in a subdirectory. You can find
+the relevant configuration in the
+[passenger manual](http://www.modrails.com/documentation/Users%20guide%20Apache.html#deploying_rails_to_sub_uri).
+
+#### Using a Reverse Proxy
+
+TODO
+
+#### Considerations when deploying to a subdirectory
+
+Deploying to a subdirectory in production has implications on various parts of
+Rails.
+
+* development environment:
+* testing environment:
+* serving static assets:
+* asset pipeline:
+
Rails Environment Settings
--------------------------
@@ -575,9 +585,7 @@ Some parts of Rails can also be configured externally by supplying environment v
* `ENV["RAILS_ENV"]` defines the Rails environment (production, development, test, and so on) that Rails will run under.
-* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you deploy your application to a subdirectory.
-
-* `ENV["RAILS_ASSET_ID"]` will override the default cache-busting timestamps that Rails generates for downloadable assets.
+* `ENV["RAILS_RELATIVE_URL_ROOT"]` is used by the routing code to recognize URLs when you [deploy your application to a subdirectory](configuring.html#deploy-to-a-subdirectory-relative-url-root).
* `ENV["RAILS_CACHE_ID"]` and `ENV["RAILS_APP_VERSION"]` are used to generate expanded cache keys in Rails' caching code. This allows you to have multiple separate caches from the same application.
@@ -602,9 +610,9 @@ Rails has 5 initialization events which can be hooked into (listed in the order
* `to_prepare`: Run after the initializers are run for all Railties (including the application itself), but before eager loading and the middleware stack is built. More importantly, will run upon every request in `development`, but only once (during boot-up) in `production` and `test`.
-* `before_eager_load`: This is run directly before eager loading occurs, which is the default behaviour for the `production` environment and not for the `development` environment.
+* `before_eager_load`: This is run directly before eager loading occurs, which is the default behavior for the `production` environment and not for the `development` environment.
-* `after_initialize`: Run directly after the initialization of the application, but before the application initializers are run.
+* `after_initialize`: Run directly after the initialization of the application, after the application initializers in `config/initializers` are run.
To define an event for these hooks, use the block syntax within a `Rails::Application`, `Rails::Railtie` or `Rails::Engine` subclass:
@@ -626,21 +634,21 @@ Rails.application.config.before_initialize do
end
```
-WARNING: Some parts of your application, notably observers and routing, are not yet set up at the point where the `after_initialize` block is called.
+WARNING: Some parts of your application, notably routing, are not yet set up at the point where the `after_initialize` block is called.
### `Rails::Railtie#initializer`
-Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `initialize_whiny_nils` initializer from Active Support:
+Rails has several initializers that run on startup that are all defined by using the `initializer` method from `Rails::Railtie`. Here's an example of the `set_helpers_path` initializer from Action Controller:
```ruby
-initializer "active_support.initialize_whiny_nils" do |app|
- require 'active_support/whiny_nil' if app.config.whiny_nils
+initializer "action_controller.set_helpers_path" do |app|
+ ActionController::Helpers.helpers_path = app.helpers_paths
end
```
The `initializer` method takes three arguments with the first being the name for the initializer and the second being an options hash (not shown here) and the third being a block. The `:before` key in the options hash can be specified to specify which initializer this new initializer must run before, and the `:after` key will specify which initializer to run this initializer _after_.
-Initializers defined using the `initializer` method will be ran in the order they are defined in, with the exception of ones that use the `:before` or `:after` methods.
+Initializers defined using the `initializer` method will be run in the order they are defined in, with the exception of ones that use the `:before` or `:after` methods.
WARNING: You may put your initializer before or after any other initializer in the chain, as long as it is logical. Say you have 4 initializers called "one" through "four" (defined in that order) and you define "four" to go _before_ "four" but _after_ "three", that just isn't logical and Rails will not be able to determine your initializer order.
@@ -656,11 +664,11 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `load_active_support` Requires `active_support/dependencies` which sets up the basis for Active Support. Optionally requires `active_support/all` if `config.active_support.bare` is un-truthful, which is the default.
-* `initialize_logger` Initializes the logger (an `ActiveSupport::BufferedLogger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`.
+* `initialize_logger` Initializes the logger (an `ActiveSupport::Logger` object) for the application and makes it accessible at `Rails.logger`, provided that no initializer inserted before this point has defined `Rails.logger`.
* `initialize_cache` If `Rails.cache` isn't set yet, initializes the cache by referencing the value in `config.cache_store` and stores the outcome as `Rails.cache`. If this object responds to the `middleware` method, its middleware is inserted before `Rack::Runtime` in the middleware stack.
-* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer -- which runs only if `cache_classes` is set to `false` -- uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
+* `set_clear_dependencies_hook` Provides a hook for `active_record.set_dispatch_hooks` to use, which will run before this initializer. This initializer - which runs only if `cache_classes` is set to `false` - uses `ActionDispatch::Callbacks.after` to remove the constants which have been referenced during the request from the object space so that they will be reloaded during the following request.
* `initialize_dependency_mechanism` If `config.cache_classes` is true, configures `ActiveSupport::Dependencies.mechanism` to `require` dependencies rather than `load` them.
@@ -668,37 +676,19 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `i18n.callbacks` In the development environment, sets up a `to_prepare` callback which will call `I18n.reload!` if any of the locales have changed since the last request. In production mode this callback will only run on the first request.
-* `active_support.initialize_whiny_nils` Requires `active_support/whiny_nil` if `config.whiny_nils` is true. This file will output errors such as:
-
- ```
- Called id for nil, which would mistakenly be 4 -- if you really wanted the id of nil, use object_id
- ```
-
- And:
-
- ```
- You have a nil object when you didn't expect it!
- You might have expected an instance of Array.
- The error occurred while evaluating nil.each
- ```
-
* `active_support.deprecation_behavior` Sets up deprecation reporting for environments, defaulting to `:log` for development, `:notify` for production and `:stderr` for test. If a value isn't set for `config.active_support.deprecation` then this initializer will prompt the user to configure this line in the current environment's `config/environments` file. Can be set to an array of values.
* `active_support.initialize_time_zone` Sets the default time zone for the application based on the `config.time_zone` setting, which defaults to "UTC".
-* `active_support.initialize_beginning_of_week` Sets the default beginnig of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`.
+* `active_support.initialize_beginning_of_week` Sets the default beginning of week for the application based on `config.beginning_of_week` setting, which defaults to `:monday`.
* `action_dispatch.configure` Configures the `ActionDispatch::Http::URL.tld_length` to be set to the value of `config.action_dispatch.tld_length`.
-* `action_view.cache_asset_ids` Sets `ActionView::Helpers::AssetTagHelper::AssetPaths.cache_asset_ids` to `false` when Active Support loads, but only if `config.cache_classes` is too.
-
-* `action_view.javascript_expansions` Registers the expansions set up by `config.action_view.javascript_expansions` and `config.action_view.stylesheet_expansions` to be recognized by Action View and therefore usable in the views.
-
* `action_view.set_configs` Sets up Action View by using the settings in `config.action_view` by `send`'ing the method names as setters to `ActionView::Base` and passing the values through.
-* `action_controller.logger` Sets `ActionController::Base.logger` -- if it's not already set -- to `Rails.logger`.
+* `action_controller.logger` Sets `ActionController::Base.logger` - if it's not already set - to `Rails.logger`.
-* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` -- if it's not already set -- to `Rails.cache`.
+* `action_controller.initialize_framework_caches` Sets `ActionController::Base.cache_store` - if it's not already set - to `Rails.cache`.
* `action_controller.set_configs` Sets up Action Controller by using the settings in `config.action_controller` by `send`'ing the method names as setters to `ActionController::Base` and passing the values through.
@@ -706,7 +696,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `active_record.initialize_timezone` Sets `ActiveRecord::Base.time_zone_aware_attributes` to true, as well as setting `ActiveRecord::Base.default_timezone` to UTC. When attributes are read from the database, they will be converted into the time zone specified by `Time.zone`.
-* `active_record.logger` Sets `ActiveRecord::Base.logger` -- if it's not already set -- to `Rails.logger`.
+* `active_record.logger` Sets `ActiveRecord::Base.logger` - if it's not already set - to `Rails.logger`.
* `active_record.set_configs` Sets up Active Record by using the settings in `config.active_record` by `send`'ing the method names as setters to `ActiveRecord::Base` and passing the values through.
@@ -716,7 +706,7 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `active_record.set_dispatch_hooks` Resets all reloadable connections to the database if `config.cache_classes` is set to `false`.
-* `action_mailer.logger` Sets `ActionMailer::Base.logger` -- if it's not already set -- to `Rails.logger`.
+* `action_mailer.logger` Sets `ActionMailer::Base.logger` - if it's not already set - to `Rails.logger`.
* `action_mailer.set_configs` Sets up Action Mailer by using the settings in `config.action_mailer` by `send`'ing the method names as setters to `ActionMailer::Base` and passing the values through.
@@ -742,11 +732,11 @@ Below is a comprehensive list of all the initializers found in Rails in the orde
* `engines_blank_point` Provides a point-in-initialization to hook into if you wish to do anything before engines are loaded. After this point, all railtie and engine initializers are run.
-* `add_generator_templates` Finds templates for generators at `lib/templates` for the application, railities and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference.
+* `add_generator_templates` Finds templates for generators at `lib/templates` for the application, railties and engines and adds these to the `config.generators.templates` setting, which will make the templates available for all generators to reference.
* `ensure_autoload_once_paths_as_subset` Ensures that the `config.autoload_once_paths` only contains paths from `config.autoload_paths`. If it contains extra paths, then an exception will be raised.
-* `add_to_prepare_blocks` The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be ran per request in development, or before the first request in production.
+* `add_to_prepare_blocks` The block for every `config.to_prepare` call in the application, a railtie or engine is added to the `to_prepare` callbacks for Action Dispatch which will be run per request in development, or before the first request in production.
* `add_builtin_route` If the application is running under the development environment then this will append the route for `rails/info/properties` to the application routes. This route provides the detailed information such as Rails and Ruby version for `public/index.html` in a default Rails application.
@@ -773,8 +763,19 @@ development:
timeout: 5000
```
-Since the connection pooling is handled inside of ActiveRecord by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit.
+Since the connection pooling is handled inside of Active Record by default, all application servers (Thin, mongrel, Unicorn etc.) should behave the same. Initially, the database connection pool is empty and it will create additional connections as the demand for them increases, until it reaches the connection pool limit.
Any one request will check out a connection the first time it requires access to the database, after which it will check the connection back in, at the end of the request, meaning that the additional connection slot will be available again for the next request in the queue.
+If you try to use more connections than are available, Active Record will block
+and wait for a connection from the pool. When it cannot get connection, a timeout
+error similar to given below will be thrown.
+
+```ruby
+ActiveRecord::ConnectionTimeoutError - could not obtain a database connection within 5 seconds. The max pool size is currently 5; consider increasing it:
+```
+
+If you get the above error, you might want to increase the size of connection
+pool by incrementing the `pool` option in `database.yml`
+
NOTE. If you have enabled `Rails.threadsafe!` mode then there could be a chance that several threads may be accessing multiple connections simultaneously. So depending on your current request load, you could very well have multiple threads contending for a limited amount of connections.
diff --git a/guides/source/contributing_to_ruby_on_rails.md b/guides/source/contributing_to_ruby_on_rails.md
index f89317a44a..a6956eb009 100644
--- a/guides/source/contributing_to_ruby_on_rails.md
+++ b/guides/source/contributing_to_ruby_on_rails.md
@@ -1,15 +1,17 @@
Contributing to Ruby on Rails
=============================
-This guide covers ways in which _you_ can become a part of the ongoing development of Ruby on Rails. After reading it, you should be familiar with:
+This guide covers ways in which _you_ can become a part of the ongoing development of Ruby on Rails.
-* Using GitHub to report issues
-* Cloning master and running the test suite
-* Helping to resolve existing issues
-* Contributing to the Ruby on Rails documentation
-* Contributing to the Ruby on Rails code
+After reading this guide, you will know:
-Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation -- all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
+* How to use GitHub to report issues.
+* How to clone master and run the test suite.
+* How to help resolve existing issues.
+* How to contribute to the Ruby on Rails documentation.
+* How to contribute to the Ruby on Rails code.
+
+Ruby on Rails is not "someone else's framework." Over the years, hundreds of people have contributed to Ruby on Rails ranging from a single character to massive architectural changes or significant documentation - all with the goal of making Ruby on Rails better for everyone. Even if you don't feel up to writing code or documentation yet, there are a variety of other ways that you can contribute, from reporting issues to testing patches.
--------------------------------------------------------------------------------
@@ -22,21 +24,50 @@ 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 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.)
-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.
+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.
Then, don't get your hopes up! Unless you have a "Code Red, Mission Critical, the World is Coming to an End" kind of bug, you're creating this issue report in the hope that others with the same problem will be able to collaborate with you on solving it. Do not expect that the issue report will automatically see any activity or that others will jump to fix it. Creating an issue like this is mostly to help yourself start on the path of fixing the problem and for others to confirm it with an "I'm having this problem too" comment.
+### Create a Self-Contained gist for Active Record and Action Controller Issues
+
+If you are filing a bug report, please use
+[Active Record template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_gem.rb) or
+[Action Controller template for gems](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_gem.rb)
+if the bug is found in a published gem, and
+[Active Record template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/active_record_master.rb) or
+[Action Controller template for master](https://github.com/rails/rails/blob/master/guides/bug_report_templates/action_controller_master.rb)
+if the bug happens in the master branch.
+
### Special Treatment for Security Issues
WARNING: Please do not report security vulnerabilities with public GitHub issue reports. The [Rails security policy page](http://rubyonrails.org/security) details the procedure to follow for security issues.
### What about Feature Requests?
-Please don't put "feature request" items into GitHub Issues. If there's a new feature that you want to see added to Ruby on Rails, you'll need to write the code yourself - or convince someone else to partner with you to write the code. Later in this guide you'll find detailed instructions for proposing a patch to Ruby on Rails. If you enter a wishlist item in GitHub Issues with no code, you can expect it to be marked "invalid" as soon as it's reviewed.
-
-If you'd like feedback on an idea for a feature before doing the work for make a patch, please send an email to the [rails-core mailing list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You might get no response, which means that everyone is indifferent. You might find someone who's also interested in building that feature. You might get a "This won't be accepted." But it's the proper place to discuss new ideas. GitHub Issues are not a particularly good venue for the sometimes long and involved discussions new features require.
+Please don't put "feature request" items into GitHub Issues. If there's a new
+feature that you want to see added to Ruby on Rails, you'll need to write the
+code yourself - or convince someone else to partner with you to write the code.
+Later in this guide you'll find detailed instructions for proposing a patch to
+Ruby on Rails. If you enter a wishlist item in GitHub Issues with no code, you
+can expect it to be marked "invalid" as soon as it's reviewed.
+
+Sometimes, the line between 'bug' and 'feature' is a hard one to draw.
+Generally, a feature is anything that adds new behavior, while a bug is
+anything that fixes already existing behavior that is mis-behaving. Sometimes,
+the core team will have to make a judgement call. That said, the distinction
+generally just affects which release your patch will get in to; we love feature
+submissions! They just won't get backported to maintenance branches.
+
+If you'd like feedback on an idea for a feature before doing the work for make
+a patch, please send an email to the [rails-core mailing
+list](https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core). You
+might get no response, which means that everyone is indifferent. You might find
+someone who's also interested in building that feature. You might get a "This
+won't be accepted." But it's the proper place to discuss new ideas. GitHub
+Issues are not a particularly good venue for the sometimes long and involved
+discussions new features require.
Setting Up a Development Environment
------------------------------------
@@ -51,6 +82,22 @@ The easiest and recommended way to get a development environment ready to hack i
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
---------------------
@@ -83,13 +130,13 @@ You can also run any single test separately:
$ 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` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
+You can invoke `test_jdbcmysql`, `test_jdbcsqlite3` or `test_jdbcpostgresql` also. See the file `activerecord/RUNNING_UNIT_TESTS.rdoc` for information on running more targeted database tests, or the file `ci/travis.rb` for the test suite run by the continuous integration server.
### Warnings
The test suite runs with warnings enabled. Ideally, Ruby on Rails should issue no warnings, but there may be a few, as well as some from third-party libraries. Please ignore (or fix!) them, if any, and submit patches that do not issue new warnings.
-As of this writing (December, 2010) they are specially noisy with Ruby 1.9. If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
+As of this writing (December, 2010) they are especially noisy with Ruby 1.9. If you are sure about what you are doing and would like to have a more clear output, there's a way to override the flag:
```bash
$ RUBYOPT=-W0 bundle exec rake test
@@ -146,7 +193,7 @@ After applying their branch, test it out! Here are some things to think about:
Once you're happy that the pull request contains a good change, comment on the GitHub issue indicating your approval. Your comment should indicate that you like the change and what you like about it. Something like:
<blockquote>
-I like the way you've restructured that code in generate_finder_sql -- much nicer. The tests look good too.
+I like the way you've restructured that code in generate_finder_sql - much nicer. The tests look good too.
</blockquote>
If your comment simply says "+1", then odds are that other reviewers aren't going to take it too seriously. Show that you took the time to review the pull request.
@@ -156,9 +203,15 @@ Contributing to the Rails Documentation
Ruby on Rails has two main sets of documentation: the guides help you in learning about Ruby on Rails, and the API is a reference.
-You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/lifo/docrails/translating-rails-guides).
+You can help improve the Rails guides by making them more coherent, consistent or readable, adding missing information, correcting factual errors, fixing typos, or bringing it up to date with the latest edge Rails. To get involved in the translation of Rails guides, please see [Translating Rails Guides](https://wiki.github.com/rails/docrails/translating-rails-guides).
+
+You can either open a pull request to [Rails](http://github.com/rails/rails) or
+ask the [Rails core team](http://rubyonrails.org/core) for commit access on
+[docrails](http://github.com/rails/docrails) if you contribute regularly.
+Please do not open pull requests in docrails, if you'd like to get feedback on your
+change, ask for it in [Rails](http://github.com/rails/rails) instead.
-If you're confident about your changes, you can push them directly yourself via [docrails](https://github.com/lifo/docrails). Docrails is a branch with an **open commit policy** and public write access. Commits to docrails are still reviewed, but this happens after they are pushed. Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation.
+Docrails is merged with master regularly, so you are effectively editing the Ruby on Rails documentation.
If you are unsure of the documentation changes, you can create an issue in the [Rails](https://github.com/rails/rails/issues) issues tracker on GitHub.
@@ -166,7 +219,7 @@ When working with documentation, please take into account the [API Documentation
NOTE: As explained earlier, ordinary code patches should have proper documentation coverage. Docrails is only used for isolated documentation improvements.
-NOTE: To help our CI servers you can add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
+NOTE: To help our CI servers you should add [ci skip] to your documentation commit message to skip build on that commit. Please remember to use it for commits containing only documentation changes.
WARNING: Docrails has a very strict policy: no code can be touched whatsoever, no matter how trivial or small the change. Only RDoc and guides can be edited via docrails. Also, CHANGELOGs should never be edited in docrails.
@@ -188,41 +241,55 @@ $ cd rails
$ git checkout -b my_new_branch
```
-It doesn’t matter much what name you use, because this branch will only exist on your local computer and your personal repository on Github. It won't be part of the Rails Git repository.
+It doesn't matter much what name you use, because this branch will only exist on your local computer and your personal repository on GitHub. It won't be part of the Rails Git repository.
### 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 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:
* Get the code right.
* Use Rails idioms and helpers.
* Include tests that fail without your code, and pass with it.
* Update the (surrounding) documentation, examples elsewhere, and the guides: whatever is affected by your contribution.
+It is not customary in Rails to run the full test suite before pushing
+changes. The railties test suite in particular takes a long time, and even
+more if the source code is mounted in `/vagrant` as happens in the recommended
+workflow with the [rails-dev-box](https://github.com/rails/rails-dev-box).
+
+As a compromise, test what your code obviously affects, and if the change is
+not in railties run the whole test suite of the affected component. If all is
+green that's enough to propose your contribution. We have [Travis CI](https://travis-ci.org/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
-Rails follows a simple set of coding style conventions.
+Rails follows a simple set of coding style conventions:
* Two spaces, no tabs (for indentation).
* No trailing whitespace. Blank lines should not have any spaces.
* Indent after private/protected.
+* Use Ruby >= 1.9 syntax for hashes. Prefer `{ a: :b }` over `{ :a => :b }`.
* Prefer `&&`/`||` over `and`/`or`.
* Prefer class << self over self.method for class methods.
* Use `MyClass.my_method(my_arg)` not `my_method( my_arg )` or `my_method my_arg`.
-* Use a = b and not a=b.
+* Use `a = b` and not `a=b`.
+* Use assert_not methods instead of refute.
+* Prefer `method { do_stuff }` instead of `method{do_stuff}` for single-line blocks.
* Follow the conventions in the source you see used already.
-The above are guidelines -- please use your best judgment in using them.
+The above are guidelines - please use your best judgment in using them.
### 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, commiting a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG.
+You should add an entry to the CHANGELOG of the framework that you modified if you're adding or removing a feature, committing a bug fix or adding deprecation notices. Refactorings and documentation changes generally should not go to the CHANGELOG.
-A CHANGELOG entry should summarize what was changed and should end with author's name. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach issue's number. Here is an example CHANGELOG entry:
+A CHANGELOG entry should summarize what was changed and should end with author's name and it should go on top of a CHANGELOG. You can use multiple lines if you need more space and you can attach code examples indented with 4 spaces. If a change is related to a specific issue, you should attach issue's number. Here is an example CHANGELOG entry:
```
* Summary of a change that briefly describes what was changed. You can use multiple
@@ -243,9 +310,13 @@ Your name can be added directly after the last word if you don't provide any cod
### Sanity Check
-You should not be the only person who looks at the code before you submit it. You know at least one other Rails developer, right? Show them what you’re doing and ask for feedback. Doing this in private before you push a patch out publicly is the “smoke test†for a patch: if you can’t convince one other developer of the beauty of your code, you’re unlikely to convince the core team either.
-
-You might want also to check out the [RailsBridge BugMash](http://wiki.railsbridge.org/projects/railsbridge/wiki/BugMash) as a way to get involved in a group effort to improve Rails. This can help you get started and help you check your code when you're writing your first patches.
+You should not be the only person who looks at the code before you submit it.
+If you know someone else who uses Rails, try asking them if they'll check out
+your work. If you don't know anyone else using Rails, try hopping into the IRC
+room or posting about your idea to the rails-core mailing list. Doing this in
+private before you push a patch out publicly is the “smoke test†for a patch:
+if you can’t convince one other developer of the beauty of your code, you’re
+unlikely to convince the core team either.
### Commit Your Changes
@@ -287,9 +358,9 @@ You can also add bullet points:
TIP. Please squash your commits into a single commit when appropriate. This simplifies future cherry picks, and also keeps the git log clean.
-### Update Master
+### Update Your Branch
-It’s pretty likely that other changes to master have happened while you were working. Go get them:
+It's pretty likely that other changes to master have happened while you were working. Go get them:
```bash
$ git checkout master
@@ -348,7 +419,7 @@ Update your fork:
$ git push origin master
```
-If you want to update another branches:
+If you want to update another branch:
```bash
$ git checkout branch_name
@@ -359,21 +430,81 @@ $ git push origin branch_name
### Issue a Pull Request
-Navigate to the Rails repository you just pushed to (e.g. https://github.com/your-user-name/rails) and press "Pull Request" in the upper right hand corner.
-
-Write your branch name in the branch field (this is filled with "master" by default) and press "Update Commit Range".
+Navigate to the Rails repository you just pushed to (e.g.
+https://github.com/your-user-name/rails) and click on "Pull Requests" seen in
+the right panel. On the next page, press "New pull request" in the upper right
+hand corner.
-Ensure the changesets you introduced are included in the "Commits" tab. Ensure that the "Files Changed" incorporate all of your changes.
+Click on "Edit", if you need to change the branches being compared (it compares
+"master" by default) and press "Click to create a pull request for this
+comparison".
-Fill in some details about your potential patch including a meaningful title. When finished, press "Send pull request". The Rails core team will be notified about your submission.
+Ensure the changesets you introduced are included. Fill in some details about
+your potential patch including a meaningful title. When finished, press "Send
+pull request". The Rails core team will be notified about your submission.
### Get some Feedback
-Now you need to get other people to look at your patch, just as you've looked at other people's patches. You can use the [rubyonrails-core mailing list](http://groups.google.com/group/rubyonrails-core/) or the #rails-contrib channel on IRC freenode for this. You might also try just talking to Rails developers that you know.
+Most pull requests will go through a few iterations before they get merged.
+Different contributors will sometimes have different opinions, and often
+patches will need revised before they can get merged.
+
+Some contributors to Rails have email notifications from GitHub turned on, but
+others do not. Furthermore, (almost) everyone who works on Rails is a
+volunteer, and so it may take a few days for you to get your first feedback on
+a pull request. Don't despair! Sometimes it's quick, sometimes it's slow. Such
+is the open source life.
+
+If it's been over a week, and you haven't heard anything, you might want to try
+and nudge things along. You can use the [rubyonrails-core mailing
+list](http://groups.google.com/group/rubyonrails-core/) for this. You can also
+leave another comment on the pull request.
+
+While you're waiting for feedback on your pull request, open up a few other
+pull requests and give someone else some! I'm sure they'll appreciate it in
+the same way that you appreciate feedback on your patches.
### Iterate as Necessary
-It’s entirely possible that the feedback you get will suggest changes. Don’t get discouraged: the whole point of contributing to an active open source project is to tap into community knowledge. If people are encouraging you to tweak your code, then it’s worth making the tweaks and resubmitting. If the feedback is that your code doesn’t belong in the core, you might still think about releasing it as a gem.
+It's entirely possible that the feedback you get will suggest changes. Don't get discouraged: the whole point of contributing to an active open source project is to tap into 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.
+
+#### Squashing commits
+
+One of the things that we may ask you to do is "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
+can be a bit easier to follow. Rails is a large project, and a bunch of
+extraneous commits can add a lot of noise.
+
+In order to do this, you'll need to have a git remote that points at the main
+Rails repository. This is useful anyway, but just in case you don't have it set
+up, make sure that you do this first:
+
+```bash
+$ git remote add upstream https://github.com/rails/rails.git
+```
+
+You can call this remote whatever you'd like, but if you don't use `upstream`,
+then change the name to your own in the instructions below.
+
+Given that your remote branch is called `my_pull_request`, then you can do the
+following:
+
+```bash
+$ git fetch upstream
+$ git checkout my_pull_request
+$ git rebase upstream/master
+$ git rebase -i
+
+< Choose 'squash' for all of your commits except the first one. >
+< Edit the commit message to make sense, and describe all your changes. >
+
+$ 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
diff --git a/guides/source/credits.html.erb b/guides/source/credits.html.erb
index e25168d58d..5beae9c29b 100644
--- a/guides/source/credits.html.erb
+++ b/guides/source/credits.html.erb
@@ -28,11 +28,11 @@ Ruby on Rails Guides: Credits
<h3 class="section">Rails Guides Authors</h3>
<%= author('Ryan Bigg', 'radar', 'radar.png') do %>
-Ryan Bigg works as a consultant at <a href="http://rubyx.com">RubyX</a> and has been working with Rails since 2006. He's co-authoring a book called <a href="http://manning.com/katz">Rails 3 in Action</a> and he's written many gems which can be seen on <a href="http://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
+ Ryan Bigg works as the Community Manager at <a href="http://spreecommerce.com">Spree Commerce</a> and has been working with Rails since 2006. He's the author of <a href="https://leanpub.com/multi-tenancy-rails">Multi Tenancy With Rails</a> and co-author of <a href="http://manning.com/bigg2">Rails 4 in Action</a>. He's written many gems which can be seen on <a href="https://github.com/radar">his GitHub page</a> and he also tweets prolifically as <a href="http://twitter.com/ryanbigg">@ryanbigg</a>.
<% end %>
<%= author('Oscar Del Ben', 'oscardelben', 'oscardelben.jpg') do %>
-Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">Github account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>.
+Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wildfire</a>. He's a regular open source contributor (<a href="https://github.com/oscardelben">GitHub account</a>) and tweets regularly at <a href="https://twitter.com/oscardelben">@oscardelben</a>.
<% end %>
<%= author('Frederick Cheung', 'fcheung') do %>
@@ -40,7 +40,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<% end %>
<%= author('Tore Darell', 'toretore') do %>
- Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. His home on the internet is his blog <a href="http://tore.darell.no">Sneaky Abstractions</a>.
+ Tore Darell is an independent developer based in Menton, France who specialises in cruft-free web applications using Ruby, Rails and unobtrusive JavaScript. His home on the Internet is his blog <a href="http://tore.darell.no">Sneaky Abstractions</a>.
<% end %>
<%= author('Jeff Dean', 'zilkey') do %>
@@ -74,3 +74,7 @@ Oscar Del Ben is a software engineer at <a href="http://www.wildfireapp.com/">Wi
<%= author('Heiko Webers', 'hawe') do %>
Heiko Webers is the founder of <a href="http://www.bauland42.de">bauland42</a>, a German web application security consulting and development company focused on Ruby on Rails. He blogs at the <a href="http://www.rorsecurity.info">Ruby on Rails Security Project</a>. After 10 years of desktop application development, Heiko has rarely looked back.
<% end %>
+
+<%= author('Akshay Surve', 'startupjockey', 'akshaysurve.jpg') do %>
+ Akshay Surve is the Founder at <a href="http://www.deltax.com">DeltaX</a>, hackathon specialist, a midnight code junkie and ocassionally writes prose. You can connect with him on <a href="https://twitter.com/akshaysurve">Twitter</a>, <a href="http://www.linkedin.com/in/akshaysurve">Linkedin</a>, <a href="http://www.akshaysurve.com/">Personal Blog</a> or <a href="http://www.quora.com/Akshay-Surve">Quora</a>.
+<% end %>
diff --git a/guides/source/debugging_rails_applications.md b/guides/source/debugging_rails_applications.md
index a72f54a1b8..226137c89a 100644
--- a/guides/source/debugging_rails_applications.md
+++ b/guides/source/debugging_rails_applications.md
@@ -1,12 +1,14 @@
Debugging Rails Applications
============================
-This guide introduces techniques for debugging Ruby on Rails applications. By referring to this guide, you will be able to:
+This guide introduces techniques for debugging Ruby on Rails applications.
-* Understand the purpose of debugging
-* Track down problems and issues in your application that your tests aren't identifying
-* Learn the different ways of debugging
-* Analyze the stack trace
+After reading this guide, you will know:
+
+* The purpose of debugging.
+* How to track down problems and issues in your application that your tests aren't identifying.
+* The different ways of debugging.
+* How to analyze the stack trace.
--------------------------------------------------------------------------------
@@ -21,13 +23,13 @@ One common task is to inspect the contents of a variable. In Rails, you can do t
### `debug`
-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:
+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 %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @post.title %>
</p>
```
@@ -56,7 +58,7 @@ Displaying an instance variable, or any other object or method, in YAML format c
<%= simple_format @post.to_yaml %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @post.title %>
</p>
```
@@ -86,7 +88,7 @@ Another useful method for displaying object values is `inspect`, especially when
<%= [1, 2, 3, 4, 5].inspect %>
<p>
<b>Title:</b>
- <%=h @post.title %>
+ <%= @post.title %>
</p>
```
@@ -105,7 +107,7 @@ It can also be useful to save information to log files at runtime. Rails maintai
### What is the Logger?
-Rails makes use of the `ActiveSupport::BufferedLogger` class to write log information. You can also substitute another logger such as `Log4r` if you wish.
+Rails makes use of the `ActiveSupport::Logger` class to write log information. You can also substitute another logger such as `Log4r` if you wish.
You can specify an alternative logger in your `environment.rb` or any environment file:
@@ -164,7 +166,7 @@ class PostsController < ApplicationController
logger.debug "The post was saved and now the user is going to be redirected..."
redirect_to(@post)
else
- render :action => "new"
+ render action: "new"
end
end
@@ -172,7 +174,7 @@ class PostsController < ApplicationController
end
```
-Here's an example of the log generated by this method:
+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]
@@ -192,11 +194,13 @@ Redirected to #<Post:0x20af760>
Completed in 0.01224 (81 reqs/sec) | DB: 0.00044 (3%) | 302 Found [http://localhost/posts]
```
-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.
+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.
### Tagged Logging
-When running multi-user, multi-account applications, it’s often useful to be able to filter the logs using some custom rules. `TaggedLogging` in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications.
+When running multi-user, multi-account applications, it's often useful
+to be able to filter the logs using some custom rules. `TaggedLogging`
+in Active Support helps in doing exactly that by stamping log lines with subdomains, request ids, and anything else to aid debugging such applications.
```ruby
logger = ActiveSupport::TaggedLogging.new(Logger.new(STDOUT))
@@ -205,6 +209,37 @@ logger.tagged("BCX", "Jason") { logger.info "Stuff" } # Logs "
logger.tagged("BCX") { logger.tagged("Jason") { logger.info "Stuff" } } # Logs "[BCX] [Jason] Stuff"
```
+### Impact of Logs on Performance
+Logging will always have a small impact on performance of your rails app,
+ particularly when logging to disk.However, there are a few subtleties:
+
+Using the `:debug` level will have a greater performance penalty than `:fatal`,
+ as a far greater number of strings are being evaluated and written to the
+ log output (e.g. disk).
+
+Another potential pitfall is that if you have many calls to `Logger` like this
+ in your code:
+
+```ruby
+logger.debug "Person attributes hash: #{@person.attributes.inspect}"
+```
+
+In the above example, There will be a performance impact even if the allowed
+output level doesn't include debug. The reason is that Ruby has to evaluate
+these strings, which includes instantiating the somewhat heavy `String` object
+and interpolating the variables, and which takes time.
+Therefore, it's recommended to pass blocks to the logger methods, as these are
+only evaluated if the output level is the same or included in the allowed level
+(i.e. lazy loading). The same code rewritten would be:
+
+```ruby
+logger.debug {"Person attributes hash: #{@person.attributes.inspect}"}
+```
+
+The contents of the block, and therefore the string interpolation, is only
+evaluated if debug is enabled. This performance savings is only really
+noticeable with large amounts of logging, but it's a good practice to employ.
+
Debugging with the `debugger` gem
---------------------------------
@@ -214,7 +249,7 @@ The debugger can also help you if you want to learn about the Rails source code
### Setup
-Rails uses the `debugger` gem to set breakpoints and step through live code. To install it, just run:
+You can use the `debugger` gem to set breakpoints and step through live code in Rails. To install it, just run:
```bash
$ gem install debugger
@@ -233,7 +268,7 @@ class PeopleController < ApplicationController
end
```
-If you see the message in the console or logs:
+If you see this message in the console or logs:
```
***** Debugger requested, but was not available: Start server with --debugger to enable *****
@@ -244,12 +279,12 @@ Make sure you have started your web server with the option `--debugger`:
```bash
$ rails server --debugger
=> Booting WEBrick
-=> Rails 3.0.0 application starting on http://0.0.0.0:3000
+=> Rails 4.0.0 application starting on http://0.0.0.0:3000
=> Debugger enabled
...
```
-TIP: In development mode, you can dynamically `require \'debugger\'` instead of restarting the server, if it was started without `--debugger`.
+TIP: In development mode, you can dynamically `require \'debugger\'` instead of restarting the server, even if it was started without `--debugger`.
### The Shell
@@ -264,7 +299,7 @@ For example:
(rdb:7)
```
-Now it's time to explore and dig into your application. A good place to start is by asking the debugger for help... so type: `help` (You didn't see that coming, right?)
+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`
```
(rdb:7) help
@@ -279,7 +314,7 @@ condition down finish list ps save thread var
continue edit frame method putl set tmate where
```
-TIP: To view the help menu for any command use `help <command-name>` in active debug mode. For example: _`help var`_
+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.
@@ -287,7 +322,7 @@ This command shows you where you are in the code by printing 10 lines centered a
```
(rdb:7) list
-[1, 10] in /PathToProject/posts_controller.rb
+[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
1 class PostsController < ApplicationController
2 # GET /posts
3 # GET /posts.json
@@ -297,7 +332,7 @@ This command shows you where you are in the code by printing 10 lines centered a
7
8 respond_to do |format|
9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
+ 10 format.json { render json: @posts }
```
If you repeat the `list` command, this time using just `l`, the next ten lines of the file will be printed out.
@@ -323,7 +358,7 @@ On the other hand, to see the previous ten lines you should type `list-` (or `l-
```
(rdb:7) l-
-[1, 10] in /PathToProject/posts_controller.rb
+[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
1 class PostsController < ApplicationController
2 # GET /posts
3 # GET /posts.json
@@ -333,7 +368,7 @@ On the other hand, to see the previous ten lines you should type `list-` (or `l-
7
8 respond_to do |format|
9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
+ 10 format.json { render json: @posts }
```
This way you can move inside the file, being able to see the code above and over the line you added the `debugger`.
@@ -341,7 +376,7 @@ Finally, to see where you are in the code again you can type `list=`
```
(rdb:7) list=
-[1, 10] in /PathToProject/posts_controller.rb
+[1, 10] in /PathTo/project/app/controllers/posts_controller.rb
1 class PostsController < ApplicationController
2 # GET /posts
3 # GET /posts.json
@@ -351,7 +386,7 @@ Finally, to see where you are in the code again you can type `list=`
7
8 respond_to do |format|
9 format.html # index.html.erb
- 10 format.json { render :json => @posts }
+ 10 format.json { render json: @posts }
```
### The Context
@@ -500,7 +535,7 @@ TIP: You can use the debugger while using `rails console`. Just remember to `req
```
$ rails console
-Loading development environment (Rails 3.1.0)
+Loading development environment (Rails 4.0.0)
>> require "debugger"
=> []
>> author = Author.first
@@ -644,7 +679,7 @@ In this section, you will learn how to find and fix such leaks by using tool suc
[Valgrind](http://valgrind.org/) is a Linux-only application for detecting C-based memory leaks and race conditions.
-There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, a C extension in the interpreter calls `malloc()` but is doesn't properly call `free()`, this memory won't be available until the app terminates.
+There are Valgrind tools that can automatically detect many memory management and threading bugs, and profile your programs in detail. For example, if a C extension in the interpreter calls `malloc()` but doesn't properly call `free()`, this memory won't be available until the app terminates.
For further information on how to install Valgrind and use with Ruby, refer to [Valgrind and Ruby](http://blog.evanweaver.com/articles/2008/02/05/valgrind-and-ruby/) by Evan Weaver.
@@ -653,21 +688,20 @@ 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.
+* [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](http://github.com/cldwalker/debugger)
-* [Article: Debugging a Rails application with ruby-debug](http://www.sitepoint.com/article/debug-rails-app-ruby-debug/)
-* [ruby-debug Basics screencast](http://brian.maybeyoureinsane.net/blog/2007/05/07/ruby-debug-basics-screencast/)
+* [debugger Homepage](https://github.com/cldwalker/debugger)
+* [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)
* [Ryan Bates' logger screencast](http://railscasts.com/episodes/56-the-logger)
* [Debugging with ruby-debug](http://bashdb.sourceforge.net/ruby-debug.html)
-* [ruby-debug cheat sheet](http://cheat.errtheblog.com/s/rdebug/)
-* [Ruby on Rails Wiki: How to Configure Logging](http://wiki.rubyonrails.org/rails/pages/HowtoConfigureLogging)
diff --git a/guides/source/development_dependencies_install.md b/guides/source/development_dependencies_install.md
index aaf14f29c1..c4e5789a1a 100644
--- a/guides/source/development_dependencies_install.md
+++ b/guides/source/development_dependencies_install.md
@@ -3,6 +3,12 @@ Development Dependencies Install
This guide covers how to setup an environment for Ruby on Rails core development.
+After reading this guide, you will know:
+
+* How to set up your machine for Rails development
+* How to run specific groups of unit tests from the Rails test suite
+* How the ActiveRecord portion of the Rails test suite operates
+
--------------------------------------------------------------------------------
The Easy Way
@@ -19,8 +25,10 @@ In case you can't use the Rails development box, see section above, these are th
Ruby on Rails uses Git for source code control. The [Git homepage](http://git-scm.com/) has installation instructions. There are a variety of resources on the net that will help you get familiar with Git:
-* [Everyday Git](http://schacon.github.com/git/everyday.html) will teach you just enough about Git to get by.
-* The [PeepCode screencast](https://peepcode.com/products/git) on Git ($9) is easier to follow.
+* [Try Git course](http://try.github.io/) is an interactive course that will teach you the basics.
+* The [official Documentation](http://git-scm.com/documentation) is pretty comprehensive and also contains some videos with the basics of Git
+* [Everyday Git](http://schacon.github.io/git/everyday.html) will teach you just enough about Git to get by.
+* The [PeepCode screencast](https://peepcode.com/products/git) on Git is easier to follow.
* [GitHub](http://help.github.com) offers links to a variety of Git resources.
* [Pro Git](http://git-scm.com/book) is an entire book about Git with a Creative Commons license.
@@ -49,9 +57,24 @@ If you are on Fedora or CentOS, you can run
$ sudo yum install libxml2 libxml2-devel libxslt libxslt-devel
```
-If you have any problems with these libraries, you should install them manually compiling the source code. Just follow the instructions at the [Red Hat/CentOS section of the Nokogiri tutorials](http://nokogiri.org/tutorials/installing_nokogiri.html#red_hat__centos) .
+If you are running Arch Linux, you're done with:
+
+```bash
+$ sudo pacman -S libxml2 libxslt
+```
-Also, SQLite3 and its development files for the `sqlite3-ruby` gem -- in Ubuntu you're done with just
+On FreeBSD, you just have to run:
+
+```bash
+# pkg_add -r libxml2 libxslt
+```
+
+Alternatively, you can install the `textproc/libxml2` and `textproc/libxslt`
+ports.
+
+If you have any problems with these libraries, you can install them manually by compiling the source code. Just follow the instructions at the [Red Hat/CentOS section of the Nokogiri tutorials](http://nokogiri.org/tutorials/installing_nokogiri.html#red_hat__centos) .
+
+Also, SQLite3 and its development files for the `sqlite3-ruby` gem - in Ubuntu you're done with just
```bash
$ sudo apt-get install sqlite3 libsqlite3-dev
@@ -63,7 +86,21 @@ And if you are on Fedora or CentOS, you're done with
$ sudo yum install sqlite3 sqlite3-devel
```
-Get a recent version of [Bundler](http://gembundler.com/:)
+If you are on Arch Linux, you will need to run:
+
+```bash
+$ sudo pacman -S sqlite
+```
+
+For FreeBSD users, you're done with:
+
+```bash
+# pkg_add -r sqlite3
+```
+
+Or compile the `databases/sqlite3` port.
+
+Get a recent version of [Bundler](http://gembundler.com/)
```bash
$ gem install bundler
@@ -76,7 +113,29 @@ and run:
$ bundle install --without db
```
-This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon. With dependencies installed, you can run the test suite with:
+This command will install all dependencies except the MySQL and PostgreSQL Ruby drivers. We will come back to these soon.
+
+NOTE: If you would like to run the tests that use memcached, you need to ensure that you have it installed and running.
+
+You can use homebrew to install memcached on OSX:
+
+```bash
+$ brew install memcached
+```
+
+On Ubuntu you can install it with apt-get:
+
+```bash
+$ sudo apt-get install memcached
+```
+
+Or use yum on Fedora or CentOS:
+
+```bash
+$ sudo yum install memcached
+```
+
+With the dependencies now installed, you can run the test suite with:
```bash
$ bundle exec rake test
@@ -89,20 +148,27 @@ $ cd actionpack
$ bundle exec rake test
```
-If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests of the `railties/test/generators` directory only:
+If you want to run the tests located in a specific directory use the `TEST_DIR` environment variable. For example, this will run the tests in the `railties/test/generators` directory only:
```bash
$ cd railties
$ TEST_DIR=generators bundle exec rake test
```
-You can run any single test separately too:
+You can run the tests for a particular file by using:
```bash
$ cd actionpack
$ bundle exec ruby -Itest test/template/form_helper_test.rb
```
+Or, you can run a single test in a particular file:
+
+```bash
+$ cd actionpack
+$ bundle exec ruby -Itest path/to/test.rb -n test_name
+```
+
### Active Record Setup
The test suite of Active Record attempts to run four times: once for SQLite3, once for each of the two MySQL gems (`mysql` and `mysql2`), and once for PostgreSQL. We are going to see now how to set up the environment for them.
@@ -129,18 +195,40 @@ $ sudo yum install mysql-server mysql-devel
$ sudo yum install postgresql-server postgresql-devel
```
-After that run:
+If you are running Arch Linux, MySQL isn't supported anymore so you will need to
+use MariaDB instead (see [this announcement](https://www.archlinux.org/news/mariadb-replaces-mysql-in-repositories/)):
+
+```bash
+$ sudo pacman -S mariadb libmariadbclient mariadb-clients
+$ sudo pacman -S postgresql postgresql-libs
+```
+
+FreeBSD users will have to run the following:
+
+```bash
+# pkg_add -r mysql56-client mysql56-server
+# pkg_add -r postgresql92-client postgresql92-server
+```
+
+Or install them through ports (they are located under the `databases` folder).
+If you run into troubles during the installation of MySQL, please see
+[the MySQL documentation](http://dev.mysql.com/doc/refman/5.1/en/freebsd-installation.html).
+
+After that, run:
```bash
$ rm .bundle/config
$ bundle install
```
-We need first to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file).
+First, we need to delete `.bundle/config` because Bundler remembers in that file that we didn't want to install the "db" group (alternatively you can edit the file).
In order to be able to run the test suite against MySQL you need to create a user named `rails` with privileges on the test databases:
```bash
+$ mysql -uroot -p
+
+mysql> CREATE USER 'rails'@'localhost';
mysql> GRANT ALL PRIVILEGES ON activerecord_unittest.*
to 'rails'@'localhost';
mysql> GRANT ALL PRIVILEGES ON activerecord_unittest2.*
@@ -167,8 +255,22 @@ $ cd activerecord
$ bundle exec rake postgresql:build_databases
```
+It is possible to build databases for both PostgreSQL and MySQL with
+
+```bash
+$ cd activerecord
+$ bundle exec rake db:create
+```
+
+You can cleanup the databases using
+
+```bash
+$ cd activerecord
+$ bundle exec rake db:drop
+```
+
NOTE: Using the rake task to create the test databases ensures they have the correct character set and collation.
NOTE: You'll see the following warning (or localized warning) during activating HStore extension in PostgreSQL 9.1.x or earlier: "WARNING: => is deprecated as an operator".
-If you’re using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
+If you're using another database, check the file `activerecord/test/config.yml` or `activerecord/test/config.example.yml` for default connection information. You can edit `activerecord/test/config.yml` to provide different credentials on your machine if you must, but obviously you should not push any such changes back to Rails.
diff --git a/guides/source/documents.yaml b/guides/source/documents.yaml
index 0b22423798..1bf9ff95e1 100644
--- a/guides/source/documents.yaml
+++ b/guides/source/documents.yaml
@@ -1,4 +1,4 @@
--
+-
name: Start Here
documents:
-
@@ -9,13 +9,21 @@
name: Models
documents:
-
+ name: Active Record Basics
+ 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
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 and Callbacks
- url: active_record_validations_callbacks.html
- description: This guide covers how you can use Active Record validations and callbacks.
+ name: Active Record Validations
+ url: active_record_validations.html
+ description: This guide covers how you can use Active Record validations
+ -
+ name: Active Record Callbacks
+ url: active_record_callbacks.html
+ description: This guide covers how you can use Active Record callbacks.
-
name: Active Record Associations
url: association_basics.html
@@ -26,82 +34,86 @@
description: This guide covers the database query interface provided by Active Record.
-
name: Views
- documents:
- -
+ documents:
+ -
+ name: Action View Overview
+ url: action_view_overview.html
+ description: This guide provides an introduction to Action View and introduces a few of the more common view helpers.
+ work_in_progress: true
+ -
name: Layouts and Rendering in Rails
url: layouts_and_rendering.html
description: This guide covers the basic layout features of Action Controller and Action View, including rendering and redirecting, using content_for blocks, and working with partials.
- -
+ -
name: Action View Form Helpers
url: form_helpers.html
description: Guide to using built-in Form helpers.
-
name: Controllers
documents:
- -
+ -
name: Action Controller Overview
url: action_controller_overview.html
description: This guide covers how controllers work and how they fit into the request cycle in your application. It includes sessions, filters, and cookies, data streaming, and dealing with exceptions raised by a request, among other topics.
- -
+ -
name: Rails Routing from the Outside In
url: routing.html
description: This guide covers the user-facing features of Rails routing. If you want to understand how to use routing in your own Rails applications, start here.
-
name: Digging Deeper
documents:
- -
+ -
name: Active Support Core Extensions
url: active_support_core_extensions.html
description: This guide documents the Ruby core extensions defined in Active Support.
- -
+ -
name: Rails Internationalization API
url: i18n.html
description: This guide covers how to add internationalization to your applications. Your application will be able to translate content to different languages, change pluralization rules, use correct date formats for each country and so on.
- -
+ -
name: Action Mailer Basics
url: action_mailer_basics.html
- work_in_progress: true
description: This guide describes how to use Action Mailer to send and receive emails.
- -
+ -
name: Testing Rails Applications
url: testing.html
work_in_progress: true
description: This is a rather comprehensive guide to doing both unit and functional tests in Rails. It covers everything from 'What is a test?' to the testing APIs. Enjoy.
- -
+ -
name: Securing Rails Applications
url: security.html
description: This guide describes common security problems in web applications and how to avoid them with Rails.
- -
+ -
name: Debugging Rails Applications
url: debugging_rails_applications.html
description: This guide describes how to debug Rails applications. It covers the different ways of achieving this and how to understand what is happening "behind the scenes" of your code.
- -
- name: Performance Testing Rails Applications
- url: performance_testing.html
- description: This guide covers the various ways of performance testing a Ruby on Rails application.
- -
+ -
name: Configuring Rails Applications
url: configuring.html
description: This guide covers the basic configuration settings for a Rails application.
- -
+ -
name: Rails Command Line Tools and Rake Tasks
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.
- -
+ -
+ name: Working with JavaScript in Rails
+ url: working_with_javascript_in_rails.html
+ description: This guide covers the built-in Ajax/JavaScript functionality of Rails.
+ -
name: Getting Started with Engines
url: engines.html
description: This guide explains how to write a mountable engine.
work_in_progress: true
- -
+ -
name: The Rails Initialization Process
work_in_progress: true
url: initialization.html
@@ -109,38 +121,45 @@
-
name: Extending Rails
documents:
- -
+ -
name: The Basics of Creating Rails Plugins
work_in_progress: true
url: plugins.html
description: This guide covers how to build a plugin to extend the functionality of Rails.
- -
+ -
name: Rails on Rack
url: rails_on_rack.html
description: This guide covers Rails integration with Rack and interfacing with other Rack components.
- -
+ -
name: Creating and Customizing Rails Generators
url: generators.html
description: This guide covers the process of adding a brand new generator to your extension or providing an alternative to an element of a built-in Rails generator (such as providing alternative test stubs for the scaffold generator).
-
name: Contributing to Ruby on Rails
documents:
- -
+ -
name: Contributing to Ruby on Rails
url: contributing_to_ruby_on_rails.html
description: Rails is not 'somebody else's framework.' This guide covers a variety of ways that you can get involved in the ongoing development of Rails.
- -
+ -
name: API Documentation Guidelines
url: api_documentation_guidelines.html
description: This guide documents the Ruby on Rails API documentation guidelines.
- -
+ -
name: Ruby on Rails Guides Guidelines
url: ruby_on_rails_guides_guidelines.html
description: This guide documents the Ruby on Rails guides guidelines.
-
+ name: Maintenance Policy
+ documents:
+ -
+ name: Maintenance Policy
+ url: maintenance_policy.html
+ description: What versions of Ruby on Rails are currently supported, and when to expect new versions.
+-
name: Release Notes
documents:
- -
+ -
name: Upgrading Ruby on Rails
url: upgrading_ruby_on_rails.html
work_in_progress: true
@@ -157,15 +176,15 @@
name: Ruby on Rails 3.1 Release Notes
url: 3_1_release_notes.html
description: Release notes for Rails 3.1.
- -
+ -
name: Ruby on Rails 3.0 Release Notes
url: 3_0_release_notes.html
description: Release notes for Rails 3.0.
- -
+ -
name: Ruby on Rails 2.3 Release Notes
url: 2_3_release_notes.html
description: Release notes for Rails 2.3.
- -
+ -
name: Ruby on Rails 2.2 Release Notes
url: 2_2_release_notes.html
description: Release notes for Rails 2.2.
diff --git a/guides/source/engines.md b/guides/source/engines.md
index 97af423f3e..c71b728ef7 100644
--- a/guides/source/engines.md
+++ b/guides/source/engines.md
@@ -1,22 +1,24 @@
Getting Started with Engines
============================
-In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface. You will learn the following things in this guide:
+In this guide you will learn about engines and how they can be used to provide additional functionality to their host applications through a clean and very easy-to-use interface.
-* What makes an engine
-* How to generate an engine
-* Building features for the engine
-* Hooking the engine into an application
-* Overriding engine functionality in the application
+After reading this guide, you will know:
+
+* What makes an engine.
+* How to generate an engine.
+* Building features for the engine.
+* Hooking the engine into an application.
+* Overriding engine functionality in the application.
--------------------------------------------------------------------------------
What are engines?
-----------------
-Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behaviour from `Rails::Engine`.
+Engines can be considered miniature applications that provide functionality to their host applications. A Rails application is actually just a "supercharged" engine, with the `Rails::Application` class inheriting a lot of its behavior from `Rails::Engine`.
-Therefore, engines and applications can be thought of almost the same thing, just with very minor differences, as you'll see throughout this guide. Engines and applications also share a common structure.
+Therefore, engines and applications can be thought of almost the same thing, just with subtle differences, as you'll see throughout this guide. Engines and applications also share a common structure.
Engines are also closely related to plugins where the two share a common `lib` directory structure and are both generated using the `rails plugin new` generator. The difference being that an engine is considered a "full plugin" by Rails as indicated by the `--full` option that's passed to the generator command, but this guide will refer to them simply as "engines" throughout. An engine **can** be a plugin, and a plugin **can** be an engine.
@@ -26,14 +28,14 @@ Engines can also be isolated from their host applications. This means that an ap
It's important to keep in mind at all times that the application should **always** take precedence over its engines. An application is the object that has final say in what goes on in the universe (with the universe being the application's environment) where the engine should only be enhancing it, rather than changing it drastically.
-To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/resolve/refinerycms), a CMS engine.
+To see demonstrations of other engines, check out [Devise](https://github.com/plataformatec/devise), an engine that provides authentication for its parent applications, or [Forem](https://github.com/radar/forem), an engine that provides forum functionality. There's also [Spree](https://github.com/spree/spree) which provides an e-commerce platform, and [RefineryCMS](https://github.com/refinery/refinerycms), a CMS engine.
Finally, engines would not have been possible without the work of James Adam, Piotr Sarnacki, the Rails Core Team, and a number of other people. If you ever meet them, don't forget to say thanks!
Generating an engine
--------------------
-To generate an engine with Rails 3.2, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal:
+To generate an engine, you will need to run the plugin generator and pass it options as appropriate to the need. For the "blorgh" example, you will need to create a "mountable" engine, running this command in a terminal:
```bash
$ rails plugin new blorgh --mountable
@@ -55,7 +57,7 @@ The `--full` option tells the generator that you want to create an engine, inclu
end
```
* A file at `lib/blorgh/engine.rb` which is identical in function to a standard Rails application's `config/application.rb` file:
-
+
```ruby
module Blorgh
class Engine < ::Rails::Engine
@@ -70,12 +72,12 @@ The `--mountable` option tells the generator that you want to create a "mountabl
* A namespaced `ApplicationHelper` stub
* A layout view template for the engine
* Namespace isolation to `config/routes.rb`:
-
+
```ruby
Blorgh::Engine.routes.draw do
end
```
-
+
* Namespace isolation to `lib/blorgh/engine.rb`:
```ruby
@@ -89,7 +91,7 @@ The `--mountable` option tells the generator that you want to create a "mountabl
Additionally, the `--mountable` option tells the generator to mount the engine inside the dummy testing application located at `test/dummy` by adding the following to the dummy application's routes file at `test/dummy/config/routes.rb`:
```ruby
-mount Blorgh::Engine, :at => "blorgh"
+mount Blorgh::Engine, at: "blorgh"
```
### Inside an engine
@@ -99,10 +101,10 @@ mount Blorgh::Engine, :at => "blorgh"
At the root of this brand new engine's directory lives a `blorgh.gemspec` file. When you include the engine into an application later on, you will do so with this line in the Rails application's `Gemfile`:
```ruby
-gem 'blorgh', :path => "vendor/engines/blorgh"
+gem 'blorgh', path: "vendor/engines/blorgh"
```
-By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`.
+Don't forget to run `bundle install` as usual. By specifying it as a gem within the `Gemfile`, Bundler will load it as such, parsing this `blorgh.gemspec` file and requiring a file within the `lib` directory called `lib/blorgh.rb`. This file requires the `blorgh/engine.rb` file (located at `lib/blorgh/engine.rb`) and defines a base module called `Blorgh`.
```ruby
require "blorgh/engine"
@@ -147,9 +149,9 @@ Lastly, the `app/views` directory contains a `layouts` folder which contains a f
If you don't want to force a layout on to users of the engine, then you can delete this file and reference a different layout in the controllers of your engine.
-#### `script` directory
+#### `bin` directory
-This directory contains one file, `script/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
+This directory contains one file, `bin/rails`, which enables you to use the `rails` sub-commands and generators just like you would within an application. This means that you will very easily be able to generate new controllers and models for this engine by running commands like this:
```bash
rails g model
@@ -169,7 +171,7 @@ end
This line mounts the engine at the path `/blorgh`, which will make it accessible through the application only at that path.
-Also in the test directory is the `test/integration` directory, where integration tests for the engine should be placed. Other directories can be created in the `test` directory also. For example, you may wish to create a `test/models` directory for your models tests.
+In the test directory there is the `test/integration` directory, where integration tests for the engine should be placed. Other directories can be created in the `test` directory as well. For example, you may wish to create a `test/models` directory for your models tests.
Providing engine functionality
------------------------------
@@ -230,7 +232,8 @@ Blorgh::Engine.routes.draw do
end
```
-Note here that the routes are drawn upon the `Blorgh::Engine` object rather than the `YourApp::Application` class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the [test directory](#test-directory) section. This is also what causes the engine's routes to be isolated from those routes that are within the application. This is discussed further in the [Routes](#routes) section of this guide.
+Note here that the routes are drawn upon the `Blorgh::Engine` object rather than the `YourApp::Application` class. This is so that the engine routes are confined to the engine itself and can be mounted at a specific point as shown in the [test directory](#test-directory) section. It also causes the engine's routes to be isolated from those routes that are within the application. The [Routes](#routes) section of
+this guide describes it in details.
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`).
@@ -256,11 +259,11 @@ module Blorgh
end
```
-This helps prevent conflicts with any other engine or application that may have a post resource also.
+This helps prevent conflicts with any other engine or application that may have a post resource as well.
-Finally, two files that are the assets for this resource are generated, `app/assets/javascripts/blorgh/posts.js` and `app/assets/javascripts/blorgh/posts.css`. You'll see how to use these a little later.
+Finally, two files that are the assets for this resource are generated, `app/assets/javascripts/blorgh/posts.js` and `app/assets/stylesheets/blorgh/posts.css`. You'll see how to use these a little later.
-By default, the scaffold styling is not applied to the engine as the engine's layout file, `app/views/blorgh/application.html.erb` doesn't load it. To make this apply, insert this line into the `<head>` tag of this layout:
+By default, the scaffold styling is not applied to the engine as the engine's layout file, `app/views/layouts/blorgh/application.html.erb` doesn't load it. To make this apply, insert this line into the `<head>` tag of this layout:
```erb
<%= stylesheet_link_tag "scaffold" %>
@@ -278,14 +281,14 @@ If you'd rather play around in the console, `rails console` will also work just
One final thing is that the `posts` resource for this engine should be the root of the engine. Whenever someone goes to the root path where the engine is mounted, they should be shown a list of posts. This can be made to happen if this line is inserted into the `config/routes.rb` file inside the engine:
```ruby
-root :to => "posts#index"
+root to: "posts#index"
```
Now people will only need to go to the root of the engine to see all the posts, rather than visiting `/posts`. This means that instead of `http://localhost:3000/blorgh/posts`, you only need to go to `http://localhost:3000/blorgh` now.
### Generating a comments resource
-Now that the engine has the ability to create new blog posts, it only makes sense to add commenting functionality as well. To do get this, you'll need to generate a comment model, a comment controller and then modify the posts scaffold to display comments and allow people to create new ones.
+Now that the engine can create new blog posts, it only makes sense to add commenting functionality as well. To do this, you'll need to generate a comment model, a comment controller and then modify the posts scaffold to display comments and allow people to create new ones.
Run the model generator and tell it to generate a `Comment` model, with the related table having two columns: a `post_id` integer and `text` text column.
@@ -304,7 +307,11 @@ create test/models/blorgh/comment_test.rb
create test/fixtures/blorgh/comments.yml
```
-This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`.
+This generator call will generate just the necessary model files it needs, namespacing the files under a `blorgh` directory and creating a model class called `Blorgh::Comment`. Now run the migration to create our blorgh_comments table:
+
+```bash
+$ rake db:migrate
+```
To show the comments on a post, edit `app/views/blorgh/posts/show.html.erb` and add this line before the "Edit" link:
@@ -343,7 +350,7 @@ Next, the partial that this line will render needs to exist. Create a new direct
<h3>New comment</h3>
<%= form_for [@post, @post.comments.build] do |f| %>
<p>
- <%= f.label :text %><br />
+ <%= f.label :text %><br>
<%= f.text_area :text %>
</p>
<%= f.submit %>
@@ -390,10 +397,15 @@ The form will be making a `POST` request to `/posts/:post_id/comments`, which wi
```ruby
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @comment = @post.comments.create(comment_params)
flash[:notice] = "Comment has been created!"
- redirect_to post_path
+ redirect_to posts_path
end
+
+private
+ def comment_params
+ params.require(:comment).permit(:text)
+ end
```
This is the final part required to get the new comment form working. Displaying the comments however, is not quite right yet. If you were to create a comment right now you would see this error:
@@ -438,7 +450,7 @@ gem 'devise'
However, because you are developing the `blorgh` engine on your local machine, you will need to specify the `:path` option in your `Gemfile`:
```ruby
-gem 'blorgh', :path => "/path/to/blorgh"
+gem 'blorgh', path: "/path/to/blorgh"
```
As described earlier, by placing the gem in the `Gemfile` it will be loaded when Rails is loaded, as it will first require `lib/blorgh.rb` in the engine and then `lib/blorgh/engine.rb`, which is the file that defines the major pieces of functionality for the engine.
@@ -446,7 +458,7 @@ As described earlier, by placing the gem in the `Gemfile` it will be loaded when
To make the engine's functionality accessible from within an application, it needs to be mounted in that application's `config/routes.rb` file:
```ruby
-mount Blorgh::Engine, :at => "/blog"
+mount Blorgh::Engine, at: "/blog"
```
This line will mount the engine at `/blog` in the application. Making it accessible at `http://localhost:3000/blog` when the application runs with `rails server`.
@@ -467,7 +479,7 @@ If you have multiple engines that need migrations copied over, use `railties:ins
$ rake railties:install:migrations
```
-This command, when run for the first time will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this:
+This command, when run for the first time, will copy over all the migrations from the engine. When run the next time, it will only copy over migrations that haven't been copied over already. The first run for this command will output something such as this:
```bash
Copied migration [timestamp_1]_create_blorgh_posts.rb from blorgh
@@ -512,24 +524,32 @@ First, the `author_name` text field needs to be added to the `app/views/blorgh/p
```html+erb
<div class="field">
- <%= f.label :author_name %><br />
+ <%= f.label :author_name %><br>
<%= f.text_field :author_name %>
</div>
```
+Next, we need to update our `Blorgh::PostController#post_params` method to permit the new form parameter:
+
+```ruby
+def post_params
+ params.require(:post).permit(:title, :text, :author_name)
+end
+```
+
The `Blorgh::Post` model should then have some code to convert the `author_name` field into an actual `User` object and associate it as that post's `author` before the post is saved. It will also need to have an `attr_accessor` setup for this field so that the setter and getter methods are defined for it.
To do all this, you'll need to add the `attr_accessor` for `author_name`, the association for the author and the `before_save` call into `app/models/blorgh/post.rb`. The `author` association will be hard-coded to the `User` class for the time being.
```ruby
attr_accessor :author_name
-belongs_to :author, :class_name => "User"
+belongs_to :author, class_name: "User"
before_save :set_author
private
def set_author
- self.author = User.find_or_create_by_name(author_name)
+ self.author = User.find_or_create_by(name: author_name)
end
```
@@ -563,7 +583,7 @@ Run this migration using this command:
$ rake db:migrate
```
-Now with all the pieces in place, an action will take place that will associate an author -- represented by a record in the `users` table -- with a post, represented by the `blorgh_posts` table from the engine.
+Now with all the pieces in place, an action will take place that will associate an author - represented by a record in the `users` table - with a post, represented by the `blorgh_posts` table from the engine.
Finally, the author's name should be displayed on the post's page. Add this code above the "Title" output inside `app/views/blorgh/posts/show.html.erb`:
@@ -609,63 +629,76 @@ This section covers how to make the `User` class configurable, followed by gener
#### Setting configuration settings in the application
-The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `user_class` that will be used to specify what the class representing users is inside the application.
+The next step is to make the class that represents a `User` in the application customizable for the engine. This is because, as explained before, that class may not always be `User`. To make this customizable, the engine will have a configuration setting called `author_class` that will be used to specify what the class representing users is inside the application.
To define this configuration setting, you should use a `mattr_accessor` inside the `Blorgh` module for the engine, located at `lib/blorgh.rb` inside the engine. Inside this module, put this line:
```ruby
-mattr_accessor :user_class
+mattr_accessor :author_class
```
-This method works like its brothers `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.user_class`.
+This method works like its brothers `attr_accessor` and `cattr_accessor`, but provides a setter and getter method on the module with the specified name. To use it, it must be referenced using `Blorgh.author_class`.
The next step is switching the `Blorgh::Post` model over to this new setting. For the `belongs_to` association inside this model (`app/models/blorgh/post.rb`), it will now become this:
```ruby
-belongs_to :author, :class_name => Blorgh.user_class
+belongs_to :author, class_name: Blorgh.author_class
```
The `set_author` method also located in this class should also use this class:
```ruby
-self.author = Blorgh.user_class.constantize.find_or_create_by_name(author_name)
+self.author = Blorgh.author_class.constantize.find_or_create_by(name: author_name)
```
-To save having to call `constantize` on the `user_class` result all the time, you could instead just override the `user_class` getter method inside the `Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the saved value before returning the result:
+To save having to call `constantize` on the `author_class` result all the time, you could instead just override the `author_class` getter method inside the `Blorgh` module in the `lib/blorgh.rb` file to always call `constantize` on the saved value before returning the result:
```ruby
-def self.user_class
- @@user_class.constantize
+def self.author_class
+ @@author_class.constantize
end
```
This would then turn the above code for `set_author` into this:
```ruby
-self.author = Blorgh.user_class.find_or_create_by_name(author_name)
+self.author = Blorgh.author_class.find_or_create_by(name: author_name)
```
-Resulting in something a little shorter, and more implicit in its behaviour. The `user_class` method should always return a `Class` object.
+Resulting in something a little shorter, and more implicit in its behavior. The `author_class` method should always return a `Class` object.
+
+Since we changed the `author_class` method to no longer return a
+`String` but a `Class` we must also modify our `belongs_to` definition
+in the `Blorgh::Post` model:
+
+```ruby
+belongs_to :author, class_name: Blorgh.author_class.to_s
+```
To set this configuration setting within the application, an initializer should be used. By using an initializer, the configuration will be set up before the application starts and calls the engine's models which may depend on this configuration setting existing.
Create a new initializer at `config/initializers/blorgh.rb` inside the application where the `blorgh` engine is installed and put this content in it:
```ruby
-Blorgh.user_class = "User"
+Blorgh.author_class = "User"
```
WARNING: It's very important here to use the `String` version of the class, rather than the class itself. If you were to use the class, Rails would attempt to load that class and then reference the related table, which could lead to problems if the table wasn't already existing. Therefore, a `String` should be used and then converted to a class using `constantize` in the engine later on.
Go ahead and try to create a new post. You will see that it works exactly in the same way as before, except this time the engine is using the configuration setting in `config/initializers/blorgh.rb` to learn what the class is.
-There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by_name` method which returns an object of that class to be associated with a post when it's created. This object, of course, should have some sort of identifier by which it can be referenced.
+There are now no strict dependencies on what the class is, only what the API for the class must be. The engine simply requires this class to define a `find_or_create_by` method which returns an object of that class to be associated with a post when it's created. This object, of course, should have some sort of identifier by which it can be referenced.
#### General engine configuration
Within an engine, there may come a time where you wish to use things such as initializers, internationalization or other configuration options. The great news is that these things are entirely possible because a Rails engine shares much the same functionality as a Rails application. In fact, a Rails application's functionality is actually a superset of what is provided by engines!
-If you wish to use an initializer -- code that should run before the engine is loaded -- the place for it is the `config/initializers` folder. This directory's functionality is explained in the [Initializers section](http://guides.rubyonrails.org/configuring.html#initializers) of the Configuring guide, and works precisely the same way as the `config/initializers` directory inside an application. Same goes for if you want to use a standard initializer.
+If you wish to use an initializer - code that should run before the engine is
+loaded - the place for it is the `config/initializers` folder. This directory's
+functionality is explained in the
+[Initializers section](configuring.html#initializers) of the Configuring guide,
+and works precisely the same way as the `config/initializers` directory inside
+an application. Same goes for if you want to use a standard initializer.
For locales, simply place the locale files in the `config/locales` directory, just like you would in an application.
@@ -678,7 +711,7 @@ The `test` directory should be treated like a typical Rails testing environment,
### Functional tests
-A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application -- the `test/dummy` application -- rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this:
+A matter worth taking into consideration when writing functional tests is that the tests are going to be running on an application - the `test/dummy` application - rather than your engine. This is due to the setup of the testing environment; an engine needs an application as a host for testing its main functionality, especially controllers. This means that if you were to make a typical `GET` to a controller in a controller's functional test like this:
```ruby
get :index
@@ -687,7 +720,7 @@ get :index
It may not function correctly. This is because the application doesn't know how to route these requests to the engine unless you explicitly tell it **how**. To do this, you must pass the `:use_route` option (as a parameter) on these requests also:
```ruby
-get :index, :use_route => :blorgh
+get :index, use_route: :blorgh
```
This tells the application that you still want to perform a `GET` request to the `index` action of this controller, just that you want to use the engine's route to get there, rather than the application.
@@ -699,10 +732,36 @@ This section explains how to add and/or override engine MVC functionality in the
### Overriding Models and Controllers
-Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main applicaiton. This is usually implemented by using the decorator pattern.
+Engine model and controller classes can be extended by open classing them in the main Rails application (since model and controller classes are just Ruby classes that inherit Rails specific functionality). Open classing an Engine class redefines it for use in the main application. This is usually implemented by using the decorator pattern.
For simple class modifications use `Class#class_eval`, and for complex class modifications, consider using `ActiveSupport::Concern`.
+#### A note on Decorators and loading code
+
+Because these decorators are not referenced by your Rails application itself,
+Rails' autoloading system will not kick in and load your decorators. This
+means that you need to require them yourself.
+
+Here is some sample code to do this:
+
+```ruby
+# lib/blorgh/engine.rb
+module Blorgh
+ class Engine < ::Rails::Engine
+ isolate_namespace Blorgh
+
+ config.to_prepare do
+ Dir.glob(Rails.root + "app/decorators/**/*_decorator*.rb").each do |c|
+ require_dependency(c)
+ end
+ end
+ end
+end
+```
+
+This doesn't apply to just Decorators, but anything that you add in an engine
+that isn't referenced by your main application.
+
#### Implementing Decorator Pattern Using Class#class_eval
**Adding** `Post#time_since_created`,
@@ -751,10 +810,9 @@ end
#### Implementing Decorator Pattern Using ActiveSupport::Concern
-Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html) helps manage load order of interlinked dependencies at run time allowing you to significantly modularize your code.
+Using `Class#class_eval` is great for simple adjustments, but for more complex class modifications, you might want to consider using [`ActiveSupport::Concern`](http://edgeapi.rubyonrails.org/classes/ActiveSupport/Concern.html). ActiveSupport::Concern manages load order of interlinked dependent modules and classes at run time allowing you to significantly modularize your code.
-**Adding** `Post#time_since_created`<br/>
-**Overriding** `Post#summary`
+**Adding** `Post#time_since_created` and **Overriding** `Post#summary`
```ruby
# MyApp/app/models/blorgh/post.rb
@@ -787,19 +845,18 @@ module Blorgh::Concerns::Models::Post
extend ActiveSupport::Concern
# 'included do' causes the included code to be evaluated in the
- # conext where it is included (post.rb), rather than be
+ # context where it is included (post.rb), rather than be
# executed in the module's context (blorgh/concerns/models/post).
included do
attr_accessor :author_name
- belongs_to :author, :class_name => "User"
+ belongs_to :author, class_name: "User"
before_save :set_author
private
-
- def set_author
- self.author = User.find_or_create_by_name(author_name)
- end
+ def set_author
+ self.author = User.find_or_create_by(name: author_name)
+ end
end
def summary
@@ -837,7 +894,7 @@ Try this now by creating a new file at `app/views/blorgh/posts/index.html.erb` a
### Routes
-Routes inside an engine are, by default, isolated from the application. This is done by the `isolate_namespace` call inside the `Engine` class. This essentially means that the application and its engines can have identically named routes, and that they will not clash.
+Routes inside an engine are, by default, isolated from the application. This is done by the `isolate_namespace` call inside the `Engine` class. This essentially means that the application and its engines can have identically named routes and they will not clash.
Routes inside an engine are drawn on the `Engine` class within `config/routes.rb`, like this:
@@ -897,7 +954,7 @@ INFO. Remember that in order to use languages like Sass or CoffeeScript, you sho
There are some situations where your engine's assets are not required by the host application. For example, say that you've created
an admin functionality that only exists for your engine. In this case, the host application doesn't need to require `admin.css`
-or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorg/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation.
+or `admin.js`. Only the gem's admin layout needs these assets. It doesn't make sense for the host app to include `"blorgh/admin.css"` in it's stylesheets. In this situation, you should explicitly define these assets for precompilation.
This tells sprockets to add your engine assets when `rake assets:precompile` is ran.
You can define assets for precompilation in `engine.rb`
@@ -908,13 +965,14 @@ initializer "blorgh.assets.precompile" do |app|
end
```
-For more information, read the [Asset Pipeline guide](http://guides.rubyonrails.org/asset_pipeline.html)
+For more information, read the [Asset Pipeline guide](asset_pipeline.html)
### Other gem dependencies
-Gem dependencies inside an engine should be specified inside the `.gemspec` file at the root of the engine. The reason for this is because the engine may
+Gem dependencies inside an engine should be specified inside the
+`.gemspec` file at the root of the engine. The reason is that the engine may
be installed as a gem. If dependencies were to be specified inside the `Gemfile`,
-these would not be recognised by a traditional gem install and so they would not
+these would not be recognized by a traditional gem install and so they would not
be installed, causing the engine to malfunction.
To specify a dependency that should be installed with the engine during a
diff --git a/guides/source/form_helpers.md b/guides/source/form_helpers.md
index ec1f1d4df1..4b6d8a93f0 100644
--- a/guides/source/form_helpers.md
+++ b/guides/source/form_helpers.md
@@ -1,17 +1,17 @@
-Rails Form helpers
-==================
+Form Helpers
+============
-Forms in web applications are an essential interface for user input. However, form markup can quickly become tedious to write and maintain because of form control naming and their numerous attributes. Rails deals away with these complexities by providing view helpers for generating form markup. However, since they have different use-cases, developers are required to know all the differences between similar helper methods before putting them to use.
+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.
-In this guide you will:
+After reading this guide, you will know:
-* Create search forms and similar kind of generic forms not representing any specific model in your application
-* Make model-centric forms for creation and editing of specific database records
-* Generate select boxes from multiple types of data
-* Understand the date and time helpers Rails provides
-* Learn what makes a file upload form different
-* Learn some cases of building forms to external resources
-* Find out how to build complex forms
+* 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 generate select boxes from multiple types of data.
+* The date and time helpers Rails provides.
+* What makes a file upload form different.
+* Some cases of building forms to external resources.
+* How to build complex forms.
--------------------------------------------------------------------------------
@@ -57,7 +57,7 @@ One of the most basic forms you see on the web is a search form. This form conta
To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and `submit_tag`, respectively. Like this:
```erb
-<%= form_tag("/search", :method => "get") do %>
+<%= form_tag("/search", method: "get") do %>
<%= label_tag(:q, "Search for:") %>
<%= text_field_tag(:q) %>
<%= submit_tag("Search") %>
@@ -67,7 +67,7 @@ To create this form you will use `form_tag`, `label_tag`, `text_field_tag`, and
This will generate the following HTML:
```html
-<form accept-charset="UTF-8" action="/search" method="get">
+<form accept-charset="UTF-8" action="/search" method="get"><div style="margin:0;padding:0;display:inline"><input name="utf8" type="hidden" value="&#x2713;" /></div>
<label for="q">Search for:</label>
<input id="q" name="q" type="text" />
<input name="commit" type="submit" value="Search" />
@@ -87,14 +87,14 @@ The `form_tag` helper accepts 2 arguments: the path for the action and an option
As with the `link_to` helper, the path argument doesn't have to be a string; it can be a hash of URL parameters recognizable by Rails' routing mechanism, which will turn the hash into a valid URL. However, since both arguments to `form_tag` are hashes, you can easily run into a problem if you would like to specify both. For instance, let's say you write this:
```ruby
-form_tag(:controller => "people", :action => "search", :method => "get", :class => "nifty_form")
+form_tag(controller: "people", action: "search", method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search?method=get&class=nifty_form" method="post">'
```
Here, `method` and `class` are appended to the query string of the generated URL because even though you mean to write two hashes, you really only specified one. So you need to tell Ruby which is which by delimiting the first hash (or both) with curly brackets. This will generate the HTML you expect:
```ruby
-form_tag({:controller => "people", :action => "search"}, :method => "get", :class => "nifty_form")
+form_tag({controller: "people", action: "search"}, method: "get", class: "nifty_form")
# => '<form accept-charset="UTF-8" action="/people/search" method="get" class="nifty_form">'
```
@@ -148,14 +148,16 @@ Output:
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".
-NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and make it easier for users to click the inputs by expanding the clickable region.
+NOTE: Always use labels for checkbox and radio buttons. They associate text with a specific option and,
+by expanding the clickable region,
+make it easier for users to click the inputs.
### Other Helpers of Interest
Other form controls worth mentioning are textareas, password fields, hidden fields, search fields, telephone fields, date fields, time fields, color fields, datetime fields, datetime-local fields, month fields, week fields, URL fields and email fields:
```erb
-<%= text_area_tag(:message, "Hi, nice site", :size => "24x6") %>
+<%= text_area_tag(:message, "Hi, nice site", size: "24x6") %>
<%= password_field_tag(:password) %>
<%= hidden_field_tag(:parent_id, "5") %>
<%= search_field(:user, :name) %>
@@ -215,11 +217,11 @@ will produce output similar to
<input id="person_name" name="person[name]" type="text" value="Henry"/>
```
-Upon form submission the value entered by the user will be stored in `params[:person][:name]`. The `params[:person]` hash is suitable for passing to `Person.new` or, if `@person` is an instance of Person, `@person.update_attributes`. While the name of an attribute is the most common second parameter to these helpers this is not compulsory. In the example above, as long as person objects have a `name` and a `name=` method Rails will be happy.
+Upon form submission the value entered by the user will be stored in `params[:person][:name]`. The `params[:person]` hash is suitable for passing to `Person.new` or, if `@person` is an instance of Person, `@person.update`. While the name of an attribute is the most common second parameter to these helpers this is not compulsory. In the example above, as long as person objects have a `name` and a `name=` method Rails will be happy.
WARNING: You must pass the name of an instance variable, i.e. `:person` or `"person"`, not an actual instance of your model object.
-Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations and Callbacks](./active_record_validations_callbacks.html#displaying-validation-errors-in-the-view) guide.
+Rails provides helpers for displaying the validation errors associated with a model object. These are covered in detail by the [Active Record Validations](./active_record_validations.html#displaying-validation-errors-in-views) guide.
### Binding a Form to an Object
@@ -236,9 +238,9 @@ end
The corresponding view `app/views/articles/new.html.erb` using `form_for` looks like this:
```erb
-<%= form_for @article, :url => { :action => "create" }, :html => {:class => "nifty_form"} do |f| %>
+<%= form_for @article, url: {action: "create"}, html: {class: "nifty_form"} do |f| %>
<%= f.text_field :title %>
- <%= f.text_area :body, :size => "60x12" %>
+ <%= f.text_area :body, size: "60x12" %>
<%= f.submit "Create" %>
<% end %>
```
@@ -267,7 +269,7 @@ The helper methods called on the form builder are identical to the model object
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| %>
+<%= form_for @person, url: {action: "create"} do |person_form| %>
<%= person_form.text_field :name %>
<%= fields_for @person.contact_detail do |contact_details_form| %>
<%= contact_details_form.text_field :phone_number %>
@@ -288,7 +290,7 @@ The object yielded by `fields_for` is a form builder like the one yielded by `fo
### Relying on Record Identification
-The Article model is directly available to users of the application, so -- following the best practices for developing with Rails -- you should declare it **a resource**:
+The Article model is directly available to users of the application, so - following the best practices for developing with Rails - you should declare it **a resource**:
```ruby
resources :articles
@@ -301,13 +303,13 @@ When dealing with RESTful resources, calls to `form_for` can get significantly e
```ruby
## Creating a new article
# long-style:
-form_for(@article, :url => articles_path)
+form_for(@article, url: articles_path)
# same thing, short-style (record identification gets used):
form_for(@article)
## Editing an existing article
# long-style:
-form_for(@article, :url => article_path(@article), :html => { :method => "patch" })
+form_for(@article, url: article_path(@article), html: {method: "patch"})
# short-style:
form_for(@article)
```
@@ -342,7 +344,7 @@ The Rails framework encourages RESTful design of your applications, which means
Rails works around this issue by emulating other methods over POST with a hidden input named `"_method"`, which is set to reflect the desired method:
```ruby
-form_tag(search_path, :method => "patch")
+form_tag(search_path, method: "patch")
```
output:
@@ -379,7 +381,7 @@ Here you have a list of cities whose names are presented to the user. Internally
### The Select and Option Tags
-The most generic helper is `select_tag`, which -- as the name implies -- simply generates the `SELECT` tag that encapsulates an options string:
+The most generic helper is `select_tag`, which - as the name implies - simply generates the `SELECT` tag that encapsulates an options string:
```erb
<%= select_tag(:city_id, '<option value="1">Lisbon</option>...') %>
@@ -419,14 +421,14 @@ 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 `:inlude_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.
+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:
@@ -441,7 +443,7 @@ In most cases form controls will be tied to a specific database model and as you
```ruby
# controller:
-@person = Person.new(:city_id => 2)
+@person = Person.new(city_id: 2)
```
```erb
@@ -449,7 +451,7 @@ In most cases form controls will be tied to a specific database model and as you
<%= select(:person, :city_id, [['Lisbon', 1], ['Madrid', 2], ...]) %>
```
-Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one -- Rails will do this for you by reading from the `@person.city_id` attribute.
+Notice that the third parameter, the options array, is the same kind of argument you pass to `options_for_select`. One advantage here is that you don't have to worry about pre-selecting the correct city if the user already has one - Rails will do this for you by reading from the `@person.city_id` attribute.
As with other helpers, if you were to use the `select` helper on a form builder scoped to the `@person` object, the syntax would be:
@@ -458,7 +460,7 @@ 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_attributes`. Another way of looking at this is that form helpers only edit attributes. You should also be aware of the potential security ramifications of allowing users to edit foreign keys directly. You may wish to consider the use of `attr_protected` and `attr_accessible`. For further details on this, see the [Ruby On Rails Security Guide](security.html#mass-assignment).
+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
@@ -495,7 +497,7 @@ To leverage time zone support in Rails, you have to ask your users what time zon
There is also `time_zone_options_for_select` helper for a more manual (therefore more customizable) way of doing this. Read the API documentation to learn about the possible arguments for these two methods.
-Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/chrislerum/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails).
+Rails _used_ to have a `country_select` helper for choosing countries, but this has been extracted to the [country_select plugin](https://github.com/stefanpenner/country_select). When using this, be aware that the exclusion or inclusion of certain names from the list can be somewhat controversial (and was the reason this functionality was extracted from Rails).
Using Date and Time Form Helpers
--------------------------------
@@ -512,7 +514,7 @@ Both of these families of helpers will create a series of select boxes for the d
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 %>
+<%= select_date Date.today, prefix: :start_date %>
```
outputs (with actual option values omitted for brevity)
@@ -534,7 +536,7 @@ The `:prefix` option is the key used to retrieve the hash of date components fro
### Model Object Helpers
`select_date` does not work well with forms that update or create Active Record objects as Active Record expects each element of the `params` hash to correspond to one attribute.
-The model object helpers for dates and times submit parameters with special names, when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example:
+The model object helpers for dates and times submit parameters with special names; when Active Record sees parameters with such names it knows they must be combined with the other parameters and given to a constructor appropriate to the column type. For example:
```erb
<%= date_select :person, :birth_date %>
@@ -551,10 +553,10 @@ outputs (with actual option values omitted for brevity)
which results in a `params` hash like
```ruby
-{:person => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
+{'person' => {'birth_date(1i)' => '2008', 'birth_date(2i)' => '11', 'birth_date(3i)' => '22'}}
```
-When this is passed to `Person.new` (or `update_attributes`), Active Record spots that these parameters should all be used to construct the `birth_date` attribute and uses the suffixed information to determine in which order it should pass these parameters to functions such as `Date.civil`.
+When this is passed to `Person.new` (or `update`), Active Record spots that these parameters should all be used to construct the `birth_date` attribute and uses the suffixed information to determine in which order it should pass these parameters to functions such as `Date.civil`.
### Common Options
@@ -566,7 +568,7 @@ NOTE: In many cases the built-in date pickers are clumsy as they do not aid the
### Individual Components
-Occasionally you need to display just a single date component such as a year or a month. Rails provides a series of helpers for this, one for each component `select_year`, `select_month`, `select_day`, `select_hour`, `select_minute`, `select_second`. These helpers are fairly straightforward. By default they will generate an input field named after the time component (for example "year" for `select_year`, "month" for `select_month` etc.) although this can be 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.
+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
@@ -585,7 +587,7 @@ A common task is uploading some sort of file, whether it's a picture of a person
The following two forms both upload a file.
```erb
-<%= form_tag({:action => :upload}, :multipart => true) do %>
+<%= form_tag({action: :upload}, multipart: true) do %>
<%= file_field_tag 'picture' %>
<% end %>
@@ -594,8 +596,6 @@ The following two forms both upload a file.
<% end %>
```
-NOTE: Since Rails 3.1, forms rendered using `form_for` have their encoding set to `multipart/form-data` automatically once a `file_field` is used inside the block. Previous versions required you to set this explicitly.
-
Rails provides the usual pair of helpers: the barebones `file_field_tag` and the model oriented `file_field`. The only difference with other helpers is that you cannot set a default value for file inputs as this would have no meaning. As you would expect in the first case the uploaded file is in `params[:picture]` and in the second case in `params[:person][:picture]`.
### What Gets Uploaded
@@ -605,7 +605,7 @@ The object in the `params` hash is an instance of a subclass of IO. Depending on
```ruby
def upload
uploaded_io = params[:person][:picture]
- File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'w') do |file|
+ File.open(Rails.root.join('public', 'uploads', uploaded_io.original_filename), 'wb') do |file|
file.write(uploaded_io.read)
end
end
@@ -617,12 +617,12 @@ NOTE: If the user has not selected a file the corresponding parameter will be an
### Dealing with Ajax
-Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `:remote => true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission.
+Unlike other forms making an asynchronous file upload form is not as simple as providing `form_for` with `remote: true`. With an Ajax form the serialization is done by JavaScript running inside the browser and since JavaScript cannot read files from your hard drive the file cannot be uploaded. The most common workaround is to use an invisible iframe that serves as the target for the form submission.
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| %>
@@ -633,7 +633,7 @@ As mentioned previously the object yielded by `form_for` and `fields_for` is an
can be replaced with
```erb
-<%= form_for @person, :builder => LabellingFormBuilder do |f| %>
+<%= form_for @person, builder: LabellingFormBuilder do |f| %>
<%= f.text_field :first_name %>
<% end %>
```
@@ -648,12 +648,12 @@ class LabellingFormBuilder < ActionView::Helpers::FormBuilder
end
```
-If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `:builder => LabellingFormBuilder` option.
+If you reuse this frequently you could define a `labeled_form_for` helper that automatically applies the `builder: LabellingFormBuilder` option.
The form builder used also determines what happens when you do
```erb
-<%= render :partial => f %>
+<%= 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.
@@ -664,7 +664,7 @@ Understanding Parameter Naming Conventions
As you've seen in the previous sections, values from forms can be at the top level of the `params` hash or nested in another hash. For example in a standard `create`
action for a Person model, `params[:person]` would usually be a hash of all the attributes for the person to create. The `params` hash can also contain arrays, arrays of hashes and so on.
-Fundamentally HTML forms don't know about any sort of structured data, all they generate is name–value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.
+Fundamentally HTML forms don't know about any sort of structured data, all they generate is name-value pairs, where pairs are just plain strings. The arrays and hashes you see in your application are the result of some parameter naming conventions that Rails uses.
TIP: You may find you can try out examples in this section faster by using the console to directly invoke Racks' parameter parser. For example,
@@ -737,7 +737,7 @@ You might want to render a form with a set of edit fields for each of a person's
<%= form_for @person do |person_form| %>
<%= person_form.text_field :name %>
<% @person.addresses.each do |address| %>
- <%= person_form.fields_for address, :index => address do |address_form|%>
+ <%= person_form.fields_for address, index: address do |address_form|%>
<%= address_form.text_field :city %>
<% end %>
<% end %>
@@ -765,7 +765,7 @@ Rails knows that all these inputs should be part of the person hash because you
To create more intricate nestings, you can specify the first part of the input name (`person[address]` in the previous example) explicitly, for example
```erb
-<%= fields_for 'person[address][primary]', address, :index => address do |address_form| %>
+<%= fields_for 'person[address][primary]', address, index: address do |address_form| %>
<%= address_form.text_field :city %>
<% end %>
```
@@ -778,7 +778,7 @@ will create inputs like
As a general rule the final input name is the concatenation of the name given to `fields_for`/`form_for`, the index value and the name of the attribute. You can also pass an `:index` option directly to helpers such as `text_field`, but it is usually less repetitive to specify this at the form builder level rather than on individual input controls.
-As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `:index => address` so
+As a shortcut you can append [] to the name and omit the `:index` option. This is the same as specifying `index: address` so
```erb
<%= fields_for 'person[address][primary][]', address do |address_form| %>
@@ -791,10 +791,10 @@ produces exactly the same output as the previous example.
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:
+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:
```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 %>
```
@@ -802,15 +802,15 @@ If you need to post some data to an external resource it is still great to build
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:
```erb
-<%= form_tag 'http://farfar.away/form', :authenticity_token => false) do %>
+<%= form_tag 'http://farfar.away/form', authenticity_token: false) do %>
Form contents
<% end %>
```
-The same technique is available for the `form_for` too:
+The same technique is also available for `form_for`:
```erb
-<%= form_for @invoice, :url => external_url, :authenticity_token => 'external_token' do |f| %>
+<%= form_for @invoice, url: external_url, authenticity_token: 'external_token' do |f| %>
Form contents
<% end %>
```
@@ -818,7 +818,7 @@ The same technique is available for the `form_for` too:
Or if you don't want to render an `authenticity_token` field:
```erb
-<%= form_for @invoice, :url => external_url, :authenticity_token => false do |f| %>
+<%= form_for @invoice, url: external_url, authenticity_token: false do |f| %>
Form contents
<% end %>
```
@@ -830,23 +830,20 @@ Many apps grow beyond simple forms editing a single object. For example when cre
### Configuring the Model
-Active Record provides model level support via the `accepts_nested_attributes_for` method:
+Active Record provides model level support via the `accepts_nested_attributes_for` method:
```ruby
class Person < ActiveRecord::Base
has_many :addresses
accepts_nested_attributes_for :addresses
-
- attr_accessible :name, :addresses_attributes
end
class Address < ActiveRecord::Base
belongs_to :person
- attr_accessible :kind, :street
end
```
-This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses. When using `attr_accessible` or `attr_protected` you must mark `addresses_attributes` as accessible as well as the other attributes of `Person` and `Address` that should be mass assigned.
+This creates an `addresses_attributes=` method on `Person` that allows you to create, update and (optionally) destroy addresses.
### Building the Form
@@ -884,38 +881,52 @@ end
```ruby
{
- :person => {
- :name => 'John Doe',
- :addresses_attributes => {
- '0' => {
- :kind => 'Home',
- :street => '221b Baker Street',
- },
- '1' => {
- :kind => 'Office',
- :street => '31 Spooner Street'
- }
- }
+ 'person' => {
+ 'name' => 'John Doe',
+ 'addresses_attributes' => {
+ '0' => {
+ 'kind' => 'Home',
+ 'street' => '221b Baker Street'
+ },
+ '1' => {
+ 'kind' => 'Office',
+ 'street' => '31 Spooner Street'
+ }
}
+ }
}
```
-The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address.
+The keys of the `:addresses_attributes` hash are unimportant, they need merely be different for each address.
-If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `:include_id => false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an id.
+If the associated object is already saved, `fields_for` autogenerates a hidden input with the `id` of the saved record. You can disable this by passing `include_id: false` to `fields_for`. You may wish to do this if the autogenerated input is placed in a location where an input tag is not valid HTML or when using an ORM where children do not have an id.
### The Controller
-You do not need to write any specific controller code to use nested attributes. Create and update records as you would with a simple form.
+As usual you need to
+[whitelist the parameters](action_controller_overview.html#strong-parameters) in
+the controller before you pass them to the model:
+
+```ruby
+def create
+ @person = Person.new(person_params)
+ # ...
+end
+
+private
+ def person_params
+ params.require(:person).permit(:name, addresses_attributes: [:id, :kind, :street])
+ end
+```
### Removing Objects
-You can allow users to delete associated objects by passing `allow_destroy => true` to `accepts_nested_attributes_for`
+You can allow users to delete associated objects by passing `allow_destroy: true` to `accepts_nested_attributes_for`
```ruby
class Person < ActiveRecord::Base
has_many :addresses
- accepts_nested_attributes_for :addresses, :allow_destroy => true
+ accepts_nested_attributes_for :addresses, allow_destroy: true
end
```
@@ -937,6 +948,16 @@ If the hash of attributes for an object contains the key `_destroy` with a value
<% end %>
```
+Don't forget to update the whitelisted params in your controller to also include
+the `_destroy` field:
+
+```ruby
+def person_params
+ params.require(:person).
+ permit(:name, addresses_attributes: [:id, :kind, :street, :_destroy])
+end
+```
+
### Preventing Empty Records
It is often useful to ignore sets of fields that the user has not filled in. You can control this by passing a `:reject_if` proc to `accepts_nested_attributes_for`. This proc will be called with each hash of attributes submitted by the form. If the proc returns `false` then Active Record will not build an associated object for that hash. The example below only tries to build an address if the `kind` attribute is set.
@@ -944,7 +965,7 @@ It is often useful to ignore sets of fields that the user has not filled in. You
```ruby
class Person < ActiveRecord::Base
has_many :addresses
- accepts_nested_attributes_for :addresses, :reject_if => lambda {|attributes| attributes['kind'].blank?}
+ accepts_nested_attributes_for :addresses, reject_if: lambda {|attributes| attributes['kind'].blank?}
end
```
@@ -952,4 +973,4 @@ As a convenience you can instead pass the symbol `:all_blank` which will create
### Adding Fields on the Fly
-Rather than rendering multiple sets of fields ahead of time you may wish to add them only when a user clicks on an 'Add new child' button. Rails does not provide any builtin support for this. When generating new sets of fields you must ensure the 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 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.
diff --git a/guides/source/generators.md b/guides/source/generators.md
index d56bbe853c..e9c8ef0225 100644
--- a/guides/source/generators.md
+++ b/guides/source/generators.md
@@ -3,20 +3,18 @@ Creating and Customizing Rails Generators & Templates
Rails generators are an essential tool if you plan to improve your workflow. With this guide you will learn how to create generators and customize existing ones.
-In this guide you will:
+After reading this guide, you will know:
-* Learn how to see which generators are available in your application
-* Create a generator using templates
-* Learn how Rails searches for generators before invoking them
-* Customize your scaffold by creating new generators
-* Customize your scaffold by changing generator templates
-* Learn how to use fallbacks to avoid overwriting a huge set of generators
-* Learn how to create an application template
+* 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 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.
+* How to create an application template.
--------------------------------------------------------------------------------
-NOTE: This guide is about generators in Rails 3, previous versions are not covered.
-
First Contact
-------------
@@ -37,7 +35,7 @@ $ rails generate helper --help
Creating Your First Generator
-----------------------------
-Since Rails 3.0, generators are built on top of [Thor](https://github.com/wycats/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`.
+Since Rails 3.0, generators are built on top of [Thor](https://github.com/erikhuda/thor). Thor provides powerful options parsing and a great API for manipulating files. For instance, let's build a generator that creates an initializer file named `initializer.rb` inside `config/initializers`.
The first step is to create a file at `lib/generators/initializer_generator.rb` with the following content:
@@ -49,7 +47,7 @@ class InitializerGenerator < Rails::Generators::Base
end
```
-NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
+NOTE: `create_file` is a method provided by `Thor::Actions`. Documentation for `create_file` and other Thor methods can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)
Our new generator is quite simple: it inherits from `Rails::Generators::Base` and has one method definition. When a generator is invoked, each public method in the generator is executed sequentially in the order that it is defined. Finally, we invoke the `create_file` method that will create a file at the given destination with the given content. If you are familiar with the Rails Application Templates API, you'll feel right at home with the new generators API.
@@ -164,7 +162,7 @@ Rails own generators are flexible enough to let you customize scaffolding. They
config.generators do |g|
g.orm :active_record
g.template_engine :erb
- g.test_framework :test_unit, :fixture => true
+ g.test_framework :test_unit, fixture: true
end
```
@@ -173,12 +171,13 @@ Before we customize our workflow, let's first see what our scaffold looks like:
```bash
$ rails generate scaffold User name:string
invoke active_record
- create db/migrate/20091120125558_create_users.rb
+ create db/migrate/20130924151154_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
- route resources :users
+ invoke resource_route
+ route resources :users
invoke scaffold_controller
create app/controllers/users_controller.rb
invoke erb
@@ -194,34 +193,50 @@ $ rails generate scaffold User name:string
create app/helpers/users_helper.rb
invoke test_unit
create test/helpers/users_helper_test.rb
- invoke stylesheets
- create app/assets/stylesheets/scaffold.css
+ invoke jbuilder
+ create app/views/users/index.json.jbuilder
+ create app/views/users/show.json.jbuilder
+ invoke assets
+ invoke coffee
+ create app/assets/javascripts/users.js.coffee
+ invoke scss
+ create app/assets/stylesheets/users.css.scss
+ invoke scss
+ create app/assets/stylesheets/scaffolds.css.scss
```
Looking at this output, it's easy to understand how generators work in Rails 3.0 and above. The scaffold generator doesn't actually generate anything, it just invokes others to do the work. This allows us to add/replace/remove any of those invocations. For instance, the scaffold generator invokes the scaffold_controller generator, which invokes erb, test_unit and helper generators. Since each generator has a single responsibility, they are easy to reuse, avoiding code duplication.
-Our first customization on the workflow will be to stop generating stylesheets and test fixtures for scaffolds. We can achieve that by changing our configuration to the following:
+Our first customization on the workflow will be to stop generating stylesheets, javascripts and test fixtures for scaffolds. We can achieve that by changing our configuration to the following:
```ruby
config.generators do |g|
g.orm :active_record
g.template_engine :erb
- g.test_framework :test_unit, :fixture => false
+ g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
-If we generate another resource with the scaffold generator, we can see that neither stylesheets nor fixtures are created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
+If we generate another resource with the scaffold generator, we can see that stylesheets, javascripts and fixtures are not created anymore. If you want to customize it further, for example to use DataMapper and RSpec instead of Active Record and TestUnit, it's just a matter of adding their gems to your application and configuring your generators.
To demonstrate this, we are going to create a new helper generator that simply adds some instance variable readers. First, we create a generator within the rails namespace, as this is where rails searches for generators used as hooks:
```bash
$ rails generate generator rails/my_helper
+ create lib/generators/rails/my_helper
+ create lib/generators/rails/my_helper/my_helper_generator.rb
+ create lib/generators/rails/my_helper/USAGE
+ create lib/generators/rails/my_helper/templates
```
-After that, we can delete both the `templates` directory and the `source_root` class method from our new generators, because we are not going to need them. So our new generator looks like the following:
+After that, we can delete both the `templates` directory and the `source_root`
+class method call from our new generator, because we are not going to need them.
+Add the method below, so our generator looks like the following:
```ruby
+# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
@@ -237,6 +252,7 @@ We can try out our new generator by creating a helper for users:
```bash
$ rails generate my_helper products
+ create app/helpers/products_helper.rb
```
And it will generate the following helper file in `app/helpers`:
@@ -253,8 +269,9 @@ Which is what we expected. We can now tell scaffold to use our new helper genera
config.generators do |g|
g.orm :active_record
g.template_engine :erb
- g.test_framework :test_unit, :fixture => false
+ g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
g.helper :my_helper
end
```
@@ -275,6 +292,7 @@ Since Rails 3.0, this is easy to do due to the hooks concept. Our new helper doe
To do that, we can change the generator this way:
```ruby
+# lib/generators/rails/my_helper/my_helper_generator.rb
class Rails::MyHelperGenerator < Rails::Generators::NamedBase
def create_helper_file
create_file "app/helpers/#{file_name}_helper.rb", <<-FILE
@@ -292,7 +310,7 @@ Now, when the helper generator is invoked and TestUnit is configured as the test
```ruby
# Search for :helper instead of :my_helper
-hook_for :test_framework, :as => :helper
+hook_for :test_framework, as: :helper
```
And now you can re-run scaffold for another resource and see it generating tests as well!
@@ -306,7 +324,7 @@ In Rails 3.0 and above, generators don't just look in the source root for templa
```erb
module <%= class_name %>Helper
- attr_reader :<%= plural_name %>, <%= plural_name.singularize %>
+ attr_reader :<%= plural_name %>, :<%= plural_name.singularize %>
end
```
@@ -316,8 +334,9 @@ and revert the last change in `config/application.rb`:
config.generators do |g|
g.orm :active_record
g.template_engine :erb
- g.test_framework :test_unit, :fixture => false
+ g.test_framework :test_unit, fixture: false
g.stylesheets false
+ g.javascripts false
end
```
@@ -334,8 +353,9 @@ We can easily simulate this behavior by changing our `config/application.rb` onc
config.generators do |g|
g.orm :active_record
g.template_engine :erb
- g.test_framework :shoulda, :fixture => false
+ g.test_framework :shoulda, fixture: false
g.stylesheets false
+ g.javascripts false
# Add a fallback!
g.fallbacks[:shoulda] = :test_unit
@@ -347,11 +367,12 @@ Now, if you create a Comment scaffold, you will see that the shoulda generators
```bash
$ rails generate scaffold Comment body:text
invoke active_record
- create db/migrate/20091120151323_create_comments.rb
+ create db/migrate/20130924143118_create_comments.rb
create app/models/comment.rb
invoke shoulda
create test/models/comment_test.rb
create test/fixtures/comments.yml
+ invoke resource_route
route resources :comments
invoke scaffold_controller
create app/controllers/comments_controller.rb
@@ -362,13 +383,19 @@ $ rails generate scaffold Comment body:text
create app/views/comments/show.html.erb
create app/views/comments/new.html.erb
create app/views/comments/_form.html.erb
- create app/views/layouts/comments.html.erb
invoke shoulda
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
+ invoke assets
+ invoke coffee
+ create app/assets/javascripts/comments.js.coffee
+ invoke scss
```
Fallbacks allow your generators to have a single responsibility, increasing code reuse and reducing the amount of duplication.
@@ -376,18 +403,18 @@ Fallbacks allow your generators to have a single responsibility, increasing code
Application Templates
---------------------
-Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template".
+Now that you've seen how generators can be used _inside_ an application, did you know they can also be used to _generate_ applications too? This kind of generator is referred as a "template". This is a brief overview of the Templates API. For detailed documentation see the [Rails Application Templates guide](rails_application_templates.html).
```ruby
-gem("rspec-rails", :group => "test")
-gem("cucumber-rails", :group => "test")
+gem "rspec-rails", group: "test"
+gem "cucumber-rails", group: "test"
if yes?("Would you like to install Devise?")
- gem("devise")
- generate("devise:install")
+ gem "devise"
+ generate "devise:install"
model_name = ask("What would you like the user model to be called? [user]")
model_name = "user" if model_name.blank?
- generate("devise", model_name)
+ generate "devise", model_name
end
```
@@ -404,7 +431,7 @@ This command will generate the `Thud` application, and then apply the template t
Templates don't have to be stored on the local system, the `-m` option also supports online templates:
```bash
-$ rails new thud -m https://gist.github.com/722911.txt
+$ rails new thud -m https://gist.github.com/radar/722911/raw/
```
Whilst the final section of this guide doesn't cover how to generate the most awesome template known to man, it will take you through the methods available at your disposal so that you can develop it yourself. These same methods are also available for generators.
@@ -414,15 +441,15 @@ Generator methods
The following are methods available for both generators and templates for Rails.
-NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/wycats/thor/master/Thor/Actions.html)
+NOTE: Methods provided by Thor are not covered this guide and can be found in [Thor's documentation](http://rdoc.info/github/erikhuda/thor/master/Thor/Actions.html)
### `gem`
Specifies a gem dependency of the application.
```ruby
-gem("rspec", :group => "test", :version => "2.1.0")
-gem("devise", "1.1.5")
+gem "rspec", group: "test", version: "2.1.0"
+gem "devise", "1.1.5"
```
Available options are:
@@ -434,13 +461,13 @@ Available options are:
Any additional options passed to this method are put on the end of the line:
```ruby
-gem("devise", :git => "git://github.com/plataformatec/devise", :branch => "master")
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
```
The above code will put the following line into `Gemfile`:
```ruby
-gem "devise", :git => "git://github.com/plataformatec/devise", :branch => "master"
+gem "devise", git: "git://github.com/plataformatec/devise", branch: "master"
```
### `gem_group`
@@ -466,7 +493,7 @@ add_source "http://gems.github.com"
Injects a block of code into a defined position in your file.
```ruby
-inject_into_file 'name_of_file.rb', :after => "#The code goes below this line. Don't forget the Line break at the end\n" do <<-'RUBY'
+inject_into_file 'name_of_file.rb', after: "#The code goes below this line. Don't forget the Line break at the end\n" do <<-'RUBY'
puts "Hello World"
RUBY
end
@@ -503,7 +530,7 @@ Available options are:
* `:env` - Specify an environment for this configuration option. If you wish to use this option with the block syntax the recommended syntax is as follows:
```ruby
-application(nil, :env => "development") do
+application(nil, env: "development") do
"config.asset_host = 'http://localhost:3000'"
end
```
@@ -514,9 +541,9 @@ Runs the specified git command:
```ruby
git :init
-git :add => "."
-git :commit => "-m First commit!"
-git :add => "onefile.rb", :rm => "badfile.cxx"
+git add: "."
+git commit: "-m First commit!"
+git add: "onefile.rb", rm: "badfile.cxx"
```
The values of the hash here being the arguments or options passed to the specific git command. As per the final example shown here, multiple git commands can be specified at a time, but the order of their running is not guaranteed to be the same as the order that they were specified in.
@@ -526,13 +553,13 @@ The values of the hash here being the arguments or options passed to the specifi
Places a file into `vendor` which contains the specified code.
```ruby
-vendor("sekrit.rb", '#top secret stuff')
+vendor "sekrit.rb", '#top secret stuff'
```
This method also takes a block:
```ruby
-vendor("seeds.rb") do
+vendor "seeds.rb" do
"puts 'in ur app, seeding ur database'"
end
```
@@ -542,13 +569,13 @@ end
Places a file into `lib` which contains the specified code.
```ruby
-lib("special.rb", 'p Rails.root')
+lib "special.rb", "p Rails.root"
```
This method also takes a block:
```ruby
-lib("super_special.rb") do
+lib "super_special.rb" do
puts "Super special!"
end
```
@@ -558,15 +585,15 @@ end
Creates a Rake file in the `lib/tasks` directory of the application.
```ruby
-rakefile("test.rake", 'hello there')
+rakefile "test.rake", "hello there"
```
This method also takes a block:
```ruby
-rakefile("test.rake") do
+rakefile "test.rake" do
%Q{
- task :rock => :environment do
+ task rock: :environment do
puts "Rockin'"
end
}
@@ -578,14 +605,14 @@ end
Creates an initializer in the `config/initializers` directory of the application:
```ruby
-initializer("begin.rb", "puts 'this is the beginning'")
+initializer "begin.rb", "puts 'this is the beginning'"
```
-This method also takes a block:
+This method also takes a block, expected to return a string:
```ruby
-initializer("begin.rb") do
- puts "Almost done!"
+initializer "begin.rb" do
+ "puts 'this is the beginning'"
end
```
@@ -594,7 +621,7 @@ end
Runs the specified generator where the first argument is the generator name and the remaining arguments are passed directly to the generator.
```ruby
-generate("scaffold", "forums title:string description:text")
+generate "scaffold", "forums title:string description:text"
```
@@ -603,7 +630,7 @@ generate("scaffold", "forums title:string description:text")
Runs the specified Rake task.
```ruby
-rake("db:migrate")
+rake "db:migrate"
```
Available options are:
@@ -624,7 +651,7 @@ capify!
Adds text to the `config/routes.rb` file:
```ruby
-route("resources :people")
+route "resources :people"
```
### `readme`
@@ -632,5 +659,5 @@ route("resources :people")
Output the contents of a file in the template's `source_path`, usually a README.
```ruby
-readme("README")
+readme "README"
```
diff --git a/guides/source/getting_started.md b/guides/source/getting_started.md
index 9f23f9fc42..2f322d15da 100644
--- a/guides/source/getting_started.md
+++ b/guides/source/getting_started.md
@@ -1,10 +1,11 @@
Getting Started with Rails
==========================
-This guide covers getting up and running with Ruby on Rails. After reading it,
-you should be familiar with:
+This guide covers getting up and running with Ruby on Rails.
-* Installing Rails, creating a new Rails application, and connecting your
+After reading this guide, you will know:
+
+* How to install Rails, create a new Rails application, and connect your
application to a database.
* The general layout of a Rails application.
* The basic principles of MVC (Model, View, Controller) and RESTful design.
@@ -12,9 +13,6 @@ you should be familiar with:
--------------------------------------------------------------------------------
-WARNING. This Guide is based on Rails 3.2. Some of the code shown here will not
-work in earlier versions of Rails.
-
Guide Assumptions
-----------------
@@ -23,15 +21,15 @@ application from scratch. It does not assume that you have any prior experience
with Rails. However, to get the most out of it, you need to have some
prerequisites installed:
-* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or higher
-* The [RubyGems](http://rubyforge.org/frs/?group_id=126) packaging system
- * To learn more about RubyGems, please read the [RubyGems User Guide](http://docs.rubygems.org/read/book/1)
+* The [Ruby](http://www.ruby-lang.org/en/downloads) language version 1.9.3 or newer
+* The [RubyGems](http://rubygems.org) packaging system
+ * To learn more about RubyGems, please read the [RubyGems Guides](http://guides.rubygems.org)
* A working installation of the [SQLite3 Database](http://www.sqlite.org)
Rails is a web application framework running on the Ruby programming language.
If you have no prior experience with Ruby, you will find a very steep learning
curve diving straight into Rails. There are some good free resources on the
-internet for learning Ruby, including:
+Internet for learning Ruby, including:
* [Mr. Neighborly's Humble Little Ruby Book](http://www.humblelittlerubybook.com)
* [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)
@@ -47,7 +45,7 @@ code while accomplishing more than many other languages and frameworks.
Experienced Rails developers also report that it makes web application
development more fun.
-Rails is opinionated software. It makes the assumption that there is a "best"
+Rails is opinionated software. It makes the assumption that there is the "best"
way to do things, and it's designed to encourage that way - and in some cases to
discourage alternatives. If you learn "The Rails Way" you'll probably discover a
tremendous increase in productivity. If you persist in bringing old habits from
@@ -56,9 +54,11 @@ learned elsewhere, you may have a less happy experience.
The Rails philosophy includes two major guiding principles:
-* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and over again is a bad thing.
-* Convention Over Configuration - means that Rails makes assumptions about what you want to do and how you're going to
-do it, rather than requiring you to specify every little thing through endless configuration files.
+* DRY - "Don't Repeat Yourself" - suggests that writing the same code over and
+ over again is a bad thing.
+* Convention Over Configuration - means that Rails makes assumptions about what
+ you want to do and how you're going to do it, rather than requiring you to
+ specify every little thing through endless configuration files.
Creating a New Rails Project
----------------------------
@@ -66,22 +66,27 @@ 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/lifo/docrails/tree/master/guides/code/getting_started).
+[here](https://github.com/rails/docrails/tree/master/guides/code/getting_started).
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.
-TIP: The examples below use # and $ to denote superuser and regular user terminal prompts respectively in a UNIX-like OS. If you are using Windows, your prompt will look something like c:\source_code>
+TIP: The examples below use `#` and `$` to denote superuser and regular
+user terminal prompts respectively in a UNIX-like OS. If you are using
+Windows, your prompt will look something like `c:\source_code>`
### Installing Rails
-Open up a command line prompt. On a mac this is called terminal, on windows it is called command prompt. Any commands prefaced with a dollar sign `$` should be run in the command line. Verify sure you have a current version of Ruby installed:
+Open up a command line prompt. On Mac OS X open Terminal.app, on Windows choose
+"Run" from your Start menu and type 'cmd.exe'. Any commands prefaced with a
+dollar sign `$` should be run in the command line. Verify that you have a
+current version of Ruby installed:
```bash
$ ruby -v
-ruby 1.9.3p194
+ruby 2.0.0p247
```
To install Rails, use the `gem install` command provided by RubyGems:
@@ -91,57 +96,66 @@ $ gem install rails
```
TIP. A number of tools exist to help you quickly install Ruby and Ruby
-on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org), while Mac OS X users can use
-[Rails One Click](http://railsoneclick.com).
+on Rails on your system. Windows users can use [Rails Installer](http://railsinstaller.org),
+while Mac OS X users can use [Rails One Click](http://railsoneclick.com).
-To verify that you have everything installed correctly, you should be able to run the following:
+To verify that you have everything installed correctly, you should be able to
+run the following:
```bash
$ rails --version
```
-If it says something like "Rails 3.2.8" you are ready to continue.
+If it says something like "Rails 4.0.0", you are ready to continue.
### Creating the Blog Application
-Rails comes with a number of generators that are designed to make your development life easier. One of these is the new application generator, which will provide you with the foundation of a Rails application so that you don't have to write it yourself.
+Rails comes with a number of scripts called generators that are designed to make
+your development life easier by creating everything that's necessary to start
+working on a particular task. One of these is the new application generator,
+which will provide you with the foundation of a fresh Rails application so that
+you don't have to write it yourself.
-To use this generator, open a terminal, navigate to a directory where you have rights to create files, and type:
+To use this generator, open a terminal, navigate to a directory where you have
+rights to create files, and type:
```bash
$ rails new blog
```
-This will create a Rails application called Blog in a directory called blog and install the gem dependencies that are already mentioned in `Gemfile` using `bundle install`.
+This will create a Rails application called Blog in a directory called blog and
+install the gem dependencies that are already mentioned in `Gemfile` using
+`bundle install`.
-TIP: You can see all of the command line options that the Rails
-application builder accepts by running `rails new -h`.
+TIP: You can see all of the command line options that the Rails application
+builder accepts by running `rails new -h`.
-After you create the blog application, switch to its folder to continue work directly in that application:
+After you create the blog application, switch to its folder to continue work
+directly in that application:
```bash
$ cd blog
```
-The `rails new blog` command we ran above created a folder in your
-working directory called `blog`. The `blog` directory has a number of
-auto-generated files and folders that make up the structure of a Rails
-application. Most of the work in this tutorial will happen in the `app/` folder, but here's a basic rundown on the function of each of the files and folders that Rails created by default:
+The `rails new blog` command we ran above created a folder in your working
+directory called `blog`. The `blog` directory has a number of auto-generated
+files and folders that make up the structure of a Rails application. Most of the
+work in this tutorial will happen in the `app/` folder, but here's a basic
+rundown on the function of each of the files and folders that Rails created by default:
| 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.|
-|config/|Configure your application's runtime rules, routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html)|
+|bin/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.|
+|config/|Configure your application's runtime rules, routes, database, and more. This is covered in more detail in [Configuring Rails Applications](configuring.html)|
|config.ru|Rack configuration for Rack based servers used to start the application.|
|db/|Contains your current database schema, as well as the database migrations.|
-|doc/|In-depth documentation for your application.|
-|Gemfile<br />Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) |
+|Gemfile<br>Gemfile.lock|These files allow you to specify what gem dependencies are needed for your Rails application. These files are used by the Bundler gem. For more information about Bundler, see [the Bundler website](http://gembundler.com) |
|lib/|Extended modules for your application.|
|log/|Application log files.|
|public/|The only folder seen to the world as-is. Contains the static files and compiled assets.|
|Rakefile|This file locates and loads tasks that can be run from the command line. The task definitions are defined throughout the components of Rails. Rather than changing Rakefile, you should add your own tasks by adding files to the lib/tasks directory of your application.|
|README.rdoc|This is a brief instruction manual for your application. You should edit this file to tell others what your application does, how to set it up, and so on.|
-|script/|Contains the rails script that starts your app and can contain other scripts you use to deploy or run your application.|
|test/|Unit tests, fixtures, and other test apparatus. These are covered in [Testing Rails Applications](testing.html)|
|tmp/|Temporary files (like cache, pid and session files)|
|vendor/|A place for all third-party code. In a typical Rails application, this includes Ruby Gems and the Rails source code (if you optionally install it into your project).|
@@ -149,35 +163,65 @@ application. Most of the work in this tutorial will happen in the `app/` folder,
Hello, Rails!
-------------
-To begin with, let's get some text up on screen quickly. To do this, you need to get your Rails application server running.
+To begin with, let's get some text up on screen quickly. To do this, you need to
+get your Rails application server running.
### Starting up the Web Server
-You actually have a functional Rails application already. To see it, you need to start a web server on your development machine. You can do this by running:
+You actually have a functional Rails application already. To see it, you need to
+start a web server on your development machine. You can do this by running the
+following in the root directory of your rails application:
```bash
$ rails server
```
-TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the absence of a runtime will give you an `execjs` error. Usually Mac OS X and Windows come with a JavaScript runtime installed. Rails adds the `therubyracer` gem to Gemfile in a commented line for new apps and you can uncomment if you need it. `therubyrhino` is the recommended runtime for JRuby users and is added by default to Gemfile in apps generated under JRuby. You can investigate about all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme).
+TIP: Compiling CoffeeScript to JavaScript requires a JavaScript runtime and the
+absence of a runtime will give you an `execjs` error. Usually Mac OS X and
+Windows come with a JavaScript runtime installed. Rails adds the `therubyracer`
+gem to Gemfile in a commented line for new apps and you can uncomment if you
+need it. `therubyrhino` is the recommended runtime for JRuby users and is added
+by default to Gemfile in apps generated under JRuby. You can investigate about
+all the supported runtimes at [ExecJS](https://github.com/sstephenson/execjs#readme).
-This will fire up WEBrick, a webserver built into Ruby by default. To see your application in action, open a browser window and navigate to <http://localhost:3000>. You should see the Rails default information page:
+This will fire up WEBrick, a webserver built into Ruby by default. To see your
+application in action, open a browser window and navigate to <http://localhost:3000>.
+You should see the Rails default information page:
-![Welcome Aboard screenshot](images/rails_welcome.png)
+![Welcome Aboard screenshot](images/getting_started/rails_welcome.png)
-TIP: To stop the web server, hit Ctrl+C in the terminal window where it's running. To verify the server has stopped you should see your command prompt cursor again. For most unix like systems including mac this will be a dollar sign `$`. In development mode, Rails does not generally require you to restart the server; changes you make in files will be automatically picked up by the server.
+TIP: To stop the web server, hit Ctrl+C in the terminal window where it's
+running. To verify the server has stopped you should see your command prompt
+cursor again. For most UNIX-like systems including Mac OS X this will be a
+dollar sign `$`. In development mode, Rails does not generally require you to
+restart the server; changes you make in files will be automatically picked up by
+the server.
-The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it makes sure that you have your software configured correctly enough to serve a page. You can also click on the _About your application’s environment_ link to see a summary of your application's environment.
+The "Welcome Aboard" page is the _smoke test_ for a new Rails application: it
+makes sure that you have your software configured correctly enough to serve a
+page. You can also click on the _About your application's environment_ link to
+see a summary of your application's environment.
### Say "Hello", Rails
-To get Rails saying "Hello", you need to create at minimum a _controller_ and a _view_.
+To get Rails saying "Hello", you need to create at minimum a _controller_ and a
+_view_.
-A controller's purpose is to receive specific requests for the application. _Routing_ decides which controller receives which requests. Often, there is more than one route to each controller, and different routes can be served by different _actions_. Each action's purpose is to collect information to provide it to a view.
+A controller's purpose is to receive specific requests for the application.
+_Routing_ decides which controller receives which requests. Often, there is more
+than one route to each controller, and different routes can be served by
+different _actions_. Each action's purpose is to collect information to provide
+it to a view.
-A view's purpose is to display this information in a human readable format. An important distinction to make is that it is the _controller_, not the view, where information is collected. The view should just display that information. By default, view templates are written in a language called ERB (Embedded Ruby) which is converted by the request cycle in Rails before being sent to the user.
+A view's purpose is to display this information in a human readable format. An
+important distinction to make is that it is the _controller_, not the view,
+where information is collected. The view should just display that information.
+By default, view templates are written in a language called ERB (Embedded Ruby)
+which is converted by the request cycle in Rails before being sent to the user.
-To create a new controller, you will need to run the "controller" generator and tell it you want a controller called "welcome" with an action called "index", just like this:
+To create a new controller, you will need to run the "controller" generator and
+tell it you want a controller called "welcome" with an action called "index",
+just like this:
```bash
$ rails generate controller welcome index
@@ -204,9 +248,12 @@ 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 and edit it to contain a single line of code:
+Open the `app/views/welcome/index.html.erb` file in your text editor. Delete all
+of the existing code in the file, and replace it with the following single line
+of code:
```html
<h1>Hello, Rails!</h1>
@@ -214,11 +261,10 @@ Open the `app/views/welcome/index.html.erb` file in your text editor and edit it
### Setting the Application Home Page
-Now that we have made the controller and view, we need to tell Rails when we want Hello Rails! to show up. In our case, we want it to show up when we navigate to the root URL of our site, <http://localhost:3000>. At the moment, however, the "Welcome Aboard" smoke test is occupying that spot.
-
-To fix this, delete the `index.html` file located inside the `public` directory of the application.
-
-You need to do this because Rails will serve any static file in the `public` directory that matches a route in preference to any dynamic content you generate from the controllers. The `index.html` file is special: it will be served if a request comes in at the root route, e.g. <http://localhost:3000>. If another request such as <http://localhost:3000/welcome> happened, a static file at `public/welcome.html` would be served first, but only if it existed.
+Now that we have made the controller and view, we need to tell Rails when we
+want `Hello, Rails!` to show up. In our case, we want it to show up when we
+navigate to the root URL of our site, <http://localhost:3000>. At the moment,
+"Welcome Aboard" is occupying that spot.
Next, you have to tell Rails where your actual home page is located.
@@ -232,112 +278,195 @@ Blog::Application.routes.draw do
# first created -> highest priority.
# ...
# You can have the root of your site routed with "root"
- # just remember to delete public/index.html.
- # root :to => "welcome#index"
+ # root "welcome#index"
```
-This is your application's _routing file_ which holds entries in a special DSL (domain-specific language) that tells Rails how to connect incoming requests to controllers and actions. This file contains many sample routes on commented lines, and one of them actually shows you how to connect the root of your site to a specific controller and action. Find the line beginning with `root :to` and uncomment it. It should look something like the following:
+This is your application's _routing file_ which holds entries in a special DSL
+(domain-specific language) that tells Rails how to connect incoming requests to
+controllers and actions. This file contains many sample routes on commented
+lines, and one of them actually shows you how to connect the root of your site
+to a specific controller and action. Find the line beginning with `root` and
+uncomment it. It should look something like the following:
```ruby
-root :to => "welcome#index"
+root "welcome#index"
```
-The `root :to => "welcome#index"` tells Rails to map requests to the root of the application to the welcome controller's index action and `get "welcome/index"` tells Rails to map requests to <http://localhost:3000/welcome/index> to the welcome controller's index action. This was created earlier when you ran the controller generator (`rails generate controller welcome index`).
+The `root "welcome#index"` tells Rails to map requests to the root of the
+application to the welcome controller's index action and `get "welcome/index"`
+tells Rails to map requests to <http://localhost:3000/welcome/index> to the
+welcome controller's index action. This was created earlier when you ran the
+controller generator (`rails generate controller welcome index`).
-If you navigate to <http://localhost:3000> in your browser, you'll see the `Hello, Rails!` message you put into `app/views/welcome/index.html.erb`, indicating that this new route is indeed going to `WelcomeController`'s `index` action and is rendering the view correctly.
+If you navigate to <http://localhost:3000> in your browser, you'll see the
+`Hello, Rails!` message you put into `app/views/welcome/index.html.erb`,
+indicating that this new route is indeed going to `WelcomeController`'s `index`
+action and is rendering the view correctly.
-NOTE. For more information about routing, refer to [Rails Routing from the Outside In](routing.html).
+TIP: For more information about routing, refer to [Rails Routing from the Outside In](routing.html).
Getting Up and Running
----------------------
-Now that you've seen how to create a controller, an action and a view, let's create something with a bit more substance.
+Now that you've seen how to create a controller, an action and a view, let's
+create something with a bit more substance.
-In the Blog application, you will now create a new _resource_. A resource is the term used for a collection of similar objects, such as posts, people or animals. You can create, read, update and destroy items for a resource and these operations are referred to as _CRUD_ operations.
+In the Blog application, you will now create a new _resource_. A resource is the
+term used for a collection of similar objects, such as posts, people or animals.
+You can create, read, update and destroy items for a resource and these
+operations are referred to as _CRUD_ operations.
-In the next section, you will add the ability to create new posts in your application and be able to view them. This is the "C" and the "R" from CRUD: creation and reading. The form for doing this will look like this:
+Rails provides a `resources` method which can be used to declare a standard REST
+resource. Here's what `config/routes.rb` should look like after the _post resource_
+is declared.
-![The new post form](images/getting_started/new_post.png)
+```ruby
+Blog::Application.routes.draw do
-It will look a little basic for now, but that's ok. We'll look at improving the styling for it afterwards.
+ resources :posts
-### Laying down the ground work
+ root "welcome#index"
+end
+```
-The first thing that you are going to need to create a new post within the application is a place to do that. A great place for that would be at `/posts/new`. If you attempt to navigate to that now -- by visiting <http://localhost:3000/posts/new> -- Rails will give you a routing error:
+If you run `rake routes`, you'll see that it has defined routes for all the
+standard RESTful actions. The meaning of the prefix column (and other columns)
+will be seen later, but for now notice that Rails has inferred the
+singular form `post` and makes meaningful use of the distinction.
-![A routing error, no route matches /posts/new](images/getting_started/routing_error_no_route_matches.png)
+```bash
+$ rake routes
+ Prefix Verb URI Pattern Controller#Action
+ posts GET /posts(.:format) posts#index
+ POST /posts(.:format) posts#create
+ new_post GET /posts/new(.:format) posts#new
+edit_post GET /posts/:id/edit(.:format) posts#edit
+ post GET /posts/:id(.:format) posts#show
+ PATCH /posts/:id(.:format) posts#update
+ PUT /posts/:id(.:format) posts#update
+ DELETE /posts/:id(.:format) posts#destroy
+ root / welcome#index
+```
-This is because there is nowhere inside the routes for the application -- defined inside `config/routes.rb` -- that defines this route. By default, Rails has no routes configured at all, besides the root route you defined earlier, and so you must define your routes as you need them.
+In the next section, you will add the ability to create new posts in your
+application and be able to view them. This is the "C" and the "R" from CRUD:
+creation and reading. The form for doing this will look like this:
- To do this, you're going to need to create a route inside `config/routes.rb` file, on a new line between the `do` and the `end` for the `draw` method:
+![The new post form](images/getting_started/new_post.png)
-```ruby
-get "posts/new"
-```
+It will look a little basic for now, but that's ok. We'll look at improving the
+styling for it afterwards.
-This route is a super-simple route: it defines a new route that only responds to `GET` requests, and that the route is at `posts/new`. But how does it know where to go without the use of the `:to` option? Well, Rails uses a sensible default here: Rails will assume that you want this route to go to the new action inside the posts controller.
+### Laying down the ground work
-With the route defined, requests can now be made to `/posts/new` in the application. Navigate to <http://localhost:3000/posts/new> and you'll see another routing error:
+The first thing that you are going to need to create a new post within the
+application is a place to do that. A great place for that would be at `/posts/new`.
+With the route already defined, requests can now be made to `/posts/new` in the
+application. Navigate to <http://localhost:3000/posts/new> and you'll see a
+routing error:
![Another routing error, uninitialized constant PostsController](images/getting_started/routing_error_no_controller.png)
-This error is happening because this route need a controller to be defined. The route is attempting to find that controller so it can serve the request, but with the controller undefined, it just can't do that. The solution to this particular problem is simple: you need to create a controller called `PostsController`. You can do this by running this command:
+This error occurs because the route needs to have a controller defined in order
+to serve the request. The solution to this particular problem is simple: create
+a controller called `PostsController`. You can do this by running this command:
```bash
$ rails g controller posts
```
-If you open up the newly generated `app/controllers/posts_controller.rb` you'll see a fairly empty controller:
+If you open up the newly generated `app/controllers/posts_controller.rb` you'll
+see a fairly empty controller:
```ruby
class PostsController < ApplicationController
end
```
-A controller is simply a class that is defined to inherit from `ApplicationController`. It's inside this class that you'll define methods that will become the actions for this controller. These actions will perform CRUD operations on the posts within our system.
+A controller is simply a class that is defined to inherit from `ApplicationController`.
+It's inside this class that you'll define methods that will become the actions
+for this controller. These actions will perform CRUD operations on the posts
+within our system.
+
+NOTE: There are `public`, `private` and `protected` methods in `Ruby`
+(for more details you can check on [Programming Ruby](http://www.ruby-doc.org/docs/ProgrammingRuby/)).
+But only `public` methods can be actions for controllers.
If you refresh <http://localhost:3000/posts/new> now, you'll get a new error:
![Unknown action new for PostsController!](images/getting_started/unknown_action_new_for_posts.png)
-This error indicates that Rails cannot find the `new` action inside the `PostsController` that you just generated. This is because when controllers are generated in Rails they are empty by default, unless you tell it you wanted actions during the generation process.
+This error indicates that Rails cannot find the `new` action inside the `PostsController`
+that you just generated. This is because when controllers are generated in Rails
+they are empty by default, unless you tell it you wanted actions during the
+generation process.
-To manually define an action inside a controller, all you need to do is to define a new method inside the controller. Open `app/controllers/posts_controller.rb` and inside the `PostsController` class, define a `new` method like this:
+To manually define an action inside a controller, all you need to do is to
+define a new method inside the controller. Open `app/controllers/posts_controller.rb`
+and inside the `PostsController` class, define a `new` method like this:
```ruby
def new
end
```
-With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new> you'll see another error:
+With the `new` method defined in `PostsController`, if you refresh <http://localhost:3000/posts/new>
+you'll see another error:
![Template is missing for posts/new](images/getting_started/template_is_missing_posts_new.png)
-You're getting this error now because Rails expects plain actions like this one to have views associated with them to display their information. With no view available, Rails errors out.
+You're getting this error now because Rails expects plain actions like this one
+to have views associated with them to display their information. With no view
+available, Rails errors out.
-In the above image, the bottom line has been truncated. Let's see what the full thing looks like:
+In the above image, the bottom line has been truncated. Let's see what the full
+thing looks like:
<blockquote>
-Missing template posts/new, application/new with {:locale=>[:en], :formats=>[:html], :handlers=>[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
+Missing template posts/new, application/new with {locale:[:en], formats:[:html], handlers:[:erb, :builder, :coffee]}. Searched in: * "/path/to/blog/app/views"
</blockquote>
-That's quite a lot of text! Let's quickly go through and understand what each part of it does.
-
-The first part identifies what template is missing. In this case, it's the `posts/new` template. Rails will first look for this template. If not found, then it will attempt to load a template called `application/new`. It looks for one here because the `PostsController` inherits from `ApplicationController`.
-
-The next part of the message contains a hash. The `:locale` key in this hash simply indicates what spoken language template should be retrieved. By default, this is the English -- or "en" -- template. The next key, `:formats` specifies the format of template to be served in response . The default format is `:html`, and so Rails is looking for an HTML template. The final key, `:handlers`, is telling us what _template handlers_ could be used to render our template. `:erb` is most commonly used for HTML templates, `:builder` is used for XML templates, and `:coffee` uses CoffeeScript to build JavaScript templates.
-
-The final part of this message tells us where Rails has looked for the templates. Templates within a basic Rails application like this are kept in a single location, but in more complex applications it could be many different paths.
-
-The simplest template that would work in this case would be one located at `app/views/posts/new.html.erb`. The extension of this file name is key: the first extension is the _format_ of the template, and the second extension is the _handler_ that will be used. Rails is attempting to find a template called `posts/new` within `app/views` for the application. The format for this template can only be `html` and the handler must be one of `erb`, `builder` or `coffee`. Because you want to create a new HTML form, you will be using the `ERB` language. Therefore the file should be called `posts/new.html.erb` and needs to be located inside the `app/views` directory of the application.
-
-Go ahead now and create a new file at `app/views/posts/new.html.erb` and write this content in it:
+That's quite a lot of text! Let's quickly go through and understand what each
+part of it does.
+
+The first part identifies what template is missing. In this case, it's the
+`posts/new` template. Rails will first look for this template. If not found,
+then it will attempt to load a template called `application/new`. It looks for
+one here because the `PostsController` inherits from `ApplicationController`.
+
+The next part of the message contains a hash. The `:locale` key in this hash
+simply indicates what spoken language template should be retrieved. By default,
+this is the English - or "en" - template. The next key, `:formats` specifies the
+format of template to be served in response. The default format is `:html`, and
+so Rails is looking for an HTML template. The final key, `:handlers`, is telling
+us what _template handlers_ could be used to render our template. `:erb` is most
+commonly used for HTML templates, `:builder` is used for XML templates, and
+`:coffee` uses CoffeeScript to build JavaScript templates.
+
+The final part of this message tells us where Rails has looked for the templates.
+Templates within a basic Rails application like this are kept in a single
+location, but in more complex applications it could be many different paths.
+
+The simplest template that would work in this case would be one located at
+`app/views/posts/new.html.erb`. The extension of this file name is key: the
+first extension is the _format_ of the template, and the second extension is the
+_handler_ that will be used. Rails is attempting to find a template called
+`posts/new` within `app/views` for the application. The format for this template
+can only be `html` and the handler must be one of `erb`, `builder` or `coffee`.
+Because you want to create a new HTML form, you will be using the `ERB`
+language. Therefore the file should be called `posts/new.html.erb` and needs to
+be located inside the `app/views` directory of the application.
+
+Go ahead now and create a new file at `app/views/posts/new.html.erb` and write
+this content in it:
```html
<h1>New Post</h1>
```
-When you refresh <http://localhost:3000/posts/new> you'll now see that the page has a title. The route, controller, action and view are now working harmoniously! It's time to create the form for a new post.
+When you refresh <http://localhost:3000/posts/new> you'll now see that the page
+has a title. The route, controller, action and view are now working
+harmoniously! It's time to create the form for a new post.
### The first form
@@ -363,14 +492,21 @@ method called `form_for`. To use this method, add this code into `app/views/post
<% end %>
```
-If you refresh the page now, you'll see the exact same form as in the example. Building forms in Rails is really just that easy!
+If you refresh the page now, you'll see the exact same form as in the example.
+Building forms in Rails is really just that easy!
When you call `form_for`, you pass it an identifying object for this
form. In this case, it's the symbol `:post`. This tells the `form_for`
helper what this form is for. Inside the block for this method, the
-`FormBuilder` object -- represented by `f` -- is used to build two labels and two text fields, one each for the title and text of a post. Finally, a call to `submit` on the `f` object will create a submit button for the form.
+`FormBuilder` object - represented by `f` - is used to build two labels and two
+text fields, one each for the title and text of a post. Finally, a call to
+`submit` on the `f` object will create a submit button for the form.
-There's one problem with this form though. If you inspect the HTML that is generated, by viewing the source of the page, you will see that the `action` attribute for the form is pointing at `/posts/new`. This is a problem because this route goes to the very page that you're on right at the moment, and that route should only be used to display the form for a new post.
+There's one problem with this form though. If you inspect the HTML that is
+generated, by viewing the source of the page, you will see that the `action`
+attribute for the form is pointing at `/posts/new`. This is a problem because
+this route goes to the very page that you're on right at the moment, and that
+route should only be used to display the form for a new post.
The form needs to use a different URL in order to go somewhere else.
This can be done quite simply with the `:url` option of `form_for`.
@@ -380,26 +516,48 @@ like this is called "create", and so the form should be pointed to that action.
Edit the `form_for` line inside `app/views/posts/new.html.erb` to look like this:
```html+erb
-<%= form_for :post, :url => { :action => :create } do |f| %>
+<%= form_for :post, url: posts_path do |f| %>
```
-In this example, a `Hash` object is passed to the `:url` option. What Rails will do with this is that it will point the form to the `create` action of the current controller, the `PostsController`, and will send a `POST` request to that route. For this to work, you will need to add a route to `config/routes.rb`, right underneath the one for "posts/new":
+In this example, the `posts_path` helper is passed to the `:url` option.
+To see what Rails will do with this, we look back at the output of
+`rake routes`:
-```ruby
-post "posts" => "posts#create"
+```bash
+$ rake routes
+ Prefix Verb URI Pattern Controller#Action
+ posts GET /posts(.:format) posts#index
+ POST /posts(.:format) posts#create
+ new_post GET /posts/new(.:format) posts#new
+edit_post GET /posts/:id/edit(.:format) posts#edit
+ post GET /posts/:id(.:format) posts#show
+ PATCH /posts/:id(.:format) posts#update
+ PUT /posts/:id(.:format) posts#update
+ DELETE /posts/:id(.:format) posts#destroy
+ root / welcome#index
```
-By using the `post` method rather than the `get` method, Rails will define a route that will only respond to POST methods. The POST method is the typical method used by forms all over the web.
+The `posts_path` helper tells Rails to point the form
+to the URI Pattern associated with the `posts` 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 `PostsController`.
-With the form and its associated route defined, you will be able to fill in the form and then click the submit button to begin the process of creating a new post, so go ahead and do that. When you submit the form, you should see a familiar error:
+With the form and its associated route defined, you will be able to fill in the
+form and then click the submit button to begin the process of creating a new
+post, so go ahead and do that. When you submit the form, you should see a
+familiar error:
![Unknown action create for PostsController](images/getting_started/unknown_action_create_for_posts.png)
-You now need to create the `create` action within the `PostsController` for this to work.
+You now need to create the `create` action within the `PostsController` for this
+to work.
### Creating posts
-To make the "Unknown action" go away, you can define a `create` action within the `PostsController` class in `app/controllers/posts_controller.rb`, underneath the `new` action:
+To make the "Unknown action" go away, you can define a `create` action within
+the `PostsController` class in `app/controllers/posts_controller.rb`, underneath
+the `new` action:
```ruby
class PostsController < ApplicationController
@@ -411,25 +569,38 @@ class PostsController < ApplicationController
end
```
-If you re-submit the form now, you'll see another familiar error: a template is missing. That's ok, we can ignore that for now. What the `create` action should be doing is saving our new post to a database.
+If you re-submit the form now, you'll see another familiar error: a template is
+missing. That's ok, we can ignore that for now. What the `create` action should
+be doing is saving our new post to a database.
-When a form is submitted, the fields of the form are sent to Rails as _parameters_. These parameters can then be referenced inside the controller actions, typically to perform a particular task. To see what these parameters look like, change the `create` action to this:
+When a form is submitted, the fields of the form are sent to Rails as
+_parameters_. These parameters can then be referenced inside the controller
+actions, typically to perform a particular task. To see what these parameters
+look like, change the `create` action to this:
```ruby
def create
- render :text => params[:post].inspect
+ render text: params[:post].inspect
end
```
-The `render` method here is taking a very simple hash with a key of `text` and value of `params[:post].inspect`. The `params` method is the object which represents the parameters (or fields) coming in from the form. The `params` method returns a `HashWithIndifferentAccess` object, which allows you to access the keys of the hash using either strings or symbols. In this situation, the only parameters that matter are the ones from the form.
+The `render` method here is taking a very simple hash with a key of `text` and
+value of `params[:post].inspect`. The `params` method is the object which
+represents the parameters (or fields) coming in from the form. The `params`
+method returns an `ActiveSupport::HashWithIndifferentAccess` object, which
+allows you to access the keys of the hash using either strings or symbols. In
+this situation, the only parameters that matter are the ones from the form.
-If you re-submit the form one more time you'll now no longer get the missing template error. Instead, you'll see something that looks like the following:
+If you re-submit the form one more time you'll now no longer get the missing
+template error. Instead, you'll see something that looks like the following:
```ruby
{"title"=>"First post!", "text"=>"This is my first post."}
```
-This action is now displaying the parameters for the post that are coming in from the form. However, this isn't really all that helpful. Yes, you can see the parameters but nothing in particular is being done with them.
+This action is now displaying the parameters for the post that are coming in
+from the form. However, this isn't really all that helpful. Yes, you can see the
+parameters but nothing in particular is being done with them.
### Creating the Post model
@@ -489,9 +660,10 @@ run this migration. The action defined in this method is also reversible, which
means Rails knows how to reverse the change made by this migration, in case you
want to reverse it later. When you run this migration it will create a
`posts` table with one string column and a text column. It also creates two
-timestamp fields to allow Rails to track post creation and update times. More
-information about Rails migrations can be found in the "Rails Database
-Migrations":migrations.html guide.
+timestamp fields to allow Rails to track post creation and update times.
+
+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:
@@ -517,8 +689,8 @@ invoking the command: `rake db:migrate RAILS_ENV=production`.
### Saving data in the controller
-Back in `posts_controller`, we need to change the `create` action
-to use the new `Post` model to save the data in the database. Open that file
+Back in `PostsController`, we need to change the `create` action
+to use the new `Post` model to save the data in the database. Open `app/controllers/posts_controller.rb`
and change the `create` action to look like this:
```ruby
@@ -526,38 +698,74 @@ def create
@post = Post.new(params[:post])
@post.save
- redirect_to :action => :show, :id => @post.id
+ redirect_to @post
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[:post]` contains the attributes we're interested in). Then,
-`@post.save` is responsible for saving the model in the database.
-Finally, we redirect the user to the `show` action,
-which we'll define later.
+database columns. In the first line we do just that
+(remember that `params[:post]` contains the attributes we're interested in).
+Then, `@post.save` is responsible for saving the model in the database.
+Finally, we redirect the user to the `show` action, which we'll define later.
TIP: As we'll see later, `@post.save` returns a boolean indicating
-wherever the model was saved or not.
+whether the model was saved or not.
+
+If you now go to
+<http://localhost:3000/posts/new> you'll *almost* be able to create a post. Try
+it! You should get an error that looks like this:
+
+![Forbidden attributes for new post](images/getting_started/forbidden_attributes_for_new_post.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:
+
+```ruby
+def create
+ @post = Post.new(post_params)
+
+ @post.save
+ redirect_to @post
+end
+
+private
+ def post_params
+ params.require(:post).permit(:title, :text)
+ end
+```
+
+See the `permit`? It allows us to accept both `title` and `text` in this
+action.
+
+TIP: Note that `def post_params` is private. This new approach prevents an
+attacker from setting the model's attributes by manipulating the hash passed to
+the model.
+For more information, refer to
+[this blog post about Strong Parameters](http://weblog.rubyonrails.org/2012/3/21/strong-parameters/).
### Showing Posts
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. Open `config/routes.rb` and add the following route:
+`show` action before proceeding.
+
+As we have seen in the output of `rake routes`, the route for `show` action is
+as follows:
```ruby
-get "posts/:id" => "posts#show"
+post GET /posts/:id(.:format) posts#show
```
The special syntax `:id` tells rails that this route expects an `:id`
-parameter, which in our case will be the id of the post. Note that this
-time we had to specify the actual mapping, `posts#show` because
-otherwise Rails would not know which action to render.
+parameter, which in our case will be the id of the post.
-As we did before, we need to add the `show` action in the
-`posts_controller` and its respective view.
+As we did before, we need to add the `show` action in
+`app/controllers/posts_controller.rb` and its respective view.
```ruby
def show
@@ -566,11 +774,12 @@ end
```
A couple of things to note. We use `Post.find` to find the post we're
-interested in. We also use an instance variable (prefixed by `@`) to
-hold a reference to the post object. We do this because Rails will pass all instance
+interested in, passing in `params[:id]` to get the `:id` parameter from the
+request. We also use an instance variable (prefixed by `@`) to hold a
+reference to the post object. We do this because Rails will pass all instance
variables to the view.
-Now, create a new file `app/view/posts/show.html.erb` with the following
+Now, create a new file `app/views/posts/show.html.erb` with the following
content:
```html+erb
@@ -585,22 +794,21 @@ content:
</p>
```
-Finally, if you now go to
-<http://localhost:3000/posts/new> you'll
-be able to create a post. Try it!
+With this change, you should finally be able to create new posts.
+Visit <http://localhost:3000/posts/new> and give it a try!
![Show action for posts](images/getting_started/show_action_for_posts.png)
### Listing all posts
-We still need a way to list all our posts, so let's do that. As usual,
-we'll need a route placed into `config/routes.rb`:
+We still need a way to list all our posts, so let's do that.
+The route for this as per output of `rake routes` is:
```ruby
-get "posts" => "posts#index"
+posts GET /posts(.:format) posts#index
```
-And an action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file:
+Add the corresponding `index` action for that route inside the `PostsController` in the `app/controllers/posts_controller.rb` file:
```ruby
def index
@@ -628,7 +836,8 @@ And then finally a view for this action, located at `app/views/posts/index.html.
</table>
```
-Now if you go to `http://localhost:3000/posts` you will see a list of all the posts that you have created.
+Now if you go to `http://localhost:3000/posts` you will see a list of all the
+posts that you have created.
### Adding links
@@ -639,30 +848,36 @@ Open `app/views/welcome/index.html.erb` and modify it as follows:
```html+erb
<h1>Hello, Rails!</h1>
-<%= link_to "My Blog", :controller => "posts" %>
+<%= link_to 'My Blog', controller: 'posts' %>
```
The `link_to` method is one of Rails' built-in view helpers. It creates a
hyperlink based on text to display and where to go - in this case, to the path
for posts.
-Let's add links to the other views as well, starting with adding this "New Post" link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag:
+Let's add links to the other views as well, starting with adding this "New Post"
+link to `app/views/posts/index.html.erb`, placing it above the `<table>` tag:
```erb
-<%= link_to 'New post', :action => :new %>
+<%= link_to 'New post', new_post_path %>
```
-This link will allow you to bring up the form that lets you create a new post. You should also add a link to this template -- `app/views/posts/new.html.erb` -- to go back to the `index` action. Do this by adding this underneath the form in this template:
+This link will allow you to bring up the form that lets you create a new post.
+You should also add a link to this template - `app/views/posts/new.html.erb` -
+to go back to the `index` action. Do this by adding this underneath the form in
+this template:
```erb
<%= form_for :post do |f| %>
...
<% end %>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
-Finally, add another link to the `app/views/posts/show.html.erb` template to go back to the `index` action as well, so that people who are viewing a single post can go back and view the whole list again:
+Finally, add another link to the `app/views/posts/show.html.erb` template to go
+back to the `index` action as well, so that people who are viewing a single post
+can go back and view the whole list again:
```html+erb
<p>
@@ -675,7 +890,7 @@ Finally, add another link to the `app/views/posts/show.html.erb` template to go
<%= @post.text %>
</p>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
TIP: If you want to link to an action in the same controller, you don't
@@ -686,7 +901,7 @@ TIP: In development mode (which is what you're working in by default), Rails
reloads your application with every browser request, so there's no need to stop
and restart the web server when a change is made.
-### Allowing the update of fields
+### Adding Some Validation
The model file, `app/models/post.rb` is about as simple as it can get:
@@ -701,37 +916,21 @@ your Rails models for free, including basic database CRUD (Create, Read, Update,
Destroy) operations, data validation, as well as sophisticated search support
and the ability to relate multiple models to one another.
-Rails includes methods to help you secure some of your model fields.
-Open the `app/models/post.rb` file and edit it:
-
-```ruby
-class Post < ActiveRecord::Base
- attr_accessible :text, :title
-end
-```
-
-This change will ensure that all changes made through HTML forms can edit the content of the text and title fields.
-It will not be possible to define any other field value through forms. You can still define them by calling the `field=` method of course.
-Accessible attributes and the mass assignment problem is covered in details in the [Security guide](security.html#mass-assignment)
-
-### Adding Some Validation
-
Rails includes methods to help you validate the data that you send to models.
Open the `app/models/post.rb` file and edit it:
```ruby
class Post < ActiveRecord::Base
- attr_accessible :text, :title
-
- validates :title, :presence => true,
- :length => { :minimum => 5 }
+ validates :title, presence: true,
+ length: { minimum: 5 }
end
```
-These changes will ensure that all posts 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 and Callbacks](active_record_validations_callbacks.html#validations-overview)
+These changes will ensure that all posts have a title that is at least five
+characters long. Rails can validate a variety of conditions in a model,
+including the presence or uniqueness of columns, their format, and the
+existence of associated objects. Validations are covered in detail in [Active
+Record Validations](active_record_validations.html)
With the validation now in place, when you call `@post.save` on an invalid
post, it will return `false`. If you open `app/controllers/posts_controller.rb`
@@ -746,21 +945,29 @@ def new
end
def create
- @post = Post.new(params[:post])
+ @post = Post.new(post_params)
if @post.save
- redirect_to :action => :show, :id => @post.id
+ redirect_to @post
else
render 'new'
end
end
+
+private
+ def post_params
+ params.require(:post).permit(:title, :text)
+ end
```
The `new` action is now creating a new instance variable called `@post`, and
you'll see why that is in just a few moments.
-Notice that inside the `create` action we use `render` instead of `redirect_to` when `save`
-returns `false`. The `render` method is used so that the `@post` object is passed back to the `new` template when it is rendered. This rendering is done within the same request as the form submission, whereas the `redirect_to` will tell the browser to issue another request.
+Notice that inside the `create` action we use `render` instead of `redirect_to`
+when `save` returns `false`. The `render` method is used so that the `@post`
+object is passed back to the `new` template when it is rendered. This rendering
+is done within the same request as the form submission, whereas the `redirect_to`
+will tell the browser to issue another request.
If you reload
<http://localhost:3000/posts/new> and
@@ -770,9 +977,9 @@ something went wrong. To do that, you'll modify
`app/views/posts/new.html.erb` to check for error messages:
```html+erb
-<%= form_for :post, :url => { :action => :create } do |f| %>
+<%= form_for :post, url: posts_path do |f| %>
<% if @post.errors.any? %>
- <div id="errorExplanation">
+ <div id="error_explanation">
<h2><%= pluralize(@post.errors.count, "error") %> prohibited
this post from being saved:</h2>
<ul>
@@ -797,7 +1004,7 @@ something went wrong. To do that, you'll modify
</p>
<% end %>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
A few things are going on. We check if there are any errors with
@@ -805,9 +1012,10 @@ A few things are going on. We check if there are any errors with
errors with `@post.errors.full_messages`.
`pluralize` is a rails helper that takes a number and a string as its
-arguments. If the number is greater than one, the string will be automatically pluralized.
+arguments. If the number is greater than one, the string will be automatically
+pluralized.
-The reason why we added `@post = Post.new` in `posts_controller` is that
+The reason why we added `@post = Post.new` in the `PostsController` is that
otherwise `@post` would be `nil` in our view, and calling
`@post.errors.any?` would throw an error.
@@ -822,17 +1030,10 @@ attempt to do just that on the new post form [(http://localhost:3000/posts/new)]
### Updating Posts
-We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating posts.
-
-The first step we'll take is adding a `edit` action to `posts_controller`.
-
-Start by adding a route to `config/routes.rb`:
-
-```ruby
-get "posts/:id/edit" => "posts#edit"
-```
+We've covered the "CR" part of CRUD. Now let's focus on the "U" part, updating
+posts.
-And then add the controller action:
+The first step we'll take is adding an `edit` action to the `PostsController`.
```ruby
def edit
@@ -847,10 +1048,9 @@ it look as follows:
```html+erb
<h1>Editing post</h1>
-<%= form_for :post, :url => { :action => :update, :id => @post.id },
-:method => :put do |f| %>
+<%= form_for :post, url: post_path(@post), method: :patch do |f| %>
<% if @post.errors.any? %>
- <div id="errorExplanation">
+ <div id="error_explanation">
<h2><%= pluralize(@post.errors.count, "error") %> prohibited
this post from being saved:</h2>
<ul>
@@ -875,46 +1075,46 @@ it look as follows:
</p>
<% end %>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
This time we point the form to the `update` action, which is not defined yet
but will be very soon.
-The `:method => :put` option tells Rails that we want this form to be
-submitted via the `PUT`, HTTP method which is the HTTP method you're expected to use to
+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`.
-Next, we need to add the `update` action. The file
-`config/routes.rb` will need just one more line:
-
-```ruby
-put "posts/:id" => "posts#update"
-```
-
-And then create the `update` action in `app/controllers/posts_controller.rb`:
+Next we need to create the `update` action in `app/controllers/posts_controller.rb`:
```ruby
def update
@post = Post.find(params[:id])
- if @post.update_attributes(params[:post])
- redirect_to :action => :show, :id => @post.id
+ if @post.update(post_params)
+ redirect_to @post
else
render 'edit'
end
end
+
+private
+ def post_params
+ params.require(:post).permit(:title, :text)
+ end
```
-The new method, `update_attributes`, is used when you want to update a record
+The new method, `update`, is used when you want to update a record
that already exists, and it accepts a hash containing the attributes
that you want to update. As before, if there was an error updating the
post we want to show the form back to the user.
-TIP: you don't need to pass all attributes to `update_attributes`. For
-example, if you'd call `@post.update_attributes(:title => 'A new title')`
+We reuse the `post_params` method that we defined earlier for the create action.
+
+TIP: You don't need to pass all attributes to `update`. For
+example, if you'd call `@post.update(title: 'A new title')`
Rails would only update the `title` attribute, leaving all other
attributes untouched.
@@ -927,16 +1127,15 @@ appear next to the "Show" link:
<tr>
<th>Title</th>
<th>Text</th>
- <th></th>
- <th></th>
+ <th colspan="2"></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 'Show', post_path(post) %></td>
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
</tr>
<% end %>
</table>
@@ -949,8 +1148,8 @@ the template:
```html+erb
...
-<%= link_to 'Back', :action => :index %>
-| <%= link_to 'Edit', :action => :edit, :id => @post.id %>
+<%= link_to 'Back', posts_path %>
+| <%= link_to 'Edit', edit_post_path(@post) %>
```
And here's how our app looks so far:
@@ -959,42 +1158,21 @@ And here's how our app looks so far:
### Using partials to clean up duplication in views
-`partials` are what Rails uses to remove duplication in views. Here's a
-simple example:
-
-```html+erb
-# app/views/user/show.html.erb
-
-<h1><%= @user.name %></h1>
-
-<%= render 'user_details' %>
-
-# app/views/user/_user_details.html.erb
-
-<%= @user.location %>
-
-<%= @user.about_me %>
-```
-
-The `users/show` template will automatically include the content of the
-`users/_user_details` template. Note that partials are prefixed by an underscore,
-as to not be confused with regular views. However, you don't include the
-underscore when including them with the `helper` method.
+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.
TIP: You can read more about partials in the
[Layouts and Rendering in Rails](layouts_and_rendering.html) guide.
-Our `edit` action looks very similar to the `new` action, in fact they
-both share the same code for displaying the form. Lets clean them up by
-using a partial.
-
Create a new file `app/views/posts/_form.html.erb` with the following
content:
```html+erb
<%= form_for @post do |f| %>
<% if @post.errors.any? %>
- <div id="errorExplanation">
+ <div id="error_explanation">
<h2><%= pluralize(@post.errors.count, "error") %> prohibited
this post from being saved:</h2>
<ul>
@@ -1021,17 +1199,22 @@ content:
```
Everything except for the `form_for` declaration remained the same.
-How `form_for` can figure out the right `action` and `method` attributes
-when building the form will be explained in just a moment. For now, let's update the
-`app/views/posts/new.html.erb` view to use this new partial, rewriting it
-completely:
+The reason we can use this shorter, simpler `form_for` declaration
+to stand in for either of the other forms is that `@post` 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).
+
+Now, let's update the `app/views/posts/new.html.erb` view to use this new
+partial, rewriting it completely:
```html+erb
<h1>New post</h1>
<%= render 'form' %>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
Then do the same for the `app/views/posts/edit.html.erb` view:
@@ -1041,66 +1224,17 @@ Then do the same for the `app/views/posts/edit.html.erb` view:
<%= render 'form' %>
-<%= link_to 'Back', :action => :index %>
+<%= link_to 'Back', posts_path %>
```
-Point your browser to <http://localhost:3000/posts/new> and
-try creating a new post. Everything still works. Now try editing the
-post and you'll receive the following error:
-
-![Undefined method post_path](images/getting_started/undefined_method_post_path.png)
-
-To understand this error, you need to understand how `form_for` works.
-When you pass an object to `form_for` and you don't specify a `:url`
-option, Rails will try to guess the `action` and `method` options by
-checking if the passed object is a new record or not. Rails follows the
-REST convention, so to create a new `Post` object it will look for a
-route named `posts_path`, and to update a `Post` object it will look for
-a route named `post_path` and pass the current object. Similarly, rails
-knows that it should create new objects via POST and update them via
-PUT.
-
-If you run `rake routes` from the console you'll see that we already
-have a `posts_path` route, which was created automatically by Rails when we
-defined the route for the index action.
-However, we don't have a `post_path` yet, which is the reason why we
-received an error before. With your server running you can view your routes by visiting [localhost:3000/rails/info/routes](http://localhost:3000/rails/info/routes), or you can generate them from the command line by running `rake routes`:
-
-```bash
-$ rake routes
-
- posts GET /posts(.:format) posts#index
-posts_new GET /posts/new(.:format) posts#new
- POST /posts(.:format) posts#create
- GET /posts/:id(.:format) posts#show
- GET /posts/:id/edit(.:format) posts#edit
- PUT /posts/:id(.:format) posts#update
- root / welcome#index
-```
-
-To fix this, open `config/routes.rb` and modify the `get "posts/:id"`
-line like this:
-
-```ruby
-get "posts/:id" => "posts#show", :as => :post
-```
-
-The `:as` option tells the `get` method that we want to make routing helpers
-called `post_url` and `post_path` available to our application. These are
-precisely the methods that the `form_for` needs when editing a post, and so now
-you'll be able to update posts again.
-
-NOTE: The `:as` option is available on the `post`, `put`, `delete` and `match`
-routing methods also.
-
### Deleting Posts
We're now ready to cover the "D" part of CRUD, deleting posts from the
-database. Following the REST convention, we're going to add a route for
-deleting posts to `config/routes.rb`:
+database. Following the REST convention, the route for
+deleting posts as per output of `rake routes` is:
```ruby
-delete "posts/:id" => "posts#destroy"
+DELETE /posts/:id(.:format) posts#destroy
```
The `delete` routing method should be used for routes that destroy
@@ -1108,19 +1242,19 @@ resources. If this was left as a typical `get` route, it could be possible for
people to craft malicious URLs like this:
```html
-<a href='http://yoursite.com/posts/1/destroy'>look at this cat!</a>
+<a href='http://example.com/posts/1/destroy'>look at this cat!</a>
```
We use the `delete` method for destroying resources, and this route is mapped to
-the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't exist yet, but is
-provided below:
+the `destroy` action inside `app/controllers/posts_controller.rb`, which doesn't
+exist yet, but is provided below:
```ruby
def destroy
@post = Post.find(params[:id])
@post.destroy
- redirect_to :action => :index
+ redirect_to posts_path
end
```
@@ -1128,7 +1262,7 @@ 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
+Finally, add a 'Destroy' link to your `index` action template
(`app/views/posts/index.html.erb`) to wrap everything
together.
@@ -1138,97 +1272,46 @@ together.
<tr>
<th>Title</th>
<th>Text</th>
- <th></th>
- <th></th>
- <th></th>
+ <th colspan="3"></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>
+ <td><%= link_to 'Show', post_path(post) %></td>
+ <td><%= link_to 'Edit', edit_post_path(post) %></td>
+ <td><%= link_to 'Destroy', post_path(post),
+ method: :delete, data: { confirm: 'Are you sure?' } %></td>
</tr>
<% end %>
</table>
```
-Here we're using `link_to` in a different way. We wrap the
-`:action` and `:id` attributes in a hash so that we can pass those two keys in
-first as one argument, and then the final two keys as another argument. The `:method` and `:'data-confirm'`
-options are used as HTML5 attributes so that when the link is clicked,
-Rails will first show a confirm dialog to the user, and then submit the link with method `delete`.
-This is done via the JavaScript file `jquery_ujs` which is automatically included
-into your application's layout (`app/views/layouts/application.html.erb`) when you
-generated the application. Without this file, the confirmation dialog box wouldn't appear.
+Here we're using `link_to` in a different way. We pass the named route as the
+second argument, and then the options as another argument. The `:method` and
+`:'data-confirm'` options are used as HTML5 attributes so that when the link is
+clicked, Rails will first show a confirm dialog to the user, and then submit the
+link with method `delete`. This is done via the JavaScript file `jquery_ujs`
+which is automatically included into your application's layout
+(`app/views/layouts/application.html.erb`) when you generated the application.
+Without this file, the confirmation dialog box wouldn't appear.
![Confirm Dialog](images/getting_started/confirm_dialog.png)
Congratulations, you can now create, show, list, update and destroy
-posts. In the next section will see how Rails can aid us when creating
-REST applications, and how we can refactor our Blog app to take
-advantage of it.
-
-### Going Deeper into REST
-
-We've now covered all the CRUD actions of a REST app. We did so by
-declaring separate routes with the appropriate verbs into
-`config/routes.rb`. Here's how that file looks so far:
-
-```ruby
-get "posts" => "posts#index"
-get "posts/new"
-post "posts" => "posts#create"
-get "posts/:id" => "posts#show", :as => :post
-get "posts/:id/edit" => "posts#edit"
-put "posts/:id" => "posts#update"
-delete "posts/:id" => "posts#destroy"
-```
-
-That's a lot to type for covering a single **resource**. Fortunately,
-Rails provides a `resources` method which can be used to declare a
-standard REST resource. Here's how `config/routes.rb` looks after the
-cleanup:
-
-```ruby
-Blog::Application.routes.draw do
-
- resources :posts
-
- root :to => "welcome#index"
-end
-```
-
-If you run `rake routes`, you'll see that all the routes that we
-declared before are still available:
-
-```bash
-$ rake routes
- posts GET /posts(.:format) posts#index
- POST /posts(.:format) posts#create
- new_post GET /posts/new(.:format) posts#new
-edit_post GET /posts/:id/edit(.:format) posts#edit
- post GET /posts/:id(.:format) posts#show
- PUT /posts/:id(.:format) posts#update
- DELETE /posts/:id(.:format) posts#destroy
- root / welcome#index
-```
-
-Also, if you go through the motions of creating, updating and deleting
-posts the app still works as before.
+posts.
TIP: In general, Rails encourages the use of resources objects in place
-of declaring routes manually. It was only done in this guide as a learning
-exercise. For more information about routing, see
+of declaring routes manually.
+For more information about routing, see
[Rails Routing from the Outside In](routing.html).
Adding a Second Model
---------------------
-It's time to add a second model to the application. The second model will handle comments on
-posts.
+It's time to add a second model to the application. The second model will handle
+comments on posts.
### Generating a Model
@@ -1249,16 +1332,15 @@ This command will generate four files:
| test/models/comment_test.rb | Testing harness for the comments model |
| test/fixtures/comments.yml | Sample comments for use in testing |
-First, take a look at `comment.rb`:
+First, take a look at `app/models/comment.rb`:
```ruby
class Comment < ActiveRecord::Base
belongs_to :post
- attr_accessible :body, :commenter
end
```
-This is very similar to the `post.rb` model that you saw earlier. The difference
+This is very similar to the `Post` model that you saw earlier. The difference
is the line `belongs_to :post`, which sets up an Active Record _association_.
You'll learn a little about associations in the next section of this guide.
@@ -1271,19 +1353,17 @@ class CreateComments < ActiveRecord::Migration
create_table :comments do |t|
t.string :commenter
t.text :body
- t.references :post
+ t.references :post, index: true
t.timestamps
end
-
- add_index :comments, :post_id
end
end
```
The `t.references` line sets up a foreign key column for the association between
-the two models. And the `add_index` line sets up an index for this association
-column. Go ahead and run the migration:
+the two models. An index for this association is also created on this column.
+Go ahead and run the migration:
```bash
$ rake db:migrate
@@ -1295,10 +1375,8 @@ run against the current database, so in this case you will just see:
```bash
== CreateComments: migrating =================================================
-- create_table(:comments)
- -> 0.0008s
--- add_index(:comments, :post_id)
- -> 0.0003s
-== CreateComments: migrated (0.0012s) ========================================
+ -> 0.0115s
+== CreateComments: migrated (0.0119s) ========================================
```
### Associating Models
@@ -1311,8 +1389,8 @@ this way:
* One post can have many comments.
In fact, this is very close to the syntax that Rails uses to declare this
-association. You've already seen the line of code inside the Comment model that
-makes each comment belong to a Post:
+association. You've already seen the line of code inside the `Comment` model
+(app/models/comment.rb) that makes each comment belong to a Post:
```ruby
class Comment < ActiveRecord::Base
@@ -1320,14 +1398,14 @@ class Comment < ActiveRecord::Base
end
```
-You'll need to edit the `post.rb` file to add the other side of the association:
+You'll need to edit `app/models/post.rb` to add the other side of the association:
```ruby
class Post < ActiveRecord::Base
- validates :title, :presence => true,
- :length => { :minimum => 5 }
-
has_many :comments
+ validates :title, presence: true,
+ length: { minimum: 5 }
+ [...]
end
```
@@ -1384,7 +1462,7 @@ the post show page to see their comment now listed. Due to this, our
spam comments when they arrive.
So first, we'll wire up the Post show template
-(`/app/views/posts/show.html.erb`) to let us make a new comment:
+(`app/views/posts/show.html.erb`) to let us make a new comment:
```html+erb
<p>
@@ -1400,11 +1478,11 @@ So first, we'll wire up the Post show template
<h2>Add a comment:</h2>
<%= form_for([@post, @post.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1412,23 +1490,28 @@ So first, we'll wire up the Post show template
</p>
<% end %>
-<%= link_to 'Edit Post', edit_post_path(@post) %> |
-<%= link_to 'Back to Posts', posts_path %>
+<%= link_to 'Back', posts_path %>
+| <%= link_to 'Edit', edit_post_path(@post) %>
```
This adds a form on the `Post` show page that creates a new comment by
calling the `CommentsController` `create` action. The `form_for` call here uses
an array, which will build a nested route, such as `/posts/1/comments`.
-Let's wire up the `create`:
+Let's wire up the `create` in `app/controllers/comments_controller.rb`:
```ruby
class CommentsController < ApplicationController
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @comment = @post.comments.create(comment_params)
redirect_to post_path(@post)
end
+
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
@@ -1475,11 +1558,11 @@ template. This is where we want the comment to show, so let's add that to the
<h2>Add a comment:</h2>
<%= form_for([@post, @post.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1541,11 +1624,11 @@ following:
<h2>Add a comment:</h2>
<%= form_for([@post, @post.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1571,11 +1654,11 @@ create a file `app/views/comments/_form.html.erb` containing:
```html+erb
<%= form_for([@post, @post.comments.build]) do |f| %>
<p>
- <%= f.label :commenter %><br />
+ <%= f.label :commenter %><br>
<%= f.text_field :commenter %>
</p>
<p>
- <%= f.label :body %><br />
+ <%= f.label :body %><br>
<%= f.text_area :body %>
</p>
<p>
@@ -1597,6 +1680,9 @@ Then you make the `app/views/posts/show.html.erb` look like the following:
<%= @post.text %>
</p>
+<h2>Comments</h2>
+<%= render @post.comments %>
+
<h2>Add a comment:</h2>
<%= render "comments/form" %>
@@ -1635,22 +1721,21 @@ So first, let's add the delete link in the
<p>
<%= link_to 'Destroy Comment', [comment.post, comment],
- :method => :delete,
- :data => { :confirm => 'Are you sure?' } %>
+ method: :delete,
+ data: { confirm: 'Are you sure?' } %>
</p>
```
Clicking this new "Destroy Comment" link will fire off a `DELETE
-/posts/:id/comments/:id` to our `CommentsController`, which can then use
+/posts/:post_id/comments/:id` to our `CommentsController`, which can then use
this to find the comment we want to delete, so let's add a destroy action to our
-controller:
+controller (`app/controllers/comments_controller.rb`):
```ruby
class CommentsController < ApplicationController
-
def create
@post = Post.find(params[:post_id])
- @comment = @post.comments.create(params[:comment])
+ @comment = @post.comments.create(comment_params)
redirect_to post_path(@post)
end
@@ -1661,6 +1746,10 @@ class CommentsController < ApplicationController
redirect_to post_path(@post)
end
+ private
+ def comment_params
+ params.require(:comment).permit(:commenter, :body)
+ end
end
```
@@ -1678,15 +1767,18 @@ model, `app/models/post.rb`, as follows:
```ruby
class Post < ActiveRecord::Base
- validates :title, :presence => true,
- :length => { :minimum => 5 }
- has_many :comments, :dependent => :destroy
+ has_many :comments, dependent: :destroy
+ validates :title, presence: true,
+ length: { minimum: 5 }
+ [...]
end
```
Security
--------
+### Basic Authentication
+
If you were to publish your blog online, anybody would be able to add, edit and
delete posts or delete comments.
@@ -1700,12 +1792,12 @@ action if that method allows it.
To use the authentication system, we specify it at the top of our
`PostsController`, in this case, we want the user to be authenticated on every
-action, except for `index` and `show`, so we write that:
+action, except for `index` and `show`, so we write that in `app/controllers/posts_controller.rb`:
```ruby
class PostsController < ApplicationController
- http_basic_authenticate_with :name => "dhh", :password => "secret", :except => [:index, :show]
+ http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
def index
@posts = Post.all
@@ -1715,24 +1807,38 @@ class PostsController < ApplicationController
```
We also only want to allow authenticated users to delete comments, so in the
-`CommentsController` we write:
+`CommentsController` (`app/controllers/comments_controller.rb`) we write:
```ruby
class CommentsController < ApplicationController
- http_basic_authenticate_with :name => "dhh", :password => "secret", :only => :destroy
+ http_basic_authenticate_with name: "dhh", password: "secret", only: :destroy
def create
@post = Post.find(params[:post_id])
...
end
+
# snipped for brevity
```
Now if you try to create a new post, you will be greeted with a basic HTTP
Authentication challenge
-![Basic HTTP Authentication Challenge](images/challenge.png)
+![Basic HTTP Authentication Challenge](images/getting_started/challenge.png)
+
+Other authentication methods are available for Rails applications. Two popular
+authentication add-ons for Rails are the [Devise](https://github.com/plataformatec/devise)
+rails engine and the [Authlogic](https://github.com/binarylogic/authlogic) gem,
+along with a number of others.
+
+
+### Other Security Considerations
+
+Security, especially in web applications, is a broad and detailed area. Security
+in your Rails application is covered in more depth in
+The [Ruby on Rails Security Guide](security.html)
+
What's Next?
------------
@@ -1747,10 +1853,19 @@ free to consult these support resources:
* The [Ruby on Rails mailing list](http://groups.google.com/group/rubyonrails-talk)
* The [#rubyonrails](irc://irc.freenode.net/#rubyonrails) channel on irc.freenode.net
-Rails also comes with built-in help that you can generate using the rake command-line utility:
+Rails also comes with built-in help that you can generate using the rake
+command-line utility:
+
+* Running `rake doc:guides` will put a full copy of the Rails Guides in the
+ `doc/guides` folder of your application. Open `doc/guides/index.html` in your
+ web browser to explore the Guides.
+* Running `rake doc:rails` will put a full copy of the API documentation for
+ Rails in the `doc/api` folder of your application. Open `doc/api/index.html`
+ in your web browser to explore the API documentation.
-* Running `rake doc:guides` will put a full copy of the Rails Guides in the `doc/guides` folder of your application. Open `doc/guides/index.html` in your web browser to explore the Guides.
-* Running `rake doc:rails` will put a full copy of the API documentation for Rails in the `doc/api` folder of your application. Open `doc/api/index.html` in your web browser to explore the API documentation.
+TIP: To be able to generate the Rails Guides locally with the `doc:guides` rake
+task you need to install the RedCloth gem. Add it to your `Gemfile` and run
+`bundle install` and you're ready to go.
Configuration Gotchas
---------------------
@@ -1770,13 +1885,13 @@ cannot be automatically detected by Rails and corrected.
Two very common sources of data that are not UTF-8:
-* Your text editor: Most text editors (such as Textmate), default to saving files as
+* Your text editor: Most text editors (such as TextMate), default to saving files as
UTF-8. If your text editor does not, this can result in special characters that you
enter in your templates (such as é) to appear as a diamond with a question mark inside
- in the browser. This also applies to your I18N translation files.
+ in the browser. This also applies to your i18n translation files.
Most editors that do not already default to UTF-8 (such as some versions of
Dreamweaver) offer a way to change the default to UTF-8. Do so.
-* Your database. Rails defaults to converting data from your database into UTF-8 at
+* Your database: Rails defaults to converting data from your database into UTF-8 at
the boundary. However, if your database is not using UTF-8 internally, it may not
be able to store all characters that your users enter. For instance, if your database
is using Latin-1 internally, and your user enters a Russian, Hebrew, or Japanese
diff --git a/guides/source/i18n.md b/guides/source/i18n.md
index eda1881e73..33daa79133 100644
--- a/guides/source/i18n.md
+++ b/guides/source/i18n.md
@@ -7,21 +7,28 @@ The process of "internationalization" usually means to abstract all strings and
So, in the process of _internationalizing_ your Rails application you have to:
-* Ensure you have support for i18n
-* Tell Rails where to find locale dictionaries
-* Tell Rails how to set, preserve and switch locales
+* Ensure you have support for i18n.
+* Tell Rails where to find locale dictionaries.
+* Tell Rails how to set, preserve and switch locales.
In the process of _localizing_ your application you'll probably want to do the following three things:
-* Replace or supplement Rails' default locale -- e.g. date and time formats, month names, Active Record model names, etc.
-* Abstract strings in your application into keyed dictionaries -- e.g. flash messages, static text in your views, etc.
-* Store the resulting dictionaries somewhere
+* Replace or supplement Rails' default locale - e.g. date and time formats, month names, Active Record model names, etc.
+* Abstract strings in your application into keyed dictionaries - e.g. flash messages, static text in your views, etc.
+* Store the resulting dictionaries somewhere.
This guide will walk you through the I18n API and contains a tutorial on how to internationalize a Rails application from the start.
+After reading this guide, you will know:
+
+* How I18n works in Ruby on Rails
+* How to correctly use I18n into a RESTful application in various ways
+* How to use I18n to translate ActiveRecord errors or ActionMailer E-mail subjects
+* Some other tools to go further with the translation process of your application
+
--------------------------------------------------------------------------------
-NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Rails [I18n Wiki](http://rails-i18n.org/wiki) for more information.
+NOTE: The Ruby I18n framework provides you with all necessary means for internationalization/localization of your Rails application. You may, however, use any of various plugins and extensions available, which add additional functionality or features. See the Ruby [I18n Wiki](http://ruby-i18n.org/wiki) for more information.
How I18n in Ruby on Rails Works
-------------------------------
@@ -31,13 +38,13 @@ Internationalization is a complex problem. Natural languages differ in so many w
* providing support for English and similar languages out of the box
* making it easy to customize and extend everything for other languages
-As part of this solution, **every static string in the Rails framework** -- e.g. Active Record validation messages, time and date formats -- **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults.
+As part of this solution, **every static string in the Rails framework** - e.g. Active Record validation messages, time and date formats - **has been internationalized**, so _localization_ of a Rails application means "over-riding" these defaults.
### The Overall Architecture of the Library
Thus, the Ruby I18n gem is split into two parts:
-* The public API of the i18n framework -- a Ruby module with public methods that define how the library works
+* The public API of the i18n framework - a Ruby module with public methods that define how the library works
* A default backend (which is intentionally named _Simple_ backend) that implements these methods
As a user you should always only access the public methods on the I18n module, but it is useful to know about the capabilities of the backend.
@@ -90,17 +97,17 @@ en:
hello: "Hello world"
```
-This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Record validation messages in the [`activerecord/lib/active_record/locale/en.yml`](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
+This means, that in the `:en` locale, the key _hello_ will map to the _Hello world_ string. Every string inside Rails is internationalized in this way, see for instance Active Model validation messages in the [`activemodel/lib/active_model/locale/en.yml`](https://github.com/rails/rails/blob/master/activemodel/lib/active_model/locale/en.yml) file or time and date formats in the [`activesupport/lib/active_support/locale/en.yml`](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml) file. You can use YAML or standard Ruby Hashes to store translations in the default (Simple) backend.
The I18n library will use **English** as a **default locale**, i.e. if you don't set a different locale, `:en` will be used for looking up translations.
-NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize2](https://github.com/joshmh/globalize2/tree/master) may help you implement it.
+NOTE: The i18n library takes a **pragmatic approach** to locale keys (after [some discussion](http://groups.google.com/group/rails-i18n/browse_thread/thread/14dede2c7dbe9470/80eec34395f64f3c?hl=en), including only the _locale_ ("language") part, like `:en`, `:pl`, not the _region_ part, like `:en-US` or `:en-GB`, which are traditionally used for separating "languages" and "regional setting" or "dialects". Many international applications use only the "language" element of a locale such as `:cs`, `:th` or `:es` (for Czech, Thai and Spanish). However, there are also regional differences within different language groups that may be important. For instance, in the `:en-US` locale you would have $ as a currency symbol, while in `:en-GB`, you would have £. Nothing stops you from separating regional and other settings in this way: you just have to provide full "English - United Kingdom" locale in a `:en-GB` dictionary. Various [Rails I18n plugins](http://rails-i18n.org/wiki) such as [Globalize3](https://github.com/svenfuchs/globalize3) may help you implement it.
The **translations load path** (`I18n.load_path`) is just a Ruby Array of paths to your translation files that will be loaded automatically and available in your application. You can pick whatever directory and translation file naming scheme makes sense for you.
NOTE: The backend will lazy-load these translations when a translation is looked up for the first time. This makes it possible to just swap the backend with something else even after translations have already been announced.
-The default initializer `locale.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.
+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.
```ruby
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
@@ -130,12 +137,12 @@ If you want to translate your Rails application to a **single language other tha
However, you would probably like to **provide support for more locales** in your application. In such case, you need to set and pass the locale between requests.
-WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>, however **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer. Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
+WARNING: You may be tempted to store the chosen locale in a _session_ or a <em>cookie</em>, however **do not do this**. The locale should be transparent and a part of the URL. This way you won't break people's basic assumptions about the web itself: if you send a URL to a friend, they should see the same page and content as you. A fancy word for this would be that you're being [<em>RESTful</em>](http://en.wikipedia.org/wiki/Representational_State_Transfer). Read more about the RESTful approach in [Stefan Tilkov's articles](http://www.infoq.com/articles/rest-introduction). Sometimes there are exceptions to this rule and those are discussed below.
-The _setting part_ is easy. You can set the locale in a `before_filter` in the `ApplicationController` like this:
+The _setting part_ is easy. You can set the locale in a `before_action` in the `ApplicationController` like this:
```ruby
-before_filter :set_locale
+before_action :set_locale
def set_locale
I18n.locale = params[:locale] || I18n.default_locale
@@ -158,7 +165,7 @@ One option you have is to set the locale from the domain name where your applica
You can implement it like this in your `ApplicationController`:
```ruby
-before_filter :set_locale
+before_action :set_locale
def set_locale
I18n.locale = extract_locale_from_tld || I18n.default_locale
@@ -172,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.include?(parsed_locale.to_sym) ? parsed_locale : nil
end
```
@@ -201,11 +208,11 @@ This solution has aforementioned advantages, however, you may not be able or may
### Setting the Locale from the URL Params
-The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_filter_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case.
+The most usual way of setting (and passing) the locale would be to include it in URL params, as we did in the `I18n.locale = params[:locale]` _before_action_ in the first example. We would like to have URLs like `www.example.com/books?locale=ja` or `www.example.com/ja/books` in this case.
This approach has almost the same set of advantages as setting the locale from 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).
@@ -215,7 +222,7 @@ We can include something like this in our `ApplicationController` then:
# app/controllers/application_controller.rb
def default_url_options(options={})
logger.debug "default_url_options is passed options: #{options.inspect}\n"
- { :locale => I18n.locale }
+ { locale: I18n.locale }
end
```
@@ -238,29 +245,29 @@ If you don't want to force the use of a locale in your routes you can use an opt
```ruby
# config/routes.rb
-scope "(:locale)", :locale => /en|nl/ do
+scope "(:locale)", locale: /en|nl/ do
resources :books
end
```
With this approach you will not get a `Routing Error` when accessing your resources such as `http://localhost:3001/books` without a locale. This is useful for when you want to use the default locale when one is not specified.
-Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root :to => "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.)
+Of course, you need to take special care of the root URL (usually "homepage" or "dashboard") of your application. An URL like `http://localhost:3001/nl` will not work automatically, because the `root to: "books#index"` declaration in your `routes.rb` doesn't take locale into account. (And rightly so: there's only one "root" URL.)
You would probably need to map URLs like these:
```ruby
# config/routes.rb
-match '/:locale' => 'dashboard#index'
+get '/:locale' => 'dashboard#index'
```
Do take special care about the **order of your routes**, so this route declaration does not "eat" other ones. (You may want to add it directly before the `root :to` declaration.)
-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 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).
### Setting the Locale from the Client Supplied Information
-In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites -- see the box about _sessions_, _cookies_ and RESTful architecture above.
+In specific cases, it would make sense to set the locale from client-supplied information, i.e. not from the URL. This information may come for example from the users' preferred language (set in their browser), can be based on the users' geographical location inferred from their IP, or users can provide it simply by choosing the locale in your application interface and saving it to their profile. This approach is more suitable for web-based applications or services, not for websites - see the box about _sessions_, _cookies_ and RESTful architecture above.
#### Using `Accept-Language`
@@ -275,21 +282,22 @@ def set_locale
I18n.locale = extract_locale_from_accept_language_header
logger.debug "* Locale set to '#{I18n.locale}'"
end
+
private
-def extract_locale_from_accept_language_header
- request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
-end
+ def extract_locale_from_accept_language_header
+ request.env['HTTP_ACCEPT_LANGUAGE'].scan(/^[a-z]{2}/).first
+ end
```
-Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb).
+Of course, in a production environment you would need much more robust code, and could use a plugin such as Iain Hecker's [http_accept_language](https://github.com/iain/http_accept_language/tree/master) or even Rack middleware such as Ryan Tomayko's [locale](https://github.com/rack/rack-contrib/blob/master/lib/rack/contrib/locale.rb).
#### Using GeoIP (or Similar) Database
-Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above -- you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned.
+Another way of choosing the locale from client information would be to use a database for mapping the client IP to the region, such as [GeoIP Lite Country](http://www.maxmind.com/app/geolitecountry). The mechanics of the code would be very similar to the code above - you would need to query the database for the user's IP, and look up your preferred locale for the country/region/city returned.
#### User Profile
-You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above -- you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value.
+You can also provide users of your application with means to set (and possibly over-ride) the locale in your application interface, as well. Again, mechanics for this approach would be very similar to the code above - you'd probably let users choose a locale from a dropdown list and save it to their profile in the database. Then you'd set the locale to this value.
Internationalizing your Application
-----------------------------------
@@ -303,7 +311,18 @@ You most probably have something like this in one of your applications:
```ruby
# config/routes.rb
Yourapp::Application.routes.draw do
- root :to => "home#index"
+ root to: "home#index"
+end
+```
+
+```ruby
+# app/controllers/application_controller.rb
+class ApplicationController < ActionController::Base
+ before_action :set_locale
+
+ def set_locale
+ I18n.locale = params[:locale] || I18n.default_locale
+ end
end
```
@@ -381,7 +400,7 @@ You can use variables in the translation messages and pass their values from the
```erb
# app/views/home/index.html.erb
-<%=t 'greet_username', :user => "Bill", :message => "Goodbye" %>
+<%=t 'greet_username', user: "Bill", message: "Goodbye" %>
```
```yaml
@@ -392,13 +411,13 @@ en:
### Adding Date/Time Formats
-OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option -- by default the `:default` format is used.
+OK! Now let's add a timestamp to the view, so we can demo the **date/time localization** feature as well. To localize the time format you pass the Time object to `I18n.l` or (preferably) use Rails' `#l` helper. You can pick a format by passing the `:format` option - by default the `:default` format is used.
```erb
# app/views/home/index.html.erb
<h1><%=t :hello_world %></h1>
<p><%= flash[:notice] %></p
-<p><%= l Time.now, :format => :short %></p>
+<p><%= l Time.now, format: :short %></p>
```
And in our pirate translations file let's add a time format (it's already there in Rails' defaults for English):
@@ -415,7 +434,7 @@ So that would give you:
![rails i18n demo localized time to pirate](images/i18n/demo_localized_pirate.png)
-TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected (at least for the 'pirate' locale). Of course, there's a great chance that somebody already did all the work by **translating Rails' defaults for your locale**. See the [rails-i18n repository at Github](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for an archive of various locale files. When you put such file(s) in `config/locales/` directory, they will automatically be ready for use.
+TIP: Right now you might need to add some more date/time formats in order to make the I18n backend work as expected (at least for the 'pirate' locale). Of course, there's a great chance that somebody already did all the work by **translating Rails' defaults for your locale**. See the [rails-i18n repository at GitHub](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for an archive of various locale files. When you put such file(s) in `config/locales/` directory, they will automatically be ready for use.
### Inflection Rules For Other Locales
@@ -492,10 +511,10 @@ I18n.t :message
I18n.t 'message'
```
-The `translate` method also takes a `:scope` option which can contain one or more additional keys that will be used to specify a “namespace†or scope for a translation key:
+The `translate` method also takes a `:scope` option which can contain one or more additional keys that will be used to specify a "namespace" or scope for a translation key:
```ruby
-I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
```
This looks up the `:record_invalid` message in the Active Record error messages.
@@ -510,9 +529,9 @@ Thus the following calls are equivalent:
```ruby
I18n.t 'activerecord.errors.messages.record_invalid'
-I18n.t 'errors.messages.record_invalid', :scope => :active_record
-I18n.t :record_invalid, :scope => 'activerecord.errors.messages'
-I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
+I18n.t 'errors.messages.record_invalid', scope: :active_record
+I18n.t :record_invalid, scope: 'activerecord.errors.messages'
+I18n.t :record_invalid, scope: [:activerecord, :errors, :messages]
```
#### Defaults
@@ -520,7 +539,7 @@ I18n.t :record_invalid, :scope => [:activerecord, :errors, :messages]
When a `:default` option is given, its value will be returned if the translation is missing:
```ruby
-I18n.t :missing, :default => 'Not here'
+I18n.t :missing, default: 'Not here'
# => 'Not here'
```
@@ -529,7 +548,7 @@ If the `:default` value is a Symbol, it will be used as a key and translated. On
E.g., the following first tries to translate the key `:missing` and then the key `:also_missing.` As both do not yield a result, the string "Not here" will be returned:
```ruby
-I18n.t :missing, :default => [:also_missing, 'Not here']
+I18n.t :missing, default: [:also_missing, 'Not here']
# => 'Not here'
```
@@ -538,7 +557,7 @@ I18n.t :missing, :default => [:also_missing, 'Not here']
To look up multiple translations at once, an array of keys can be passed:
```ruby
-I18n.t [:odd, :even], :scope => 'errors.messages'
+I18n.t [:odd, :even], scope: 'errors.messages'
# => ["must be odd", "must be even"]
```
@@ -546,7 +565,7 @@ Also, a key can translate to a (potentially nested) hash of grouped translations
```ruby
I18n.t 'activerecord.errors.messages'
-# => { :inclusion => "is not included in the list", :exclusion => ... }
+# => {:inclusion=>"is not included in the list", :exclusion=> ... }
```
#### "Lazy" Lookup
@@ -573,8 +592,8 @@ In many cases you want to abstract your translations so that **variables can be
All options besides `:default` and `:scope` that are passed to `#translate` will be interpolated to the translation:
```ruby
-I18n.backend.store_translations :en, :thanks => 'Thanks %{name}!'
-I18n.translate :thanks, :name => 'Jeremy'
+I18n.backend.store_translations :en, thanks: 'Thanks %{name}!'
+I18n.translate :thanks, name: 'Jeremy'
# => 'Thanks Jeremy!'
```
@@ -587,14 +606,14 @@ In English there are only one singular and one plural form for a given string, e
The `:count` interpolation variable has a special role in that it both is interpolated to the translation and used to pick a pluralization from the translations according to the pluralization rules defined by CLDR:
```ruby
-I18n.backend.store_translations :en, :inbox => {
- :one => 'one message',
- :other => '%{count} messages'
+I18n.backend.store_translations :en, inbox: {
+ one: 'one message',
+ other: '%{count} messages'
}
-I18n.translate :inbox, :count => 2
+I18n.translate :inbox, count: 2
# => '2 messages'
-I18n.translate :inbox, :count => 1
+I18n.translate :inbox, count: 1
# => 'one message'
```
@@ -623,8 +642,8 @@ I18n.l Time.now
Explicitly passing a locale:
```ruby
-I18n.t :foo, :locale => :de
-I18n.l Time.now, :locale => :de
+I18n.t :foo, locale: :de
+I18n.l Time.now, locale: :de
```
The `I18n.locale` defaults to `I18n.default_locale` which defaults to :`en`. The default locale can be set like this:
@@ -665,9 +684,9 @@ For example a Ruby Hash providing translations can look like this:
```ruby
{
- :pt => {
- :foo => {
- :bar => "baz"
+ pt: {
+ foo: {
+ bar: "baz"
}
}
}
@@ -694,13 +713,13 @@ en:
long: "%B %d, %Y"
```
-So, all of the following equivalent lookups will return the `:short` date format `"%B %d"`:
+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]
+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.
@@ -724,6 +743,19 @@ en:
Then `User.model_name.human` will return "Dude" and `User.human_attribute_name("login")` will return "Handle".
+You can also set a plural form for model names, adding as following:
+
+```ruby
+en:
+ activerecord:
+ models:
+ user:
+ one: Dude
+ other: Dudes
+```
+
+Then `User.model_name.human(count: 2)` will return "Dudes". With `count: 1` or without params will return "Dude".
+
#### Error Message Scopes
Active Record validation error messages can also be translated easily. Active Record gives you a couple of namespaces where you can place your message translations in order to provide different messages and translation for certain models, attributes, and/or validations. It also transparently takes single table inheritance into account.
@@ -734,7 +766,7 @@ Consider a User model with a validation for the name attribute like this:
```ruby
class User < ActiveRecord::Base
- validates :name, :presence => true
+ validates :name, presence: true
end
```
@@ -764,7 +796,7 @@ For example, you might have an Admin model inheriting from User:
```ruby
class Admin < User
- validates :name, :presence => true
+ validates :name, presence: true
end
```
@@ -811,6 +843,7 @@ So, for example, instead of the default error message `"can not be blank"` you c
| numericality | :equal_to | :equal_to | count |
| numericality | :less_than | :less_than | count |
| numericality | :less_than_or_equal_to | :less_than_or_equal_to | count |
+| numericality | :only_integer | :not_an_integer | - |
| numericality | :odd | :odd | - |
| numericality | :even | :even | - |
@@ -835,21 +868,43 @@ en:
NOTE: In order to use this helper, you need to install [DynamicForm](https://github.com/joelmoss/dynamic_form)
gem by adding this line to your Gemfile: `gem 'dynamic_form'`.
+### Translations for Action Mailer E-Mail Subjects
+
+If you don't pass a subject to the `mail` method, Action Mailer will try to find
+it in your translations. The performed lookup will use the pattern
+`<mailer_scope>.<action_name>.subject` to construct the key.
+
+```ruby
+# user_mailer.rb
+class UserMailer < ActionMailer::Base
+ def welcome(user)
+ #...
+ end
+end
+```
+
+```yaml
+en:
+ user_mailer:
+ welcome:
+ subject: "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.
#### Action View Helper Methods
-* `distance_of_time_in_words` translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See [datetime.distance_in_words](https://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L51) translations.
+* `distance_of_time_in_words` translates and pluralizes its result and interpolates the number of seconds, minutes, hours, and so on. See [datetime.distance_in_words](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L4) translations.
-* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15) for translations. `datetime_select` also looks up the order option from [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L83) scope if applicable.
+* `datetime_select` and `select_month` use translated month names for populating the resulting select tag. See [date.month_names](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L15) for translations. `datetime_select` also looks up the order option from [date.order](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L18) (unless you pass the option explicitly). All date selection helpers translate the prompt using the translations in the [datetime.prompts](https://github.com/rails/rails/blob/master/actionview/lib/action_view/locale/en.yml#L39) scope if applicable.
-* The `number_to_currency`, `number_with_precision`, `number_to_percentage`, `number_with_delimiter`, and `number_to_human_size` helpers use the number format settings located in the [number](https://github.com/rails/rails/blob/master/actionpack/lib/action_view/locale/en.yml#L2) scope.
+* The `number_to_currency`, `number_with_precision`, `number_to_percentage`, `number_with_delimiter`, and `number_to_human_size` helpers use the number format settings located in the [number](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L37) scope.
#### Active Model Methods
-* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L29) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes".
+* `model_name.human` and `human_attribute_name` use translations for model names and attribute names if available in the [activerecord.models](https://github.com/rails/rails/blob/master/activerecord/lib/active_record/locale/en.yml#L36) scope. They also support translations for inherited class names (e.g. for use with STI) as explained above in "Error message scopes".
* `ActiveModel::Errors#generate_message` (which is used by Active Model validations but may also be used manually) uses `model_name.human` and `human_attribute_name` (see above). It also translates the error message and supports translations for inherited class names as explained above in "Error message scopes".
@@ -857,7 +912,7 @@ Rails uses fixed strings and other localizations, such as format strings and oth
#### Active Support Methods
-* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L30) scope.
+* `Array#to_sentence` uses format settings as given in the [support.array](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/locale/en.yml#L33) scope.
Customize your I18n Setup
-------------------------
@@ -891,11 +946,11 @@ ReservedInterpolationKey # the translation contains a reserved interpolation
UnknownFileType # the backend does not know how to handle a file type that was added to I18n.load_path
```
-The I18n API will catch all of these exceptions when they are thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for `MissingTranslationData` exceptions. When a `MissingTranslationData` exception has been caught, it will return the exception’s error message string containing the missing key/scope.
+The I18n API will catch all of these exceptions when they are thrown in the backend and pass them to the default_exception_handler method. This method will re-raise all exceptions except for `MissingTranslationData` exceptions. When a `MissingTranslationData` exception has been caught, it will return the exception's error message string containing the missing key/scope.
The reason for this is that during development you'd usually want your views to still render even though a translation is missing.
-In other contexts you might want to change this behaviour, though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module or a class with `#call` method:
+In other contexts you might want to change this behavior, though. E.g. the default exception handling does not allow to catch missing translations during automated tests easily. For this purpose a different exception handler can be specified. The specified exception handler must be a method on the I18n module or a class with `#call` method:
```ruby
module I18n
@@ -925,12 +980,12 @@ else
end
```
-Another example where the default behaviour is less desirable is the Rails TranslationHelper which provides the method `#t` (as well as `#translate`). When a `MissingTranslationData` exception occurs in this context, the helper wraps the message into a span with the CSS class `translation_missing`.
+Another example where the default behavior is less desirable is the Rails TranslationHelper which provides the method `#t` (as well as `#translate`). When a `MissingTranslationData` exception occurs in this context, the helper wraps the message into a span with the CSS class `translation_missing`.
To do so, the helper forces `I18n#translate` to raise exceptions no matter what exception handler is defined by setting the `:raise` option:
```ruby
-I18n.t :foo, :raise => true # always re-raises exceptions from the backend
+I18n.t :foo, raise: true # always re-raises exceptions from the backend
```
Conclusion
@@ -956,8 +1011,8 @@ Resources
* [rails-i18n.org](http://rails-i18n.org) - Homepage of the rails-i18n project. You can find lots of useful resources on the [wiki](http://rails-i18n.org/wiki).
* [Google group: rails-i18n](http://groups.google.com/group/rails-i18n) - The project's mailing list.
-* [Github: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
-* [Github: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem.
+* [GitHub: rails-i18n](https://github.com/svenfuchs/rails-i18n/tree/master) - Code repository for the rails-i18n project. Most importantly you can find lots of [example translations](https://github.com/svenfuchs/rails-i18n/tree/master/rails/locale) for Rails that should work for your application in most cases.
+* [GitHub: i18n](https://github.com/svenfuchs/i18n/tree/master) - Code repository for the i18n gem.
* [Lighthouse: rails-i18n](http://i18n.lighthouseapp.com/projects/14948-rails-i18n/overview) - Issue tracker for the rails-i18n project.
* [Lighthouse: i18n](http://i18n.lighthouseapp.com/projects/14947-ruby-i18n/overview) - Issue tracker for the i18n gem.
diff --git a/guides/source/index.html.erb b/guides/source/index.html.erb
index 74805b2754..57c224c165 100644
--- a/guides/source/index.html.erb
+++ b/guides/source/index.html.erb
@@ -9,10 +9,7 @@ Ruby on Rails Guides
<% content_for :index_section do %>
<div id="subCol">
<dl>
- <dd class="kindle">Rails Guides are also available for the <%= link_to 'Kindle', 'https://kindle.amazon.com' %>
-and <%= link_to 'Free Kindle Reading Apps', 'http://www.amazon.com/gp/kindle/kcp' %> for the iPad,
-iPhone, Mac, Android, etc. Download them from <%= link_to 'here', @mobi %>.
- </dd>
+ <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>
</div>
@@ -22,7 +19,7 @@ iPhone, Mac, Android, etc. Download them from <%= link_to 'here', @mobi %>.
<h3><%= section['name'] %></h3>
<dl>
<% section['documents'].each do |document| %>
- <%= guide(document['name'], document['url'], :work_in_progress => document['work_in_progress']) do %>
+ <%= guide(document['name'], document['url'], work_in_progress: document['work_in_progress']) do %>
<p><%= document['description'] %></p>
<% end %>
<% end %>
diff --git a/guides/source/initialization.md b/guides/source/initialization.md
index c582acd3a3..fe6b1ad906 100644
--- a/guides/source/initialization.md
+++ b/guides/source/initialization.md
@@ -4,15 +4,20 @@ The Rails Initialization Process
This guide explains the internals of the initialization process in Rails
as of Rails 4. It is an extremely in-depth guide and recommended for advanced Rails developers.
-* Using `rails server`
+After reading this guide, you will know:
+
+* How to use `rails server`.
+* The timeline of Rails' initialization sequence.
+* Where different files are required by the boot sequence.
+* How the Rails::Server interface is defined and used.
--------------------------------------------------------------------------------
This guide goes through every method call that is
required to boot up the Ruby on Rails stack for a default Rails 4
application, explaining each part in detail along the way. For this
-guide, we will be focusing on what happens when you execute +rails
-server+ to boot your app.
+guide, we will be focusing on what happens when you execute `rails server`
+to boot your app.
NOTE: Paths in this guide are relative to Rails or a Rails application unless otherwise specified.
@@ -24,128 +29,18 @@ quickly.
Launch!
-------
-A Rails application is usually started with the command `rails server`.
+Let's start to boot and initialize the app. It all begins with your app's
+`bin/rails` executable. A Rails application is usually started by running
+`rails console` or `rails server`.
### `bin/rails`
-The actual `rails` command is kept in _bin/rails_:
-
-```ruby
-#!/usr/bin/env ruby
-
-if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git'))
- railties_path = File.expand_path('../../lib', __FILE__)
- $:.unshift(railties_path)
-end
-require "rails/cli"
-```
-
-This file will first attempt to push the `railties/lib` directory if
-present, and then requires `rails/cli`.
-
-### `railties/lib/rails/cli.rb`
-
-This file looks like this:
-
-```ruby
-require 'rbconfig'
-require 'rails/script_rails_loader'
-
-# If we are inside a Rails application this method performs an exec and thus
-# the rest of this script is not run.
-Rails::ScriptRailsLoader.exec_script_rails!
-
-require 'rails/ruby_version_check'
-Signal.trap("INT") { puts; exit(1) }
-
-if ARGV.first == 'plugin'
- ARGV.shift
- require 'rails/commands/plugin_new'
-else
- require 'rails/commands/application'
-end
-```
-
-The `rbconfig` file from the Ruby standard library provides us with the `RbConfig` class which contains detailed information about the Ruby environment, including how Ruby was compiled. We can see this in use in `railties/lib/rails/script_rails_loader`.
-
-```ruby
-require 'pathname'
-
-module Rails
- module ScriptRailsLoader
- RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
- SCRIPT_RAILS = File.join('script', 'rails')
- ...
-
- end
-end
-```
-
-The `rails/script_rails_loader` file uses `RbConfig::Config` to obtain the `bin_dir` and `ruby_install_name` values for the configuration which together form the path to the Ruby interpreter. The `RbConfig::CONFIG["EXEEXT"]` will suffix this path with ".exe" if the script is running on Windows. This constant is used later on in `exec_script_rails!`. As for the `SCRIPT_RAILS` constant, we'll see that when we get to the `in_rails_application?` method.
-
-Back in `rails/cli`, the next line is this:
-
-```ruby
-Rails::ScriptRailsLoader.exec_script_rails!
-```
-
-This method is defined in `rails/script_rails_loader`:
-
-```ruby
-def self.exec_script_rails!
- cwd = Dir.pwd
- return unless in_rails_application? || in_rails_application_subdirectory?
- exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
- Dir.chdir("..") do
- # Recurse in a chdir block: if the search fails we want to be sure
- # the application is generated in the original working directory.
- exec_script_rails! unless cwd == Dir.pwd
- end
-rescue SystemCallError
- # could not chdir, no problem just return
-end
-```
-
-This method will first check if the current working directory (`cwd`) is a Rails application or a subdirectory of one. This is determined by the `in_rails_application?` method:
-
-```ruby
-def self.in_rails_application?
- File.exists?(SCRIPT_RAILS)
-end
-```
-
-The `SCRIPT_RAILS` constant defined earlier is used here, with `File.exists?` checking for its presence in the current directory. If this method returns `false` then `in_rails_application_subdirectory?` will be used:
-
-```ruby
-def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
- File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)
-end
-```
-
-This climbs the directory tree until it reaches a path which contains a `script/rails` file. If a directory containing this file is reached then this line will run:
-
-```ruby
-exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
-```
-
-This is effectively the same as running `ruby script/rails [arguments]`, where `[arguments]` at this point in time is simply "server".
-
-Rails Initialization
---------------------
-
-Only now we finally start the real initialization process, beginning
-with `script/rails`.
-
-TIP: If you execute `script/rails` directly from your Rails app you will
-skip executing all the code that we've just described.
-
-### `script/rails`
-
This file is as follows:
```ruby
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-require File.expand_path('../../config/boot', __FILE__)
+#!/usr/bin/env ruby
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
require 'rails/commands'
```
@@ -165,37 +60,36 @@ require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
In a standard Rails application, there's a `Gemfile` which declares all
dependencies of the application. `config/boot.rb` sets
`ENV['BUNDLE_GEMFILE']` to the location of this file. If the Gemfile
-exists, `bundler/setup` is then required.
-
-The gems that a Rails 4 application depends on are as follows:
-
-TODO: change these when the Rails 4 release is near.
-
-* abstract (1.0.0)
-* actionmailer (4.0.0.beta)
-* actionpack (4.0.0.beta)
-* activemodel (4.0.0.beta)
-* activerecord (4.0.0.beta)
-* activesupport (4.0.0.beta)
-* arel (2.0.7)
-* builder (3.0.0)
-* bundler (1.0.6)
-* erubis (2.6.6)
-* i18n (0.5.0)
-* mail (2.2.12)
-* mime-types (1.16)
-* polyglot (0.3.1)
-* rack (1.2.1)
-* rack-cache (0.5.3)
-* rack-mount (0.6.13)
-* rack-test (0.5.6)
-* rails (4.0.0.beta)
-* railties (4.0.0.beta)
-* rake (0.8.7)
-* sqlite3-ruby (1.3.2)
-* thor (0.14.6)
-* treetop (1.4.9)
-* tzinfo (0.3.23)
+exists, then `bundler/setup` is required. The require is used by Bundler to
+configure the load path for your Gemfile's dependencies.
+
+A standard Rails application depends on several gems, specifically:
+
+* abstract
+* actionmailer
+* actionpack
+* activemodel
+* activerecord
+* activesupport
+* arel
+* builder
+* bundler
+* erubis
+* i18n
+* mail
+* mime-types
+* polyglot
+* rack
+* rack-cache
+* rack-mount
+* rack-test
+* rails
+* railties
+* rake
+* sqlite3-ruby
+* thor
+* treetop
+* tzinfo
### `rails/commands.rb`
@@ -224,22 +118,22 @@ If we used `s` rather than `server`, Rails will use the `aliases` defined in the
```ruby
when 'server'
- # Change to the application's path if there is no config.ru file in current dir.
- # This allows us to run script/rails server from other directories, but still get
+ # Change to the application's path if there is no config.ru file in current directory.
+ # This allows us to run `rails server` from other directories, but still get
# the main config.ru and properly set the tmp directory.
Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
require 'rails/commands/server'
- Rails::Server.new.tap { |server|
+ Rails::Server.new.tap do |server|
# We need to require application after the server sets environment,
# otherwise the --environment option given to the server won't propagate.
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
- }
+ end
```
-This file will change into the root of the directory (a path two directories back from `APP_PATH` which points at `config/application.rb`), but only if the `config.ru` file isn't found. This then requires `rails/commands/server` which sets up the `Rails::Server` class.
+This file will change into the Rails root directory (a path two directories up from `APP_PATH` which points at `config/application.rb`), but only if the `config.ru` file isn't found. This then requires `rails/commands/server` which sets up the `Rails::Server` class.
```ruby
require 'fileutils'
@@ -255,11 +149,11 @@ module Rails
### `actionpack/lib/action_dispatch.rb`
Action Dispatch is the routing component of the Rails framework.
-It adds functionalities like routing, session, and common middlewares.
+It adds functionality like routing, session, and common middlewares.
### `rails/commands/server.rb`
-The `Rails::Server` class is defined in this file as inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`:
+The `Rails::Server` class is defined in this file by inheriting from `Rack::Server`. When `Rails::Server.new` is called, this calls the `initialize` method in `rails/commands/server.rb`:
```ruby
def initialize(*)
@@ -323,12 +217,12 @@ With the `default_options` set to this:
```ruby
def default_options
{
- :environment => ENV['RACK_ENV'] || "development",
- :pid => nil,
- :Port => 9292,
- :Host => "0.0.0.0",
- :AccessLog => [],
- :config => "config.ru"
+ environment: ENV['RACK_ENV'] || "development",
+ pid: nil,
+ Port: 9292,
+ Host: "0.0.0.0",
+ AccessLog: [],
+ config: "config.ru"
}
end
```
@@ -361,9 +255,9 @@ set earlier) is required.
### `config/application`
-When `require APP_PATH` is executed, `config/application.rb` is loaded.
-This file exists in your app and it's free for you to change based
-on your needs.
+When `require APP_PATH` is executed, `config/application.rb` is loaded (recall
+that `APP_PATH` is defined in `bin/rails`). This file exists in your application
+and it's free for you to change based on your needs.
### `Rails::Server#start`
@@ -374,7 +268,7 @@ def start
url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Call with -d to detach" unless options[:daemonize]
+ puts "=> Run `rails server -h` for more startup options"
trap(:INT) { exit }
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
@@ -456,7 +350,7 @@ end
The interesting part for a Rails app is the last line, `server.run`. Here we encounter the `wrapped_app` method again, which this time
we're going to explore more (even though it was executed before, and
-thus memorized by now).
+thus memoized by now).
```ruby
@wrapped_app ||= build_app app
@@ -483,7 +377,7 @@ The `options[:config]` value defaults to `config.ru` which contains this:
```ruby
# This file is used by Rack-based servers to start the application.
-require ::File.expand_path('../config/environment', __FILE__)
+require ::File.expand_path('../config/environment', __FILE__)
run <%= app_const %>
```
@@ -498,7 +392,7 @@ app = eval "Rack::Builder.new {( " + cfgfile + "\n )}.to_app",
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:
```ruby
-require ::File.expand_path('../config/environment', __FILE__)
+require ::File.expand_path('../config/environment', __FILE__)
```
### `config/environment.rb`
@@ -534,7 +428,7 @@ require "rails"
action_controller
action_mailer
rails/test_unit
- sprockets/rails
+ sprockets
).each do |framework|
begin
require "#{framework}/railtie"
@@ -549,14 +443,16 @@ inside each of those frameworks, but you're encouraged to try and
explore them on your own.
For now, just keep in mind that common functionality like Rails engines,
-I18n and Rails configuration is all being defined here.
+I18n and Rails configuration are all being defined here.
### Back to `config/environment.rb`
-When `config/application.rb` has finished loading Rails, and defined
-your application namespace, you go back to `config/environment.rb`,
-where your application is initialized. For example, if you application was called
-`Blog`, here you would find `Blog::Application.initialize!`, which is
+The rest of `config/application.rb` defines the configuration for the
+`Rails::Application` which will be used once the application is fully
+initialized. When `config/application.rb` has finished loading Rails and defined
+the application namespace, we go back to `config/environment.rb`,
+where the application is initialized. For example, if the application was called
+`Blog`, here we would find `Blog::Application.initialize!`, which is
defined in `rails/application.rb`
### `railties/lib/rails/application.rb`
@@ -572,14 +468,31 @@ def initialize!(group=:default) #:nodoc:
end
```
-As you can see, you can only initialize an app once. This is also where the initializers are run.
+As you can see, you can only initialize an app once. The initializers are run through
+the `run_initializers` method which is defined in `railties/lib/rails/initializable.rb`
+
+```ruby
+def run_initializers(group=:default, *args)
+ return if instance_variable_defined?(:@ran)
+ initializers.tsort_each do |initializer|
+ initializer.run(*args) if initializer.belongs_to?(group)
+ end
+ @ran = true
+end
+```
-TODO: review this
+The run_initializers code itself is tricky. What Rails is doing here is
+traversing all the class ancestors looking for those that respond to an
+`initializers` method. It then sorts the ancestors by name, and runs them.
+For example, the `Engine` class will make all the engines available by
+providing an `initializers` method on them.
-The initializers code itself is tricky. What Rails is doing here is it
-traverses all the class ancestors looking for an `initializers` method,
-sorting them and running them. For example, the `Engine` class will make
-all the engines available by providing the `initializers` method.
+The `Rails::Application` class, as defined in `railties/lib/rails/application.rb`
+defines `bootstrap`, `railtie`, and `finisher` initializers. The `bootstrap` initializers
+prepare the application (like initializing the logger) while the `finisher`
+initializers (like building the middleware stack) are run last. The `railtie`
+initializers are the initializers which have been defined on the `Rails::Application`
+itself and are run between the `bootstrap` and `finishers`.
After this is done we go back to `Rack::Server`
@@ -656,7 +569,7 @@ def self.run(app, options={})
else
server.register('/', Rack::Handler::Mongrel.new(app))
end
- yield server if block_given?
+ yield server if block_given?
server.run.join
end
```
diff --git a/guides/source/kindle/KINDLE.md b/guides/source/kindle/KINDLE.md
index 08937e053e..8c4fad18aa 100644
--- a/guides/source/kindle/KINDLE.md
+++ b/guides/source/kindle/KINDLE.md
@@ -10,7 +10,7 @@
## Resources
* [Stack Overflow: Kindle Periodical Format](http://stackoverflow.com/questions/5379565/kindle-periodical-format)
- * Example Periodical [.ncx](https://gist.github.com/808c971ed087b839d462) and [.opf](https://gist.github.com/d6349aa8488eca2ee6d0)
+ * Example Periodical [.ncx](https://gist.github.com/mipearson/808c971ed087b839d462) and [.opf](https://gist.github.com/mipearson/d6349aa8488eca2ee6d0)
* [Kindle Publishing Guidelines](http://kindlegen.s3.amazonaws.com/AmazonKindlePublishingGuidelines.pdf)
* [KindleGen & Kindle Previewer](http://www.amazon.com/gp/feature.html?ie=UTF8&docId=1000234621)
diff --git a/guides/source/kindle/rails_guides.opf.erb b/guides/source/kindle/rails_guides.opf.erb
index 4e07664fd0..547abcbc19 100644
--- a/guides/source/kindle/rails_guides.opf.erb
+++ b/guides/source/kindle/rails_guides.opf.erb
@@ -32,7 +32,7 @@
<item id="toc" media-type="application/x-dtbncx+xml" href="toc.ncx" />
- <item id="cover" media-type="image/jpeg" href="images/rails_guides_kindle_cover.jpg"/>
+ <item id="cover" media-type="image/jpg" href="images/rails_guides_kindle_cover.jpg"/>
</manifest>
<spine toc="toc">
diff --git a/guides/source/kindle/toc.html.erb b/guides/source/kindle/toc.html.erb
index e013797dee..f310edd3a1 100644
--- a/guides/source/kindle/toc.html.erb
+++ b/guides/source/kindle/toc.html.erb
@@ -20,5 +20,5 @@ Ruby on Rails Guides
<ul>
<li><a href="credits.html">Credits</a></li>
<li><a href="copyright.html">Copyright &amp; License</a></li>
-<ul>
+</ul>
</div>
diff --git a/guides/source/layout.html.erb b/guides/source/layout.html.erb
index 397dd62638..0513066f5a 100644
--- a/guides/source/layout.html.erb
+++ b/guides/source/layout.html.erb
@@ -48,7 +48,7 @@
<ul class="nav">
<li><a class="nav-item" href="index.html">Home</a></li>
<li class="guides-index guides-index-large">
- <a href="index.html" onclick="guideMenu(); return false;" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a>
+ <a href="index.html" id="guidesMenu" class="guides-index-item nav-item">Guides Index</a>
<div id="guides" class="clearfix" style="display: none;">
<hr />
<% ['L', 'R'].each do |position| %>
@@ -101,17 +101,15 @@
You're encouraged to help improve the quality of this guide.
</p>
<p>
- If you see any typos or factual errors you are confident to
- patch, please clone <%= link_to 'docrails', 'https://github.com/lifo/docrails' %>
- and push the change yourself. That branch of Rails has public write access.
- Commits are still reviewed, but that happens after you've submitted your
- contribution. <%= link_to 'docrails', 'https://github.com/lifo/docrails' %> is
- cross-merged with master periodically.
+ Please contribute if you see any typos or factual errors.
+ To get started, you can read our <%= link_to 'documentation contributions', 'http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html#contributing-to-the-rails-documentation' %> section.
</p>
<p>
You may also find incomplete content, or stuff that is not up to date.
- Please do add any missing documentation for master. Check the
- <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
+ Please do add any missing documentation for master. Make sure to check
+ <%= link_to 'Edge Guides','http://edgeguides.rubyonrails.org' %> first to verify
+ if the issues are already fixed or not on the master branch.
+ Check the <%= link_to 'Ruby on Rails Guides Guidelines', 'ruby_on_rails_guides_guidelines.html' %>
for style and conventions.
</p>
<p>
@@ -141,7 +139,7 @@
<script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushSql.js"></script>
<script type="text/javascript" src="javascripts/syntaxhighlighter/shBrushPlain.js"></script>
<script type="text/javascript">
- SyntaxHighlighter.all()
+ SyntaxHighlighter.all();
$(guidesIndex.bind);
</script>
</body>
diff --git a/guides/source/layouts_and_rendering.md b/guides/source/layouts_and_rendering.md
index 8277859232..b5d66d08ba 100644
--- a/guides/source/layouts_and_rendering.md
+++ b/guides/source/layouts_and_rendering.md
@@ -1,12 +1,14 @@
Layouts and Rendering in Rails
==============================
-This guide covers the basic layout features of Action Controller and Action View. By referring to this guide, you will be able to:
+This guide covers the basic layout features of Action Controller and Action View.
-* Use the various rendering methods built into Rails
-* Create layouts with multiple content sections
-* Use partials to DRY up your views
-* Use nested layouts (sub-templates)
+After reading this guide, you will know:
+
+* How to use the various rendering methods built into Rails.
+* How to create layouts with multiple content sections.
+* How to use partials to DRY up your views.
+* How to use nested layouts (sub-templates).
--------------------------------------------------------------------------------
@@ -81,12 +83,12 @@ If we want to display the properties of all the books in our view, we can do so
<td><%= book.content %></td>
<td><%= link_to "Show", book %></td>
<td><%= link_to "Edit", edit_book_path(book) %></td>
- <td><%= link_to "Remove", book, :method => :delete, :data => { :confirm => "Are you sure?" } %></td>
+ <td><%= link_to "Remove", book, method: :delete, data: { confirm: "Are you sure?" } %></td>
</tr>
<% end %>
</table>
-<br />
+<br>
<%= link_to "New book", new_book_path %>
```
@@ -95,7 +97,7 @@ NOTE: The actual rendering is done by subclasses of `ActionView::TemplateHandler
### Using `render`
-In most cases, the `ActionController::Base#render` method does the heavy lifting of rendering your application's content for use by a browser. There are a variety of ways to customize the behaviour of `render`. You can render the default view for a Rails template, or a specific template, or a file, or inline code, or nothing at all. You can render text, JSON, or XML. You can specify the content type or HTTP status of the rendered response as well.
+In most cases, the `ActionController::Base#render` method does the heavy lifting of rendering your application's content for use by a browser. There are a variety of ways to customize the behavior of `render`. You can render the default view for a Rails template, or a specific template, or a file, or inline code, or nothing at all. You can render text, JSON, or XML. You can specify the content type or HTTP status of the rendered response as well.
TIP: If you want to see the exact results of a call to `render` without needing to inspect it in a browser, you can call `render_to_string`. This method takes exactly the same options as `render`, but it returns a string instead of sending a response back to the browser.
@@ -104,7 +106,7 @@ TIP: If you want to see the exact results of a call to `render` without needing
Perhaps the simplest thing you can do with `render` is to render nothing at all:
```ruby
-render :nothing => true
+render nothing: true
```
If you look at the response for this using cURL, you will see the following:
@@ -124,18 +126,18 @@ Cache-Control: no-cache
$
```
-We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for AJAX requests where all you want to send back to the browser is an acknowledgment that the request was completed.
+We see there is an empty response (no data after the `Cache-Control` line), but the request was successful because Rails has set the response to 200 OK. You can set the `:status` option on render to change this response. Rendering nothing can be useful for Ajax requests where all you want to send back to the browser is an acknowledgment that the request was completed.
TIP: You should probably be using the `head` method, discussed later in this guide, instead of `render :nothing`. This provides additional flexibility and makes it explicit that you're only generating HTTP headers.
#### Rendering an Action's View
-If you want to render the view that corresponds to a different action within the same template, you can use `render` with the name of the view:
+If you want to render the view that corresponds to a different template within the same controller, you can use `render` with the name of the view:
```ruby
def update
@book = Book.find(params[:id])
- if @book.update_attributes(params[:book])
+ if @book.update(params[:book])
redirect_to(@book)
else
render "edit"
@@ -143,14 +145,14 @@ def update
end
```
-If the call to `update_attributes` fails, calling the `update` action in this controller will render the `edit.html.erb` template belonging to the same controller.
+If the call to `update` fails, calling the `update` action in this controller will render the `edit.html.erb` template belonging to the same controller.
If you prefer, you can use a symbol instead of a string to specify the action to render:
```ruby
def update
@book = Book.find(params[:id])
- if @book.update_attributes(params[:book])
+ if @book.update(params[:book])
redirect_to(@book)
else
render :edit
@@ -158,21 +160,6 @@ def update
end
```
-To be explicit, you can use `render` with the `:action` option (though this is no longer necessary in Rails 3.0):
-
-```ruby
-def update
- @book = Book.find(params[:id])
- if @book.update_attributes(params[:book])
- redirect_to(@book)
- else
- render :action => "edit"
- end
-end
-```
-
-WARNING: Using `render` with `:action` is a frequent source of confusion for Rails newcomers. The specified action is used to determine which view to render, but Rails does _not_ run any of the code for that action in the controller. Any instance variables that you require in the view must be set up in the current action before calling `render`.
-
#### Rendering an Action's Template from Another Controller
What if you want to render a template from an entirely different controller from the one that contains the action code? You can also do that with `render`, which accepts the full path (relative to `app/views`) of the template to render. For example, if you're running code in an `AdminProductsController` that lives in `app/controllers/admin`, you can render the results of an action to a template in `app/views/products` this way:
@@ -184,7 +171,7 @@ render "products/show"
Rails knows that this view belongs to a different controller because of the embedded slash character in the string. If you want to be explicit, you can use the `:template` option (which was required on Rails 2.2 and earlier):
```ruby
-render :template => "products/show"
+render template: "products/show"
```
#### Rendering an Arbitrary File
@@ -198,13 +185,12 @@ render "/u/apps/warehouse_app/current/app/views/products/show"
Rails determines that this is a file render because of the leading slash character. To be explicit, you can use the `:file` option (which was required on Rails 2.2 and earlier):
```ruby
-render :file =>
- "/u/apps/warehouse_app/current/app/views/products/show"
+render file: "/u/apps/warehouse_app/current/app/views/products/show"
```
The `:file` option takes an absolute file-system path. Of course, you need to have rights to the view that you're using to render the content.
-NOTE: By default, the file is rendered without using the current layout. If you want Rails to put the file into the current layout, you need to add the `:layout => true` option.
+NOTE: By default, the file is rendered without using the current layout. If you want Rails to put the file into the current layout, you need to add the `layout: true` option.
TIP: If you're running Rails on Microsoft Windows, you should use the `:file` option to render a file, because Windows filenames do not have the same format as Unix filenames.
@@ -216,19 +202,19 @@ In fact, in the BooksController class, inside of the update action where we want
```ruby
render :edit
-render :action => :edit
+render action: :edit
render "edit"
render "edit.html.erb"
-render :action => "edit"
-render :action => "edit.html.erb"
+render action: "edit"
+render action: "edit.html.erb"
render "books/edit"
render "books/edit.html.erb"
-render :template => "books/edit"
-render :template => "books/edit.html.erb"
+render template: "books/edit"
+render template: "books/edit.html.erb"
render "/path/to/rails/app/views/books/edit"
render "/path/to/rails/app/views/books/edit.html.erb"
-render :file => "/path/to/rails/app/views/books/edit"
-render :file => "/path/to/rails/app/views/books/edit.html.erb"
+render file: "/path/to/rails/app/views/books/edit"
+render file: "/path/to/rails/app/views/books/edit.html.erb"
```
Which one you use is really a matter of style and convention, but the rule of thumb is to use the simplest one that makes sense for the code you are writing.
@@ -238,8 +224,7 @@ Which one you use is really a matter of style and convention, but the rule of th
The `render` method can do without a view completely, if you're willing to use the `:inline` option to supply ERB as part of the method call. This is perfectly valid:
```ruby
-render :inline =>
- "<% products.each do |p| %><p><%= p.name %></p><% end %>"
+render inline: "<% products.each do |p| %><p><%= p.name %></p><% end %>"
```
WARNING: There is seldom any good reason to use this option. Mixing ERB into your controllers defeats the MVC orientation of Rails and will make it harder for other developers to follow the logic of your project. Use a separate erb view instead.
@@ -247,8 +232,7 @@ WARNING: There is seldom any good reason to use this option. Mixing ERB into you
By default, inline rendering uses ERB. You can force it to use Builder instead with the `:type` option:
```ruby
-render :inline =>
- "xml.p {'Horrid coding practice!'}", :type => :builder
+render inline: "xml.p {'Horrid coding practice!'}", type: :builder
```
#### Rendering Text
@@ -256,19 +240,19 @@ render :inline =>
You can send plain text - with no markup at all - back to the browser by using the `:text` option to `render`:
```ruby
-render :text => "OK"
+render text: "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 `: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.
#### Rendering JSON
-JSON is a JavaScript data format used by many AJAX libraries. Rails has built-in support for converting objects to JSON and rendering that JSON back to the browser:
+JSON is a JavaScript data format used by many Ajax libraries. Rails has built-in support for converting objects to JSON and rendering that JSON back to the browser:
```ruby
-render :json => @product
+render json: @product
```
TIP: You don't need to call `to_json` on the object that you want to render. If you use the `:json` option, `render` will automatically call `to_json` for you.
@@ -278,7 +262,7 @@ TIP: You don't need to call `to_json` on the object that you want to render. If
Rails also has built-in support for converting objects to XML and rendering that XML back to the caller:
```ruby
-render :xml => @product
+render xml: @product
```
TIP: You don't need to call `to_xml` on the object that you want to render. If you use the `:xml` option, `render` will automatically call `to_xml` for you.
@@ -288,7 +272,7 @@ TIP: You don't need to call `to_xml` on the object that you want to render. If y
Rails can render vanilla JavaScript:
```ruby
-render :js => "alert('Hello Rails');"
+render js: "alert('Hello Rails');"
```
This will send the supplied string to the browser with a MIME type of `text/javascript`.
@@ -299,15 +283,15 @@ Calls to the `render` method generally accept four options:
* `:content_type`
* `:layout`
-* `:status`
* `:location`
+* `:status`
##### The `:content_type` Option
By default, Rails will serve the results of a rendering operation with the MIME content-type of `text/html` (or `application/json` if you use the `:json` option, or `application/xml` for the `:xml` option.). There are times when you might like to change this, and you can do so by setting the `:content_type` option:
```ruby
-render :file => filename, :content_type => "application/rss"
+render file: filename, content_type: "application/rss"
```
##### The `:layout` Option
@@ -317,33 +301,94 @@ With most of the options to `render`, the rendered content is displayed as part
You can use the `:layout` option to tell Rails to use a specific file as the layout for the current action:
```ruby
-render :layout => "special_layout"
+render layout: "special_layout"
```
You can also tell Rails to render with no layout at all:
```ruby
-render :layout => false
+render layout: false
```
-##### The `:status` Option
+##### The `:location` Option
-Rails will automatically generate a response with the correct HTTP status code (in most cases, this is `200 OK`). You can use the `:status` option to change this:
+You can use the `:location` option to set the HTTP `Location` header:
```ruby
-render :status => 500
-render :status => :forbidden
+render xml: photo, location: photo_url(photo)
```
-Rails understands both numeric and symbolic status codes.
-
-##### The `:location` Option
+##### The `:status` Option
-You can use the `:location` option to set the HTTP `Location` header:
+Rails will automatically generate a response with the correct HTTP status code (in most cases, this is `200 OK`). You can use the `:status` option to change this:
```ruby
-render :xml => photo, :location => photo_url(photo)
-```
+render status: 500
+render status: :forbidden
+```
+
+Rails understands both numeric status codes and the corresponding symbols shown below.
+
+| Response Class | HTTP Status Code | Symbol |
+| ------------------- | ---------------- | -------------------------------- |
+| **Informational** | 100 | :continue |
+| | 101 | :switching_protocols |
+| | 102 | :processing |
+| **Success** | 200 | :ok |
+| | 201 | :created |
+| | 202 | :accepted |
+| | 203 | :non_authoritative_information |
+| | 204 | :no_content |
+| | 205 | :reset_content |
+| | 206 | :partial_content |
+| | 207 | :multi_status |
+| | 208 | :already_reported |
+| | 226 | :im_used |
+| **Redirection** | 300 | :multiple_choices |
+| | 301 | :moved_permanently |
+| | 302 | :found |
+| | 303 | :see_other |
+| | 304 | :not_modified |
+| | 305 | :use_proxy |
+| | 306 | :reserved |
+| | 307 | :temporary_redirect |
+| | 308 | :permanent_redirect |
+| **Client Error** | 400 | :bad_request |
+| | 401 | :unauthorized |
+| | 402 | :payment_required |
+| | 403 | :forbidden |
+| | 404 | :not_found |
+| | 405 | :method_not_allowed |
+| | 406 | :not_acceptable |
+| | 407 | :proxy_authentication_required |
+| | 408 | :request_timeout |
+| | 409 | :conflict |
+| | 410 | :gone |
+| | 411 | :length_required |
+| | 412 | :precondition_failed |
+| | 413 | :request_entity_too_large |
+| | 414 | :request_uri_too_long |
+| | 415 | :unsupported_media_type |
+| | 416 | :requested_range_not_satisfiable |
+| | 417 | :expectation_failed |
+| | 422 | :unprocessable_entity |
+| | 423 | :locked |
+| | 424 | :failed_dependency |
+| | 426 | :upgrade_required |
+| | 428 | :precondition_required |
+| | 429 | :too_many_requests |
+| | 431 | :request_header_fields_too_large |
+| **Server Error** | 500 | :internal_server_error |
+| | 501 | :not_implemented |
+| | 502 | :bad_gateway |
+| | 503 | :service_unavailable |
+| | 504 | :gateway_timeout |
+| | 505 | :http_version_not_supported |
+| | 506 | :variant_also_negotiates |
+| | 507 | :insufficient_storage |
+| | 508 | :loop_detected |
+| | 510 | :not_extended |
+| | 511 | :network_authentication_required |
#### Finding Layouts
@@ -379,7 +424,7 @@ You can use a symbol to defer the choice of layout until a request is processed:
```ruby
class ProductsController < ApplicationController
- layout "products_layout"
+ layout :products_layout
def show
@product = Product.find(params[:id])
@@ -409,7 +454,7 @@ Layouts specified at the controller level support the `:only` and `:except` opti
```ruby
class ProductsController < ApplicationController
- layout "product", :except => [:index, :rss]
+ layout "product", except: [:index, :rss]
end
```
@@ -454,7 +499,7 @@ Layout declarations cascade downward in the hierarchy, and more specific layout
def index
@old_posts = Post.older
- render :layout => "old"
+ render layout: "old"
end
# ...
end
@@ -478,9 +523,9 @@ For example, here's some code that will trigger this error:
def show
@book = Book.find(params[:id])
if @book.special?
- render :action => "special_show"
+ render action: "special_show"
end
- render :action => "regular_show"
+ render action: "regular_show"
end
```
@@ -490,9 +535,9 @@ If `@book.special?` evaluates to `true`, Rails will start the rendering process
def show
@book = Book.find(params[:id])
if @book.special?
- render :action => "special_show" and return
+ render action: "special_show" and return
end
- render :action => "regular_show"
+ render action: "regular_show"
end
```
@@ -504,7 +549,7 @@ Note that the implicit render done by ActionController detects if `render` has b
def show
@book = Book.find(params[:id])
if @book.special?
- render :action => "special_show"
+ render action: "special_show"
end
end
```
@@ -530,7 +575,7 @@ redirect_to :back
Rails uses HTTP status code 302, a temporary redirect, when you call `redirect_to`. If you'd like to use a different status code, perhaps 301, a permanent redirect, you can use the `:status` option:
```ruby
-redirect_to photos_path, :status => 301
+redirect_to photos_path, status: 301
```
Just like the `:status` option for `render`, `:status` for `redirect_to` accepts both numeric and symbolic header designations.
@@ -547,9 +592,9 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
- render :action => "index"
+ render action: "index"
end
end
```
@@ -562,9 +607,9 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
- redirect_to :action => :index
+ redirect_to action: :index
end
end
```
@@ -581,10 +626,11 @@ def index
end
def show
- @book = Book.find_by_id(params[:id])
+ @book = Book.find_by(id: params[:id])
if @book.nil?
@books = Book.all
- render "index", :alert => "Your book was not found!"
+ flash.now[:alert] = "Your book was not found"
+ render "index"
end
end
```
@@ -593,7 +639,7 @@ This would detect that there are no books with the specified ID, populate the `@
### Using `head` To Build Header-Only Responses
-The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method takes one parameter, which is interpreted as a hash of header names and values. For example, you can return only an error header:
+The `head` method can be used to send responses with only headers to the browser. It provides a more obvious alternative to calling `render :nothing`. The `head` method accepts a number or symbol (see [reference table](#the-status-option)) representing a HTTP status code. The options argument is interpreted as a hash of header names and values. For example, you can return only an error header:
```ruby
head :bad_request
@@ -615,7 +661,7 @@ Cache-Control: no-cache
Or you can use other HTTP headers to convey other information:
```ruby
-head :created, :location => photo_path(@photo)
+head :created, location: photo_path(@photo)
```
Which would produce:
@@ -661,21 +707,21 @@ WARNING: The asset tag helpers do _not_ verify the existence of the assets at th
The `auto_discovery_link_tag` helper builds HTML that most browsers and newsreaders can use to detect the presence of RSS or Atom feeds. It takes the type of the link (`:rss` or `:atom`), a hash of options that are passed through to url_for, and a hash of options for the tag:
```erb
-<%= auto_discovery_link_tag(:rss, {:action => "feed"},
- {:title => "RSS Feed"}) %>
+<%= auto_discovery_link_tag(:rss, {action: "feed"},
+ {title: "RSS Feed"}) %>
```
There are three tag options available for the `auto_discovery_link_tag`:
* `:rel` specifies the `rel` value in the link. The default value is "alternate".
* `:type` specifies an explicit MIME type. Rails will generate an appropriate MIME type automatically.
-* `:title` specifies the title of the link. The default value is the uppercased `:type` value, for example, "ATOM" or "RSS".
+* `:title` specifies the title of the link. The default value is the uppercase `:type` value, for example, "ATOM" or "RSS".
#### Linking to JavaScript Files with the `javascript_include_tag`
The `javascript_include_tag` helper returns an HTML `script` tag for each source provided.
-If you are using Rails with the [Asset Pipeline](asset_pipeline.html) enabled, this helper will generate a link to `/assets/javascripts/` rather than `public/javascripts` which was used in earlier versions of Rails. This link is then served by the Sprockets gem, which was introduced in Rails 3.1.
+If you are using Rails with the [Asset Pipeline](asset_pipeline.html) enabled, this helper will generate a link to `/assets/javascripts/` rather than `public/javascripts` which was used in earlier versions of Rails. This link is then served by the asset pipeline.
A JavaScript file within a Rails application or Rails engine goes in one of three locations: `app/assets`, `lib/assets` or `vendor/assets`. These locations are explained in detail in the [Asset Organization section in the Asset Pipeline Guide](asset_pipeline.html#asset-organization)
@@ -711,72 +757,6 @@ To include `http://example.com/main.js`:
<%= javascript_include_tag "http://example.com/main.js" %>
```
-If the application does not use the asset pipeline, the `:defaults` option loads jQuery by default:
-
-```erb
-<%= javascript_include_tag :defaults %>
-```
-
-Outputting `script` tags such as this:
-
-```html
-<script src="/javascripts/jquery.js"></script>
-<script src="/javascripts/jquery_ujs.js"></script>
-```
-
-These two files for jQuery, `jquery.js` and `jquery_ujs.js` must be placed inside `public/javascripts` if the application doesn't use the asset pipeline. These files can be downloaded from the [jquery-rails repository on GitHub](https://github.com/indirect/jquery-rails/tree/master/vendor/assets/javascripts)
-
-WARNING: If you are using the asset pipeline, this tag will render a `script` tag for an asset called `defaults.js`, which would not exist in your application unless you've explicitly created it.
-
-And you can in any case override the `:defaults` expansion in `config/application.rb`:
-
-```ruby
-config.action_view.javascript_expansions[:defaults] = %w(foo.js bar.js)
-```
-
-You can also define new defaults:
-
-```ruby
-config.action_view.javascript_expansions[:projects] = %w(projects.js tickets.js)
-```
-
-And use them by referencing them exactly like `:defaults`:
-
-```erb
-<%= javascript_include_tag :projects %>
-```
-
-When using `:defaults`, if an `application.js` file exists in `public/javascripts` it will be included as well at the end.
-
-Also, if the asset pipeline is disabled, the `:all` expansion loads every JavaScript file in `public/javascripts`:
-
-```erb
-<%= javascript_include_tag :all %>
-```
-
-Note that your defaults of choice will be included first, so they will be available to all subsequently included files.
-
-You can supply the `:recursive` option to load files in subfolders of `public/javascripts` as well:
-
-```erb
-<%= javascript_include_tag :all, :recursive => true %>
-```
-
-If you're loading multiple JavaScript files, you can create a better user experience by combining multiple files into a single download. To make this happen in production, specify `:cache => true` in your `javascript_include_tag`:
-
-```erb
-<%= javascript_include_tag "main", "columns", :cache => true %>
-```
-
-By default, the combined file will be delivered as `javascripts/all.js`. You can specify a location for the cached asset file instead:
-
-```erb
-<%= javascript_include_tag "main", "columns",
- :cache => "cache/main/display" %>
-```
-
-You can even use dynamic paths such as `cache/#{current_site}/main/display`.
-
#### Linking to CSS Files with the `stylesheet_link_tag`
The `stylesheet_link_tag` helper returns an HTML `<link>` tag for each source provided.
@@ -798,7 +778,7 @@ To include `app/assets/stylesheets/main.css` and `app/assets/stylesheets/columns
To include `app/assets/stylesheets/main.css` and `app/assets/stylesheets/photos/columns.css`:
```erb
-<%= stylesheet_link_tag "main", "/photos/columns" %>
+<%= stylesheet_link_tag "main", "photos/columns" %>
```
To include `http://example.com/main.css`:
@@ -810,41 +790,14 @@ To include `http://example.com/main.css`:
By default, the `stylesheet_link_tag` creates links with `media="screen" rel="stylesheet"`. You can override any of these defaults by specifying an appropriate option (`:media`, `:rel`):
```erb
-<%= stylesheet_link_tag "main_print", :media => "print" %>
-```
-
-If the asset pipeline is disabled, the `all` option links every CSS file in `public/stylesheets`:
-
-```erb
-<%= stylesheet_link_tag :all %>
-```
-
-You can supply the `:recursive` option to link files in subfolders of `public/stylesheets` as well:
-
-```erb
-<%= stylesheet_link_tag :all, :recursive => true %>
+<%= stylesheet_link_tag "main_print", media: "print" %>
```
-If you're loading multiple CSS files, you can create a better user experience by combining multiple files into a single download. To make this happen in production, specify `:cache => true` in your `stylesheet_link_tag`:
-
-```erb
-<%= stylesheet_link_tag "main", "columns", :cache => true %>
-```
-
-By default, the combined file will be delivered as `stylesheets/all.css`. You can specify a location for the cached asset file instead:
-
-```erb
-<%= stylesheet_link_tag "main", "columns",
- :cache => "cache/main/display" %>
-```
-
-You can even use dynamic paths such as `cache/#{current_site}/main/display`.
-
#### Linking to Images with the `image_tag`
The `image_tag` helper builds an HTML `<img />` tag to the specified file. By default, files are loaded from `public/images`.
-WARNING: Note that you must specify the extension of the image. Previous versions of Rails would allow you to just use the image name and would append `.png` if no extension was given but Rails 3.0 does not.
+WARNING: Note that you must specify the extension of the image.
```erb
<%= image_tag "header.png" %>
@@ -859,28 +812,28 @@ You can supply a path to the image if you like:
You can supply a hash of additional HTML options:
```erb
-<%= image_tag "icons/delete.gif", {:height => 45} %>
+<%= image_tag "icons/delete.gif", {height: 45} %>
```
You can supply alternate text for the image which will be used if the user has images turned off in their browser. If you do not specify an alt text explicitly, it defaults to the file name of the file, capitalized and with no extension. For example, these two image tags would return the same code:
```erb
<%= image_tag "home.gif" %>
-<%= image_tag "home.gif", :alt => "Home" %>
+<%= image_tag "home.gif", alt: "Home" %>
```
You can also specify a special size tag, in the format "{width}x{height}":
```erb
-<%= image_tag "home.gif", :size => "50x20" %>
+<%= image_tag "home.gif", size: "50x20" %>
```
In addition to the above special tags, you can supply a final hash of standard HTML options, such as `:class`, `:id` or `:name`:
```erb
-<%= image_tag "home.gif", :alt => "Go Home",
- :id => "HomeImage",
- :class => "nav_bar" %>
+<%= image_tag "home.gif", alt: "Go Home",
+ id: "HomeImage",
+ class: "nav_bar" %>
```
#### Linking to Videos with the `video_tag`
@@ -897,15 +850,15 @@ Produces
<video src="/videos/movie.ogg" />
```
-Like an `image_tag` you can supply a path, either absolute, or relative to the `public/videos` directory. Additionally you can specify the `:size => "#{width}x#{height}"` option just like an `image_tag`. Video tags can also have any of the HTML options specified at the end (`id`, `class` et al).
+Like an `image_tag` you can supply a path, either absolute, or relative to the `public/videos` directory. Additionally you can specify the `size: "#{width}x#{height}"` option just like an `image_tag`. Video tags can also have any of the HTML options specified at the end (`id`, `class` et al).
The video tag also supports all of the `<video>` HTML options through the HTML options hash, including:
-* `:poster => "image_name.png"`, provides an image to put in place of the video before it starts playing.
-* `:autoplay => true`, starts playing the video on page load.
-* `:loop => true`, loops the video once it gets to the end.
-* `:controls => true`, provides browser supplied controls for the user to interact with the video.
-* `:autobuffer => true`, the video will pre load the file for the user on page load.
+* `poster: "image_name.png"`, provides an image to put in place of the video before it starts playing.
+* `autoplay: true`, starts playing the video on page load.
+* `loop: true`, loops the video once it gets to the end.
+* `controls: true`, provides browser supplied controls for the user to interact with the video.
+* `autobuffer: true`, the video will pre load the file for the user on page load.
You can also specify multiple videos to play by passing an array of videos to the `video_tag`:
@@ -937,9 +890,9 @@ You can also supply a hash of additional options, such as `:id`, `:class` etc.
Like the `video_tag`, the `audio_tag` has special options:
-* `:autoplay => true`, starts playing the audio on page load
-* `:controls => true`, provides browser supplied controls for the user to interact with the audio.
-* `:autobuffer => true`, the audio will pre load the file for the user on page load.
+* `autoplay: true`, starts playing the audio on page load
+* `controls: true`, provides browser supplied controls for the user to interact with the audio.
+* `autobuffer: true`, the audio will pre load the file for the user on page load.
### Understanding `yield`
@@ -1041,7 +994,7 @@ TIP: For content that is shared among all pages in your application, you can use
A partial can use its own layout file, just as a view can use a layout. For example, you might call a partial like this:
```erb
-<%= render :partial => "link_area", :layout => "graybar" %>
+<%= render partial: "link_area", layout: "graybar" %>
```
This would look for a partial named `_link_area.html.erb` and render it using the layout `_graybar.html.erb`. Note that layouts for partials follow the same leading-underscore naming as regular partials, and are placed in the same folder with the partial that they belong to (not in the master `layouts` folder).
@@ -1057,7 +1010,7 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>New zone</h1>
<%= error_messages_for :zone %>
- <%= render :partial => "form", :locals => { :zone => @zone } %>
+ <%= render partial: "form", locals: {zone: @zone} %>
```
* `edit.html.erb`
@@ -1065,7 +1018,7 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<h1>Editing zone</h1>
<%= error_messages_for :zone %>
- <%= render :partial => "form", :locals => { :zone => @zone } %>
+ <%= render partial: "form", locals: {zone: @zone} %>
```
* `_form.html.erb`
@@ -1073,7 +1026,7 @@ You can also pass local variables into partials, making them even more powerful
```html+erb
<%= form_for(zone) do |f| %>
<p>
- <b>Zone name</b><br />
+ <b>Zone name</b><br>
<%= f.text_field :name %>
</p>
<p>
@@ -1087,13 +1040,11 @@ Although the same partial will be rendered into both views, Action View's submit
Every partial also has a local variable with the same name as the partial (minus the underscore). You can pass an object in to this local variable via the `:object` option:
```erb
-<%= render :partial => "customer", :object => @new_customer %>
+<%= render partial: "customer", object: @new_customer %>
```
Within the `customer` partial, the `customer` variable will refer to `@new_customer` from the parent view.
-WARNING: In previous versions of Rails, the default local variable would look for an instance variable with the same name as the partial in the parent. This behavior was deprecated in 2.3 and has been removed in Rails 3.0.
-
If you have an instance of a model to render into a partial, you can use a shorthand syntax:
```erb
@@ -1110,7 +1061,7 @@ Partials are very useful in rendering collections. When you pass a collection to
```html+erb
<h1>Products</h1>
- <%= render :partial => "product", :collection => @products %>
+ <%= render partial: "product", collection: @products %>
```
* `_product.html.erb`
@@ -1121,7 +1072,7 @@ Partials are very useful in rendering collections. When you pass a collection to
When a partial is called with a pluralized collection, then the individual instances of the partial have access to the member of the collection being rendered via a variable named after the partial. In this case, the partial is `_product`, and within the `_product` partial, you can refer to `product` to get the instance that is being rendered.
-In Rails 3.0, there is also a shorthand for this. Assuming `@products` is a collection of `product` instances, you can simply write this in the `index.html.erb` to produce the same result:
+There is also a shorthand for this. Assuming `@products` is a collection of `product` instances, you can simply write this in the `index.html.erb` to produce the same result:
```html+erb
<h1>Products</h1>
@@ -1163,28 +1114,28 @@ In the event that the collection is empty, `render` will return nil, so it shoul
To use a custom local variable name within the partial, specify the `:as` option in the call to the partial:
```erb
-<%= render :partial => "product", :collection => @products, :as => :item %>
+<%= render partial: "product", collection: @products, as: :item %>
```
With this change, you can access an instance of the `@products` collection as the `item` local variable within the partial.
-You can also pass in arbitrary local variables to any partial you are rendering with the `:locals => {}` option:
+You can also pass in arbitrary local variables to any partial you are rendering with the `locals: {}` option:
```erb
-<%= render :partial => "products", :collection => @products,
- :as => :item, :locals => {:title => "Products Page"} %>
+<%= render partial: "products", collection: @products,
+ as: :item, locals: {title: "Products Page"} %>
```
Would render a partial `_products.html.erb` once for each instance of `product` in the `@products` instance variable passing the instance to the partial as a local variable called `item` and to each partial, make the local variable `title` available with the value `Products Page`.
-TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `:as => :value` option.
+TIP: Rails also makes a counter variable available within a partial called by the collection, named after the member of the collection followed by `_counter`. For example, if you're rendering `@products`, within the partial you can refer to `product_counter` to tell you how many times the partial has been rendered. This does not work in conjunction with the `as: :value` option.
You can also specify a second partial to be rendered between instances of the main partial by using the `:spacer_template` option:
#### Spacer Templates
```erb
-<%= render :partial => @products, :spacer_template => "product_ruler" %>
+<%= render partial: @products, spacer_template: "product_ruler" %>
```
Rails will render the `_product_ruler` partial (with no data passed in to it) between each pair of `_product` partials.
@@ -1194,7 +1145,7 @@ Rails will render the `_product_ruler` partial (with no data passed in to it) be
When rendering collections it is also possible to use the `:layout` option:
```erb
-<%= render :partial => "product", :collection => @products, :layout => "special_layout" %>
+<%= render partial: "product", collection: @products, layout: "special_layout" %>
```
The layout will be rendered together with the partial for each item in the collection. The current object and object_counter variables will be available in the layout as well, the same way they do within the partial.
@@ -1235,9 +1186,9 @@ On pages generated by `NewsController`, you want to hide the top menu and add a
<div id="right_menu">Right menu items here</div>
<%= content_for?(:news_content) ? yield(:news_content) : yield %>
<% end %>
- <%= render :template => "layouts/application" %>
+ <%= render template: "layouts/application" %>
```
That's it. The News views will use the new layout, hiding the top menu and adding a new right menu inside the "content" div.
-There are several ways of getting similar results with different sub-templating schemes using this technique. Note that there is no limit in nesting levels. One can use the `ActionView::render` method via `render :template => 'layouts/news'` to base a new layout on the News layout. If you are sure you will not subtemplate the `News` layout, you can replace the `content_for?(:news_content) ? yield(:news_content) : yield` with simply `yield`.
+There are several ways of getting similar results with different sub-templating schemes using this technique. Note that there is no limit in nesting levels. One can use the `ActionView::render` method via `render template: 'layouts/news'` to base a new layout on the News layout. If you are sure you will not subtemplate the `News` layout, you can replace the `content_for?(:news_content) ? yield(:news_content) : yield` with simply `yield`.
diff --git a/guides/source/maintenance_policy.md b/guides/source/maintenance_policy.md
new file mode 100644
index 0000000000..93729c6f72
--- /dev/null
+++ b/guides/source/maintenance_policy.md
@@ -0,0 +1,56 @@
+Maintenance Policy for Ruby on Rails
+====================================
+
+Support of the Rails framework is divided into four groups: New features, bug
+fixes, security issues, and severe security issues. They are handled as
+follows, all versions in x.y.z format
+
+--------------------------------------------------------------------------------
+
+New Features
+------------
+
+New features are only added to the master branch and will not be made available
+in point releases.
+
+Bug Fixes
+---------
+
+Only the latest release series will receive bug fixes. When enough bugs are
+fixed and its deemed worthy to release a new gem, this is the branch it happens
+from.
+
+**Currently included series:** 4.0.z
+
+Security Issues
+---------------
+
+The current release series and the next most recent one will receive patches
+and new versions in case of a security issue.
+
+These releases are created by taking the last released version, applying the
+security patches, and releasing. Those patches are then applied to the end of
+the x-y-stable branch. For example, a theoretical 1.2.3 security release would
+be built from 1.2.2, and then added to the end of 1-2-stable. This means that
+security releases are easy to upgrade to if you're running the latest version
+of Rails.
+
+**Currently included series:** 4.0.z, 3.2.z
+
+Severe Security Issues
+----------------------
+
+For severe security issues we will provide new versions as above, and also the
+last major release series will receive patches and new versions. The
+classification of the security issue is judged by the core team.
+
+**Currently included series:** 4.0.z, 3.2.z
+
+Unsupported Release Series
+--------------------------
+
+When a release series is no longer supported, it's your own responsibility to
+deal with bugs and security issues. We may provide backports of the fixes and
+publish them to git, however there will be no new versions released. If you are
+not comfortable maintaining your own versions, you should upgrade to a
+supported version.
diff --git a/guides/source/migrations.md b/guides/source/migrations.md
index 57db14f30c..b7283d16cc 100644
--- a/guides/source/migrations.md
+++ b/guides/source/migrations.md
@@ -1,41 +1,39 @@
-Migrations
-==========
+Active Record Migrations
+========================
-Migrations are a convenient way for you to alter your database in a structured
-and organized manner. You could edit fragments of SQL by hand but you would then
-be responsible for telling other developers that they need to go and run them.
-You'd also have to keep track of which changes need to be run against the
-production machines next time you deploy.
+Migrations are a feature of Active Record that allows you to evolve your
+database schema over time. Rather than write schema modifications in pure SQL,
+migrations allow you to use an easy Ruby DSL to describe changes to your
+tables.
-Active Record tracks which migrations have already been run so all you have to
-do is update your source and run `rake db:migrate`. Active Record will work out
-which migrations should be run. Active Record will also update your `db/schema.rb` file to match the up-to-date structure of your database.
+After reading this guide, you will know:
-Migrations also allow you to describe these transformations using Ruby. The
-great thing about this is that (like most of Active Record's functionality) it
-is database independent: you don't need to worry about the precise syntax of
-`CREATE TABLE` any more than you worry about variations on `SELECT *` (you can
-drop down to raw SQL for database specific features). For example, you could use
-SQLite3 in development, but MySQL in production.
+* The generators you can use to create them.
+* The methods Active Record provides to manipulate your database.
+* The Rake tasks that manipulate migrations and your schema.
+* How migrations relate to `schema.rb`.
-In this guide, you'll learn all about migrations including:
+--------------------------------------------------------------------------------
-* The generators you can use to create them
-* The methods Active Record provides to manipulate your database
-* The Rake tasks that manipulate them
-* How they relate to `schema.rb`
+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.
-Anatomy of a Migration
-----------------------
+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
+remove tables, columns, or entries. Active Record knows how to update your
+schema along this timeline, bringing it from whatever point it is in the
+history to the latest version. Active Record will also update your
+`db/schema.rb` file to match the up-to-date structure of your database.
-Before we dive into the details of a migration, here are a few examples of the
-sorts of things you can do:
+Here's an example of a migration:
```ruby
class CreateProducts < ActiveRecord::Migration
- def up
+ def change
create_table :products do |t|
t.string :name
t.text :description
@@ -43,102 +41,68 @@ class CreateProducts < ActiveRecord::Migration
t.timestamps
end
end
-
- def down
- drop_table :products
- end
end
```
-This migration adds a table called `products` with a string column called `name`
-and a text column called `description`. A primary key column called `id` will
-also be added, however since this is the default we do not need to explicitly specify it.
-The timestamp columns `created_at` and `updated_at` which Active Record
-populates automatically will also be added. Reversing this migration is as
-simple as dropping the table.
+This migration adds a table called `products` with a string column called
+`name` and a text column called `description`. A primary key column called `id`
+will also be added implicitly, as it's the default primary key for all Active
+Record models. The `timestamps` macro adds two columns, `created_at` and
+`updated_at`. These special columns are automatically managed by Active Record
+if they exist.
-Migrations are not limited to changing the schema. You can also use them to fix
-bad data in the database or populate new fields:
+Note that we define the change that we want to happen moving forward in time.
+Before this migration is run, there will be no table. After, the table will
+exist. Active Record knows how to reverse this migration as well: if we roll
+this migration back, it will remove the table.
+
+On databases that support transactions with statements that change the schema,
+migrations are wrapped in a transaction. If the database does not support this
+then when a migration fails the parts of it that succeeded will not be rolled
+back. You will have to rollback the changes that were made by hand.
+
+NOTE: There are certain queries that can't run inside a transaction. If your
+adapter supports DDL transactions you can use `disable_ddl_transaction!` to
+disable them for a single migration.
+
+If you wish for a migration to do something that Active Record doesn't know how
+to reverse, you can use `reversible`:
```ruby
-class AddReceiveNewsletterToUsers < ActiveRecord::Migration
- def up
- change_table :users do |t|
- t.boolean :receive_newsletter, :default => false
+class ChangeProductsPrice < ActiveRecord::Migration
+ def change
+ reversible do |dir|
+ change_table :products do |t|
+ dir.up { t.change :price, :string }
+ dir.down { t.change :price, :integer }
+ end
end
- User.update_all :receive_newsletter => true
- end
-
- def down
- remove_column :users, :receive_newsletter
end
end
```
-NOTE: Some [caveats](#using-models-in-your-migrations) apply to using models in
-your migrations.
-
-This migration adds a `receive_newsletter` column to the `users` table. We want
-it to default to `false` for new users, but existing users are considered to
-have already opted in, so we use the User model to set the flag to `true` for
-existing users.
-
-### Using the change method
-
-Rails 3.1 and up makes migrations smarter by providing a `change` method.
-This method is preferred for writing constructive migrations (adding columns or
-tables). The migration knows how to migrate your database and reverse it when
-the migration is rolled back without the need to write a separate `down` method.
+Alternatively, you can use `up` and `down` instead of `change`:
```ruby
-class CreateProducts < ActiveRecord::Migration
- def change
- create_table :products do |t|
- t.string :name
- t.text :description
+class ChangeProductsPrice < ActiveRecord::Migration
+ def up
+ change_table :products do |t|
+ t.change :price, :string
+ end
+ end
- t.timestamps
+ def down
+ change_table :products do |t|
+ t.change :price, :integer
end
end
end
```
-### Migrations are Classes
-
-A migration is a subclass of `ActiveRecord::Migration` that implements
-two methods: `up` (perform the required transformations) and `down` (revert
-them).
-
-Active Record provides methods that perform common data definition tasks in a
-database independent way (you'll read about them in detail later):
-
-* `add_column`
-* `add_reference`
-* `add_index`
-* `change_column`
-* `change_table`
-* `create_table`
-* `create_join_table`
-* `drop_table`
-* `remove_column`
-* `remove_index`
-* `rename_column`
-* `remove_reference`
-
-If you need to perform tasks specific to your database (e.g., create a
-[foreign key](#active-record-and-referential-integrity) constraint) then the
-`execute` method allows you to execute arbitrary SQL. A migration is just a
-regular Ruby class so you're not limited to these functions. For example, after
-adding a column you could write code to set the value of that column for
-existing records (if necessary using your models).
-
-On databases that support transactions with statements that change the schema
-(such as PostgreSQL or SQLite3), migrations are wrapped in a transaction. If the
-database does not support this (for example MySQL) then when a migration fails
-the parts of it that succeeded will not be rolled back. You will have to rollback
-the changes that were made by hand.
+Creating a Migration
+--------------------
-### What's in a Name
+### Creating a Standalone Migration
Migrations are stored as files in the `db/migrate` directory, one for each
migration class. The name of the file is of the form
@@ -148,139 +112,48 @@ of the migration. The name of the migration class (CamelCased version)
should match the latter part of the file name. For example
`20080906120000_create_products.rb` should define class `CreateProducts` and
`20080906120001_add_details_to_products.rb` should define
-`AddDetailsToProducts`. If you do feel the need to change the file name then you
-<em>have to</em> update the name of the class inside or Rails will complain
-about a missing class.
-
-Internally Rails only uses the migration's number (the timestamp) to identify
-them. Prior to Rails 2.1 the migration number started at 1 and was incremented
-each time a migration was generated. With multiple developers it was easy for
-these to clash requiring you to rollback migrations and renumber them. With
-Rails 2.1+ this is largely avoided by using the creation time of the migration
-to identify them. You can revert to the old numbering scheme by adding the
-following line to `config/application.rb`.
-
-```ruby
-config.active_record.timestamped_migrations = false
-```
-
-The combination of timestamps and recording which migrations have been run
-allows Rails to handle common situations that occur with multiple developers.
-
-For example, Alice adds migrations `20080906120000` and `20080906123000` and Bob
-adds `20080906124500` and runs it. Alice finishes her changes and checks in her
-migrations and Bob pulls down the latest changes. When Bob runs `rake db:migrate`,
-Rails knows that it has not run Alice's two migrations so it executes the `up` method for each migration.
-
-Of course this is no substitution for communication within the team. For
-example, if Alice's migration removed a table that Bob's migration assumed to
-exist, then trouble would certainly strike.
-
-### Changing Migrations
+`AddDetailsToProducts`. Rails uses this timestamp to determine which migration
+should be run and in what order, so if you're copying a migration from another
+application or generate a file yourself, be aware of its position in the order.
-Occasionally you will make a mistake when writing a migration. If you have
-already run the migration then you cannot just edit the migration and run the
-migration again: Rails thinks it has already run the migration and so will do
-nothing when you run `rake db:migrate`. You must rollback the migration (for
-example with `rake db:rollback`), edit your migration and then run `rake db:migrate` to run the corrected version.
-
-In general, editing existing migrations is not a good idea. You will be creating
-extra work for yourself and your co-workers and cause major headaches if the
-existing version of the migration has already been run on production machines.
-Instead, you should write a new migration that performs the changes you require.
-Editing a freshly generated migration that has not yet been committed to source
-control (or, more generally, which has not been propagated beyond your
-development machine) is relatively harmless.
-
-### Supported Types
-
-Active Record supports the following database column types:
-
-* `:binary`
-* `:boolean`
-* `:date`
-* `:datetime`
-* `:decimal`
-* `:float`
-* `:integer`
-* `:primary_key`
-* `:string`
-* `:text`
-* `:time`
-* `:timestamp`
-
-These will be mapped onto an appropriate underlying database type. For example,
-with MySQL the type `:string` is mapped to `VARCHAR(255)`. You can create
-columns of types not supported by Active Record when using the non-sexy syntax such as
-
-```ruby
-create_table :products do |t|
- t.column :name, 'polygon', :null => false
-end
-```
-
-This may however hinder portability to other databases.
-
-Creating a Migration
---------------------
-
-### Creating a Model
-
-The model and scaffold generators will create migrations appropriate for adding
-a new model. This migration will already contain instructions for creating the
-relevant table. If you tell Rails what columns you want, then statements for
-adding these columns will also be created. For example, running
+Of course, calculating timestamps is no fun, so Active Record provides a
+generator to handle making it for you:
```bash
-$ rails generate model Product name:string description:text
+$ rails generate migration AddPartNumberToProducts
```
-TIP: All lines starting with a dollar sign `$` are intended to be run on the command line.
-
-will create a migration that looks like this
+This will create an empty but appropriately named migration:
```ruby
-class CreateProducts < ActiveRecord::Migration
+class AddPartNumberToProducts < ActiveRecord::Migration
def change
- create_table :products do |t|
- t.string :name
- t.text :description
-
- t.timestamps
- end
end
end
```
-You can append as many column name/type pairs as you want. By default, the
-generated migration will include `t.timestamps` (which creates the
-`updated_at` and `created_at` columns that are automatically populated
-by Active Record).
-
-### Creating a Standalone Migration
-
-If you are creating migrations for other purposes (e.g., to add a column
-to an existing table) then you can also use the migration generator:
+If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is
+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
+$ rails generate migration AddPartNumberToProducts part_number:string
```
-This will create an empty but appropriately named migration:
+will generate
```ruby
class AddPartNumberToProducts < ActiveRecord::Migration
def change
+ add_column :products, :part_number, :string
end
end
```
-If the migration name is of the form "AddXXXToYYY" or "RemoveXXXFromYYY" and is
-followed by a list of column names and types then a migration containing the
-appropriate `add_column` and `remove_column` statements will be created.
+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
+$ rails generate migration AddPartNumberToProducts part_number:string:index
```
will generate
@@ -289,11 +162,13 @@ will generate
class AddPartNumberToProducts < ActiveRecord::Migration
def change
add_column :products, :part_number, :string
+ add_index :products, :part_number
end
end
```
-Similarly,
+
+Similarly, you can generate a migration to remove a column from the command line:
```bash
$ rails generate migration RemovePartNumberFromProducts part_number:string
@@ -303,17 +178,13 @@ generates
```ruby
class RemovePartNumberFromProducts < ActiveRecord::Migration
- def up
- remove_column :products, :part_number
- end
-
- def down
- add_column :products, :part_number, :string
+ def change
+ remove_column :products, :part_number, :string
end
end
```
-You are not limited to one magically generated column. For example
+You are not limited to one magically generated column. For example:
```bash
$ rails generate migration AddDetailsToProducts part_number:string price:decimal
@@ -330,15 +201,33 @@ class AddDetailsToProducts < ActiveRecord::Migration
end
```
+If the migration name is of the form "CreateXXX" and is
+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
+```
+
+generates
+
+```ruby
+class CreateProducts < ActiveRecord::Migration
+ def change
+ create_table :products do |t|
+ t.string :name
+ t.string :part_number
+ end
+ end
+end
+```
+
As always, what has been generated for you is just a starting point. You can add
or remove from it as you see fit by editing the
-@db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb@ file.
-
-NOTE: The generated migration file for destructive migrations will still be
-old-style using the `up` and `down` methods. This is because Rails needs to know
-the original data types defined when you made the original changes.
+`db/migrate/YYYYMMDDHHMMSS_add_details_to_products.rb` file.
-Also, the generator accepts column type as `references`(also available as `belongs_to`). For instance
+Also, the generator accepts column type as `references`(also available as
+`belongs_to`). For instance:
```bash
$ rails generate migration AddUserRefToProducts user:references
@@ -349,27 +238,75 @@ generates
```ruby
class AddUserRefToProducts < ActiveRecord::Migration
def change
- add_reference :products, :user, :index => true
+ add_reference :products, :user, index: true
+ end
+end
+```
+
+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
+```
+
+will produce the following migration:
+
+```ruby
+class CreateJoinTableCustomerProduct < ActiveRecord::Migration
+ def change
+ create_join_table :customers, :products do |t|
+ # t.index [:customer_id, :product_id]
+ # t.index [:product_id, :customer_id]
+ end
+ end
+end
+```
+
+### Model Generators
+
+The model and scaffold generators will create migrations appropriate for adding
+a new model. This migration will already contain instructions for creating the
+relevant table. If you tell Rails what columns you want, then statements for
+adding these columns will also be created. For example, running:
+
+```bash
+$ rails generate model Product name:string description:text
+```
+
+will create a migration that looks like this
+
+```ruby
+class CreateProducts < ActiveRecord::Migration
+ def change
+ create_table :products do |t|
+ t.string :name
+ t.text :description
+
+ t.timestamps
+ end
end
end
```
-This migration will create a user_id column and appropriate index.
+You can append as many column name/type pairs as you want.
### Supported Type Modifiers
-You can also specify some options just after the field type between curly braces. You can use the
-following 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
* `scale` Defines the scale for the `decimal` fields
* `polymorphic` Adds a `type` column for `belongs_to` associations
+* `null` Allows or disallows `NULL` values in the column.
-For instance, running
+For instance, running:
```bash
-$ rails generate migration AddDetailsToProducts price:decimal{5,2} supplier:references{polymorphic}
+$ rails generate migration AddDetailsToProducts 'price:decimal{5,2}' supplier:references{polymorphic}
```
will produce a migration that looks like this
@@ -377,8 +314,8 @@ will produce a migration that looks like this
```ruby
class AddDetailsToProducts < ActiveRecord::Migration
def change
- add_column :products, :price, :precision => 5, :scale => 2
- add_reference :products, :user, :polymorphic => true, :index => true
+ add_column :products, :price, :decimal, precision: 5, scale: 2
+ add_reference :products, :supplier, polymorphic: true, index: true
end
end
```
@@ -391,8 +328,9 @@ get to work!
### Creating a Table
-Migration method `create_table` will be one of your workhorses. A typical use
-would be
+The `create_table` method is one of the most fundamental, but most of the time,
+will be generated for you from using a model or scaffold generator. A typical
+use would be
```ruby
create_table :products do |t|
@@ -403,35 +341,15 @@ end
which creates a `products` table with a column called `name` (and as discussed
below, an implicit `id` column).
-The object yielded to the block allows you to create columns on the table. There
-are two ways of doing it. The first (traditional) form looks like
-
-```ruby
-create_table :products do |t|
- t.column :name, :string, :null => false
-end
-```
-
-The second form, the so called "sexy" migration, drops the somewhat redundant
-`column` method. Instead, the `string`, `integer`, etc. methods create a column
-of that type. Subsequent parameters are the same.
-
-```ruby
-create_table :products do |t|
- t.string :name, :null => false
-end
-```
-
By default, `create_table` will create a primary key called `id`. You can change
the name of the primary key with the `:primary_key` option (don't forget to
-update the corresponding model) or, if you don't want a primary key at all (for
-example for a HABTM join table), you can pass the option `:id => false`. If you
-need to pass database specific options you can place an SQL fragment in the
-`:options` option. For example,
+update the corresponding model) or, if you don't want a primary key at all, you
+can pass the option `id: false`. If you need to pass database specific options
+you can place an SQL fragment in the `:options` option. For example:
```ruby
-create_table :products, :options => "ENGINE=BLACKHOLE" do |t|
- t.string :name, :null => false
+create_table :products, options: "ENGINE=BLACKHOLE" do |t|
+ t.string :name, null: false
end
```
@@ -441,37 +359,48 @@ will append `ENGINE=BLACKHOLE` to the SQL statement used to create the table
### Creating a Join Table
Migration method `create_join_table` creates a HABTM join table. A typical use
-would be
+would be:
```ruby
create_join_table :products, :categories
```
-which creates a `categories_products` table with two columns called `category_id` and `product_id`.
-These columns have the option `:null` set to `false` by default.
+which creates a `categories_products` table with two columns called
+`category_id` and `product_id`. These columns have the option `:null` set to
+`false` by default. This can be overridden by specifying the `:column_options`
+option.
+
+```ruby
+create_join_table :products, :categories, column_options: {null: true}
+```
+
+will create the `product_id` and `category_id` with the `:null` option as
+`true`.
-You can pass the option `:table_name` with you want to customize the table name. For example,
+You can pass the option `:table_name` when you want to customize the table
+name. For example:
```ruby
-create_join_table :products, :categories, :table_name => :categorization
+create_join_table :products, :categories, table_name: :categorization
```
will create a `categorization` table.
-By default, `create_join_table` will create two columns with no options, but you can specify these
-options using the `:column_options` option. For example,
+`create_join_table` also accepts a block, which you can use to add indices
+(which are not created by default) or additional columns:
```ruby
-create_join_table :products, :categories, :column_options => {:null => true}
+create_join_table :products, :categories do |t|
+ t.index :product_id
+ t.index :category_id
+end
```
-will create the `product_id` and `category_id` with the `:null` option as `true`.
-
### Changing Tables
A close cousin of `create_table` is `change_table`, used for changing existing
-tables. It is used in a similar fashion to `create_table` but the object yielded
-to the block knows more tricks. For example
+tables. It is used in a similar fashion to `create_table` but the object
+yielded to the block knows more tricks. For example:
```ruby
change_table :products do |t|
@@ -485,71 +414,19 @@ 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.
-### Special Helpers
-
-Active Record provides some shortcuts for common functionality. It is for
-example very common to add both the `created_at` and `updated_at` columns and so
-there is a method that does exactly that:
-
-```ruby
-create_table :products do |t|
- t.timestamps
-end
-```
-
-will create a new products table with those two columns (plus the `id` column)
-whereas
+### When Helpers aren't Enough
-```ruby
-change_table :products do |t|
- t.timestamps
-end
-```
-adds those columns to an existing table.
-
-Another helper is called `references` (also available as `belongs_to`). In its
-simplest form it just adds some readability.
-
-```ruby
-create_table :products do |t|
- t.references :category
-end
-```
-
-will create a `category_id` column of the appropriate type. Note that you pass
-the model name, not the column name. Active Record adds the `_id` for you. If
-you have polymorphic `belongs_to` associations then `references` will add both
-of the columns required:
-
-```ruby
-create_table :products do |t|
- t.references :attachment, :polymorphic => {:default => 'Photo'}
-end
-```
-
-will add an `attachment_id` column and a string `attachment_type` column with
-a default value of 'Photo'. `references` also allows you to define an
-index directly, instead of using `add_index` after the `create_table` call:
+If the helpers provided by Active Record aren't enough you can use the `execute`
+method to execute arbitrary SQL:
```ruby
-create_table :products do |t|
- t.references :category, :index => true
-end
+Products.connection.execute('UPDATE `products` SET `price`=`free` WHERE 1')
```
-will create an index identical to calling `add_index :products, :category_id`.
-
-NOTE: The `references` helper does not actually create foreign key constraints
-for you. You will need to use `execute` or a plugin that adds [foreign key
-support](#active-record-and-referential-integrity).
-
-If the helpers provided by Active Record aren't enough you can use the `execute`
-method to execute arbitrary SQL.
-
For more details and examples of individual methods, check the API documentation.
In particular the documentation for
[`ActiveRecord::ConnectionAdapters::SchemaStatements`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/SchemaStatements.html)
-(which provides the methods available in the `up` and `down` methods),
+(which provides the methods available in the `change`, `up` and `down` methods),
[`ActiveRecord::ConnectionAdapters::TableDefinition`](http://api.rubyonrails.org/classes/ActiveRecord/ConnectionAdapters/TableDefinition.html)
(which provides the methods available on the object yielded by `create_table`)
and
@@ -558,30 +435,89 @@ and
### Using the `change` Method
-The `change` method removes the need to write both `up` and `down` methods in
-those cases that Rails knows how to revert the changes automatically. Currently,
-the `change` method supports only these migration definitions:
+The `change` method is the primary way of writing migrations. It works for the
+majority of cases, where Active Record knows how to reverse the migration
+automatically. Currently, the `change` method supports only these migration
+definitions:
* `add_column`
* `add_index`
+* `add_reference`
* `add_timestamps`
* `create_table`
+* `create_join_table`
+* `drop_table` (must supply a block)
+* `drop_join_table` (must supply a block)
* `remove_timestamps`
* `rename_column`
* `rename_index`
+* `remove_reference`
* `rename_table`
-If you're going to need to use any other methods, you'll have to write the
-`up` and `down` methods instead of using the `change` method.
+`change_table` is also reversible, as long as the block does not call `change`,
+`change_default` or `remove`.
+
+If you're going to need to use any other methods, you should use `reversible`
+or write the `up` and `down` methods instead of using the `change` method.
+
+### Using `reversible`
+
+Complex migrations may require processing that Active Record doesn't know how
+to reverse. You can use `reversible` to specify what to do when running a
+migration what else to do when reverting it. For example:
+
+```ruby
+class ExampleMigration < ActiveRecord::Migration
+ def change
+ create_table :products do |t|
+ t.references :category
+ end
+
+ reversible do |dir|
+ dir.up do
+ #add a foreign key
+ execute <<-SQL
+ ALTER TABLE products
+ ADD CONSTRAINT fk_products_categories
+ FOREIGN KEY (category_id)
+ REFERENCES categories(id)
+ SQL
+ end
+ dir.down do
+ execute <<-SQL
+ ALTER TABLE products
+ DROP FOREIGN KEY fk_products_categories
+ SQL
+ end
+ end
+
+ add_column :users, :home_page_url, :string
+ rename_column :users, :email, :email_address
+ 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.
+
+Sometimes your migration will do something which is just plain irreversible; for
+example, it might destroy some data. In such cases, you can raise
+`ActiveRecord::IrreversibleMigration` in your `down` block. If someone tries
+to revert your migration, an error message will be displayed saying that it
+can't be done.
### Using the `up`/`down` Methods
-The `down` method of your migration should revert the transformations done by
-the `up` method. In other words, the database schema should be unchanged if you
-do an `up` followed by a `down`. For example, if you create a table in the `up`
-method, you should drop it in the `down` method. It is wise to reverse the
-transformations in precisely the reverse order they were made in the `up`
-method. For example,
+You can also use the old style of migration using `up` and `down` methods
+instead of the `change` method.
+The `up` method should describe the transformation you'd like to make to your
+schema, and the `down` method of your migration should revert the
+transformations done by the `up` method. In other words, the database schema
+should be unchanged if you do an `up` followed by a `down`. For example, if you
+create a table in the `up` method, you should drop it in the `down` method. It
+is wise to reverse the transformations in precisely the reverse order they were
+made in the `up` method. The example in the `reversible` section is equivalent to:
```ruby
class ExampleMigration < ActiveRecord::Migration
@@ -589,13 +525,15 @@ class ExampleMigration < ActiveRecord::Migration
create_table :products do |t|
t.references :category
end
- #add a foreign key
+
+ # add a foreign key
execute <<-SQL
ALTER TABLE products
ADD CONSTRAINT fk_products_categories
FOREIGN KEY (category_id)
REFERENCES categories(id)
SQL
+
add_column :users, :home_page_url, :string
rename_column :users, :email, :email_address
end
@@ -603,47 +541,122 @@ class ExampleMigration < ActiveRecord::Migration
def down
rename_column :users, :email_address, :email
remove_column :users, :home_page_url
+
execute <<-SQL
ALTER TABLE products
DROP FOREIGN KEY fk_products_categories
SQL
+
drop_table :products
end
end
```
-Sometimes your migration will do something which is just plain irreversible; for
-example, it might destroy some data. In such cases, you can raise
+If your migration is irreversible, you should raise
`ActiveRecord::IrreversibleMigration` from your `down` method. If someone tries
to revert your migration, an error message will be displayed saying that it
can't be done.
+### Reverting Previous Migrations
+
+You can use Active Record's ability to rollback migrations using the `revert` method:
+
+```ruby
+require_relative '2012121212_example_migration'
+
+class FixupExampleMigration < ActiveRecord::Migration
+ def change
+ revert ExampleMigration
+
+ create_table(:apples) do |t|
+ t.string :variety
+ end
+ end
+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:
+
+```ruby
+class SerializeProductListMigration < 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
+ execute <<-SQL
+ ALTER TABLE products
+ ADD CONSTRAINT fk_products_categories
+ FOREIGN KEY (category_id)
+ REFERENCES categories(id)
+ SQL
+ end
+ dir.down do
+ execute <<-SQL
+ ALTER TABLE products
+ DROP FOREIGN KEY fk_products_categories
+ SQL
+ end
+ end
+
+ # The rest of the migration was ok
+ end
+ end
+end
+```
+
+The same migration could also have been written without using `revert`
+but this would have involved a few more steps: reversing the order
+of `create_table` and `reversible`, replacing `create_table`
+by `drop_table`, and finally replacing `up` by `down` and vice-versa.
+This is all taken care of by `revert`.
+
Running Migrations
------------------
-Rails provides a set of rake tasks to work with migrations which boil down to
-running certain sets of migrations.
+Rails provides a set of Rake tasks to run certain sets of migrations.
-The very first migration related rake task you will use will probably be
-`rake db:migrate`. In its most basic form it just runs the `up` or `change`
+The very first migration related Rake task you will use will probably be
+`rake db:migrate`. In its most basic form it just runs the `change` or `up`
method for all the migrations that have not yet been run. If there are
no such migrations, it exits. It will run these migrations in order based
on the date of the migration.
Note that running the `db:migrate` also invokes the `db:schema:dump` task, which
-will update your db/schema.rb file to match the structure of your database.
+will update your `db/schema.rb` file to match the structure of your database.
If you specify a target version, Active Record will run the required migrations
-(up, down or change) until it has reached the specified version. The version
+(change, up, down) until it has reached the specified version. The version
is the numerical prefix on the migration's filename. For example, to migrate
-to version 20080906120000 run
+to version 20080906120000 run:
```bash
$ rake db:migrate VERSION=20080906120000
```
If version 20080906120000 is greater than the current version (i.e., it is
-migrating upwards), this will run the `up` method on all migrations up to and
+migrating upwards), this will run the `change` (or `up`) method
+on all migrations up to and
including 20080906120000, and will not execute any later migrations. If
migrating downwards, this will run the `down` method on all the migrations
down to, but not including, 20080906120000.
@@ -652,24 +665,25 @@ down to, but not including, 20080906120000.
A common task is to rollback the last migration. For example, if you made a
mistake in it and wish to correct it. Rather than tracking down the version
-number associated with the previous migration you can run
+number associated with the previous migration you can run:
```bash
$ rake db:rollback
```
-This will run the `down` method from the latest migration. If you need to undo
+This will rollback the latest migration, either by reverting the `change`
+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
```
-will run the `down` method from the last 3 migrations.
+will revert the last 3 migrations.
The `db:migrate:redo` task is a shortcut for doing a rollback and then migrating
back up again. As with the `db:rollback` task, you can use the `STEP` parameter
-if you need to go more than one version back, for example
+if you need to go more than one version back, for example:
```bash
$ rake db:migrate:redo STEP=3
@@ -679,32 +693,43 @@ Neither of these Rake tasks do anything you could not do with `db:migrate`. They
are simply more convenient, since you do not need to explicitly specify the
version to migrate to.
+### Setup the Database
+
+The `rake db:setup` task will create the database, load the schema and initialize
+it with the seed data.
+
### Resetting the Database
-The `rake db:reset` task will drop the database, recreate it and load the
-current schema into it.
+The `rake db:reset` task will drop the database and set it up again. This is
+functionally equivalent to `rake db:drop db:setup`.
-NOTE: This is not the same as running all the migrations - see the section on
-[schema.rb](#schema-dumping-and-you).
+NOTE: This is not the same as running all the migrations. It will only use the
+contents of the current `schema.rb` file. If a migration can't be rolled back,
+`rake db:reset` may not help you. To find out more about dumping the schema see
+[Schema Dumping and You](#schema-dumping-and-you) section.
### Running Specific Migrations
If you need to run a specific migration up or down, the `db:migrate:up` and
`db:migrate:down` tasks will do that. Just specify the appropriate version and
-the corresponding migration will have its `up` or `down` method invoked, for
-example,
+the corresponding migration will have its `change`, `up` or `down` method
+invoked, for example:
```bash
$ rake db:migrate:up VERSION=20080906120000
```
-will run the `up` method from the 20080906120000 migration. This task will first
-check whether the migration is already performed and will do nothing if Active Record believes
-that it has already been run.
+will run the 20080906120000 migration by running the `change` method (or the
+`up` method). This task will
+first check whether the migration is already performed and will do nothing if
+Active Record believes that it has already been run.
### Running Migrations in Different Environments
-By default running `rake db:migrate` will run in the `development` environment. To run migrations against another environment you can specify it using the `RAILS_ENV` environment variable while running the command. For example to run migrations against the `test` environment you could run:
+By default running `rake db:migrate` will run in the `development` environment.
+To run migrations against another environment you can specify it using the
+`RAILS_ENV` environment variable while running the command. For example to run
+migrations against the `test` environment you could run:
```bash
$ rake db:migrate RAILS_ENV=test
@@ -730,7 +755,7 @@ Several methods are provided in migrations that allow you to control all this:
| say | Takes a message argument and outputs it as is. A second boolean argument can be passed to specify whether to indent or not.
| say_with_time | Outputs text along with how long it took to run its block. If the block returns an integer it assumes it is the number of rows affected.
-For example, this migration
+For example, this migration:
```ruby
class CreateProducts < ActiveRecord::Migration
@@ -742,9 +767,12 @@ class CreateProducts < ActiveRecord::Migration
t.timestamps
end
end
+
say "Created a table"
+
suppress_messages {add_index :products, :name}
say "and an index!", true
+
say_with_time 'Waiting for a while' do
sleep 10
250
@@ -768,11 +796,33 @@ generates the following output
If you want Active Record to not output anything, then running `rake db:migrate
VERBOSE=false` will suppress all output.
+Changing Existing Migrations
+----------------------------
+
+Occasionally you will make a mistake when writing a migration. If you have
+already run the migration then you cannot just edit the migration and run the
+migration again: Rails thinks it has already run the migration and so will do
+nothing when you run `rake db:migrate`. You must rollback the migration (for
+example with `rake db:rollback`), edit your migration and then run
+`rake db:migrate` to run the corrected version.
+
+In general, editing existing migrations is not a good idea. You will be
+creating extra work for yourself and your co-workers and cause major headaches
+if the existing version of the migration has already been run on production
+machines. Instead, you should write a new migration that performs the changes
+you require. Editing a freshly generated migration that has not yet been
+committed to source control (or, more generally, which has not been propagated
+beyond your development machine) is relatively harmless.
+
+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
+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)
@@ -785,8 +835,7 @@ which contains a `Product` model:
Bob goes on vacation.
Alice creates a migration for the `products` table which adds a new column and
-initializes it. She also adds a validation to the `Product` model for the new
-column.
+initializes it:
```ruby
# db/migrate/20100513121110_add_flag_to_product.rb
@@ -794,22 +843,25 @@ column.
class AddFlagToProduct < ActiveRecord::Migration
def change
add_column :products, :flag, :boolean
- Product.update_all :flag => false
+ 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/model/product.rb
+# app/models/product.rb
class Product < ActiveRecord::Base
- validates :flag, :presence => true
+ validates :flag, inclusion: { in: [true, false] }
end
```
-Alice adds a second migration which adds and initializes another column to the
-`products` table and also adds a validation to the `Product` model for the new
-column.
+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
@@ -817,16 +869,21 @@ column.
class AddFuzzToProduct < ActiveRecord::Migration
def change
add_column :products, :fuzz, :string
- Product.update_all :fuzz => 'fuzzy'
+ 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/model/product.rb
+# app/models/product.rb
class Product < ActiveRecord::Base
- validates :flag, :fuzz, :presence => true
+ validates :flag, inclusion: { in: [true, false] }
+ validates :fuzz, presence: true
end
```
@@ -834,8 +891,8 @@ 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.
+* 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.
@@ -850,11 +907,11 @@ 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.
+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 faux model, it's a good idea to call
-`Product.reset_column_information` to refresh the `ActiveRecord` cache for the
+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:
@@ -869,7 +926,9 @@ class AddFlagToProduct < ActiveRecord::Migration
def change
add_column :products, :flag, :boolean
Product.reset_column_information
- Product.update_all :flag => false
+ reversible do |dir|
+ dir.up { Product.update_all flag: false }
+ end
end
end
```
@@ -884,7 +943,9 @@ class AddFuzzToProduct < ActiveRecord::Migration
def change
add_column :products, :fuzz, :string
Product.reset_column_information
- Product.update_all :fuzz => 'fuzzy'
+ reversible do |dir|
+ dir.up { Product.update_all fuzz: 'fuzzy' }
+ end
end
end
```
@@ -892,20 +953,20 @@ 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
+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.
+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
+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.
+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
+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.
@@ -938,12 +999,13 @@ you desire that functionality.
### Types of Schema Dumps
-There are two ways to dump the schema. This is set in `config/application.rb` by
-the `config.active_record.schema_format` setting, which may be either `:sql` or
-`:ruby`.
+There are two ways to dump the schema. This is set in `config/application.rb`
+by the `config.active_record.schema_format` setting, which may be either `:sql`
+or `:ruby`.
If `:ruby` is selected then the schema is stored in `db/schema.rb`. If you look
-at this file you'll find that it looks an awful lot like one very big migration:
+at this file you'll find that it looks an awful lot like one very big
+migration:
```ruby
ActiveRecord::Schema.define(version: 20080906171750) do
@@ -966,8 +1028,8 @@ end
In many ways this is exactly what it is. This file is created by inspecting the
database and expressing its structure using `create_table`, `add_index`, and so
on. Because this is database-independent, it could be loaded into any database
-that Active Record supports. This could be very useful if you were to distribute
-an application that is able to run against multiple databases.
+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
@@ -975,15 +1037,16 @@ 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` Rake task)
-into `db/structure.sql`. For example, for the PostgreSQL RDBMS, the
-`pg_dump` utility is used. For MySQL, this file will contain the output of `SHOW
-CREATE TABLE` for the various tables. Loading these schemas is simply a question
-of executing the SQL statements they contain. By definition, this will create a
-perfect copy of the database's structure. Using the `:sql` schema format will,
-however, prevent loading the schema into a RDBMS other than the one used to
-create it.
+Instead of using Active Record's schema dumper, the database's structure will
+be dumped using a tool specific to the database (via the `db:structure:dump`
+Rake task) into `db/structure.sql`. For example, for PostgreSQL, the `pg_dump`
+utility is used. For MySQL, this file will contain the output of
+`SHOW CREATE TABLE` for the various tables.
+
+Loading these schemas is simply a question of executing the SQL statements they
+contain. By definition, this will create a perfect copy of the database's
+structure. Using the `:sql` schema format will, however, prevent loading the
+schema into a RDBMS other than the one used to create it.
### Schema Dumps and Source Control
@@ -998,15 +1061,48 @@ the database. As such, features such as triggers or foreign key constraints,
which push some of that intelligence back into the database, are not heavily
used.
-Validations such as `validates :foreign_key, :uniqueness => true` are one way in
-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 could
-also use some plugin like [foreigner](https://github.com/matthuhiggins/foreigner)
-which add foreign key support to Active Record (including support for dumping
-foreign keys in `db/schema.rb`).
+Validations such as `validates :foreign_key, uniqueness: true` are one way in
+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`).
+
+Migrations and Seed Data
+------------------------
+
+Some people use migrations to add data to the database:
+
+```ruby
+class AddInitialProducts < ActiveRecord::Migration
+ def up
+ 5.times do |i|
+ Product.create(name: "Product ##{i}", description: "A product.")
+ end
+ end
+
+ def down
+ Product.delete_all
+ end
+end
+```
+
+However, Rails has a 'seeds' feature that should be used for seeding a database
+with initial data. It's a really simple feature: just fill up `db/seeds.rb`
+with some Ruby code, and run `rake db:seed`:
+
+```ruby
+5.times do |i|
+ Product.create(name: "Product ##{i}", description: "A product.")
+end
+```
+
+This is generally a much cleaner way to set up the database of a blank
+application.
diff --git a/guides/source/nested_model_forms.md b/guides/source/nested_model_forms.md
index b5f112e6c9..855fab18e3 100644
--- a/guides/source/nested_model_forms.md
+++ b/guides/source/nested_model_forms.md
@@ -3,13 +3,13 @@ Rails nested model forms
Creating a form for a model _and_ its associations can become quite tedious. Therefore Rails provides helpers to assist in dealing with the complexities of generating these forms _and_ the required CRUD operations to create, update, and destroy associations.
-In this guide you will:
+After reading this guide, you will know:
-* do stuff
+* do stuff.
--------------------------------------------------------------------------------
-NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it’s **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/).
+NOTE: This guide assumes the user knows how to use the [Rails form helpers](form_helpers.html) in general. Also, it's **not** an API reference. For a complete reference please visit [the Rails API documentation](http://api.rubyonrails.org/).
Model setup
@@ -56,7 +56,7 @@ end
### Custom model
-As you might have inflected from this explanation, you _don’t_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behaviour:
+As you might have inflected from this explanation, you _don't_ necessarily need an ActiveRecord::Base model to use this functionality. The following examples are sufficient to enable the nested model form behavior:
#### Single associated object
@@ -98,7 +98,7 @@ A nested model form will _only_ be built if the associated object(s) exist. This
Consider the following typical RESTful controller which will prepare a new Person instance and its `address` and `projects` associations before rendering the `new` template:
```ruby
-class PeopleController < ActionController:Base
+class PeopleController < ApplicationController
def new
@person = Person.new
@person.built_address
@@ -177,7 +177,7 @@ When this form is posted the Rails parameter parser will construct a hash like t
}
```
-That’s it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved.
+That's it. The controller will simply pass this hash on to the model from the `create` action. The model will then handle building the `address` association for you and automatically save it when the parent (`person`) is saved.
#### Nested form for a collection of associated objects
diff --git a/guides/source/performance_testing.md b/guides/source/performance_testing.md
deleted file mode 100644
index f111ce610f..0000000000
--- a/guides/source/performance_testing.md
+++ /dev/null
@@ -1,684 +0,0 @@
-Performance Testing Rails Applications
-======================================
-
-This guide covers the various ways of performance testing a Ruby on Rails
-application. By referring to this guide, you will be able to:
-
-* Understand the various types of benchmarking and profiling metrics.
-* Generate performance and benchmarking tests.
-* Install and use a GC-patched Ruby binary to measure memory usage and object
- allocation.
-* Understand the benchmarking information provided by Rails inside the log files.
-* Learn about various tools facilitating benchmarking and profiling.
-
-Performance testing is an integral part of the development cycle. It is very
-important that you don't make your end users wait for too long before the page
-is completely loaded. Ensuring a pleasant browsing experience for end users and
-cutting the cost of unnecessary hardware is important for any non-trivial web
-application.
-
---------------------------------------------------------------------------------
-
-Performance Test Cases
-----------------------
-
-Rails performance tests are a special type of integration tests, designed for
-benchmarking and profiling the test code. With performance tests, you can
-determine where your application's memory or speed problems are coming from,
-and get a more in-depth picture of those problems.
-
-In a freshly generated Rails application, `test/performance/browsing_test.rb`
-contains an example of a performance test:
-
-```ruby
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { runs: 5, metrics: [:wall_time, :memory],
- # output: 'tmp/performance', formats: [:flat] }
-
- test "homepage" do
- get '/'
- end
-end
-```
-
-This example is a simple performance test case for profiling a GET request to
-the application's homepage.
-
-### Generating Performance Tests
-
-Rails provides a generator called `performance_test` for creating new
-performance tests:
-
-```bash
-$ rails generate performance_test homepage
-```
-
-This generates `homepage_test.rb` in the `test/performance` directory:
-
-```ruby
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class HomepageTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { :runs => 5, :metrics => [:wall_time, :memory],
- # :output => 'tmp/performance', :formats => [:flat] }
-
- test "homepage" do
- get '/'
- end
-end
-```
-
-### Examples
-
-Let's assume your application has the following controller and model:
-
-```ruby
-# routes.rb
-root to: 'home#dashboard'
-resources :posts
-
-# home_controller.rb
-class HomeController < ApplicationController
- def dashboard
- @users = User.last_ten.includes(:avatars)
- @posts = Post.all_today
- end
-end
-
-# posts_controller.rb
-class PostsController < ApplicationController
- def create
- @post = Post.create(params[:post])
- redirect_to(@post)
- end
-end
-
-# post.rb
-class Post < ActiveRecord::Base
- before_save :recalculate_costly_stats
-
- def slow_method
- # I fire gallzilion queries sleeping all around
- end
-
- private
-
- def recalculate_costly_stats
- # CPU heavy calculations
- end
-end
-```
-
-#### Controller Example
-
-Because performance tests are a special kind of integration test, you can use
-the `get` and `post` methods in them.
-
-Here's the performance test for `HomeController#dashboard` and
-`PostsController#create`:
-
-```ruby
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class PostPerformanceTest < ActionDispatch::PerformanceTest
- def setup
- # Application requires logged-in user
- login_as(:lifo)
- end
-
- test "homepage" do
- get '/dashboard'
- end
-
- test "creating new post" do
- post '/posts', post: { body: 'lifo is fooling you' }
- end
-end
-```
-
-You can find more details about the `get` and `post` methods in the
-[Testing Rails Applications](testing.html) guide.
-
-#### Model Example
-
-Even though the performance tests are integration tests and hence closer to
-the request/response cycle by nature, you can still performance test pure model
-code.
-
-Performance test for `Post` model:
-
-```ruby
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class PostModelTest < ActionDispatch::PerformanceTest
- test "creation" do
- Post.create body: 'still fooling you', cost: '100'
- end
-
- test "slow method" do
- # Using posts(:awesome) fixture
- posts(:awesome).slow_method
- end
-end
-```
-
-### Modes
-
-Performance tests can be run in two modes: Benchmarking and Profiling.
-
-#### Benchmarking
-
-Benchmarking makes it easy to quickly gather a few metrics about each test run.
-By default, each test case is run **4 times** in benchmarking mode.
-
-To run performance tests in benchmarking mode:
-
-```bash
-$ rake test:benchmark
-```
-
-#### Profiling
-
-Profiling allows you to make an in-depth analysis of each of your tests by using
-an external profiler. Depending on your Ruby interpreter, this profiler can be
-native (Rubinius, JRuby) or not (MRI, which uses RubyProf). By default, each
-test case is run **once** in profiling mode.
-
-To run performance tests in profiling mode:
-
-```bash
-$ rake test:profile
-```
-
-### Metrics
-
-Benchmarking and profiling run performance tests and give you multiple metrics.
-The availability of each metric is determined by the interpreter being used—none
-of them support all metrics—and by the mode in use. A brief description of each
-metric and their availability across interpreters/modes is given below.
-
-#### Wall Time
-
-Wall time measures the real world time elapsed during the test run. It is
-affected by any other processes concurrently running on the system.
-
-#### Process Time
-
-Process time measures the time taken by the process. It is unaffected by any
-other processes running concurrently on the same system. Hence, process time
-is likely to be constant for any given performance test, irrespective of the
-machine load.
-
-#### CPU Time
-
-Similar to process time, but leverages the more accurate CPU clock counter
-available on the Pentium and PowerPC platforms.
-
-#### User Time
-
-User time measures the amount of time the CPU spent in user-mode, i.e. within
-the process. This is not affected by other processes and by the time it possibly
-spends blocked.
-
-#### Memory
-
-Memory measures the amount of memory used for the performance test case.
-
-#### Objects
-
-Objects measures the number of objects allocated for the performance test case.
-
-#### GC Runs
-
-GC Runs measures the number of times GC was invoked for the performance test case.
-
-#### GC Time
-
-GC Time measures the amount of time spent in GC for the performance test case.
-
-#### Metric Availability
-
-##### Benchmarking
-
-| Interpreter | Wall Time | Process Time | CPU Time | User Time | Memory | Objects | GC Runs | GC Time |
-| ------------ | --------- | ------------ | -------- | --------- | ------ | ------- | ------- | ------- |
-| **MRI** | yes | yes | yes | no | yes | yes | yes | yes |
-| **REE** | yes | yes | yes | no | yes | yes | yes | yes |
-| **Rubinius** | yes | no | no | no | yes | yes | yes | yes |
-| **JRuby** | yes | no | no | yes | yes | yes | yes | yes |
-
-##### Profiling
-
-| Interpreter | Wall Time | Process Time | CPU Time | User Time | Memory | Objects | GC Runs | GC Time |
-| ------------ | --------- | ------------ | -------- | --------- | ------ | ------- | ------- | ------- |
-| **MRI** | yes | yes | no | no | yes | yes | yes | yes |
-| **REE** | yes | yes | no | no | yes | yes | yes | yes |
-| **Rubinius** | yes | no | no | no | no | no | no | no |
-| **JRuby** | yes | no | no | no | no | no | no | no |
-
-NOTE: To profile under JRuby you'll need to run `export JRUBY_OPTS="-Xlaunch.inproc=false --profile.api"`
-**before** the performance tests.
-
-### Understanding the Output
-
-Performance tests generate different outputs inside `tmp/performance` directory
-depending on their mode and metric.
-
-#### Benchmarking
-
-In benchmarking mode, performance tests generate two types of outputs.
-
-##### Command Line
-
-This is the primary form of output in benchmarking mode. Example:
-
-```bash
-BrowsingTest#test_homepage (31 ms warmup)
- wall_time: 6 ms
- memory: 437.27 KB
- objects: 5,514
- gc_runs: 0
- gc_time: 19 ms
-```
-
-##### CSV Files
-
-Performance test results are also appended to `.csv` files inside `tmp/performance`.
-For example, running the default `BrowsingTest#test_homepage` will generate
-following five files:
-
-* BrowsingTest#test_homepage_gc_runs.csv
-* BrowsingTest#test_homepage_gc_time.csv
-* BrowsingTest#test_homepage_memory.csv
-* BrowsingTest#test_homepage_objects.csv
-* BrowsingTest#test_homepage_wall_time.csv
-
-As the results are appended to these files each time the performance tests are
-run in benchmarking mode, you can collect data over a period of time. This can
-be very helpful in analyzing the effects of code changes.
-
-Sample output of `BrowsingTest#test_homepage_wall_time.csv`:
-
-```bash
-measurement,created_at,app,rails,ruby,platform
-0.00738224999999992,2009-01-08T03:40:29Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00755874999999984,2009-01-08T03:46:18Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00762099999999993,2009-01-08T03:49:25Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00603075000000008,2009-01-08T04:03:29Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00619899999999995,2009-01-08T04:03:53Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00755449999999991,2009-01-08T04:04:55Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00595999999999997,2009-01-08T04:05:06Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00740450000000004,2009-01-09T03:54:47Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00603150000000008,2009-01-09T03:54:57Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-0.00771250000000012,2009-01-09T15:46:03Z,,3.0.0,ruby-1.8.7.249,x86_64-linux
-```
-
-#### Profiling
-
-In profiling mode, performance tests can generate multiple types of outputs.
-The command line output is always presented but support for the others is
-dependent on the interpreter in use. A brief description of each type and
-their availability across interpreters is given below.
-
-##### Command Line
-
-This is a very basic form of output in profiling mode:
-
-```bash
-BrowsingTest#test_homepage (58 ms warmup)
- process_time: 63 ms
- memory: 832.13 KB
- objects: 7,882
-```
-
-##### Flat
-
-Flat output shows the metric—time, memory, etc—measure in each method.
-[Check Ruby-Prof documentation for a better explanation](http://ruby-prof.rubyforge.org/files/examples/flat_txt.html).
-
-##### Graph
-
-Graph output shows the metric measure in each method, which methods call it and
-which methods it calls. [Check Ruby-Prof documentation for a better explanation](http://ruby-prof.rubyforge.org/files/examples/graph_txt.html).
-
-##### Tree
-
-Tree output is profiling information in calltree format for use by [kcachegrind](http://kcachegrind.sourceforge.net/html/Home.html)
-and similar tools.
-
-##### Output Availability
-
-| | Flat | Graph | Tree |
-| ------------ | ---- | ----- | ---- |
-| **MRI** | yes | yes | yes |
-| **REE** | yes | yes | yes |
-| **Rubinius** | yes | yes | no |
-| **JRuby** | yes | yes | no |
-
-### Tuning Test Runs
-
-Test runs can be tuned by setting the `profile_options` class variable on your
-test class.
-
-```ruby
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- self.profile_options = { runs: 5, metrics: [:wall_time, :memory] }
-
- test "homepage"
- get '/'
- end
-end
-```
-
-In this example, the test would run 5 times and measure wall time and memory.
-There are a few configurable options:
-
-| Option | Description | Default | Mode |
-| ---------- | ------------------------------------------ | ----------------------------- | --------- |
-| `:runs` | Number of runs. | Benchmarking: 4, Profiling: 1 | Both |
-| `:output` | Directory to use when writing the results. | `tmp/performance` | Both |
-| `:metrics` | Metrics to use. | See below. | Both |
-| `:formats` | Formats to output to. | See below. | Profiling |
-
-Metrics and formats have different defaults depending on the interpreter in use.
-
-| Interpreter | Mode | Default metrics | Default formats |
-| -------------- | ------------ | ------------------------------------------------------- | ----------------------------------------------- |
-| **MRI/REE** | Benchmarking | `[:wall_time, :memory, :objects, :gc_runs, :gc_time]` | N/A |
-| | Profiling | `[:process_time, :memory, :objects]` | `[:flat, :graph_html, :call_tree, :call_stack]` |
-| **Rubinius** | Benchmarking | `[:wall_time, :memory, :objects, :gc_runs, :gc_time]` | N/A |
-| | Profiling | `[:wall_time]` | `[:flat, :graph]` |
-| **JRuby** | Benchmarking | `[:wall_time, :user_time, :memory, :gc_runs, :gc_time]` | N/A |
-| | Profiling | `[:wall_time]` | `[:flat, :graph]` |
-
-As you've probably noticed by now, metrics and formats are specified using a
-symbol array with each name [underscored.](http://api.rubyonrails.org/classes/String.html#method-i-underscore)
-
-### Performance Test Environment
-
-Performance tests are run in the `test` environment. But running performance
-tests will set the following configuration parameters:
-
-```bash
-ActionController::Base.perform_caching = true
-ActiveSupport::Dependencies.mechanism = :require
-Rails.logger.level = ActiveSupport::BufferedLogger::INFO
-```
-
-As `ActionController::Base.perform_caching` is set to `true`, performance tests
-will behave much as they do in the `production` environment.
-
-### Installing GC-Patched MRI
-
-To get the best from Rails' performance tests under MRI, you'll need to build
-a special Ruby binary with some super powers.
-
-The recommended patches for each MRI version are:
-
-| Version | Patch |
-| --------------- | --------- |
-| 1.8.6 | ruby186gc |
-| 1.8.7 | ruby187gc |
-| 1.9.2 and above | gcdata |
-
-All of these can be found on [RVM's _patches_ directory](https://github.com/wayneeseguin/rvm/tree/master/patches/ruby)
-under each specific interpreter version.
-
-Concerning the installation itself, you can either do this easily by using
-[RVM](http://rvm.beginrescueend.com) or you can build everything from source,
-which is a little bit harder.
-
-#### Install Using RVM
-
-The process of installing a patched Ruby interpreter is very easy if you let RVM
-do the hard work. All of the following RVM commands will provide you with a
-patched Ruby interpreter:
-
-```bash
-$ rvm install 1.9.2-p180 --patch gcdata
-$ rvm install 1.8.7 --patch ruby187gc
-$ rvm install 1.9.2-p180 --patch ~/Downloads/downloaded_gcdata_patch.patch
-```
-
-You can even keep your regular interpreter by assigning a name to the patched
-one:
-
-```bash
-$ rvm install 1.9.2-p180 --patch gcdata --name gcdata
-$ rvm use 1.9.2-p180 # your regular ruby
-$ rvm use 1.9.2-p180-gcdata # your patched ruby
-```
-
-And it's done! You have installed a patched Ruby interpreter.
-
-#### Install From Source
-
-This process is a bit more complicated, but straightforward nonetheless. If
-you've never compiled a Ruby binary before, follow these steps to build a
-Ruby binary inside your home directory.
-
-##### Download and Extract
-
-```bash
-$ mkdir rubygc
-$ wget <the version you want from ftp://ftp.ruby-lang.org/pub/ruby>
-$ tar -xzvf <ruby-version.tar.gz>
-$ cd <ruby-version>
-```
-
-##### Apply the Patch
-
-```bash
-$ curl http://github.com/wayneeseguin/rvm/raw/master/patches/ruby/1.9.2/p180/gcdata.patch | patch -p0 # if you're on 1.9.2!
-$ curl http://github.com/wayneeseguin/rvm/raw/master/patches/ruby/1.8.7/ruby187gc.patch | patch -p0 # if you're on 1.8.7!
-```
-
-##### Configure and Install
-
-The following will install Ruby in your home directory's `/rubygc` directory.
-Make sure to replace `<homedir>` with a full patch to your actual home
-directory.
-
-```bash
-$ ./configure --prefix=/<homedir>/rubygc
-$ make && make install
-```
-
-##### Prepare Aliases
-
-For convenience, add the following lines in your `~/.profile`:
-
-```bash
-alias gcruby='~/rubygc/bin/ruby'
-alias gcrake='~/rubygc/bin/rake'
-alias gcgem='~/rubygc/bin/gem'
-alias gcirb='~/rubygc/bin/irb'
-alias gcrails='~/rubygc/bin/rails'
-```
-
-Don't forget to use your aliases from now on.
-
-### Using Ruby-Prof on MRI and REE
-
-Add Ruby-Prof to your applications' Gemfile if you want to benchmark/profile
-under MRI or REE:
-
-```ruby
-gem 'ruby-prof', git: 'git://github.com/wycats/ruby-prof.git'
-```
-
-Now run `bundle install` and you're ready to go.
-
-Command Line Tools
-------------------
-
-Writing performance test cases could be an overkill when you are looking for one
-time tests. Rails ships with two command line tools that enable quick and dirty
-performance testing:
-
-### `benchmarker`
-
-Usage:
-
-```bash
-Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]
- -r, --runs N Number of runs.
- Default: 4
- -o, --output PATH Directory to use when writing the results.
- Default: tmp/performance
- -m, --metrics a,b,c Metrics to use.
- Default: wall_time,memory,objects,gc_runs,gc_time
-```
-
-Example:
-
-```bash
-$ rails benchmarker 'Item.all' 'CouchItem.all' --runs 3 --metrics wall_time,memory
-```
-
-### `profiler`
-
-Usage:
-
-```bash
-Usage: rails profiler 'Ruby.code' 'Ruby.more_code' ... [OPTS]
- -r, --runs N Number of runs.
- Default: 1
- -o, --output PATH Directory to use when writing the results.
- Default: tmp/performance
- --metrics a,b,c Metrics to use.
- Default: process_time,memory,objects
- -m, --formats x,y,z Formats to output to.
- Default: flat,graph_html,call_tree
-```
-
-Example:
-
-```bash
-$ rails profiler 'Item.all' 'CouchItem.all' --runs 2 --metrics process_time --formats flat
-```
-
-NOTE: Metrics and formats vary from interpreter to interpreter. Pass `--help` to
-each tool to see the defaults for your interpreter.
-
-Helper Methods
---------------
-
-Rails provides various helper methods inside Active Record, Action Controller
-and Action View to measure the time taken by a given piece of code. The method
-is called `benchmark()` in all the three components.
-
-### Model
-
-```ruby
-Project.benchmark("Creating project") do
- project = Project.create("name" => "stuff")
- project.create_manager("name" => "David")
- project.milestones << Milestone.all
-end
-```
-
-This benchmarks the code enclosed in the `Project.benchmark("Creating project") do...end`
-block and prints the result to the log file:
-
-```ruby
-Creating project (185.3ms)
-```
-
-Please refer to the [API docs](http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html#method-i-benchmark)
-for additional options to `benchmark()`.
-
-### Controller
-
-Similarly, you could use this helper method inside [controllers.](http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html)
-
-```ruby
-def process_projects
- benchmark("Processing projects") do
- Project.process(params[:project_ids])
- Project.update_cached_projects
- end
-end
-```
-
-NOTE: `benchmark` is a class method inside controllers.
-
-### View
-
-And in [views](http://api.rubyonrails.org/classes/ActiveSupport/Benchmarkable.html:)
-
-```erb
-<% benchmark("Showing projects partial") do %>
- <%= render @projects %>
-<% end %>
-```
-
-Request Logging
----------------
-
-Rails log files contain very useful information about the time taken to serve
-each request. Here's a typical log file entry:
-
-```bash
-Processing ItemsController#index (for 127.0.0.1 at 2009-01-08 03:06:39) [GET]
-Rendering template within layouts/items
-Rendering items/index
-Completed in 5ms (View: 2, DB: 0) | 200 OK [http://0.0.0.0/items]
-```
-
-For this section, we're only interested in the last line:
-
-```bash
-Completed in 5ms (View: 2, DB: 0) | 200 OK [http://0.0.0.0/items]
-```
-
-This data is fairly straightforward to understand. Rails uses millisecond(ms) as
-the metric to measure the time taken. The complete request spent 5 ms inside
-Rails, out of which 2 ms were spent rendering views and none was spent
-communication with the database. It's safe to assume that the remaining 3 ms
-were spent inside the controller.
-
-Michael Koziarski has an [interesting blog post](http://www.therailsway.com/2009/1/6/requests-per-second)
-explaining the importance of using milliseconds as the metric.
-
-Useful Links
-------------
-
-### Rails Plugins and Gems
-
-* [Rails Analyzer](http://rails-analyzer.rubyforge.org)
-* [Rails Footnotes](https://github.com/josevalim/rails-footnotes/tree/master)
-* [Query Reviewer](https://github.com/nesquena/query_reviewer)
-* [MiniProfiler](http://www.miniprofiler.com)
-
-### Generic Tools
-
-* [httperf](http://www.hpl.hp.com/research/linux/httperf/)
-* [ab](http://httpd.apache.org/docs/2.2/programs/ab.html)
-* [JMeter](http://jakarta.apache.org/jmeter/)
-* [kcachegrind](http://kcachegrind.sourceforge.net/html/Home.html)
-
-### Tutorials and Documentation
-
-* [ruby-prof API Documentation](http://ruby-prof.rubyforge.org)
-* [Request Profiling Railscast](http://railscasts.com/episodes/98-request-profiling) - Outdated, but useful for understanding call graphs.
-
-Commercial Products
--------------------
-
-Rails has been lucky to have a few companies dedicated to Rails-specific
-performance tools. A couple of those are:
-
-* [New Relic](http://www.newrelic.com)
-* [Scout](http://scoutapp.com)
diff --git a/guides/source/plugins.md b/guides/source/plugins.md
index 263f5b1351..d0aa2e55a2 100644
--- a/guides/source/plugins.md
+++ b/guides/source/plugins.md
@@ -5,17 +5,17 @@ A Rails plugin is either an extension or a modification of the core framework. P
* a way for developers to share bleeding-edge ideas without hurting the stable code base
* a segmented architecture so that units of code can be fixed or updated on their own release schedule
-* an outlet for the core developers so that they don’t have to include every cool new feature under the sun
+* an outlet for the core developers so that they don't have to include every cool new feature under the sun
-After reading this guide you should be familiar with:
+After reading this guide, you will know:
-* Creating a plugin from scratch
-* Writing and running tests for the plugin
+* How to create a plugin from scratch.
+* How to write and run tests for the plugin.
This guide describes how to build a test-driven plugin that will:
-* Extend core Ruby classes like Hash and String
-* Add methods to ActiveRecord::Base in the tradition of the 'acts_as' plugins
+* Extend core Ruby classes like Hash and String.
+* Add methods to `ActiveRecord::Base` in the tradition of the `acts_as` plugins.
* Give you information about where to put generators in your plugin.
For the purpose of this guide pretend for a moment that you are an avid bird watcher.
@@ -27,19 +27,22 @@ goodness.
Setup
-----
-_"vendored plugins"_ were available in previous versions of Rails, but they are deprecated in
-Rails 3.2, and will not be available in the future.
-
Currently, Rails plugins are built as gems, _gemified plugins_. They can be shared across
different rails applications using RubyGems and Bundler if desired.
### Generate a gemified plugin.
-Rails 3.1 ships with a `rails plugin new` command which creates a
- skeleton for developing any kind of Rails extension with the ability
- to run integration tests using a dummy Rails application. See usage
- and options by asking for help:
+Rails ships with a `rails plugin new` command which creates a
+skeleton for developing any kind of Rails extension with the ability
+to run integration tests using a dummy Rails application. Create your
+plugin with the command:
+
+```bash
+$ rails plugin new yaffle
+```
+
+See usage and options by asking for help:
```bash
$ rails plugin --help
@@ -71,7 +74,7 @@ In this example you will add a method to String named `to_squawk`. To begin, cre
require 'test_helper'
-class CoreExtTest < Test::Unit::TestCase
+class CoreExtTest < ActiveSupport::TestCase
def test_to_squawk_prepends_the_word_squawk
assert_equal "squawk! Hello World", "Hello World".to_squawk
end
@@ -89,7 +92,7 @@ Run `rake` to run the test. This test should fail because we haven't implemented
Great - now you are ready to start development.
-Then in `lib/yaffle.rb` require `lib/core_ext`:
+Then in `lib/yaffle.rb` add `require "yaffle/core_ext"`:
```ruby
# yaffle/lib/yaffle.rb
@@ -129,8 +132,8 @@ $ rails console
Add an "acts_as" Method to Active Record
----------------------------------------
-A common pattern in plugins is to add a method called 'acts_as_something' to models. In this case, you
-want to write a method called 'acts_as_yaffle' that adds a 'squawk' method to your Active Record models.
+A common pattern in plugins is to add a method called `acts_as_something` to models. In this case, you
+want to write a method called `acts_as_yaffle` that adds a `squawk` method to your Active Record models.
To begin, set up your files so that you have:
@@ -139,7 +142,7 @@ To begin, set up your files so that you have:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
end
```
@@ -165,9 +168,9 @@ end
### Add a Class Method
-This plugin will expect that you've added a method to your model named 'last_squawk'. However, the
-plugin users might have already defined a method on their model named 'last_squawk' that they use
-for something else. This plugin will allow the name to be changed by adding a class method called 'yaffle_text_field'.
+This plugin will expect that you've added a method to your model named `last_squawk`. However, the
+plugin users might have already defined a method on their model named `last_squawk` that they use
+for something else. This plugin will allow the name to be changed by adding a class method called `yaffle_text_field`.
To start out, write a failing test that shows the behavior you'd like:
@@ -176,7 +179,7 @@ To start out, write a failing test that shows the behavior you'd like:
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -237,7 +240,7 @@ end
# test/dummy/app/models/wickwall.rb
class Wickwall < ActiveRecord::Base
- acts_as_yaffle :yaffle_text_field => :last_tweet
+ acts_as_yaffle yaffle_text_field: :last_tweet
end
```
@@ -324,7 +327,7 @@ To start out, write a failing test that shows the behavior you'd like:
# yaffle/test/acts_as_yaffle_test.rb
require 'test_helper'
-class ActsAsYaffleTest < Test::Unit::TestCase
+class ActsAsYaffleTest < ActiveSupport::TestCase
def test_a_hickwalls_yaffle_text_field_should_be_last_squawk
assert_equal "last_squawk", Hickwall.yaffle_text_field
@@ -402,7 +405,7 @@ Gem plugins currently in development can easily be shared from any Git repositor
commit the code to a Git repository (like GitHub) and add a line to the Gemfile of the application in question:
```ruby
-gem 'yaffle', :git => 'git://github.com/yaffle_watcher/yaffle.git'
+gem 'yaffle', git: 'git://github.com/yaffle_watcher/yaffle.git'
```
After running `bundle install`, your gem functionality will be available to the application.
diff --git a/guides/source/rails_application_templates.md b/guides/source/rails_application_templates.md
index ee5fbcfd52..711d910184 100644
--- a/guides/source/rails_application_templates.md
+++ b/guides/source/rails_application_templates.md
@@ -3,17 +3,17 @@ Rails Application Templates
Application templates are simple Ruby files containing DSL for adding gems/initializers etc. to your freshly created Rails project or an existing Rails project.
-By referring to this guide, you will be able to:
+After reading this guide, you will know:
-* Use templates to generate/customize Rails applications
-* Write your own reusable application templates using the Rails template API
+* How to use templates to generate/customize Rails applications.
+* How to write your own reusable application templates using the Rails template API.
--------------------------------------------------------------------------------
Usage
-----
-To apply a template, you need to provide the Rails generator with the location of the template you wish to apply, using -m option. This can either be path to a file or a URL.
+To apply a template, you need to provide the Rails generator with the location of the template you wish to apply using the -m option. This can either be a path to a file or a URL.
```bash
$ rails new blog -m ~/template.rb
@@ -30,25 +30,24 @@ $ rake rails:template LOCATION=http://example.com/template.rb
Template API
------------
-Rails templates API is very self explanatory and easy to understand. Here's an example of a typical Rails template:
+The Rails templates API is easy to understand. Here's an example of a typical Rails template:
```ruby
# template.rb
-run "rm public/index.html"
generate(:scaffold, "person name:string")
-route "root :to => 'people#index'"
+route "root to: 'people#index'"
rake("db:migrate")
git :init
-git :add => "."
-git :commit => %Q{ -m 'Initial commit' }
+git add: "."
+git commit: %Q{ -m 'Initial commit' }
```
-The following sections outlines the primary methods provided by the API:
+The following sections outline the primary methods provided by the API:
-### gem(name, options = {})
+### gem(*args)
-Adds a `gem` entry for the supplied gem to the generated application’s `Gemfile`.
+Adds a `gem` entry for the supplied gem to the generated application's `Gemfile`.
For example, if your application depends on the gems `bj` and `nokogiri`:
@@ -67,7 +66,7 @@ bundle install
Wraps gem entries inside a group.
-For example, if you want to load `rspec-rails` only in `development` and `test` group:
+For example, if you want to load `rspec-rails` only in the `development` and `test` groups:
```ruby
gem_group :development, :test do
@@ -85,11 +84,23 @@ For example, if you need to source a gem from "http://code.whytheluckystiff.net"
add_source "http://code.whytheluckystiff.net"
```
+### environment/application(data=nil, options={}, &block)
+
+Adds a line inside the `Application` class for `config/application.rb`.
+
+If `options[:env]` is specified, the line is appended to the corresponding file in `config/environments`.
+
+```ruby
+environment 'config.action_mailer.default_url_options = {host: "http://yourwebsite.example.com"}', env: 'production'
+```
+
+A block can be used in place of the `data` argument.
+
### vendor/lib/file/initializer(filename, data = nil, &block)
-Adds an initializer to the generated application’s `config/initializers` directory.
+Adds an initializer to the generated application's `config/initializers` directory.
-Lets say you like using `Object#not_nil?` and `Object#not_blank?`:
+Let's say you like using `Object#not_nil?` and `Object#not_blank?`:
```ruby
initializer 'bloatlol.rb', <<-CODE
@@ -105,9 +116,9 @@ initializer 'bloatlol.rb', <<-CODE
CODE
```
-Similarly `lib()` creates a file in the `lib/` directory and `vendor()` creates a file in the `vendor/` directory.
+Similarly, `lib()` creates a file in the `lib/` directory and `vendor()` creates a file in the `vendor/` directory.
-There is even `file()`, which accepts a relative path from `Rails.root` and creates all the directories/file needed:
+There is even `file()`, which accepts a relative path from `Rails.root` and creates all the directories/files needed:
```ruby
file 'app/components/foo.rb', <<-CODE
@@ -116,7 +127,7 @@ file 'app/components/foo.rb', <<-CODE
CODE
```
-That’ll create `app/components` directory and put `foo.rb` in there.
+That'll create the `app/components` directory and put `foo.rb` in there.
### rakefile(filename, data = nil, &block)
@@ -136,7 +147,7 @@ end
The above creates `lib/tasks/bootstrap.rake` with a `boot:strap` rake task.
-### generate(what, args)
+### generate(what, *args)
Runs the supplied rails generator with given arguments.
@@ -146,10 +157,10 @@ generate(:scaffold, "person", "name:string", "address:text", "age:number")
### run(command)
-Executes an arbitrary command. Just like the backticks. Let's say you want to remove the `public/index.html` file:
+Executes an arbitrary command. Just like the backticks. Let's say you want to remove the `README.rdoc` file:
```ruby
-run "rm public/index.html"
+run "rm README.rdoc"
```
### rake(command, options = {})
@@ -163,15 +174,15 @@ rake "db:migrate"
You can also run rake tasks with a different Rails environment:
```ruby
-rake "db:migrate", :env => 'production'
+rake "db:migrate", env: 'production'
```
### route(routing_code)
-This adds a routing entry to the `config/routes.rb` file. In above steps, we generated a person scaffold and also removed `public/index.html`. Now to make `PeopleController#index` as the default page for the application:
+Adds a routing entry to the `config/routes.rb` file. In the steps above, we generated a person scaffold and also removed `README.rdoc`. Now, to make `PeopleController#index` the default page for the application:
```ruby
-route "root :to => 'person#index'"
+route "root to: 'person#index'"
```
### inside(dir)
@@ -186,7 +197,7 @@ end
### ask(question)
-`ask()` gives you a chance to get some feedback from the user and use it in your templates. Lets say you want your user to name the new shiny library you’re adding:
+`ask()` gives you a chance to get some feedback from the user and use it in your templates. Let's say you want your user to name the new shiny library you're adding:
```ruby
lib_name = ask("What do you want to call the shiny library ?")
@@ -200,10 +211,10 @@ CODE
### yes?(question) or no?(question)
-These methods let you ask questions from templates and decide the flow based on the user’s answer. Lets say you want to freeze rails only if the user want to:
+These methods let you ask questions from templates and decide the flow based on the user's answer. Let's say you want to freeze rails only if the user wants to:
```ruby
-rake("rails:freeze:gems") if yes?("Freeze rails gems ?")
+rake("rails:freeze:gems") if yes?("Freeze rails gems?")
# no?(question) acts just the opposite.
```
@@ -213,6 +224,25 @@ Rails templates let you run any git command:
```ruby
git :init
-git :add => "."
-git :commit => "-a -m 'Initial commit'"
+git add: "."
+git commit: "-a -m 'Initial commit'"
+```
+
+Advanced Usage
+--------------
+
+The application template is evaluated in the context of a
+`Rails::Generators::AppGenerator` instance. It uses the `apply` action
+provided by
+[Thor](https://github.com/erikhuda/thor/blob/master/lib/thor/actions.rb#L207).
+This means you can extend and change the instance to match your needs.
+
+For example by overwriting the `source_paths` method to contain the
+location of your template. Now methods like `copy_file` will accept
+relative paths to your template's location.
+
+```ruby
+def source_paths
+ [File.expand_path(File.dirname(__FILE__))]
+end
```
diff --git a/guides/source/rails_on_rack.md b/guides/source/rails_on_rack.md
index ba96c0c0a9..d53e0cd2bd 100644
--- a/guides/source/rails_on_rack.md
+++ b/guides/source/rails_on_rack.md
@@ -1,12 +1,13 @@
Rails on Rack
=============
-This guide covers Rails integration with Rack and interfacing with other Rack components. By referring to this guide, you will be able to:
+This guide covers Rails integration with Rack and interfacing with other Rack components.
-* Create Rails Metal applications
-* Use Rack Middlewares in your Rails applications
-* Understand Action Pack's internal Middleware stack
-* Define a custom Middleware stack
+After reading this guide, you will know:
+
+* How to use Rack Middlewares in your Rails applications.
+* Action Pack's internal Middleware stack.
+* How to define a custom Middleware stack.
--------------------------------------------------------------------------------
@@ -15,7 +16,7 @@ WARNING: This guide assumes a working knowledge of Rack protocol and Rack concep
Introduction to Rack
--------------------
-bq. Rack provides a minimal, modular and adaptable interface for developing web applications in Ruby. By wrapping HTTP requests and responses in the simplest way possible, it unifies and distills the API for web servers, web frameworks, and software in between (the so-called middleware) into a single method call.
+Rack 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/)
@@ -26,7 +27,10 @@ Rails on Rack
### Rails Application's Rack Object
-`ApplicationName::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.
+`ApplicationName::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 server`
@@ -35,11 +39,11 @@ Rails on Rack
Here's how `rails server` creates an instance of `Rack::Server`
```ruby
-Rails::Server.new.tap { |server|
+Rails::Server.new.tap do |server|
require APP_PATH
Dir.chdir(Rails.application.root)
server.start
-}
+end
```
The `Rails::Server` inherits from `Rack::Server` and calls the `Rack::Server#start` method this way:
@@ -58,7 +62,7 @@ Here's how it loads the middlewares:
```ruby
def middleware
middlewares = []
- middlewares << [Rails::Rack::Debugger] if options[:debugger]
+ middlewares << [Rails::Rack::Debugger] if options[:debugger]
middlewares << [::Rack::ContentLength]
Hash.new(middlewares)
end
@@ -77,11 +81,11 @@ To use `rackup` instead of Rails' `rails server`, you can put the following insi
```ruby
# Rails.root/config.ru
-require "config/environment"
+require ::File.expand_path('../config/environment', __FILE__)
use Rack::Debugger
use Rack::ContentLength
-run ApplicationName::Application
+run Rails.application
```
And start the server:
@@ -99,9 +103,9 @@ $ rackup --help
Action Dispatcher Middleware Stack
----------------------------------
-Many of Action Dispatchers's internal components are implemented as Rack middlewares. `Rails::Application` uses `ActionDispatch::MiddlewareStack` to combine various internal and external middlewares to form a complete Rails Rack application.
+Many of Action Dispatcher's internal components are implemented as Rack middlewares. `Rails::Application` uses `ActionDispatch::MiddlewareStack` to combine various internal and external middlewares to form a complete Rails Rack application.
-NOTE: `ActionDispatch::MiddlewareStack` is Rails' equivalent of `Rack::Builder`, but built for better flexibility and more features to meet Rails' requirements.
+NOTE: `ActionDispatch::MiddlewareStack` is Rails equivalent of `Rack::Builder`, but built for better flexibility and more features to meet Rails' requirements.
### Inspecting Middleware Stack
@@ -114,6 +118,7 @@ $ rake middleware
For a freshly generated Rails application, this might produce something like:
```ruby
+use Rack::Sendfile
use ActionDispatch::Static
use Rack::Lock
use #<ActiveSupport::Cache::Strategy::LocalCache::Middleware:0x000000029a0838>
@@ -126,17 +131,17 @@ use ActionDispatch::DebugExceptions
use ActionDispatch::RemoteIp
use ActionDispatch::Reloader
use ActionDispatch::Callbacks
+use ActiveRecord::Migration::CheckPending
use ActiveRecord::ConnectionAdapters::ConnectionManagement
use ActiveRecord::QueryCache
use ActionDispatch::Cookies
use ActionDispatch::Session::CookieStore
use ActionDispatch::Flash
use ActionDispatch::ParamsParser
-use ActionDispatch::Head
+use Rack::Head
use Rack::ConditionalGet
use Rack::ETag
-use ActionDispatch::BestStandardsSupport
-run ApplicationName::Application.routes
+run MyApp::Application.routes
```
Purpose of each of this middlewares is explained in the [Internal Middlewares](#internal-middleware-stack) section.
@@ -213,7 +218,6 @@ And to remove browser related middleware,
```ruby
# config/application.rb
-config.middleware.delete "ActionDispatch::BestStandardsSupport"
config.middleware.delete "Rack::MethodOverride"
```
@@ -221,13 +225,17 @@ config.middleware.delete "Rack::MethodOverride"
Much of Action Controller's functionality is implemented as Middlewares. The following list explains the purpose of each of them:
+ **`Rack::Sendfile`**
+
+* Sets server specific X-Sendfile header. Configure this via `config.action_dispatch.x_sendfile_header` option.
+
**`ActionDispatch::Static`**
-* Used to serve static assets. Disabled if `config.serve_static_assets` is true.
+* Used to serve static assets. Disabled if `config.serve_static_assets` is `false`.
**`Rack::Lock`**
-* Sets `env["rack.multithread"]` flag to `true` and wraps the application within a Mutex.
+* Sets `env["rack.multithread"]` flag to `false` and wraps the application within a Mutex.
**`ActiveSupport::Cache::Strategy::LocalCache::Middleware`**
@@ -269,6 +277,10 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Runs the prepare callbacks before serving the request.
+ **`ActiveRecord::Migration::CheckPending`**
+
+* Checks pending migrations and raises `ActiveRecord::PendingMigrationError` if any migrations are pending.
+
**`ActiveRecord::ConnectionAdapters::ConnectionManagement`**
* Cleans active connections after each request, unless the `rack.test` key in the request environment is set to `true`.
@@ -305,10 +317,6 @@ Much of Action Controller's functionality is implemented as Middlewares. The fol
* Adds ETag header on all String bodies. ETags are used to validate cache.
- **`ActionDispatch::BestStandardsSupport`**
-
-* Enables “best standards support†so that IE8 renders some elements correctly.
-
TIP: It's possible to use any of the above middlewares in your custom Rack stack.
### Using Rack Builder
@@ -322,13 +330,13 @@ The following shows how to replace use `Rack::Builder` instead of the Rails supp
config.middleware.clear
```
-<br />
+<br>
<strong>Add a `config.ru` file to `Rails.root`</strong>
```ruby
# config.ru
use MyOwnStackFromScratch
-run ApplicationName::Application
+run Rails.application
```
Resources
@@ -336,7 +344,7 @@ Resources
### Learning Rack
-* [Official Rack Website](http://rack.github.com)
+* [Official Rack Website](http://rack.github.io)
* [Introducing Rack](http://chneukirchen.org/blog/archive/2007/02/introducing-rack.html)
* [Ruby on Rack #1 - Hello Rack!](http://m.onkey.org/ruby-on-rack-1-hello-rack)
* [Ruby on Rack #2 - The Builder](http://m.onkey.org/ruby-on-rack-2-the-builder)
diff --git a/guides/source/routing.md b/guides/source/routing.md
index 469fcf49fb..19784823f7 100644
--- a/guides/source/routing.md
+++ b/guides/source/routing.md
@@ -1,13 +1,15 @@
Rails Routing from the Outside In
=================================
-This guide covers the user-facing features of Rails routing. By referring to this guide, you will be able to:
+This guide covers the user-facing features of Rails routing.
-* Understand the code in `routes.rb`
-* Construct your own routes, using either the preferred resourceful style or the `match` method
-* Identify what parameters to expect an action to receive
-* Automatically create paths and URLs using route helpers
-* Use advanced techniques such as constraints and Rack endpoints
+After reading this guide, you will know:
+
+* How to interpret the code in `routes.rb`.
+* How to construct your own routes, using either the preferred resourceful style or the `match` method.
+* What parameters to expect an action to receive.
+* How to automatically create paths and URLs using route helpers.
+* Advanced techniques such as constraints and Rack endpoints.
--------------------------------------------------------------------------------
@@ -18,39 +20,41 @@ The Rails router recognizes URLs and dispatches them to a controller's action. I
### Connecting URLs to Code
-When your Rails application receives an incoming request
+When your Rails application receives an incoming request for:
```
GET /patients/17
```
-it asks the router to match it to a controller action. If the first matching route is
+it asks the router to match it to a controller action. If the first matching route is:
```ruby
-get "/patients/:id" => "patients#show"
+get '/patients/:id', to: 'patients#show'
```
-the request is dispatched to the `patients` controller's `show` action with `{ id: "17" }` in `params`.
+the request is dispatched to the `patients` controller's `show` action with `{ id: '17' }` in `params`.
### Generating Paths and URLs from Code
-You can also generate paths and URLs. If the route above is modified to be
+You can also generate paths and URLs. If the route above is modified to be:
```ruby
-get "/patients/:id" => "patients#show", as: "patient"
+get '/patients/:id', to: 'patients#show', as: 'patient'
```
-If your application contains this code:
+and your application contains this code in the controller:
```ruby
@patient = Patient.find(17)
```
+and this in the corresponding view:
+
```erb
-<%= link_to "Patient Record", patient_path(@patient) %>
+<%= link_to 'Patient Record', patient_path(@patient) %>
```
-The router will generate the path `/patients/17`. This reduces the brittleness of your view and makes your code easier to understand. Note that the id does not need to be specified in the route helper.
+then the router will generate the path `/patients/17`. This reduces the brittleness of your view and makes your code easier to understand. Note that the id does not need to be specified in the route helper.
Resource Routing: the Rails Default
-----------------------------------
@@ -61,23 +65,23 @@ Resource routing allows you to quickly declare all of the common routes for a gi
Browsers request pages from Rails by making a request for a URL using a specific HTTP method, such as `GET`, `POST`, `PATCH`, `PUT` and `DELETE`. Each method is a request to perform an operation on the resource. A resource route maps a number of related requests to actions in a single controller.
-When your Rails application receives an incoming request for
+When your Rails application receives an incoming request for:
```
DELETE /photos/17
```
-it asks the router to map it to a controller action. If the first matching route is
+it asks the router to map it to a controller action. If the first matching route is:
```ruby
resources :photos
```
-Rails would dispatch that request to the `destroy` method on the `photos` controller with `{ id: "17" }` in `params`.
+Rails would dispatch that request to the `destroy` method on the `photos` controller with `{ id: '17' }` in `params`.
### CRUD, Verbs, and Actions
-In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as
+In Rails, a resourceful route provides a mapping between HTTP verbs and URLs to controller actions. By convention, each action also maps to particular CRUD operations in a database. A single entry in the routing file, such as:
```ruby
resources :photos
@@ -85,19 +89,21 @@ resources :photos
creates seven different routes in your application, all mapping to the `Photos` controller:
-| HTTP Verb | Path | action | used for |
-| --------- | ---------------- | ------- | -------------------------------------------- |
-| GET | /photos | index | display a list of all photos |
-| GET | /photos/new | new | return an HTML form for creating a new photo |
-| POST | /photos | create | create a new photo |
-| GET | /photos/:id | show | display a specific photo |
-| GET | /photos/:id/edit | edit | return an HTML form for editing a photo |
-| PATCH/PUT | /photos/:id | update | update a specific photo |
-| DELETE | /photos/:id | destroy | delete a specific photo |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ---------------- | ----------------- | -------------------------------------------- |
+| GET | /photos | photos#index | display a list of all photos |
+| GET | /photos/new | photos#new | return an HTML form for creating a new photo |
+| POST | /photos | photos#create | create a new photo |
+| GET | /photos/:id | photos#show | display a specific photo |
+| GET | /photos/:id/edit | photos#edit | return an HTML form for editing a photo |
+| PATCH/PUT | /photos/:id | photos#update | update a specific photo |
+| DELETE | /photos/:id | photos#destroy | delete a specific photo |
+
+NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions.
NOTE: Rails routes are matched in the order they are specified, so if you have a `resources :photos` above a `get 'photos/poll'` the `show` action's route for the `resources` line will be matched before the `get` line. To fix this, move the `get` line **above** the `resources` line so that it is matched first.
-### Paths and URLs
+### Path and URL Helpers
Creating a resourceful route will also expose a number of helpers to the controllers in your application. In the case of `resources :photos`:
@@ -108,8 +114,6 @@ Creating a resourceful route will also expose a number of helpers to the control
Each of these helpers has a corresponding `_url` helper (such as `photos_url`) which returns the same path prefixed with the current host, port and path prefix.
-NOTE: Because the router uses the HTTP verb and URL to match inbound requests, four URLs map to seven different actions.
-
### Defining Multiple Resources at the Same Time
If you need to create routes for more than one resource, you can save a bit of typing by defining them all with a single call to `resources`:
@@ -118,7 +122,7 @@ If you need to create routes for more than one resource, you can save a bit of t
resources :photos, :books, :videos
```
-This works exactly the same as
+This works exactly the same as:
```ruby
resources :photos
@@ -128,13 +132,19 @@ resources :videos
### Singular Resources
-Sometimes, you have a resource that clients always look up without referencing an ID. For example, you would like `/profile` to always show the profile of the currently logged in user. In this case, you can use a singular resource to map `/profile` (rather than `/profile/:id`) to the `show` action.
+Sometimes, you have a resource that clients always look up without referencing an ID. For example, you would like `/profile` to always show the profile of the currently logged in user. In this case, you can use a singular resource to map `/profile` (rather than `/profile/:id`) to the `show` action:
```ruby
-get "profile" => "users#show"
+get 'profile', to: 'users#show'
```
-This resourceful route
+Passing a `String` to `match` will expect a `controller#action` format, while passing a `Symbol` will map directly to an action:
+
+```ruby
+get 'profile', to: :show
+```
+
+This resourceful route:
```ruby
resource :geocoder
@@ -142,16 +152,16 @@ resource :geocoder
creates six different routes in your application, all mapping to the `Geocoders` controller:
-| HTTP Verb | Path | action | used for |
-| --------- | -------------- | ------- | --------------------------------------------- |
-| GET | /geocoder/new | new | return an HTML form for creating the geocoder |
-| POST | /geocoder | create | create the new geocoder |
-| GET | /geocoder | show | display the one and only geocoder resource |
-| GET | /geocoder/edit | edit | return an HTML form for editing the geocoder |
-| PATCH/PUT | /geocoder | update | update the one and only geocoder resource |
-| DELETE | /geocoder | destroy | delete the geocoder resource |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | -------------- | ----------------- | --------------------------------------------- |
+| GET | /geocoder/new | geocoders#new | return an HTML form for creating the geocoder |
+| POST | /geocoder | geocoders#create | create the new geocoder |
+| GET | /geocoder | geocoders#show | display the one and only geocoder resource |
+| GET | /geocoder/edit | geocoders#edit | return an HTML form for editing the geocoder |
+| PATCH/PUT | /geocoder | geocoders#update | update the one and only geocoder resource |
+| DELETE | /geocoder | geocoders#destroy | delete the geocoder resource |
-NOTE: Because you might want to use the same controller for a singular route (`/account`) and a plural route (`/accounts/45`), singular resources map to plural controllers.
+NOTE: Because you might want to use the same controller for a singular route (`/account`) and a plural route (`/accounts/45`), singular resources map to plural controllers. So that, for example, `resource :photo` and `resources :photos` creates both singular and plural routes that map to the same controller (`PhotosController`).
A singular resourceful route generates these helpers:
@@ -161,6 +171,12 @@ A singular resourceful route generates these helpers:
As with plural resources, the same helpers ending in `_url` will also include the host, port and path prefix.
+WARNING: A [long-standing bug](https://github.com/rails/rails/issues/1769) prevents `form_for` from working automatically with singular resources. As a workaround, specify the URL for the form directly, like so:
+
+```ruby
+form_for @geocoder, url: geocoder_path do |f|
+```
+
### Controller Namespaces and Routing
You may wish to organize groups of controllers under a namespace. Most commonly, you might group a number of administrative controllers under an `Admin::` namespace. You would place these controllers under the `app/controllers/admin` directory, and you can group them together in your router:
@@ -173,55 +189,55 @@ end
This will create a number of routes for each of the `posts` and `comments` controller. For `Admin::PostsController`, Rails will create:
-| HTTP Verb | Path | action | used for |
-| --------- | --------------------- | ------- | ------------------------- |
-| GET | /admin/posts | index | admin_posts_path |
-| GET | /admin/posts/new | new | new_admin_post_path |
-| POST | /admin/posts | create | admin_posts_path |
-| GET | /admin/posts/:id | show | admin_post_path(:id) |
-| GET | /admin/posts/:id/edit | edit | edit_admin_post_path(:id) |
-| PATCH/PUT | /admin/posts/:id | update | admin_post_path(:id) |
-| DELETE | /admin/posts/:id | destroy | admin_post_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | --------------------- | ------------------- | ------------------------- |
+| GET | /admin/posts | admin/posts#index | admin_posts_path |
+| GET | /admin/posts/new | admin/posts#new | new_admin_post_path |
+| POST | /admin/posts | admin/posts#create | admin_posts_path |
+| GET | /admin/posts/:id | admin/posts#show | admin_post_path(:id) |
+| GET | /admin/posts/:id/edit | admin/posts#edit | edit_admin_post_path(:id) |
+| PATCH/PUT | /admin/posts/:id | admin/posts#update | admin_post_path(:id) |
+| DELETE | /admin/posts/:id | admin/posts#destroy | admin_post_path(:id) |
-If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use
+If you want to route `/posts` (without the prefix `/admin`) to `Admin::PostsController`, you could use:
```ruby
-scope module: "admin" do
+scope module: 'admin' do
resources :posts, :comments
end
```
-or, for a single case
+or, for a single case:
```ruby
-resources :posts, module: "admin"
+resources :posts, 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/posts` to `PostsController` (without the `Admin::` module prefix), you could use:
```ruby
-scope "/admin" do
+scope '/admin' do
resources :posts, :comments
end
```
-or, for a single case
+or, for a single case:
```ruby
-resources :posts, path: "/admin/posts"
+resources :posts, path: '/admin/posts'
```
In each of these cases, the named routes remain the same as if you did not use `scope`. In the last case, the following paths map to `PostsController`:
-| HTTP Verb | Path | action | named helper |
-| --------- | --------------------- | ------- | ------------------- |
-| GET | /admin/posts | index | posts_path |
-| GET | /admin/posts/new | new | new_post_path |
-| POST | /admin/posts | create | posts_path |
-| GET | /admin/posts/:id | show | post_path(:id) |
-| GET | /admin/posts/:id/edit | edit | edit_post_path(:id) |
-| PATCH/PUT | /admin/posts/:id | update | post_path(:id) |
-| DELETE | /admin/posts/:id | destroy | post_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | --------------------- | ----------------- | ------------------- |
+| GET | /admin/posts | posts#index | posts_path |
+| GET | /admin/posts/new | posts#new | new_post_path |
+| POST | /admin/posts | posts#create | posts_path |
+| GET | /admin/posts/:id | posts#show | post_path(:id) |
+| GET | /admin/posts/:id/edit | posts#edit | edit_post_path(:id) |
+| PATCH/PUT | /admin/posts/:id | posts#update | post_path(:id) |
+| DELETE | /admin/posts/:id | posts#destroy | post_path(:id) |
### Nested Resources
@@ -247,15 +263,15 @@ end
In addition to the routes for magazines, this declaration will also route ads to an `AdsController`. The ad URLs require a magazine:
-| HTTP Verb | Path | action | used for |
-| --------- | ------------------------------------ | ------- | -------------------------------------------------------------------------- |
-| GET | /magazines/:magazine_id/ads | index | display a list of all ads for a specific magazine |
-| GET | /magazines/:magazine_id/ads/new | new | return an HTML form for creating a new ad belonging to a specific magazine |
-| POST | /magazines/:magazine_id/ads | create | create a new ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id | show | display a specific ad belonging to a specific magazine |
-| GET | /magazines/:magazine_id/ads/:id/edit | edit | return an HTML form for editing an ad belonging to a specific magazine |
-| PATCH/PUT | /magazines/:magazine_id/ads/:id | update | update a specific ad belonging to a specific magazine |
-| DELETE | /magazines/:magazine_id/ads/:id | destroy | delete a specific ad belonging to a specific magazine |
+| HTTP Verb | Path | Controller#Action | Used for |
+| --------- | ------------------------------------ | ----------------- | -------------------------------------------------------------------------- |
+| GET | /magazines/:magazine_id/ads | ads#index | display a list of all ads for a specific magazine |
+| GET | /magazines/:magazine_id/ads/new | ads#new | return an HTML form for creating a new ad belonging to a specific magazine |
+| POST | /magazines/:magazine_id/ads | ads#create | create a new ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id | ads#show | display a specific ad belonging to a specific magazine |
+| GET | /magazines/:magazine_id/ads/:id/edit | ads#edit | return an HTML form for editing an ad belonging to a specific magazine |
+| PATCH/PUT | /magazines/:magazine_id/ads/:id | ads#update | update a specific ad belonging to a specific magazine |
+| DELETE | /magazines/:magazine_id/ads/:id | ads#destroy | delete a specific ad belonging to a specific magazine |
This will also create routing helpers such as `magazine_ads_url` and `edit_magazine_ad_path`. These helpers take an instance of Magazine as the first parameter (`magazine_ads_url(@magazine)`).
@@ -271,7 +287,7 @@ resources :publishers do
end
```
-Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize paths such as
+Deeply-nested resources quickly become cumbersome. In this case, for example, the application would recognize paths such as:
```
/publishers/1/magazines/2/photos/3
@@ -281,9 +297,94 @@ The corresponding route helper would be `publisher_magazine_photo_url`, requirin
TIP: _Resources should never be nested more than 1 level deep._
+#### Shallow Nesting
+
+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 :comments, only: [:index, :new, :create]
+end
+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 :comments, shallow: true
+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 :comments
+ resources :quotes
+ resources :drafts
+end
+```
+
+The `shallow` method of the DSL creates a scope inside of which every nesting is shallow. This generates the same routes as the previous example:
+
+```ruby
+shallow do
+ resources :posts do
+ resources :comments
+ resources :quotes
+ resources :drafts
+ end
+end
+```
+
+There exists two options for `scope` to customize shallow routes. `:shallow_path` prefixes member paths with the specified parameter:
+
+```ruby
+scope shallow_path: "sekret" do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+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 |
+
+The `:shallow_prefix` option adds the specified parameter to the named helpers:
+
+```ruby
+scope shallow_prefix: "sekret" do
+ resources :posts do
+ resources :comments, shallow: true
+ end
+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 |
+
### Routing concerns
-Routing Concerns allows you to declare common routes that can be reused inside others resources and routes.
+Routing Concerns allows you to declare common routes that can be reused inside others resources and routes. To define a concern:
```ruby
concern :commentable do
@@ -295,7 +396,7 @@ concern :image_attachable do
end
```
-These concerns can be used in resources to avoid code duplication and share behavior across routes.
+These concerns can be used in resources to avoid code duplication and share behavior across routes:
```ruby
resources :messages, concerns: :commentable
@@ -303,6 +404,19 @@ resources :messages, concerns: :commentable
resources :posts, concerns: [:commentable, :image_attachable]
```
+The above is equivalent to:
+
+```ruby
+resources :messages do
+ resources :comments
+end
+
+resources :posts do
+ resources :comments
+ resources :images, only: :index
+end
+```
+
Also you can use them in any place that you want inside the routes, for example in a scope or namespace call:
```ruby
@@ -321,34 +435,34 @@ resources :magazines do
end
```
-When using `magazine_ad_path`, you can pass in instances of `Magazine` and `Ad` instead of the numeric IDs.
+When using `magazine_ad_path`, you can pass in instances of `Magazine` and `Ad` instead of the numeric IDs:
```erb
-<%= link_to "Ad details", magazine_ad_path(@magazine, @ad) %>
+<%= link_to 'Ad details', magazine_ad_path(@magazine, @ad) %>
```
You can also use `url_for` with a set of objects, and Rails will automatically determine which route you want:
```erb
-<%= link_to "Ad details", url_for([@magazine, @ad]) %>
+<%= link_to 'Ad details', url_for([@magazine, @ad]) %>
```
In this case, Rails will see that `@magazine` is a `Magazine` and `@ad` is an `Ad` and will therefore use the `magazine_ad_path` helper. In helpers like `link_to`, you can specify just the object in place of the full `url_for` call:
```erb
-<%= link_to "Ad details", [@magazine, @ad] %>
+<%= link_to 'Ad details', [@magazine, @ad] %>
```
If you wanted to link to just a magazine:
```erb
-<%= link_to "Magazine details", @magazine %>
+<%= link_to 'Magazine details', @magazine %>
```
For other actions, you just need to insert the action name as the first element of the array:
```erb
-<%= link_to "Edit Ad", [:edit, @magazine, @ad] %>
+<%= link_to 'Edit Ad', [:edit, @magazine, @ad] %>
```
This allows you to treat instances of your models as URLs, and is a key advantage to using the resourceful style.
@@ -369,9 +483,12 @@ resources :photos do
end
```
-This will recognize `/photos/1/preview` with GET, and route to the `preview` action of `PhotosController`. It will also create the `preview_photo_url` and `preview_photo_path` helpers.
+This will recognize `/photos/1/preview` with GET, and route to the `preview` action of `PhotosController`, with the resource id value passed in `params[:id]`. It will also create the `preview_photo_url` and `preview_photo_path` helpers.
-Within the block of member routes, each route name specifies the HTTP verb that it will recognize. You can use `get`, `patch`, `put`, `post`, or `delete` here. If you don't have multiple `member` routes, you can also pass `:on` to a route, eliminating the block:
+Within the block of member routes, each route name specifies the HTTP verb
+will be recognized. You can use `get`, `patch`, `put`, `post`, or `delete` here
+. If you don't have multiple `member` routes, you can also pass `:on` to a
+route, eliminating the block:
```ruby
resources :photos do
@@ -379,6 +496,8 @@ resources :photos do
end
```
+You can leave out the `:on` option, this will create the same member route except that the resource id value will be available in `params[:photo_id]` instead of `params[:id]`.
+
#### Adding Collection Routes
To add a route to the collection:
@@ -413,9 +532,7 @@ end
This will enable Rails to recognize paths such as `/comments/new/preview` with GET, and route to the `preview` action of `CommentsController`. It will also create the `preview_new_comment_url` and `preview_new_comment_path` route helpers.
-#### A Note of Caution
-
-If you find yourself adding many extra actions to a resourceful route, it's time to stop and ask yourself whether you're disguising the presence of another resource.
+TIP: If you find yourself adding many extra actions to a resourceful route, it's time to stop and ask yourself whether you're disguising the presence of another resource.
Non-Resourceful Routes
----------------------
@@ -428,7 +545,7 @@ In particular, simple routing makes it very easy to map legacy URLs to new Rails
### Bound Parameters
-When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. Two of these symbols are special: `:controller` maps to the name of a controller in your application, and `:action` maps to the name of an action within that controller. For example, consider one of the default Rails routes:
+When you set up a regular route, you supply a series of symbols that Rails maps to parts of an incoming HTTP request. Two of these symbols are special: `:controller` maps to the name of a controller in your application, and `:action` maps to the name of an action within that controller. For example, consider this route:
```ruby
get ':controller(/:action(/:id))'
@@ -452,17 +569,17 @@ NOTE: You can't use `:namespace` or `:module` with a `:controller` path segment.
get ':controller(/:action(/:id))', controller: /admin\/[^\/]+/
```
-TIP: By default dynamic segments don't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within a dynamic segment, add a constraint that overrides this – for example, `id: /[^\/]+/` allows anything except a slash.
+TIP: By default, dynamic segments don't accept dots - this is because the dot is used as a separator for formatted routes. If you need to use a dot within a dynamic segment, add a constraint that overrides this – for example, `id: /[^\/]+/` allows anything except a slash.
### Static Segments
-You can specify static segments when creating a route:
+You can specify static segments when creating a route by not prepending a colon to a fragment:
```ruby
get ':controller/:action/:id/with_user/:user_id'
```
-This route would respond to paths such as `/photos/show/1/with_user/2`. In this case, `params` would be `{ controller: "photos", action: "show", id: "1", user_id: "2" }`.
+This route would respond to paths such as `/photos/show/1/with_user/2`. In this case, `params` would be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
### The Query String
@@ -472,14 +589,14 @@ The `params` will also include any parameters from the query string. For example
get ':controller/:action/:id'
```
-An incoming path of `/photos/show/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: "photos", action: "show", id: "1", user_id: "2" }`.
+An incoming path of `/photos/show/1?user_id=2` will be dispatched to the `show` action of the `Photos` controller. `params` will be `{ controller: 'photos', action: 'show', id: '1', user_id: '2' }`.
### Defining Defaults
You do not need to explicitly use the `:controller` and `:action` symbols within a route. You can supply them as defaults:
```ruby
-get 'photos/:id' => 'photos#show'
+get 'photos/:id', to: 'photos#show'
```
With this route, Rails will match an incoming path of `/photos/12` to the `show` action of `PhotosController`.
@@ -487,17 +604,17 @@ With this route, Rails will match an incoming path of `/photos/12` to the `show`
You can also define other defaults in a route by supplying a hash for the `:defaults` option. This even applies to parameters that you do not specify as dynamic segments. For example:
```ruby
-get 'photos/:id' => 'photos#show', defaults: { format: 'jpg' }
+get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
```
Rails would match `photos/12` to the `show` action of `PhotosController`, and set `params[:format]` to `"jpg"`.
### Naming Routes
-You can specify a name for any route using the `:as` option.
+You can specify a name for any route using the `:as` option:
```ruby
-get 'exit' => 'sessions#destroy', as: :logout
+get 'exit', to: 'sessions#destroy', as: :logout
```
This will create `logout_path` and `logout_url` as named helpers in your application. Calling `logout_path` will return `/exit`
@@ -505,7 +622,7 @@ This will create `logout_path` and `logout_url` as named helpers in your applica
You can also use this to override routing methods defined by resources, like this:
```ruby
-get ':username', to: "users#show", as: :user
+get ':username', to: 'users#show', as: :user
```
This will define a `user_path` method that will be available in controllers, helpers and views that will go to a route such as `/bob`. Inside the `show` action of `UsersController`, `params[:username]` will contain the username for the user. Change `:username` in the route definition if you do not want your parameter name to be `:username`.
@@ -515,35 +632,35 @@ This will define a `user_path` method that will be available in controllers, hel
In general, you should use the `get`, `post`, `put` and `delete` methods to constrain a route to a particular verb. You can use the `match` method with the `:via` option to match multiple verbs at once:
```ruby
-match 'photos' => 'photos#show', via: [:get, :post]
+match 'photos', to: 'photos#show', via: [:get, :post]
```
You can match all verbs to a particular route using `via: :all`:
```ruby
-match 'photos' => 'photos#show', via: :all
+match 'photos', to: 'photos#show', via: :all
```
-You should avoid routing all verbs to an action unless you have a good reason to, as routing both `GET` requests and `POST` requests to a single action has security implications.
+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.
### Segment Constraints
You can use the `:constraints` option to enforce a format for a dynamic segment:
```ruby
-get 'photos/:id' => 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
+get 'photos/:id', to: 'photos#show', constraints: { id: /[A-Z]\d{5}/ }
```
-This route would match paths such as `/photos/A12345`. You can more succinctly express the same route this way:
+This route would match paths such as `/photos/A12345`, but not `/photos/893`. You can more succinctly express the same route this way:
```ruby
-get 'photos/:id' => 'photos#show', id: /[A-Z]\d{5}/
+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' => 'posts#show', constraints: {id: /^\d/}
+get '/:id', to: 'posts#show', constraints: {id: /^\d/}
```
However, note that you don't need to use anchors because all routes are anchored at the start.
@@ -551,8 +668,8 @@ However, note that you don't need to use anchors because all routes are anchored
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:
```ruby
-get '/:id' => 'posts#show', constraints: { id: /\d.+/ }
-get '/:username' => 'users#show'
+get '/:id', to: 'posts#show', constraints: { id: /\d.+/ }
+get '/:username', to: 'users#show'
```
### Request-Based Constraints
@@ -562,14 +679,14 @@ You can also constrain a route based on any method on the <a href="action_contro
You specify a request-based constraint the same way that you specify a segment constraint:
```ruby
-get "photos", constraints: {subdomain: "admin"}
+get 'photos', constraints: {subdomain: 'admin'}
```
You can also specify constraints in a block form:
```ruby
namespace :admin do
- constraints subdomain: "admin" do
+ constraints subdomain: 'admin' do
resources :photos
end
end
@@ -591,7 +708,7 @@ class BlacklistConstraint
end
TwitterClone::Application.routes.draw do
- get "*path" => "blacklist#index",
+ get '*path', to: 'blacklist#index',
constraints: BlacklistConstraint.new
end
```
@@ -600,55 +717,49 @@ You can also specify constraints as a lambda:
```ruby
TwitterClone::Application.routes.draw do
- get "*path" => "blacklist#index",
+ get '*path', to: 'blacklist#index',
constraints: lambda { |request| Blacklist.retrieve_ips.include?(request.remote_ip) }
end
```
Both the `matches?` method and the lambda gets the `request` object as an argument.
-### Route Globbing
+### Route Globbing and Wildcard Segments
-Route globbing is a way to specify that a particular parameter should be matched to all the remaining parts of a route. For example
+Route globbing is a way to specify that a particular parameter should be matched to all the remaining parts of a route. For example:
```ruby
-get 'photos/*other' => 'photos#unknown'
+get 'photos/*other', to: 'photos#unknown'
```
-This route would match `photos/12` or `/photos/long/path/to/12`, setting `params[:other]` to `"12"` or `"long/path/to/12"`.
+This route would match `photos/12` or `/photos/long/path/to/12`, setting `params[:other]` to `"12"` or `"long/path/to/12"`. The fragments prefixed with a star are called "wildcard segments".
-Wildcard segments can occur anywhere in a route. For example,
+Wildcard segments can occur anywhere in a route. For example:
```ruby
-get 'books/*section/:title' => 'books#show'
+get 'books/*section/:title', to: 'books#show'
```
-would match `books/some/section/last-words-a-memoir` with `params[:section]` equals `"some/section"`, and `params[:title]` equals `"last-words-a-memoir"`.
+would match `books/some/section/last-words-a-memoir` with `params[:section]` equals `'some/section'`, and `params[:title]` equals `'last-words-a-memoir'`.
-Technically a route can have even more than one wildcard segment. The matcher assigns segments to parameters in an intuitive way. For example,
+Technically, a route can have even more than one wildcard segment. The matcher assigns segments to parameters in an intuitive way. For example:
```ruby
-get '*a/foo/*b' => 'test#index'
+get '*a/foo/*b', to: 'test#index'
```
-would match `zoo/woo/foo/bar/baz` with `params[:a]` equals `"zoo/woo"`, and `params[:b]` equals `"bar/baz"`.
-
-NOTE: Starting from Rails 3.1, wildcard routes will always match the optional format segment by default. For example if you have this route:
-
-```ruby
-get '*pages' => 'pages#show'
-```
+would match `zoo/woo/foo/bar/baz` with `params[:a]` equals `'zoo/woo'`, and `params[:b]` equals `'bar/baz'`.
-NOTE: By requesting `"/foo/bar.json"`, your `params[:pages]` will be equals to `"foo/bar"` with the request format of JSON. If you want the old 3.0.x behavior back, you could supply `format: false` like this:
+NOTE: By requesting `'/foo/bar.json'`, your `params[:pages]` will be equals to `'foo/bar'` with the request format of JSON. If you want the old 3.0.x behavior back, you could supply `format: false` like this:
```ruby
-get '*pages' => 'pages#show', format: false
+get '*pages', to: 'pages#show', format: false
```
NOTE: If you want to make the format segment mandatory, so it cannot be omitted, you can supply `format: true` like this:
```ruby
-get '*pages' => 'pages#show', format: true
+get '*pages', to: 'pages#show', format: true
```
### Redirection
@@ -656,20 +767,20 @@ get '*pages' => 'pages#show', format: true
You can redirect any path to another path using the `redirect` helper in your router:
```ruby
-get "/stories" => redirect("/posts")
+get '/stories', to: redirect('/posts')
```
You can also reuse dynamic segments from the match in the path to redirect to:
```ruby
-get "/stories/:name" => redirect("/posts/%{name}")
+get '/stories/:name', to: redirect('/posts/%{name}')
```
-You can also provide a block to redirect, which receives the params and the request object:
+You can also provide a block to redirect, which receives the symbolized path parameters and the request object:
```ruby
-get "/stories/:name" => redirect {|params, req| "/posts/#{params[:name].pluralize}" }
-get "/stories" => redirect {|p, req| "/posts/#{req.subdomain}" }
+get '/stories/:name', to: redirect {|path_params, req| "/posts/#{path_params[:name].pluralize}" }
+get '/stories', to: redirect {|path_params, req| "/posts/#{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.
@@ -678,35 +789,45 @@ 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 `'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:
```ruby
-match "/application.js" => Sprockets, via: :all
+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, `'posts#index'` actually expands out to `PostsController.action(:index)`, which returns a valid Rack application.
### Using `root`
-You can specify what Rails should route `"/"` to with the `root` method:
+You can specify what Rails should route `'/'` to with the `root` method:
```ruby
root to: 'pages#main'
root 'pages#main' # shortcut for the above
```
-You should put the `root` route at the top of the file, because it is the most popular route and should be matched first. You also need to delete the `public/index.html` file for the root route to take effect.
+You should put the `root` route at the top of the file, because it is the most popular route and should be matched first.
NOTE: The `root` route only routes `GET` requests to the action.
+You can also use root inside namespaces and scopes as well. For example:
+
+```ruby
+namespace :admin do
+ root to: "admin#index"
+end
+
+root to: "home#index"
+```
+
### Unicode character routes
-You can specify unicode character routes directly. For example
+You can specify unicode character routes directly. For example:
```ruby
-match 'ã“ã‚“ã«ã¡ã¯' => 'welcome#index'
+get 'ã“ã‚“ã«ã¡ã¯', to: 'welcome#index'
```
Customizing Resourceful Routes
@@ -719,23 +840,36 @@ While the default routes and helpers generated by `resources :posts` will usuall
The `:controller` option lets you explicitly specify a controller to use for the resource. For example:
```ruby
-resources :photos, controller: "images"
+resources :photos, controller: 'images'
```
will recognize incoming paths beginning with `/photos` but route to the `Images` controller:
-| HTTP Verb | Path | action | named helper |
-| --------- | ---------------- | ------- | -------------------- |
-| GET | /photos | index | photos_path |
-| GET | /photos/new | new | new_photo_path |
-| POST | /photos | create | photos_path |
-| GET | /photos/:id | show | photo_path(:id) |
-| GET | /photos/:id/edit | edit | edit_photo_path(:id) |
-| PATCH/PUT | /photos/:id | update | photo_path(:id) |
-| DELETE | /photos/:id | destroy | photo_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ---------------- | ----------------- | -------------------- |
+| GET | /photos | images#index | photos_path |
+| GET | /photos/new | images#new | new_photo_path |
+| POST | /photos | images#create | photos_path |
+| GET | /photos/:id | images#show | photo_path(:id) |
+| GET | /photos/:id/edit | images#edit | edit_photo_path(:id) |
+| PATCH/PUT | /photos/:id | images#update | photo_path(:id) |
+| DELETE | /photos/:id | images#destroy | photo_path(:id) |
NOTE: Use `photos_path`, `new_photo_path`, etc. to generate paths for this resource.
+For namespaced controllers you can use the directory notation. For example:
+
+```ruby
+resources :user_permissions, controller: 'admin/user_permissions'
+```
+
+This will route to the `Admin::UserPermissions` controller.
+
+NOTE: Only the directory notation is supported. Specifying the
+controller with Ruby constant notation (eg. `controller: 'Admin::UserPermissions'`)
+can lead to routing problems and results in
+a warning.
+
### Specifying Constraints
You can use the `:constraints` option to specify a required format on the implicit `id`. For example:
@@ -764,20 +898,20 @@ TIP: By default the `:id` parameter doesn't accept dots - this is because the do
The `:as` option lets you override the normal naming for the named route helpers. For example:
```ruby
-resources :photos, as: "images"
+resources :photos, as: 'images'
```
will recognize incoming paths beginning with `/photos` and route the requests to `PhotosController`, but use the value of the :as option to name the helpers.
-| HTTP Verb | Path | action | named helper |
-| --------- | ---------------- | ------- | -------------------- |
-| GET | /photos | index | images_path |
-| GET | /photos/new | new | new_image_path |
-| POST | /photos | create | images_path |
-| GET | /photos/:id | show | image_path(:id) |
-| GET | /photos/:id/edit | edit | edit_image_path(:id) |
-| PATCH/PUT | /photos/:id | update | image_path(:id) |
-| DELETE | /photos/:id | destroy | image_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | ---------------- | ----------------- | -------------------- |
+| GET | /photos | photos#index | images_path |
+| GET | /photos/new | photos#new | new_image_path |
+| POST | /photos | photos#create | images_path |
+| GET | /photos/:id | photos#show | image_path(:id) |
+| GET | /photos/:id/edit | photos#edit | edit_image_path(:id) |
+| PATCH/PUT | /photos/:id | photos#update | image_path(:id) |
+| DELETE | /photos/:id | photos#destroy | image_path(:id) |
### Overriding the `new` and `edit` Segments
@@ -787,7 +921,7 @@ The `:path_names` option lets you override the automatically-generated "new" and
resources :photos, path_names: { new: 'make', edit: 'change' }
```
-This would cause the routing to recognize paths such as
+This would cause the routing to recognize paths such as:
```
/photos/make
@@ -799,18 +933,18 @@ NOTE: The actual action names aren't changed by this option. The two paths shown
TIP: If you find yourself wanting to change this option uniformly for all of your routes, you can use a scope.
```ruby
-scope path_names: { new: "make" } do
+scope path_names: { new: 'make' } do
# rest of your routes
end
```
### Prefixing the Named Route Helpers
-You can use the `:as` option to prefix the named route helpers that Rails generates for a route. Use this option to prevent name collisions between routes using a path scope.
+You can use the `:as` option to prefix the named route helpers that Rails generates for a route. Use this option to prevent name collisions between routes using a path scope. For example:
```ruby
-scope "admin" do
- resources :photos, as: "admin_photos"
+scope 'admin' do
+ resources :photos, as: 'admin_photos'
end
resources :photos
@@ -821,7 +955,7 @@ This will provide route helpers such as `admin_photos_path`, `new_admin_photo_pa
To prefix a group of route helpers, use `:as` with `scope`:
```ruby
-scope "admin", as: "admin" do
+scope 'admin', as: 'admin' do
resources :photos, :accounts
end
@@ -835,7 +969,7 @@ NOTE: The `namespace` scope will automatically add `:as` as well as `:module` an
You can prefix routes with a named parameter also:
```ruby
-scope ":username" do
+scope ':username' do
resources :posts
end
```
@@ -867,26 +1001,26 @@ TIP: If your application has many RESTful routes, using `:only` and `:except` to
Using `scope`, we can alter path names generated by resources:
```ruby
-scope(path_names: { new: "neu", edit: "bearbeiten" }) do
- resources :categories, path: "kategorien"
+scope(path_names: { new: 'neu', edit: 'bearbeiten' }) do
+ resources :categories, path: 'kategorien'
end
```
Rails now creates routes to the `CategoriesController`.
-| HTTP Verb | Path | action | used for |
-| --------- | -------------------------- | ------- | ----------------------- |
-| GET | /kategorien | index | categories_path |
-| GET | /kategorien/neu | new | new_category_path |
-| POST | /kategorien | create | categories_path |
-| GET | /kategorien/:id | show | category_path(:id) |
-| GET | /kategorien/:id/bearbeiten | edit | edit_category_path(:id) |
-| PATCH/PUT | /kategorien/:id | update | category_path(:id) |
-| DELETE | /kategorien/:id | destroy | category_path(:id) |
+| HTTP Verb | Path | Controller#Action | Named Helper |
+| --------- | -------------------------- | ------------------ | ----------------------- |
+| GET | /kategorien | categories#index | categories_path |
+| GET | /kategorien/neu | categories#new | new_category_path |
+| POST | /kategorien | categories#create | categories_path |
+| GET | /kategorien/:id | categories#show | category_path(:id) |
+| GET | /kategorien/:id/bearbeiten | categories#edit | edit_category_path(:id) |
+| PATCH/PUT | /kategorien/:id | categories#update | category_path(:id) |
+| DELETE | /kategorien/:id | categories#destroy | category_path(:id) |
### Overriding the Singular Form
-If you want to define the singular form of a resource, you should add additional rules to the `Inflector`.
+If you want to define the singular form of a resource, you should add additional rules to the `Inflector`:
```ruby
ActiveSupport::Inflector.inflections do |inflect|
@@ -896,7 +1030,7 @@ end
### Using `:as` in Nested Resources
-The `:as` option overrides the automatically-generated name for the resource in nested route helpers. For example,
+The `:as` option overrides the automatically-generated name for the resource in nested route helpers. For example:
```ruby
resources :magazines do
@@ -911,7 +1045,7 @@ Inspecting and Testing Routes
Rails offers facilities for inspecting and testing your routes.
-### Seeing Existing Routes
+### Listing Existing Routes
To get a complete list of the available routes in your application, visit `http://localhost:3000/rails/info/routes` in your browser while your server is running in the **development** environment. You can also execute the `rake routes` command in your terminal to produce the same output.
@@ -949,31 +1083,31 @@ Routes should be included in your testing strategy (just like the rest of your a
#### The `assert_generates` Assertion
-`assert_generates` asserts that a particular set of options generate a particular path and can be used with default routes or custom routes.
+`assert_generates` asserts that a particular set of options generate a particular path and can be used with default routes or custom routes. For example:
```ruby
-assert_generates "/photos/1", { controller: "photos", action: "show", id: "1" }
-assert_generates "/about", controller: "pages", action: "about"
+assert_generates '/photos/1', { controller: 'photos', action: 'show', id: '1' }
+assert_generates '/about', controller: 'pages', action: 'about'
```
#### The `assert_recognizes` Assertion
-`assert_recognizes` is the inverse of `assert_generates`. It asserts that a given path is recognized and routes it to a particular spot in your application.
+`assert_recognizes` is the inverse of `assert_generates`. It asserts that a given path is recognized and routes it to a particular spot in your application. For example:
```ruby
-assert_recognizes({ controller: "photos", action: "show", id: "1" }, "/photos/1")
+assert_recognizes({ controller: 'photos', action: 'show', id: '1' }, '/photos/1')
```
You can supply a `:method` argument to specify the HTTP verb:
```ruby
-assert_recognizes({ controller: "photos", action: "create" }, { path: "photos", method: :post })
+assert_recognizes({ controller: 'photos', action: 'create' }, { path: 'photos', method: :post })
```
#### The `assert_routing` Assertion
-The `assert_routing` assertion checks the route both ways: it tests that the path generates the options, and that the options generate the path. Thus, it combines the functions of `assert_generates` and `assert_recognizes`.
+The `assert_routing` assertion checks the route both ways: it tests that the path generates the options, and that the options generate the path. Thus, it combines the functions of `assert_generates` and `assert_recognizes`:
```ruby
-assert_routing({ path: "photos", method: :post }, { controller: "photos", action: "create" })
+assert_routing({ path: 'photos', method: :post }, { controller: 'photos', action: 'create' })
```
diff --git a/guides/source/ruby_on_rails_guides_guidelines.md b/guides/source/ruby_on_rails_guides_guidelines.md
index e589a3d093..5564b0648b 100644
--- a/guides/source/ruby_on_rails_guides_guidelines.md
+++ b/guides/source/ruby_on_rails_guides_guidelines.md
@@ -3,6 +3,11 @@ Ruby on Rails Guides Guidelines
This guide documents guidelines for writing Ruby on Rails Guides. This guide follows itself in a graceful loop, serving itself as an example.
+After reading this guide, you will know:
+
+* About the conventions to be used in Rails documentation.
+* How to generate guides locally.
+
--------------------------------------------------------------------------------
Markdown
@@ -58,9 +63,13 @@ 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
+
### Generation
-To generate all the guides, just `cd` into the **`guides`** directory and execute:
+To generate all the guides, just `cd` into the `guides` directory, run `bundle install` and execute:
```
bundle exec rake guides:generate
@@ -72,8 +81,6 @@ or
bundle exec rake guides:generate:html
```
-(You may need to run `bundle install` first to install the required gems.)
-
To process `my_guide.md` and nothing else use the `ONLY` environment variable:
```
diff --git a/guides/source/security.md b/guides/source/security.md
index 5ef68d2272..d7a41497f8 100644
--- a/guides/source/security.md
+++ b/guides/source/security.md
@@ -1,15 +1,16 @@
-Ruby On Rails Security Guide
+Ruby on Rails Security Guide
============================
-This manual describes common security problems in web applications and how to avoid them with Rails. After reading it, you should be familiar with:
+This manual describes common security problems in web applications and how to avoid them with Rails.
-* All countermeasures _that are highlighted_
-* The concept of sessions in Rails, what to put in there and popular attack methods
-* How just visiting a site can be a security problem (with CSRF)
-* What you have to pay attention to when working with files or providing an administration interface
-* The Rails-specific mass assignment problem
-* How to manage users: Logging in and out and attack methods on all layers
-* And the most popular injection attack methods
+After reading this guide, you will know:
+
+* All countermeasures _that are highlighted_.
+* The concept of sessions in Rails, what to put in there and popular attack methods.
+* How just visiting a site can be a security problem (with CSRF).
+* What you have to pay attention to when working with files or providing an administration interface.
+* How to manage users: Logging in and out and attack methods on all layers.
+* And the most popular injection attack methods.
--------------------------------------------------------------------------------
@@ -57,7 +58,7 @@ WARNING: _Stealing a user's session id lets an attacker use the web application
Many web applications have an authentication system: a user provides a user name and password, the web application checks them and stores the corresponding user id in the session hash. From now on, the session is valid. On every request the application will load the user, identified by the user id in the session, without the need for new authentication. The session id in the cookie identifies the session.
-Hence, the cookie serves as temporary authentication for the web application. Everyone who seizes a cookie from someone else, may use the web application as this user – with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
+Hence, the cookie serves as temporary authentication for the web application. Anyone who seizes a cookie from someone else, may use the web application as this user - with possibly severe consequences. Here are some ways to hijack a session, and their countermeasures:
* Sniff the cookie in an insecure network. A wireless LAN can be an example of such a network. In an unencrypted wireless LAN it is especially easy to listen to the traffic of all connected clients. This is one more reason not to work from a coffee shop. For the web application builder this means to _provide a secure connection over SSL_. In Rails 3.1 and later, this could be accomplished by always forcing SSL connection in your application config file:
@@ -71,7 +72,7 @@ Hence, the cookie serves as temporary authentication for the web application. Ev
* Instead of stealing a cookie unknown to the attacker, he fixes a user's session identifier (in the cookie) known to him. Read more about this so-called session fixation later.
-The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10–$1000 (depending on the available amount of funds), $0.40–$20 for credit card numbers, $1–$8 for online auction site accounts and $4–$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
+The main objective of most attackers is to make money. The underground prices for stolen bank login accounts range from $10-$1000 (depending on the available amount of funds), $0.40-$20 for credit card numbers, $1-$8 for online auction site accounts and $4-$30 for email passwords, according to the [Symantec Global Internet Security Threat Report](http://eval.symantec.com/mktginfo/enterprise/white_papers/b-whitepaper_internet_security_threat_report_xiii_04-2008.en-us.pdf).
### Session Guidelines
@@ -92,16 +93,15 @@ Rails 2 introduced a new default session storage, CookieStore. CookieStore saves
* The client can see everything you store in a session, because it is stored in clear-text (actually Base64-encoded, so not encrypted). So, of course, _you don't want to store any secrets here_. To prevent session hash tampering, a digest is calculated from the session with a server-side secret and inserted into the end of the cookie.
-That means the security of this storage depends on this secret (and on the digest algorithm, which defaults to SHA512, which has not been compromised, yet). So _don't use a trivial secret, i.e. a word from a dictionary, or one which is shorter than 30 characters_. Put the secret in your environment.rb:
+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_.
-```ruby
-config.action_dispatch.session = {
- key: '_app_session',
- secret: '0x0dkfj3927dkc7djdh36rkckdfzsg...'
-}
-```
+`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.:
-There are, however, derivatives of CookieStore which encrypt the session hash, so the client cannot see it.
+ YourApp::Application.config.secret_key_base = '49d3f3de9ed86c74b94ad6bd0...'
+
+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.
+
+If you have received an application where the secret was exposed (e.g. an application whose source was shared), strongly consider changing the secret.
### Replay Attacks for CookieStore Sessions
@@ -134,7 +134,7 @@ This attack focuses on fixing a user's session id known to the attacker, and for
* As the new trap session is unused, the web application will require the user to authenticate.
* From now on, the victim and the attacker will co-use the web application with the same session: The session became valid and the victim didn't notice the attack.
-### Session Fixation – Countermeasures
+### Session Fixation - Countermeasures
TIP: _One line of code will protect you from session fixation._
@@ -187,11 +187,11 @@ In the <a href="#sessions">session chapter</a> you have learned that most Rails
* Bob's session at www.webapp.com is still alive, because he didn't log out a few minutes ago.
* By viewing the post, the browser finds an image tag. It tries to load the suspected image from www.webapp.com. As explained before, it will also send along the cookie with the valid session id.
* The web application at www.webapp.com verifies the user information in the corresponding session hash and destroys the project with the ID 1. It then returns a result page which is an unexpected result for the browser, so it will not display the image.
-* Bob doesn't notice the attack -- but a few days later he finds out that project number one is gone.
+* Bob doesn't notice the attack - but a few days later he finds out that project number one is gone.
-It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere – in a forum, blog post or email.
+It is important to notice that the actual crafted image or link doesn't necessarily have to be situated in the web application's domain, it can be anywhere - in a forum, blog post or email.
-CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) -- less than 0.1% in 2006 -- but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in my (and others) security contract work – _CSRF is an important security issue_.
+CSRF appears very rarely in CVE (Common Vulnerabilities and Exposures) - less than 0.1% in 2006 - but it really is a 'sleeping giant' [Grossman]. This is in stark contrast to the results in my (and others) security contract work - _CSRF is an important security issue_.
### CSRF Countermeasures
@@ -209,7 +209,7 @@ The HTTP protocol basically provides two main types of requests - GET and POST (
* The interaction _changes the state_ of the resource in a way that the user would perceive (e.g., a subscription to a service), or
* The user is _held accountable for the results_ of the interaction.
-If your web application is RESTful, you might be used to additional HTTP verbs, such as PUT or DELETE. Most of today's web browsers, however do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier.
+If your web application is RESTful, you might be used to additional HTTP verbs, such as PATCH, PUT or DELETE. Most of today's web browsers, however do not support them - only GET and POST. Rails uses a hidden `_method` field to handle this barrier.
_POST requests can be sent automatically, too_. Here is an example for a link which displays www.harmless.com as destination in the browser's status bar. In fact it dynamically creates a new form that sends a POST request.
@@ -268,7 +268,7 @@ def legacy
end
```
-This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can exploited by an attacker if he includes a host key in the URL:
+This will redirect the user to the main action if he tried to access a legacy action. The intention was to preserve the URL parameters to the legacy action and pass them to the main action. However, it can be exploited by an attacker if he includes a host key in the URL:
```
http://www.example.com/site/legacy?param1=xy&param2=23&host=www.attacker.com
@@ -288,9 +288,9 @@ This example is a Base64 encoded JavaScript which displays a simple message box.
NOTE: _Make sure file uploads don't overwrite important files, and process media files asynchronously._
-Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like “../../../etc/passwdâ€, it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so – one more reason to run web servers, database servers and other programs as a less privileged Unix user.
+Many web applications allow users to upload files. _File names, which the user may choose (partly), should always be filtered_ as an attacker could use a malicious file name to overwrite any file on the server. If you store file uploads at /var/www/uploads, and the user enters a file name like "../../../etc/passwd", it may overwrite an important file. Of course, the Ruby interpreter would need the appropriate permissions to do so - one more reason to run web servers, database servers and other programs as a less privileged Unix user.
-When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all “../†in a file name and an attacker uses a string such as “....//†- the result will be “../â€. It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master:)
+When filtering user input file names, _don't try to remove malicious parts_. Think of a situation where the web application removes all "../" in a file name and an attacker uses a string such as "....//" - the result will be "../". It is best to use a whitelist approach, which _checks for the validity of a file name with a set of accepted characters_. This is opposed to a blacklist approach which attempts to remove not allowed characters. In case it isn't a valid file name, reject it (or replace not accepted characters), but don't remove them. Here is the file name sanitizer from the [attachment_fu plugin](https://github.com/technoweenie/attachment_fu/tree/master):
```ruby
def sanitize_filename(filename)
@@ -313,7 +313,7 @@ The solution to this is best to _process media files asynchronously_: Save the m
WARNING: _Source code in uploaded files may be executed when placed in specific directories. Do not place file uploads in Rails' /public directory if it is Apache's home directory._
-The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file “file.cgi†with code in it, which will be executed when someone downloads the file.
+The popular Apache web server has an option called DocumentRoot. This is the home directory of the web site, everything in this directory tree will be served by the web server. If there are files with a certain file name extension, the code in it will be executed when requested (might require some options to be set). Examples for this are PHP and CGI files. Now think of a situation where an attacker uploads a file "file.cgi" with code in it, which will be executed when someone downloads the file.
_If your Apache DocumentRoot points to Rails' /public directory, do not put file uploads in it_, store files at least one level downwards.
@@ -327,7 +327,7 @@ Just as you have to filter file names for uploads, you have to do so for downloa
send_file('/var/www/uploads/' + params[:filename])
```
-Simply pass a file name like “../../../etc/passwd†to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
+Simply pass a file name like "../../../etc/passwd" to download the server's login information. A simple solution against this, is to _check that the requested file is in the expected directory_:
```ruby
basename = File.expand_path(File.join(File.dirname(__FILE__), '../../files'))
@@ -346,13 +346,13 @@ Intranet and administration interfaces are popular attack targets, because they
In 2007 there was the first tailor-made trojan which stole information from an Intranet, namely the "Monster for employers" web site of Monster.com, an online recruitment web application. Tailor-made Trojans are very rare, so far, and the risk is quite low, but it is certainly a possibility and an example of how the security of the client host is important, too. However, the highest threat to Intranet and Admin applications are XSS and CSRF.

-**XSS** If your application re-displays malicious user input from the extranet, the application will be vulnerable to XSS. User names, comments, spam reports, order addresses are just a few uncommon examples, where there can be XSS.
+**XSS** If your application re-displays malicious user input from the extranet, the application will be vulnerable to XSS. User names, comments, spam reports, order addresses are just a few uncommon examples, where there can be XSS.
Having one single place in the admin interface or Intranet, where the input has not been sanitized, makes the entire application vulnerable. Possible exploits include stealing the privileged administrator's cookie, injecting an iframe to steal the administrator's password or installing malicious software through browser security holes to take over the administrator's computer.
Refer to the Injection section for countermeasures against XSS. It is _recommended to use the SafeErb plugin_ also in an Intranet or administration interface.
-**CSRF** Cross-Site Reference Forgery (CSRF) is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
+**CSRF** Cross-Site Reference Forgery (CSRF) is a gigantic attack method, it allows the attacker to do everything the administrator or Intranet user may do. As you have already seen above how CSRF works, here are a few examples of what attackers can do in the Intranet or admin interface.
A real-world example is a [router reconfiguration by CSRF](http://www.h-online.com/security/Symantec-reports-first-active-attack-on-a-DSL-router--/news/102352). The attackers sent a malicious e-mail, with CSRF in it, to Mexican users. The e-mail claimed there was an e-card waiting for them, but it also contained an image tag that resulted in a HTTP-GET request to reconfigure the user's router (which is a popular model in Mexico). The request changed the DNS-settings so that requests to a Mexico-based banking site would be mapped to the attacker's site. Everyone who accessed the banking site through that router saw the attacker's fake web site and had his credentials stolen.
@@ -372,141 +372,6 @@ The common admin interface works like this: it's located at www.example.com/admi
* _Put the admin interface to a special sub-domain_ such as admin.application.com and make it a separate application with its own user management. This makes stealing an admin cookie from the usual domain, www.application.com, impossible. This is because of the same origin policy in your browser: An injected (XSS) script on www.application.com may not read the cookie for admin.application.com and vice-versa.
-Mass Assignment
----------------
-
-WARNING: _Without any precautions `Model.new(params[:model]`) allows attackers to set
-any database column's value._
-
-The mass-assignment feature may become a problem, as it allows an attacker to set
-any model's attributes by manipulating the hash passed to a model's `new()` method:
-
-```ruby
-def signup
- params[:user] # => {name:"ow3ned", admin:true}
- @user = User.new(params[:user])
-end
-```
-
-Mass-assignment saves you much work, because you don't have to set each value
-individually. Simply pass a hash to the `new` method, or `assign_attributes=`
-a hash value, to set the model's attributes to the values in the hash. The
-problem is that it is often used in conjunction with the parameters (params)
-hash available in the controller, which may be manipulated by an attacker.
-He may do so by changing the URL like this:
-
-```
-http://www.example.com/user/signup?user[name]=ow3ned&user[admin]=1
-```
-
-This will set the following parameters in the controller:
-
-```ruby
-params[:user] # => {name:"ow3ned", admin:true}
-```
-
-So if you create a new user using mass-assignment, it may be too easy to become
-an administrator.
-
-Note that this vulnerability is not restricted to database columns. Any setter
-method, unless explicitly protected, is accessible via the `attributes=` method.
-In fact, this vulnerability is extended even further with the introduction of
-nested mass assignment (and nested object forms) in Rails 2.3. The
-`accepts_nested_attributes_for` declaration provides us the ability to extend
-mass assignment to model associations (`has_many`, `has_one`,
-`has_and_belongs_to_many`). For example:
-
-```ruby
- class Person < ActiveRecord::Base
- has_many :children
-
- accepts_nested_attributes_for :children
- end
-
- class Child < ActiveRecord::Base
- belongs_to :person
- end
-```
-
-As a result, the vulnerability is extended beyond simply exposing column
-assignment, allowing attackers the ability to create entirely new records
-in referenced tables (children in this case).
-
-### Countermeasures
-
-To avoid this, Rails provides an interface for protecting attributes from
-end-user assignment called Strong Parameters. This makes Action Controller
-parameters forbidden until they have been whitelisted, so you will have to
-make a conscious choice about which attributes to allow for mass assignment
-and thus prevent accidentally exposing that which shouldn’t be exposed.
-
-NOTE. Before Strong Parameters arrived, mass-assignment protection was a
-model's task provided by Active Model. This has been extracted to the
-[ProtectedAttributes](https://github.com/rails/protected_attributes)
-gem. In order to use `attr_accessible` and `attr_protected` helpers in
-your models, you should add `protected_attributes` to your Gemfile.
-
-Why we moved mass-assignment protection out of the model and into
-the controller? The whole point of the controller is to control the
-flow between user and application, including authentication, authorization,
-and, as part of that, access control.
-
-Strong Parameters provides two methods to the `params` hash to control
-access to your attributes: `require` and `permit`. The former is used
-to mark parameters as required and the latter limits which attributes
-should be allowed for mass updating using the slice pattern. For example:
-
-```ruby
-def signup
- params[:user]
- # => {name:"ow3ned", admin:true}
- permitted_params = params.require(:user).permit(:name)
- # => {name:"ow3ned"}
-
- @user = User.new(permitted_params)
-end
-```
-
-In the example above, `require` is checking whether a `user` key is present or not
-in the parameters, if it's not present, it'll raise an `ActionController::MissingParameter`
-exception, which will be caught by `ActionController::Base` and turned into a
-400 Bad Request reply. Then `permit` whitelists the attributes that should be
-allowed for mass assignment.
-
-A good pattern to encapsulate the permissible parameters is to use a private method
-since you'll be able to reuse the same permit list between different actions.
-
-```ruby
-def signup
- @user = User.new(user_params)
- # ...
-end
-
-def update
- @user = User.find(params[:id]
- @user.update_attributes!(user_params)
- # ...
-end
-
-private
- def user_params
- params.require(:user).permit(:name)
- end
-```
-
-Also, you can specialize this method with per-user checking of permissible
-attributes.
-
-```ruby
-def user_params
- if current_user.admin?
- params.require(:user).permit(:name, :admin)
- else
- params.require(:user).permit(:name)
- end
-end
-```
-
User Management
---------------
@@ -541,7 +406,7 @@ NOTE: _Brute-force attacks on accounts are trial and error attacks on the login
A list of user names for your web application may be misused to brute-force the corresponding passwords, because most people don't use sophisticated passwords. Most passwords are a combination of dictionary words and possibly numbers. So armed with a list of user names and a dictionary, an automatic program may find the correct password in a matter of minutes.
-Because of this, most web applications will display a generic error message “user name or password not correctâ€, if one of these are not correct. If it said “the user name you entered has not been foundâ€, an attacker could automatically compile a list of user names.
+Because of this, most web applications will display a generic error message "user name or password not correct", if one of these are not correct. If it said "the user name you entered has not been found", an attacker could automatically compile a list of user names.
However, what most web application designers neglect, are the forgot-password pages. These pages often admit that the entered user name or e-mail address has (not) been found. This allows an attacker to compile a list of user names and brute-force the accounts.
@@ -567,7 +432,7 @@ Depending on your web application, there may be more ways to hijack the user's a
INFO: _A CAPTCHA is a challenge-response test to determine that the response is not generated by a computer. It is often used to protect comment forms from automatic spam bots by asking the user to type the letters of a distorted image. The idea of a negative CAPTCHA is not for a user to prove that he is human, but reveal that a robot is a robot._
-But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](http://ambethia.com/recaptcha/) is also a Rails plug-in with the same name as the API.
+But not only spam robots (bots) are a problem, but also automatic login bots. A popular CAPTCHA API is [reCAPTCHA](http://recaptcha.net/) which displays two distorted images of words from old books. It also adds an angled line, rather than a distorted background and high levels of warping on the text as earlier CAPTCHAs did, because the latter were broken. As a bonus, using reCAPTCHA helps to digitize old books. [ReCAPTCHA](https://github.com/ambethia/recaptcha/) is also a Rails plug-in with the same name as the API.
You will get two keys from the API, a public and a private key, which you have to put into your Rails environment. After that you can use the recaptcha_tags method in the view, and the verify_recaptcha method in the controller. Verify_recaptcha will return false if the validation fails.
The problem with CAPTCHAs is, they are annoying. Additionally, some visually impaired users have found certain kinds of distorted CAPTCHAs difficult to read. The idea of negative CAPTCHAs is not to ask a user to proof that he is human, but reveal that a spam robot is a bot.
@@ -582,7 +447,7 @@ Here are some ideas how to hide honeypot fields by JavaScript and/or CSS:
The most simple negative CAPTCHA is one hidden honeypot field. On the server side, you will check the value of the field: If it contains any text, it must be a bot. Then, you can either ignore the post or return a positive result, but not saving the post to the database. This way the bot will be satisfied and moves on. You can do this with annoying users, too.
-You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html:)
+You can find more sophisticated negative CAPTCHAs in Ned Batchelder's [blog post](http://nedbatchelder.com/text/stopbots.html):
* Include a field with the current UTC time-stamp in it and check it on the server. If it is too far in the past, or if it is in the future, the form is invalid.
* Randomize the field names
@@ -616,7 +481,7 @@ A good password is a long alphanumeric combination of mixed cases. As this is qu
INFO: _A common pitfall in Ruby's regular expressions is to match the string's beginning and end by ^ and $, instead of \A and \z._
-Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books make this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
+Ruby uses a slightly different approach than many other languages to match the end and the beginning of a string. That is why even many Ruby and Rails books get this wrong. So how is this a security threat? Say you wanted to loosely validate a URL field and you used a simple regular expression like this:
```ruby
/^https?:\/\/[^\n]+$/i
@@ -630,7 +495,7 @@ http://hi.com
*/
```
-This URL passes the filter because the regular expression matches – the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
+This URL passes the filter because the regular expression matches - the second line, the rest does not matter. Now imagine we had a view that showed the URL like this:
```ruby
link_to "Homepage", @user.homepage
@@ -686,8 +551,7 @@ NOTE: _When sanitizing, protecting or verifying something, whitelists over black
A blacklist can be a list of bad e-mail addresses, non-public actions or bad HTML tags. This is opposed to a whitelist which lists the good e-mail addresses, public actions, good HTML tags and so on. Although sometimes it is not possible to create a whitelist (in a SPAM filter, for example), _prefer to use whitelist approaches_:
-* Use before_filter only: [...] instead of except: [...]. This way you don't forget to turn it off for newly added actions.
-* Use attr_accessible instead of attr_protected. See the mass-assignment section for details
+* Use before_action only: [...] instead of except: [...]. This way you don't forget to turn it off for newly added actions.
* Allow &lt;strong&gt; instead of removing &lt;script&gt; against Cross-Site Scripting (XSS). See below for details.
* Don't try to correct user input by blacklists:
* This will make the attack work: "&lt;sc&lt;script&gt;ript&gt;".gsub("&lt;script&gt;", "")
@@ -782,7 +646,7 @@ INFO: _The most widespread, and one of the most devastating security vulnerabili
An entry point is a vulnerable URL and its parameters where an attacker can start an attack.
-The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter – obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
+The most common entry points are message posts, user comments, and guest books, but project titles, document names and search result pages have also been vulnerable - just about everywhere where the user can input data. But the input does not necessarily have to come from input boxes on web sites, it can be in any URL parameter - obvious, hidden or internal. Remember that the user may intercept any traffic. Applications, such as the [Live HTTP Headers Firefox plugin](http://livehttpheaders.mozdev.org/), or client-site proxies make it easy to change requests.
XSS attacks work like this: An attacker injects some code, the web application saves it and displays it on a page, later presented to a victim. Most XSS examples simply display an alert box, but it is more powerful than that. XSS can steal the cookie, hijack the session, redirect the victim to a fake website, display advertisements for the benefit of the attacker, change elements on the web site to get confidential information or install malicious software through security holes in the web browser.
@@ -834,10 +698,10 @@ You can mitigate these attacks (in the obvious way) by adding the [httpOnly](htt
With web page defacement an attacker can do a lot of things, for example, present false information or lure the victim on the attackers web site to steal the cookie, login credentials or other sensitive data. The most popular way is to include code from external sources by iframes:
```html
-<iframe name=â€StatPage†src="http://58.xx.xxx.xxx" width=5 height=5 style=â€display:noneâ€></iframe>
+<iframe name="StatPage" src="http://58.xx.xxx.xxx" width=5 height=5 style="display:none"></iframe>
```
-This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iframe is taken from an actual attack on legitimate Italian sites using the [Mpack attack framework](http://isc.sans.org/diary.html?storyid=3015). Mpack tries to install malicious software through security holes in the web browser – very successfully, 50% of the attacks succeed.
+This loads arbitrary HTML and/or JavaScript from an external source and embeds it as part of the site. This iframe is taken from an actual attack on legitimate Italian sites using the [Mpack attack framework](http://isc.sans.org/diary.html?storyid=3015). Mpack tries to install malicious software through security holes in the web browser - very successfully, 50% of the attacks succeed.
A more specialized attack could overlap the entire web site or display a login form, which looks the same as the site's original, but transmits the user name and password to the attacker's site. Or it could use CSS and/or JavaScript to hide a legitimate link in the web application, and display another one at its place which redirects to a fake web site.
@@ -854,7 +718,7 @@ _It is very important to filter malicious input, but it is also important to esc
Especially for XSS, it is important to do _whitelist input filtering instead of blacklist_. Whitelist filtering states the values allowed as opposed to the values not allowed. Blacklists are never complete.
-Imagine a blacklist deletes “script†from the user input. Now the attacker injects “&lt;scrscriptipt&gt;â€, and after the filter, “&lt;script&gt;†remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
+Imagine a blacklist deletes "script" from the user input. Now the attacker injects "&lt;scrscriptipt&gt;", and after the filter, "&lt;script&gt;" remains. Earlier versions of Rails used a blacklist approach for the strip_tags(), strip_links() and sanitize() method. So this kind of injection was possible:
```ruby
strip_tags("some<<b>script>alert('hello')<</b>/script>")
@@ -880,7 +744,7 @@ Network traffic is mostly based on the limited Western alphabet, so new characte
&#108;&#101;&#114;&#116;&#40;&#39;&#88;&#83;&#83;&#39;&#41;>
```
-This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus “get to know your enemyâ€, is the [Hackvertor](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
+This example pops up a message box. It will be recognized by the above sanitize() filter, though. A great tool to obfuscate and encode strings, and thus "get to know your enemy", is the [Hackvertor](https://hackvertor.co.uk/public). Rails' sanitize() method does a good job to fend off encoding attacks.
#### Examples from the Underground
@@ -896,9 +760,9 @@ The following is an excerpt from the [Js.Yamanner@m](http://www.symantec.com/sec
The worms exploits a hole in Yahoo's HTML/JavaScript filter, which usually filters all target and onload attributes from tags (because there can be JavaScript). The filter is applied only once, however, so the onload attribute with the worm code stays in place. This is a good example why blacklist filters are never complete and why it is hard to allow HTML/JavaScript in a web application.
-Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/article/9/Paper_A_PoC_of_a_cross_webmail_worm_XWW_called_Njuda_connection/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
+Another proof-of-concept webmail worm is Nduja, a cross-domain worm for four Italian webmail services. Find more details on [Rosario Valotta's paper](http://www.xssed.com/news/37/Nduja_Connection_A_cross_webmail_worm_XWW/). Both webmail worms have the goal to harvest email addresses, something a criminal hacker could make money with.
-In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named “login_home_index_htmlâ€, so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
+In December 2006, 34,000 actual user names and passwords were stolen in a [MySpace phishing attack](http://news.netcraft.com/archives/2006/10/27/myspace_accounts_compromised_by_phishers.html). The idea of the attack was to create a profile page named "login_home_index_html", so the URL looked very convincing. Specially-crafted HTML and CSS was used to hide the genuine MySpace content from the page and instead display its own login form.
The MySpace Samy worm will be discussed in the CSS Injection section.
@@ -920,13 +784,13 @@ So the payload is in the style attribute. But there are no quotes allowed in the
<div id="mycode" expr="alert('hah!')" style="background:url('javascript:eval(document.all.mycode.expr)')">
```
-The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word “innerHTMLâ€:
+The eval() function is a nightmare for blacklist input filters, as it allows the style attribute to hide the word "innerHTML":
```
alert(eval('document.body.inne' + 'rHTML'));
```
-The next problem was MySpace filtering the word “javascriptâ€, so the author used “java&lt;NEWLINE&gt;script" to get around this:
+The next problem was MySpace filtering the word "javascript", so the author used "java&lt;NEWLINE&gt;script" to get around this:
```html
<div id="mycode" expr="alert('hah!')" style="background:url('java↵
script:eval(document.all.mycode.expr)')">
@@ -973,7 +837,7 @@ It is recommended to _use RedCloth in combination with a whitelist input filter_
### Ajax Injection
-NOTE: _The same security precautions have to be taken for Ajax actions as for “normal†ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._
+NOTE: _The same security precautions have to be taken for Ajax actions as for "normal" ones. There is at least one exception, however: The output has to be escaped in the controller already, if the action doesn't render a view._
If you use the [in_place_editor plugin](http://dev.rubyonrails.org/browser/plugins/in_place_editing), or actions that return a string, rather than rendering a view, _you have to escape the return value in the action_. Otherwise, if the return value contains a XSS string, the malicious code will be executed upon return to the browser. Escape any input value using the h() method.
@@ -997,7 +861,7 @@ WARNING: _HTTP headers are dynamically generated and under certain circumstances
HTTP request headers have a Referer, User-Agent (client software), and Cookie field, among others. Response headers for example have a status code, Cookie and Location (redirection target URL) field. All of them are user-supplied and may be manipulated with more or less effort. _Remember to escape these header fields, too._ For example when you display the user agent in an administration area.
-Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a “referer“ field in a form to redirect to the given address:
+Besides that, it is _important to know what you are doing when building response headers partly based on user input._ For example you want to redirect the user back to a specific page. To do that you introduced a "referer" field in a form to redirect to the given address:
```ruby
redirect_to params[:referer]
@@ -1078,7 +942,7 @@ Or you can remove them.
config.action_dispatch.default_headers.clear
```
-Here is the list of common headers:
+Here is a list of common headers:
* X-Frame-Options
_'SAMEORIGIN' in Rails by default_ - allow framing on same domain. Set it to 'DENY' to deny framing at all or 'ALLOWALL' if you want to allow framing for all website.
@@ -1093,6 +957,11 @@ Used to control which sites are allowed to bypass same origin policies and send
* Strict-Transport-Security
[Used to control if the browser is allowed to only access a site over a secure connection](http://en.wikipedia.org/wiki/HTTP_Strict_Transport_Security)
+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.
+
Additional Resources
--------------------
diff --git a/guides/source/testing.md b/guides/source/testing.md
index b45aba8d55..edf4813d74 100644
--- a/guides/source/testing.md
+++ b/guides/source/testing.md
@@ -1,12 +1,13 @@
A Guide to Testing Rails Applications
=====================================
-This guide covers built-in mechanisms offered by Rails to test your
-application. By referring to this guide, you will be able to:
+This guide covers built-in mechanisms in Rails for testing your application.
-* Understand Rails testing terminology
-* Write unit, functional, and integration tests for your application
-* Identify other popular testing approaches and plugins
+After reading this guide, you will know:
+
+* Rails testing terminology.
+* How to write unit, functional, and integration tests for your application.
+* Other popular testing approaches and plugins.
--------------------------------------------------------------------------------
@@ -36,11 +37,11 @@ Rails creates a `test` folder for you as soon as you create a Rails project usin
```bash
$ ls -F test
-
-fixtures/ functional/ integration/ performance/ test_helper.rb unit/
+controllers/ helpers/ mailers/ test_helper.rb
+fixtures/ integration/ models/
```
-The `unit` directory is meant to hold tests for your models, the `functional` directory is meant to hold tests for your controllers, the `integration` directory is meant to hold tests that involve any number of controllers interacting, and the `performance` directory is meant for performance tests.
+The `models` directory is meant to hold tests for your models, the `controllers` directory is meant to hold tests for your controllers and the `integration` directory is meant to hold tests that involve any number of controllers interacting.
Fixtures are a way of organizing test data; they reside in the `fixtures` folder.
@@ -63,19 +64,35 @@ YAML-formatted fixtures are a very human-friendly way to describe your sample da
Here's a sample YAML fixture file:
```yaml
-# lo & behold! I am a YAML comment!
+# lo & behold! I am a YAML comment!
david:
- name: David Heinemeier Hansson
- birthday: 1979-10-15
- profession: Systems development
+ name: David Heinemeier Hansson
+ birthday: 1979-10-15
+ profession: Systems development
steve:
- name: Steve Ross Kellock
- birthday: 1974-09-27
- profession: guy with keyboard
+ name: Steve Ross Kellock
+ birthday: 1974-09-27
+ profession: guy with keyboard
```
-Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank space. You can place comments in a fixture file by using the # character in the first column.
+Each fixture is given a name followed by an indented list of colon-separated key/value pairs. Records are typically separated by a blank space. You can place comments in a fixture file by using the # character in the first column. Keys which resemble YAML keywords such as 'yes' and 'no' are quoted so that the YAML Parser correctly interprets them.
+
+If you are working with [associations](/association_basics.html), you can simply
+define a reference node between two different fixtures. Here's an example with
+a belongs_to/has_many association:
+
+```yaml
+# In fixtures/categories.yml
+about:
+ name: About
+
+# In fixtures/articles.yml
+one:
+ title: Welcome to Rails!
+ body: Hello world!
+ category: about
+```
#### ERB'in It Up
@@ -84,22 +101,22 @@ ERB allows you to embed Ruby code within templates. The YAML fixture format is p
```erb
<% 1000.times do |n| %>
user_<%= n %>:
- username: <%= "user%03d" % n %>
- email: <%= "user%03d@example.com" % n %>
+ username: <%= "user#{n}" %>
+ email: <%= "user#{n}@example.com" %>
<% end %>
```
#### Fixtures in Action
-Rails by default automatically loads all fixtures from the `test/fixtures` folder for your unit and functional test. Loading involves three steps:
+Rails by default automatically loads all fixtures from the `test/fixtures` folder for your models and controllers test. Loading involves three steps:
* Remove any existing data from the table corresponding to the fixture
* Load the fixture data into the table
* Dump the fixture data into a variable in case you want to access it directly
-#### Fixtures are ActiveRecord objects
+#### Fixtures are Active Record objects
-Fixtures are instances of ActiveRecord. As mentioned in point #3 above, you can access the object directly because it is automatically setup as a local variable of the test case. For example:
+Fixtures are instances of Active Record. As mentioned in point #3 above, you can access the object directly because it is automatically setup as a local variable of the test case. For example:
```ruby
# this will return the User object for the fixture named david
@@ -115,7 +132,7 @@ email(david.girlfriend.email, david.location_tonight)
Unit Testing your Models
------------------------
-In Rails, unit tests are what you write to test your models.
+In Rails, models tests are what you write to test your models.
For this guide we will be using Rails _scaffolding_. It will create the model, a migration, controller and views for the new resource in a single operation. It will also create a full test suite following Rails best practices. I will be using examples from this generated code and will be supplementing it with additional examples where necessary.
@@ -138,10 +155,9 @@ The default test stub in `test/models/post_test.rb` looks like this:
require 'test_helper'
class PostTest < ActiveSupport::TestCase
- # Replace this with your real tests.
- test "the truth" do
- assert true
- end
+ # test "the truth" do
+ # assert true
+ # end
end
```
@@ -159,9 +175,10 @@ class PostTest < 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.
-Any method defined within a `Test::Unit` test case 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::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.
-Rails adds a `test` method that takes a test name and a block. It generates a normal `Test::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
@@ -222,34 +239,30 @@ TIP: You can see all these rake tasks and their descriptions by running `rake --
### Running Tests
-Running a test is as simple as invoking the file containing the test cases through Ruby:
+Running a test is as simple as invoking the file containing the test cases through `rake test` command.
```bash
-$ ruby -Itest test/models/post_test.rb
-
-Loaded suite models/post_test
-Started
+$ rake test test/models/post_test.rb
.
-Finished in 0.023513 seconds.
-1 tests, 1 assertions, 0 failures, 0 errors
-```
+Finished tests in 0.009262s, 107.9680 tests/s, 107.9680 assertions/s.
-This will run all the test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch.
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
+```
-You can also run a particular test method from the test case by using the `-n` switch with the `test method name`.
+You can also run a particular test method from the test case by running the test and providing the `test method name`.
```bash
-$ ruby -Itest test/models/post_test.rb -n test_the_truth
-
-Loaded suite models/post_test
-Started
+$ rake test test/models/post_test.rb test_the_truth
.
-Finished in 0.023513 seconds.
-1 tests, 1 assertions, 0 failures, 0 errors
+Finished tests in 0.009064s, 110.3266 tests/s, 110.3266 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
```
+This will run all test methods from the test case. Note that `test_helper.rb` is in the `test` directory, hence this directory needs to be added to the load path using the `-I` switch.
+
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.
@@ -264,17 +277,16 @@ end
Let us run this newly added test.
```bash
-$ ruby unit/post_test.rb -n test_should_not_save_post_without_title
-Loaded suite -e
-Started
+$ rake test test/models/post_test.rb test_should_not_save_post_without_title
F
-Finished in 0.102072 seconds.
+
+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]:
-<false> is not true.
+test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
+Failed assertion, no message given.
-1 tests, 1 assertions, 1 failures, 0 errors
+1 tests, 1 assertions, 1 failures, 0 errors, 0 skips
```
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:
@@ -290,29 +302,27 @@ 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.
-<false> is not true.
+test_should_not_save_post_without_title(PostTest) [test/models/post_test.rb:6]:
+Saved the post 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
- validates :title, :presence => true
+ validates :title, presence: true
end
```
Now the test should pass. Let us verify by running the test again:
```bash
-$ ruby unit/post_test.rb -n test_should_not_save_post_without_title
-Loaded suite unit/post_test
-Started
+$ rake test test/models/post_test.rb test_should_not_save_post_without_title
.
-Finished in 0.193608 seconds.
-1 tests, 1 assertions, 0 failures, 0 errors
+Finished tests in 0.047721s, 20.9551 tests/s, 20.9551 assertions/s.
+
+1 tests, 1 assertions, 0 failures, 0 errors, 0 skips
```
Now, if you noticed, we first wrote a test which fails for a desired functionality, then we wrote some code which adds the functionality and finally we ensured that our test passes. This approach to software development is referred to as _Test-Driven Development_ (TDD).
@@ -332,18 +342,17 @@ end
Now you can see even more output in the console from running the tests:
```bash
-$ ruby unit/post_test.rb -n test_should_report_error
-Loaded suite -e
-Started
+$ rake test test/models/post_test.rb test_should_report_error
E
-Finished in 0.082603 seconds.
+
+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:0x249d354>
- /test/models/post_test.rb:6:in `test_should_report_error'
+NameError: undefined local variable or method `some_undefined_variable' for #<PostTest:0x007fe32e24afe0>
+ test/models/post_test.rb:10:in `block in <class:PostTest>'
-1 tests, 0 assertions, 0 failures, 1 errors
+1 tests, 0 assertions, 0 failures, 1 errors, 0 skips
```
Notice the 'E' in the output. It denotes a test with error.
@@ -354,31 +363,38 @@ NOTE: The execution of each test method stops as soon as any error or an asserti
Ideally, you would like to include a test for everything which could possibly break. It's a good practice to have at least one test for each of your validations and at least one test for every method in your model.
-### Assertions Available
+### Available Assertions
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 the complete list of assertions that ship with `test/unit`, 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.
+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.
| Assertion | Purpose |
| ---------------------------------------------------------------- | ------- |
-| `assert( boolean, [msg] )` | Ensures that the object/expression is true.|
+| `assert( test, [msg] )` | Ensures that `test` is true.|
+| `assert_not( test, [msg] )` | Ensures that `test` is false.|
| `assert_equal( expected, actual, [msg] )` | Ensures that `expected == actual` is true.|
| `assert_not_equal( expected, actual, [msg] )` | Ensures that `expected != actual` is true.|
| `assert_same( expected, actual, [msg] )` | Ensures that `expected.equal?(actual)` is true.|
-| `assert_not_same( expected, actual, [msg] )` | Ensures that `!expected.equal?(actual)` is true.|
+| `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 true.|
+| `assert_not_nil( obj, [msg] )` | Ensures that `obj.nil?` is false.|
| `assert_match( regexp, string, [msg] )` | Ensures that a string matches the regular expression.|
| `assert_no_match( regexp, string, [msg] )` | Ensures that a string doesn't match the regular expression.|
-| `assert_in_delta( expecting, actual, delta, [msg] )` | Ensures that the numbers `expecting` and `actual` are within `delta` of each other.|
+| `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.|
-| `assert_raise( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
+| `assert_raises( exception1, exception2, ... ) { block }` | Ensures that the given block raises one of the given exceptions.|
| `assert_nothing_raised( exception1, exception2, ... ) { block }` | Ensures that the given block doesn't raise one of the given exceptions.|
-| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is of the `class` type.|
+| `assert_instance_of( class, obj, [msg] )` | Ensures that `obj` is an instance of `class`.|
+| `assert_not_instance_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class`.|
| `assert_kind_of( class, obj, [msg] )` | Ensures that `obj` is or descends from `class`.|
-| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` has a method called `symbol`.|
-| `assert_operator( obj1, operator, obj2, [msg] )` | Ensures that `obj1.operator(obj2)` is true.|
+| `assert_not_kind_of( class, obj, [msg] )` | Ensures that `obj` is not an instance of `class` and is not descending from it.|
+| `assert_respond_to( obj, symbol, [msg] )` | Ensures that `obj` responds to `symbol`.|
+| `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_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.|
@@ -390,17 +406,14 @@ NOTE: Creating your own assertions is an advanced topic that we won't cover in t
Rails adds some custom assertions of its own to the `test/unit` framework:
-NOTE: `assert_valid(record)` has been deprecated. Please use `assert(record.valid?)` instead.
-
| Assertion | Purpose |
| --------------------------------------------------------------------------------- | ------- |
-| `assert_valid(record)` | Ensures that the passed record is valid by Active Record standards and returns any error messages if it is not.|
| `assert_difference(expressions, difference = 1, message = nil) {...}` | Test numeric difference between the return value of an expression as a result of what is evaluated in the yielded block.|
| `assert_no_difference(expressions, message = nil, &amp;block)` | Asserts that the numeric result of evaluating an expression is not changed before and after invoking the passed in block.|
| `assert_recognizes(expected_options, path, extras={}, message=nil)` | Asserts that the routing of the given path was handled correctly and that the parsed options (given in the expected_options hash) match path. Basically, it asserts that Rails recognizes the route given by expected_options.|
| `assert_generates(expected_path, options, defaults={}, extras = {}, message=nil)` | Asserts that the provided options can be used to generate the provided path. This is the inverse of assert_recognizes. The extras parameter is used to tell the request the names and values of additional request parameters that would be in a query string. The message parameter allows you to specify a custom error message for assertion failures.|
-| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range|
-| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(:controller => "weblog")` will also match the redirection of `redirect_to(:controller => "weblog", :action => "show")` and so on.|
+| `assert_response(type, message = nil)` | Asserts that the response comes with a specific status code. You can specify `:success` to indicate 200-299, `:redirect` to indicate 300-399, `:missing` to indicate 404, or `:error` to match the 500-599 range|
+| `assert_redirected_to(options = {}, message=nil)` | Assert that the redirection options passed in match those of the redirect called in the latest action. This match can be partial, such that `assert_redirected_to(controller: "weblog")` will also match the redirection of `redirect_to(controller: "weblog", action: "show")` and so on.|
| `assert_template(expected = nil, message=nil)` | Asserts that the request was rendered with the appropriate template file.|
You'll see the usage of some of these assertions in the next chapter.
@@ -425,10 +438,12 @@ Now that we have used Rails scaffold generator for our `Post` resource, it has a
Let me take you through one such test, `test_should_get_index` from the file `posts_controller_test.rb`.
```ruby
-test "should get index" do
- get :index
- assert_response :success
- assert_not_nil assigns(:posts)
+class PostsControllerTest < ActionController::TestCase
+ test "should get index" do
+ get :index
+ assert_response :success
+ assert_not_nil assigns(:posts)
+ end
end
```
@@ -460,7 +475,7 @@ Let us modify `test_should_create_post` test in `posts_controller_test.rb` so th
```ruby
test "should create post" do
assert_difference('Post.count') do
- post :create, :post => { :title => 'Some title'}
+ post :create, post: {title: 'Some title'}
end
assert_redirected_to post_path(assigns(:post))
@@ -486,7 +501,7 @@ NOTE: Functional tests do not verify whether the specified request type should b
### The Four Hashes of the Apocalypse
-After a request has been made by using one of the 5 methods (`get`, `post`, etc.) and processed, you will have 4 Hash objects ready for use:
+After a request has been made using one of the 6 methods (`get`, `post`, etc.) and processed, you will have 4 Hash objects ready for use:
* `assigns` - Any objects that are stored as instance variables in actions for use in views.
* `cookies` - Any cookies that are set.
@@ -512,6 +527,21 @@ You also have access to three instance variables in your functional tests:
* `@request` - The request
* `@response` - The response
+### Setting Headers and CGI variables
+
+Headers and cgi variables can be set directly on the `@request`
+instance variable:
+
+```ruby
+# setting a HTTP Header
+@request.headers["Accept"] = "text/plain, text/html"
+get :index # simulate the request with custom header
+
+# setting a CGI variable
+@request.headers["HTTP_REFERER"] = "http://example.com/home"
+post :create # simulate the request with custom env variable
+```
+
### Testing Templates and Layouts
If you want to make sure that the response rendered the correct template and layout, you can use the `assert_template`
@@ -521,7 +551,7 @@ method:
test "index should render correct template and layout" do
get :index
assert_template :index
- assert_template :layout => "layouts/application"
+ assert_template layout: "layouts/application"
end
```
@@ -531,7 +561,7 @@ things clearer. On the other hand, you have to include the "layouts" directory n
file in this standard layout directory. Hence,
```ruby
-assert_template :layout => "application"
+assert_template layout: "application"
```
will not work.
@@ -544,7 +574,7 @@ Hence:
```ruby
test "new should render correct layout" do
get :new
- assert_template :layout => "layouts/application", :partial => "_form"
+ assert_template layout: "layouts/application", partial: "_form"
end
```
@@ -557,7 +587,7 @@ Here's another example that uses `flash`, `assert_redirected_to`, and `assert_di
```ruby
test "should create post" do
assert_difference('Post.count') do
- post :create, :post => { :title => 'Hi', :body => 'This is my first post.'}
+ post :create, post: {title: 'Hi', body: 'This is my first post.'}
end
assert_redirected_to post_path(assigns(:post))
assert_equal 'Post was successfully created.', flash[:notice]
@@ -610,11 +640,11 @@ The `assert_select` assertion is quite powerful. For more advanced usage, refer
There are more assertions that are primarily used in testing views:
-| Assertion | Purpose |
-| ---------------------------------------------------------- | ------- |
-| `assert_select_email` | Allows you to make assertions on the body of an e-mail. |
-| `assert_select_encoded` | Allows you to make assertions on encoded HTML. It does this by un-encoding the contents of each element and then calling the block with all the un-encoded elements.|
-| `css_select(selector)` or `css_select(element, selector)` | Returns an array of all the elements selected by the _selector_. In the second variant it first matches the base _element_ and tries to match the _selector_ expression on any of its children. If there are no matches both variants return an empty array.|
+| Assertion | Purpose |
+| --------------------------------------------------------- | ------- |
+| `assert_select_email` | Allows you to make assertions on the body of an e-mail. |
+| `assert_select_encoded` | Allows you to make assertions on encoded HTML. It does this by un-encoding the contents of each element and then calling the block with all the un-encoded elements.|
+| `css_select(selector)` or `css_select(element, selector)` | Returns an array of all the elements selected by the _selector_. In the second variant it first matches the base _element_ and tries to match the _selector_ expression on any of its children. If there are no matches both variants return an empty array.|
Here's an example of using `assert_select_email`:
@@ -643,12 +673,9 @@ Here's what a freshly-generated integration test looks like:
require 'test_helper'
class UserFlowsTest < ActionDispatch::IntegrationTest
- fixtures :all
-
- # Replace this with your real tests.
- test "the truth" do
- assert true
- end
+ # test "the truth" do
+ # assert true
+ # end
end
```
@@ -689,9 +716,9 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
get "/login"
assert_response :success
- post_via_redirect "/login", :username => users(:avs).username, :password => users(:avs).password
+ post_via_redirect "/login", username: users(:david).username, password: users(:david).password
assert_equal '/welcome', path
- assert_equal 'Welcome avs!', flash[:notice]
+ assert_equal 'Welcome david!', flash[:notice]
https!(false)
get "/posts/all"
@@ -713,17 +740,17 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
test "login and browse site" do
- # User avs logs in
- avs = login(:avs)
+ # User david logs in
+ david = login(:david)
# User guest logs in
guest = login(:guest)
# Both are now available in different sessions
- assert_equal 'Welcome avs!', avs.flash[:notice]
+ assert_equal 'Welcome david!', david.flash[:notice]
assert_equal 'Welcome guest!', guest.flash[:notice]
- # User avs can browse site
- avs.browses_site
+ # User david can browse site
+ david.browses_site
# User guest can browse site as well
guest.browses_site
@@ -732,55 +759,54 @@ class UserFlowsTest < ActionDispatch::IntegrationTest
private
- module CustomDsl
- def browses_site
- get "/products/all"
- assert_response :success
- assert assigns(:products)
+ module CustomDsl
+ def browses_site
+ get "/products/all"
+ assert_response :success
+ assert assigns(:products)
+ end
end
- end
- def login(user)
- open_session do |sess|
- sess.extend(CustomDsl)
- u = users(user)
- sess.https!
- sess.post "/login", :username => u.username, :password => u.password
- assert_equal '/welcome', path
- sess.https!(false)
+ def login(user)
+ open_session do |sess|
+ sess.extend(CustomDsl)
+ u = users(user)
+ sess.https!
+ sess.post "/login", username: u.username, password: u.password
+ assert_equal '/welcome', path
+ sess.https!(false)
+ end
end
- end
end
```
Rake Tasks for Running your Tests
---------------------------------
-You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of rake tasks to help in testing. The table below lists all rake tasks that come along in the default Rakefile when you initiate a Rails project.
-
-| Tasks | Description |
-| ------------------------------- | ----------- |
-| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake` as the _test_ target is the default.|
-| `rake test:benchmark` | Benchmark the performance tests|
-| `rake test:controllers` | Runs all the controller tests from `test/controllers`|
-| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`|
-| `rake test:helpers` | Runs all the helper tests from `test/helpers`|
-| `rake test:integration` | Runs all the integration tests from `test/integration`|
-| `rake test:mailers` | Runs all the mailer tests from `test/mailers`|
-| `rake test:models` | Runs all the model tests from `test/models`|
-| `rake test:profile` | Profile the performance tests|
-| `rake test:recent` | Tests recent changes|
-| `rake test:uncommitted` | Runs all the tests which are uncommitted. Supports Subversion and Git|
-| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`|
-
-
-Brief Note About `Test::Unit`
+You don't need to set up and run your tests by hand on a test-by-test basis. Rails comes with a number of commands to help in testing. The table below lists all commands that come along in the default Rakefile when you initiate a Rails project.
+
+| Tasks | Description |
+| ----------------------- | ----------- |
+| `rake test` | Runs all unit, functional and integration tests. You can also simply run `rake test` as Rails will run all the tests by default|
+| `rake test:controllers` | Runs all the controller tests from `test/controllers`|
+| `rake test:functionals` | Runs all the functional tests from `test/controllers`, `test/mailers`, and `test/functional`|
+| `rake test:helpers` | Runs all the helper tests from `test/helpers`|
+| `rake test:integration` | Runs all the integration tests from `test/integration`|
+| `rake test:mailers` | Runs all the mailer tests from `test/mailers`|
+| `rake test:models` | Runs all the model tests from `test/models`|
+| `rake test:units` | Runs all the unit tests from `test/models`, `test/helpers`, and `test/unit`|
+
+
+Brief Note About `MiniTest`
-----------------------------
-Ruby ships with a boat load of libraries. One little gem of a library is `Test::Unit`, a framework for unit testing in Ruby. All the basic assertions discussed above are actually defined in `Test::Unit::Assertions`. The class `ActiveSupport::TestCase` which we have been using in our unit and functional tests extends `Test::Unit::TestCase`, allowing
+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 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/)
Setup and Teardown
------------------
@@ -806,13 +832,13 @@ class PostsControllerTest < ActionController::TestCase
end
test "should show post" do
- get :show, :id => @post.id
+ get :show, id: @post.id
assert_response :success
end
test "should destroy post" do
assert_difference('Post.count', -1) do
- delete :destroy, :id => @post.id
+ delete :destroy, id: @post.id
end
assert_redirected_to posts_path
@@ -831,7 +857,7 @@ Above, the `setup` method is called before each test and so `@post` is available
Let's see the earlier example by specifying `setup` callback by specifying a method name as a symbol:
```ruby
-require '../test_helper'
+require 'test_helper'
class PostsControllerTest < ActionController::TestCase
@@ -844,18 +870,18 @@ class PostsControllerTest < ActionController::TestCase
end
test "should show post" do
- get :show, :id => @post.id
+ get :show, id: @post.id
assert_response :success
end
test "should update post" do
- patch :update, :id => @post.id, :post => { }
+ patch :update, id: @post.id, post: {}
assert_redirected_to post_path(assigns(:post))
end
test "should destroy post" do
assert_difference('Post.count', -1) do
- delete :destroy, :id => @post.id
+ delete :destroy, id: @post.id
end
assert_redirected_to posts_path
@@ -863,10 +889,9 @@ class PostsControllerTest < ActionController::TestCase
private
- def initialize_post
- @post = posts(:one)
- end
-
+ def initialize_post
+ @post = posts(:one)
+ end
end
```
@@ -877,7 +902,7 @@ Like everything else in your Rails application, it is recommended that you test
```ruby
test "should route to post" do
- assert_routing '/posts/1', { :controller => "posts", :action => "show", :id => "1" }
+ assert_routing '/posts/1', {controller: "posts", action: "show", id: "1"}
end
```
@@ -888,7 +913,7 @@ Testing mailer classes requires some specific tools to do a thorough job.
### Keeping the Postman in Check
-Your mailer classes -- like every other part of your Rails application -- should be tested to ensure that it is working as expected.
+Your mailer classes - like every other part of your Rails application - should be tested to ensure that it is working as expected.
The goals of testing your mailer classes are to ensure that:
@@ -920,19 +945,24 @@ require 'test_helper'
class UserMailerTest < ActionMailer::TestCase
tests UserMailer
test "invite" do
- @expected.from = 'me@example.com'
- @expected.to = 'friend@example.com'
- @expected.subject = "You have been invited by #{@expected.from}"
- @expected.body = read_fixture('invite')
- @expected.date = Time.now
-
- assert_equal @expected.encoded, UserMailer.create_invite('me@example.com', 'friend@example.com', @expected.date).encoded
+ # 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?
+
+ # Test the body of the sent email contains what we expect it to
+ assert_equal ['me@example.com'], email.from
+ assert_equal ['friend@example.com'], email.to
+ assert_equal 'You have been invited by me@example.com', email.subject
+ assert_equal read_fixture('invite').join, email.body.to_s
end
-
end
```
-In this test, `@expected` is an instance of `TMail::Mail` that you can use in your tests. It is defined in `ActionMailer::TestCase`. The test above uses `@expected` to construct an email, which it then asserts with email created by the custom mailer. The `invite` fixture is the body of the email and is used as the sample content to assert against. The helper `read_fixture` is used to read in the content from this file.
+In the test we send the email and store the returned object in the `email`
+variable. We then ensure that it was sent (the first assert), then, in the
+second batch of assertions, we ensure that the email does indeed contain what we
+expect. The helper `read_fixture` is used to read in the content from this file.
Here's the content of the `invite` fixture:
@@ -944,9 +974,17 @@ You have been invited.
Cheers!
```
-This is the right time to understand a little more about writing tests for your mailers. The line `ActionMailer::Base.delivery_method = :test` in `config/environments/test.rb` sets the delivery method to test mode so that email will not actually be delivered (useful to avoid spamming your users while testing) but instead it will be appended to an array (`ActionMailer::Base.deliveries`).
+This is the right time to understand a little more about writing tests for your
+mailers. The line `ActionMailer::Base.delivery_method = :test` in
+`config/environments/test.rb` sets the delivery method to test mode so that
+email will not actually be delivered (useful to avoid spamming your users while
+testing) but instead it will be appended to an array
+(`ActionMailer::Base.deliveries`).
-However often in unit tests, mails will not actually be sent, simply constructed, as in the example above, where the precise content of the email is checked against what it should be.
+NOTE: The `ActionMailer::Base.deliveries` array is only reset automatically in
+`ActionMailer::TestCase` tests. If you want to have a clean slate outside Action
+Mailer tests, you can reset it manually with:
+`ActionMailer::Base.deliveries.clear`
### Functional Testing
@@ -958,7 +996,7 @@ require 'test_helper'
class UserControllerTest < ActionController::TestCase
test "invite friend" do
assert_difference 'ActionMailer::Base.deliveries.size', +1 do
- post :invite_friend, :email => 'friend@example.com'
+ post :invite_friend, email: 'friend@example.com'
end
invite_email = ActionMailer::Base.deliveries.last
@@ -969,6 +1007,47 @@ class UserControllerTest < ActionController::TestCase
end
```
+Testing helpers
+---------------
+
+In order to test helpers, all you need to do is check that the output of the
+helper method matches what you'd expect. Tests related to the helpers are
+located under the `test/helpers` directory. Rails provides a generator which
+generates both the helper and the test file:
+
+```bash
+$ rails generate helper User
+ create app/helpers/user_helper.rb
+ invoke test_unit
+ create test/helpers/user_helper_test.rb
+```
+
+The generated test file contains the following code:
+
+```ruby
+require 'test_helper'
+
+class UserHelperTest < ActionView::TestCase
+end
+```
+
+A helper is just a simple module where you can define methods which are
+available into your views. To test the output of the helper's methods, you just
+have to use a mixin like this:
+
+```ruby
+class UserHelperTest < ActionView::TestCase
+ include UserHelper
+
+ test "should return the user name" do
+ # ...
+ end
+end
+```
+
+Moreover, since the test class extends from `ActionView::TestCase`, you have
+access to Rails' helper methods such as `link_to` or `pluralize`.
+
Other Testing Approaches
------------------------
@@ -977,5 +1056,7 @@ The built-in `test/unit` based testing is not the only way to test Rails applica
* [NullDB](http://avdi.org/projects/nulldb/), a way to speed up testing by avoiding database use.
* [Factory Girl](https://github.com/thoughtbot/factory_girl/tree/master), a replacement for fixtures.
* [Machinist](https://github.com/notahat/machinist/tree/master), another replacement for fixtures.
+* [Fixture Builder](https://github.com/rdy/fixture_builder), a tool that compiles Ruby factories into fixtures before a test run.
+* [MiniTest::Spec Rails](https://github.com/metaskills/minitest-spec-rails), use the MiniTest::Spec DSL within your rails tests.
* [Shoulda](http://www.thoughtbot.com/projects/shoulda), an extension to `test/unit` with additional helpers, macros, and assertions.
* [RSpec](http://relishapp.com/rspec), a behavior-driven development framework
diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md
index 148316d170..224213268e 100644
--- a/guides/source/upgrading_ruby_on_rails.md
+++ b/guides/source/upgrading_ruby_on_rails.md
@@ -3,8 +3,6 @@ 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
--------------
@@ -18,47 +16,190 @@ The best way to be sure that your application still works after upgrading is to
Rails generally stays close to the latest released Ruby version when it's released:
-* Rails 3 and above requires Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially and you should upgrade as early as possible.
-* Rails 3.2.x will be the last branch to support Ruby 1.8.7.
-* Rails 4 will support only Ruby 1.9.3.
+* Rails 3 and above require Ruby 1.8.7 or higher. Support for all of the previous Ruby versions has been dropped officially. You should upgrade as early as possible.
+* Rails 3.2.x is the last branch to support Ruby 1.8.7.
+* Rails 4 prefers Ruby 2.0 and requires 1.9.3 or newer.
+
+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.
+
+### HTTP PATCH
+
+Rails 4 now uses `PATCH` as the primary HTTP verb for updates when a RESTful
+resource is declared in `config/routes.rb`. The `update` action is still used,
+and `PUT` requests will continue to be routed to the `update` action as well.
+So, if you're using only the standard RESTful routes, no changes need to be made:
-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 on to 1.9.2 or 1.9.3 for smooth sailing.
+```ruby
+resources :users
+```
+
+```erb
+<%= form_for @user do |f| %>
+```
+
+```ruby
+class UsersController < ApplicationController
+ def update
+ # No change needed; PATCH will be preferred, and PUT will still work.
+ end
+end
+```
+
+However, you will need to make a change if you are using `form_for` to update
+a resource in conjunction with a custom route using the `PUT` HTTP method:
+
+```ruby
+resources :users, do
+ put :update_name, on: :member
+end
+```
+
+```erb
+<%= form_for [ :update_name, @user ] do |f| %>
+```
+
+```ruby
+class UsersController < ApplicationController
+ def update_name
+ # Change needed; form_for will try to use a non-existent PATCH route.
+ end
+end
+```
+
+If the action is not being used in a public API and you are free to change the
+HTTP method, you can update your route to use `patch` instead of `put`:
+
+`PUT` requests to `/users/:id` in Rails 4 get routed to `update` as they are
+today. So, if you have an API that gets real PUT requests it is going to work.
+The router also routes `PATCH` requests to `/users/:id` to the `update` action.
+
+```ruby
+resources :users do
+ patch :update_name, on: :member
+end
+```
+
+If the action is being used in a public API and you can't change to HTTP method
+being used, you can update your form to use the `PUT` method instead:
+
+```erb
+<%= 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/25/edge-rails-patch-is-the-new-primary-http-method-for-updates/)
+on the Rails blog.
+
+#### A note about media types
+
+The errata for the `PATCH` verb [specifies that a 'diff' media type should be
+used with `PATCH`](http://www.rfc-editor.org/errata_search.php?rfc=5789). One
+such format is [JSON Patch](http://tools.ietf.org/html/rfc6902). While Rails
+does not support JSON Patch natively, it's easy enough to add support:
+
+```
+# in your controller
+def update
+ respond_to do |format|
+ format.json do
+ # perform a partial update
+ @post.update params[:post]
+ end
+
+ format.json_patch do
+ # perform sophisticated change
+ end
+ end
+end
+
+# In config/initializers/json_patch.rb:
+Mime::Type.register 'application/json-patch+json', :json_patch
+```
+
+As JSON Patch was only recently made into an RFC, there aren't a lot of great
+Ruby libraries yet. Aaron Patterson's
+[hana](https://github.com/tenderlove/hana) is one such gem, but doesn't have
+full support for the last few changes in the specification.
Upgrading from Rails 3.2 to Rails 4.0
-------------------------------------
NOTE: This section is a work in progress.
-If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting an update to Rails 4.0.
+If your application is currently on any version of Rails older than 3.2.x, you should upgrade to Rails 3.2 before attempting one to Rails 4.0.
The following changes are meant for upgrading your application to Rails 4.0.
+### Gemfile
+
+Rails 4.0 removed the `assets` group from Gemfile. You'd need to remove that
+line from your Gemfile when upgrading. You should also update your application
+file (in `config/application.rb`):
+
+```ruby
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env)
+```
+
### vendor/plugins
Rails 4.0 no longer supports loading plugins from `vendor/plugins`. You must replace any plugins by extracting them to gems and adding them to your Gemfile. If you choose not to make them gems, you can move them into, say, `lib/my_plugin/*` and add an appropriate initializer in `config/initializers/my_plugin.rb`.
-### Identity Map
+### Active Record
-Rails 4.0 has removed the identity map from Active Record, due to [some inconsistencies with associations](https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6). If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: `config.active_record.identity_map`.
+* Rails 4.0 has removed the identity map from Active Record, due to [some inconsistencies with associations](https://github.com/rails/rails/commit/302c912bf6bcd0fa200d964ec2dc4a44abe328a6). If you have manually enabled it in your application, you will have to remove the following config that has no effect anymore: `config.active_record.identity_map`.
-### Active Record
+* The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
+
+* In Rails 4.0 when a column or a table is renamed the related indexes are also renamed. If you have migrations which rename the indexes, they are no longer needed.
+
+* Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. You shouldn't use instance methods since it's now deprecated. You should change them to use class methods, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`.
+
+* Rails 4.0 has removed `attr_accessible` and `attr_protected` feature in favor of Strong Parameters. You can use the [Protected Attributes gem](https://github.com/rails/protected_attributes) for a smooth upgrade path.
+
+* If you are not using Protected Attributes, you can remove any options related to
+this gem such as `whitelist_attributes` or `mass_assignment_sanitizer` options.
+
+* Rails 4.0 requires that scopes use a callable object such as a Proc or lambda:
+
+```ruby
+ scope :active, where(active: true)
+
+ # becomes
+ scope :active, -> { where active: true }
+```
+
+* Rails 4.0 has deprecated `ActiveRecord::Fixtures` in favor of `ActiveRecord::FixtureSet`.
+
+* Rails 4.0 has deprecated `ActiveRecord::TestCase` in favor of `ActiveSupport::TestCase`.
+
+* Rails 4.0 has deprecated the old-style hash based finder API. This means that
+ methods which previously accepted "finder options" no longer do.
+
+* All dynamic methods except for `find_by_...` and `find_by_...!` are deprecated.
+ Here's how you can handle the changes:
+
+ * `find_all_by_...` becomes `where(...)`.
+ * `find_last_by_...` becomes `where(...).last`.
+ * `scoped_by_...` becomes `where(...)`.
+ * `find_or_initialize_by_...` becomes `find_or_initialize_by(...)`.
+ * `find_or_create_by_...` becomes `find_or_create_by(...)`.
+
+* Note that `where(...)` returns a relation, not an array like the old finders. If you require an `Array`, use `where(...).to_a`.
+
+* These equivalent methods may not execute the same SQL as the previous implementation.
-The `delete` method in collection associations can now receive `Fixnum` or `String` arguments as record ids, besides records, pretty much like the `destroy` method does. Previously it raised `ActiveRecord::AssociationTypeMismatch` for such arguments. From Rails 4.0 on `delete` automatically tries to find the records matching the given ids before deleting them.
+* To re-enable the old finders, you can use the [activerecord-deprecated_finders gem](https://github.com/rails/activerecord-deprecated_finders).
-Rails 4.0 has changed how orders get stacked in `ActiveRecord::Relation`. In previous versions of rails new order was applied after previous defined order. But this is no long true. Check [ActiveRecord Query guide](active_record_querying.html#ordering) for more information.
+### Active Resource
-Rails 4.0 has changed `serialized_attributes` and `attr_readonly` to class methods only. Now you shouldn't use instance methods, it's deprecated. You must change them, e.g. `self.serialized_attributes` to `self.class.serialized_attributes`.
+Rails 4.0 extracted Active Resource to its own gem. If you still need the feature you can add the [Active Resource gem](https://github.com/rails/activeresource) in your Gemfile.
### Active Model
-Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`.
-Now when confirmation validations fail the error will be attached to
-`:#{attribute}_confirmation` instead of `attribute`.
+* Rails 4.0 has changed how errors attach with the `ActiveModel::Validations::ConfirmationValidator`. Now when confirmation validations fail, the error will be attached to `:#{attribute}_confirmation` instead of `attribute`.
-Rails 4.0 has changed `ActiveModel::Serializers::JSON.include_root_in_json` default
-value to `false`. Now, Active Model Serializers and Active Record objects have the
-same default behaviour. This means that you can comment or remove the following option
-in the `config/initializers/wrap_parameters.rb` file:
+* Rails 4.0 has changed `ActiveModel::Serializers::JSON.include_root_in_json` default value to `false`. Now, Active Model Serializers and Active Record objects have the same default behaviour. This means that you can comment or remove the following option in the `config/initializers/wrap_parameters.rb` file:
```ruby
# Disable root element in JSON by default.
@@ -69,48 +210,150 @@ in the `config/initializers/wrap_parameters.rb` file:
### Action Pack
-Rails 4.0 has deprecated `ActionController::Base.page_cache_extension` option. Use
-`ActionController::Base.default_static_extension` instead.
+* Rails 4.0 introduces `ActiveSupport::KeyGenerator` and uses this as a base from which to generate and verify signed cookies (among other things). Existing signed cookies generated with Rails 3.x will be transparently upgraded if you leave your existing `secret_token` in place and add the new `secret_key_base`.
+
+```ruby
+ # config/initializers/secret_token.rb
+ Myapp::Application.config.secret_token = 'existing secret token'
+ Myapp::Application.config.secret_key_base = 'new secret key base'
+```
+
+Please note that you should wait to set `secret_key_base` until you have 100% of your userbase on Rails 4.x and are reasonably sure you will not need to rollback to Rails 3.x. This is because cookies signed based on the new `secret_key_base` in Rails 4.x are not backwards compatible with Rails 3.x. You are free to leave your existing `secret_token` in place, not set the new `secret_key_base`, and ignore the deprecation warnings until you are reasonably sure that your upgrade is otherwise complete.
+
+If you are relying on the ability for external applications or Javascript to be able to read your Rails app's signed session cookies (or signed cookies in general) you should not set `secret_key_base` until you have decoupled these concerns.
+
+* Rails 4.0 encrypts the contents of cookie-based sessions if `secret_key_base` has been set. Rails 3.x signed, but did not encrypt, the contents of cookie-based session. Signed cookies are "secure" in that they are verified to have been generated by your app and are tamper-proof. However, the contents can be viewed by end users, and encrypting the contents eliminates this caveat/concern without a significant performance penalty.
+
+Please read [Pull Request #9978](https://github.com/rails/rails/pull/9978) for details on the move to encrypted session cookies.
-Rails 4.0 has removed Action and Page caching from ActionPack. You will need to
-add the `actionpack-action_caching` gem in order to use `caches_action` and
-the `actionpack-page_caching` to use `caches_pages` in your controllers.
+* Rails 4.0 removed the `ActionController::Base.asset_path` option. Use the assets pipeline feature.
+
+* Rails 4.0 has deprecated `ActionController::Base.page_cache_extension` option. Use `ActionController::Base.default_static_extension` instead.
+
+* Rails 4.0 has removed Action and Page caching from Action Pack. You will need to add the `actionpack-action_caching` gem in order to use `caches_action` and the `actionpack-page_caching` to use `caches_pages` in your controllers.
+
+* Rails 4.0 has removed the XML parameters parser. You will need to add the `actionpack-xml_parser` gem if you require this feature.
+
+* Rails 4.0 changes the default memcached client from `memcache-client` to `dalli`. To upgrade, simply add `gem 'dalli'` to your `Gemfile`.
+
+* Rails 4.0 deprecates the `dom_id` and `dom_class` methods in controllers (they are fine in views). You will need to include the `ActionView::RecordIdentifier` module in controllers requiring this feature.
+
+* Rails 4.0 deprecates the `:confirm` option for the `link_to` helper. You should
+instead rely on a data attribute (e.g. `data: { confirm: 'Are you sure?' }`).
+This deprecation also concerns the helpers based on this one (such as `link_to_if`
+or `link_to_unless`).
+
+* Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`.
+
+* Rails 4.0 raises an `ArgumentError` if clashing named routes are defined. This can be triggered by explicitly defined named routes or by the `resources` method. Here are two examples that clash with routes named `example_path`:
+
+```ruby
+ get 'one' => 'test#example', as: :example
+ get 'two' => 'test#example', as: :example
+```
+
+```ruby
+ resources :examples
+ get 'clashing/:id' => 'test#example', as: :example
+```
-Rails 4.0 changed how `assert_generates`, `assert_recognizes`, and `assert_routing` work. Now all these assertions raise `Assertion` instead of `ActionController::RoutingError`.
+In the first case, you can simply avoid using the same name for multiple
+routes. In the second, you can use the `only` or `except` options provided by
+the `resources` method to restrict the routes created as detailed in the
+[Routing Guide](routing.html#restricting-the-routes-created).
-Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example:
+* Rails 4.0 also changed the way unicode character routes are drawn. Now you can draw unicode character routes directly. If you already draw such routes, you must change them, for example:
```ruby
-get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯'), :controller => 'welcome', :action => 'index'
+get Rack::Utils.escape('ã“ã‚“ã«ã¡ã¯'), controller: 'welcome', action: 'index'
```
becomes
```ruby
-get 'ã“ã‚“ã«ã¡ã¯', :controller => 'welcome', :action => 'index'
+get 'ã“ã‚“ã«ã¡ã¯', controller: 'welcome', action: 'index'
```
+* Rails 4.0 requires that routes using `match` must specify the request method. For example:
+
+```ruby
+ # Rails 3.x
+ match "/" => "root#index"
+
+ # becomes
+ match "/" => "root#index", via: :get
+
+ # or
+ get "/" => "root#index"
+```
+
+* Rails 4.0 has removed `ActionDispatch::BestStandardsSupport` middleware, `<!DOCTYPE html>` already triggers standards mode per http://msdn.microsoft.com/en-us/library/jj676915(v=vs.85).aspx and ChromeFrame header has been moved to `config.action_dispatch.default_headers`.
+
+Remember you must also remove any references to the middleware from your application code, for example:
+
+```ruby
+# Raise exception
+config.middleware.insert_before(Rack::Lock, ActionDispatch::BestStandardsSupport)
+```
+
+Also check your environment settings for `config.action_dispatch.best_standards_support` and remove it if present.
+
+* In Rails 4.0, precompiling assets no longer automatically copies non-JS/CSS assets from `vendor/assets` and `lib/assets`. Rails application and engine developers should put these assets in `app/assets` or configure `config.assets.precompile`.
+
+* In Rails 4.0, `ActionController::UnknownFormat` is raised when the action doesn't handle the request format. By default, the exception is handled by responding with 406 Not Acceptable, but you can override that now. In Rails 3, 406 Not Acceptable was always returned. No overrides.
+
+* In Rails 4.0, a generic `ActionDispatch::ParamsParser::ParseError` exception is raised when `ParamsParser` fails to parse request params. You will want to rescue this exception instead of the low-level `MultiJson::DecodeError`, for example.
+
+* In Rails 4.0, `SCRIPT_NAME` is properly nested when engines are mounted on an app that's served from a URL prefix. You no longer have to set `default_url_options[:script_name]` to work around overwritten URL prefixes.
+
+* Rails 4.0 deprecated `ActionController::Integration` in favor of `ActionDispatch::Integration`.
+* Rails 4.0 deprecated `ActionController::IntegrationTest` in favor of `ActionDispatch::IntegrationTest`.
+* Rails 4.0 deprecated `ActionController::PerformanceTest` in favor of `ActionDispatch::PerformanceTest`.
+* Rails 4.0 deprecated `ActionController::AbstractRequest` in favor of `ActionDispatch::Request`.
+* Rails 4.0 deprecated `ActionController::Request` in favor of `ActionDispatch::Request`.
+* Rails 4.0 deprecated `ActionController::AbstractResponse` in favor of `ActionDispatch::Response`.
+* Rails 4.0 deprecated `ActionController::Response` in favor of `ActionDispatch::Response`.
+* Rails 4.0 deprecated `ActionController::Routing` in favor of `ActionDispatch::Routing`.
+
### Active Support
-Rails 4.0 Removed the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`.
+Rails 4.0 removes the `j` alias for `ERB::Util#json_escape` since `j` is already used for `ActionView::Helpers::JavaScriptHelper#escape_javascript`.
### Helpers Loading Order
-The loading order of helpers from more than one directory has changed in Rails 4.0. Previously, helpers from all directories were gathered and then sorted alphabetically. After upgrade to Rails 4.0 helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use `helpers_path` parameter, this change will only impact the way of loading helpers from engines. If you rely on the fact that particular helper from engine loads before or after another helper from application or another engine, you should check if correct methods are available after upgrade. If you would like to change order in which engines are loaded, you can use `config.railties_order=` method.
+The order in which helpers from more than one directory are loaded has changed in Rails 4.0. Previously, they were gathered and then sorted alphabetically. After upgrading to Rails 4.0, helpers will preserve the order of loaded directories and will be sorted alphabetically only within each directory. Unless you explicitly use the `helpers_path` parameter, this change will only impact the way of loading helpers from engines. If you rely on the ordering, you should check if correct methods are available after upgrade. If you would like to change the order in which engines are loaded, you can use `config.railties_order=` method.
+
+### Active Record Observer and Action Controller Sweeper
+
+Active Record Observer and Action Controller Sweeper have been extracted to the `rails-observers` gem. You will need to add the `rails-observers` gem if you require these features.
+
+### 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:
+
+```ruby
+config.assets.js_compressor = :uglifier
+```
+
+### sass-rails
+
+* `asset-url` with two arguments is deprecated. For example: `asset-url("rails.png", image)` becomes `asset-url("rails.png")`
Upgrading from Rails 3.1 to Rails 3.2
-------------------------------------
If your application is currently on any version of Rails older than 3.1.x, you should upgrade to Rails 3.1 before attempting an update to Rails 3.2.
-The following changes are meant for upgrading your application to Rails 3.2.2, the latest 3.2.x version of Rails.
+The following changes are meant for upgrading your application to Rails 3.2.12, the latest 3.2.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.2.2'
+gem 'rails', '= 3.2.12'
group :assets do
gem 'sass-rails', '~> 3.2.3'
@@ -150,14 +393,14 @@ Upgrading from Rails 3.0 to Rails 3.1
If your application is currently on any version of Rails older than 3.0.x, you should upgrade to Rails 3.0 before attempting an update to Rails 3.1.
-The following changes are meant for upgrading your application to Rails 3.1.3, the latest 3.1.x version of Rails.
+The following changes are meant for upgrading your application to Rails 3.1.11, the latest 3.1.x version of Rails.
### Gemfile
Make the following changes to your `Gemfile`.
```ruby
-gem 'rails', '= 3.1.3'
+gem 'rails', '= 3.1.11'
gem 'mysql2'
# Needed for the new asset pipeline
@@ -246,7 +489,7 @@ Add this file with the following contents, if you wish to wrap parameters into a
# 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]
+ wrap_parameters format: [:json]
end
# Disable root element in JSON by default.
@@ -261,7 +504,7 @@ You need to change your session key to something new, or remove all sessions:
```ruby
# in config/initializers/session_store.rb
-AppName::Application.config.session_store :cookie_store, :key => 'SOMETHINGNEW'
+AppName::Application.config.session_store :cookie_store, key: 'SOMETHINGNEW'
```
or
@@ -269,3 +512,7 @@ or
```bash
$ rake db:sessions:clear
```
+
+### Remove :cache and :concat options in asset helpers references in views
+
+* With the Asset Pipeline the :cache and :concat options aren't used anymore, delete these options from your views.
diff --git a/guides/source/working_with_javascript_in_rails.md b/guides/source/working_with_javascript_in_rails.md
new file mode 100644
index 0000000000..301e0e7e6c
--- /dev/null
+++ b/guides/source/working_with_javascript_in_rails.md
@@ -0,0 +1,395 @@
+Working with JavaScript in Rails
+================================
+
+This guide covers the built-in Ajax/JavaScript functionality of Rails (and
+more); it will enable you to create rich and dynamic Ajax applications with
+ease!
+
+After reading this guide, you will know:
+
+* The basics of Ajax.
+* Unobtrusive JavaScript.
+* How Rails' built-in helpers assist you.
+* How to handle Ajax on the server side.
+* The Turbolinks gem.
+
+-------------------------------------------------------------------------------
+
+An Introduction to Ajax
+------------------------
+
+In order to understand Ajax, you must first understand what a web browser does
+normally.
+
+When you type `http://localhost:3000` into your browser's address bar and hit
+'Go,' the browser (your 'client') makes a request to the server. It parses the
+response, then fetches all associated assets, like JavaScript files,
+stylesheets and images. It then assembles the page. If you click a link, it
+does the same process: fetch the page, fetch the assets, put it all together,
+show you the results. This is called the 'request response cycle.'
+
+JavaScript can also make requests to the server, and parse the response. It
+also has the ability to update information on the page. Combining these two
+powers, a JavaScript writer can make a web page that can update just parts of
+itself, without needing to get the full page data from the server. This is a
+powerful technique that we call Ajax.
+
+Rails ships with CoffeeScript by default, and so the rest of the examples
+in this guide will be in CoffeeScript. All of these lessons, of course, apply
+to vanilla JavaScript as well.
+
+As an example, here's some CoffeeScript code that makes an Ajax request using
+the jQuery library:
+
+```coffeescript
+$.ajax(url: "/test").done (html) ->
+ $("#results").append html
+```
+
+This code fetches data from "/test", and then appends the result to the `div`
+with an id of `results`.
+
+Rails provides quite a bit of built-in support for building web pages with this
+technique. You rarely have to write this code yourself. The rest of this guide
+will show you how Rails can help you write websites in this way, but it's
+all built on top of this fairly simple technique.
+
+Unobtrusive JavaScript
+-------------------------------------
+
+Rails uses a technique called "Unobtrusive JavaScript" to handle attaching
+JavaScript to the DOM. This is generally considered to be a best-practice
+within the frontend community, but you may occasionally read tutorials that
+demonstrate other ways.
+
+Here's the simplest way to write JavaScript. You may see it referred to as
+'inline JavaScript':
+
+```html
+<a href="#" onclick="this.style.backgroundColor='#990000'">Paint it red</a>
+```
+When clicked, the link background will become red. Here's the problem: what
+happens when we have lots of JavaScript we want to execute on a click?
+
+```html
+<a href="#" onclick="this.style.backgroundColor='#009900';this.style.color='#FFFFFF';">Paint it green</a>
+```
+
+Awkward, right? We could pull the function definition out of the click handler,
+and turn it into CoffeeScript:
+
+```coffeescript
+paintIt = (element, backgroundColor, textColor) ->
+ element.style.backgroundColor = backgroundColor
+ if textColor?
+ element.style.color = textColor
+```
+
+And then on our page:
+
+```html
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+```
+
+That's a little bit better, but what about multiple links that have the same
+effect?
+
+```html
+<a href="#" onclick="paintIt(this, '#990000')">Paint it red</a>
+<a href="#" onclick="paintIt(this, '#009900', '#FFFFFF')">Paint it green</a>
+<a href="#" onclick="paintIt(this, '#000099', '#FFFFFF')">Paint it blue</a>
+```
+
+Not very DRY, eh? We can fix this by using events instead. We'll add a `data-*`
+attribute to our link, and then bind a handler to the click event of every link
+that has that attribute:
+
+```coffeescript
+paintIt = (element, backgroundColor, textColor) ->
+ element.style.backgroundColor = backgroundColor
+ if textColor?
+ element.style.color = textColor
+
+$ ->
+ $("a[data-background-color]").click ->
+ backgroundColor = $(this).data("background-color")
+ textColor = $(this).data("text-color")
+ paintIt(this, backgroundColor, textColor)
+```
+```html
+<a href="#" data-background-color="#990000">Paint it red</a>
+<a href="#" data-background-color="#009900" data-text-color="#FFFFFF">Paint it green</a>
+<a href="#" data-background-color="#000099" data-text-color="#FFFFFF">Paint it blue</a>
+```
+
+We call this 'unobtrusive' JavaScript because we're no longer mixing our
+JavaScript into our HTML. We've properly separated our concerns, making future
+change easy. We can easily add behavior to any link by adding the data
+attribute. We can run all of our JavaScript through a minimizer and
+concatenator. We can serve our entire JavaScript bundle on every page, which
+means that it'll get downloaded on the first page load and then be cached on
+every page after that. Lots of little benefits really add up.
+
+The Rails team strongly encourages you to write your CoffeeScript (and
+JavaScript) in this style, and you can expect that many libraries will also
+follow this pattern.
+
+Built-in Helpers
+----------------------
+
+Rails provides a bunch of view helper methods written in Ruby to assist you
+in generating HTML. Sometimes, you want to add a little Ajax to those elements,
+and Rails has got your back in those cases.
+
+Because of Unobtrusive JavaScript, the Rails "Ajax helpers" are actually in two
+parts: the JavaScript half and the Ruby half.
+
+[rails.js](https://github.com/rails/jquery-ujs/blob/master/src/rails.js)
+provides the JavaScript half, and the regular Ruby view helpers add appropriate
+tags to your DOM. The CoffeeScript in rails.js then listens for these
+attributes, and attaches appropriate handlers.
+
+### form_for
+
+[`form_for`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormHelper.html#method-i-form_for)
+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| %>
+ ...
+<% end %>
+```
+
+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>
+```
+
+Note the `data-remote='true'`. Now, the form will be submitted by Ajax rather
+than by the browser's normal submit mechanism.
+
+You probably don't want to just sit there with a filled out `<form>`, though.
+You probably want to do something upon a successful submission. To do that,
+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>"
+```
+
+Obviously, you'll want to be a bit more sophisticated than that, but it's a
+start. You can see more about the events [in the jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki/ajax).
+
+### form_tag
+
+[`form_tag`](http://api.rubyonrails.org/classes/ActionView/Helpers/FormTagHelper.html#method-i-form_tag)
+is very similar to `form_for`. It has a `:remote` option that you can use like
+this:
+
+```erb
+<%= form_tag('/posts', remote: true) %>
+```
+
+Everything else is the same as `form_for`. See its documentation for full
+details.
+
+### link_to
+
+[`link_to`](http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to)
+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 %>
+```
+
+which generates
+
+```html
+<a href="/posts/1" data-remote="true">a post</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
+click. We would generate some HTML like this:
+
+```erb
+<%= link_to "Delete post", @post, remote: true, method: :delete %>
+```
+
+and write some CoffeeScript like this:
+
+```coffeescript
+$ ->
+ $("a[data-remote]").on "ajax:success", (e, data, status, xhr) ->
+ alert "The post was deleted."
+```
+
+### button_to
+
+[`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 %>
+```
+
+this generates
+
+```html
+<form action="/posts/1" class="button_to" data-remote="true" method="post">
+ <div><input type="submit" value="A post"></div>
+</form>
+```
+
+Since it's just a `<form>`, all of the information on `form_for` also applies.
+
+Server-Side Concerns
+--------------------
+
+Ajax isn't just client-side, you also need to do some work on the server
+side to support it. Often, people like their Ajax requests to return JSON
+rather than HTML. Let's discuss what it takes to make that happen.
+
+### A Simple Example
+
+Imagine you have a series of users that you would like to display and provide a
+form on that same page to create a new user. The index action of your
+controller looks like this:
+
+```ruby
+class UsersController < ApplicationController
+ def index
+ @users = User.all
+ @user = User.new
+ end
+ # ...
+```
+
+The index view (`app/views/users/index.html.erb`) contains:
+
+```erb
+<b>Users</b>
+
+<ul id="users">
+<%= render @users %>
+</ul>
+
+<br>
+
+<%= form_for(@user, remote: true) do |f| %>
+ <%= f.label :name %><br>
+ <%= f.text_field :name %>
+ <%= f.submit %>
+<% end %>
+```
+
+The `app/views/users/_user.html.erb` partial contains the following:
+
+```erb
+<li><%= user.name %></li>
+```
+
+The top portion of the index page displays the users. The bottom portion
+provides a form to create a new user.
+
+The bottom form will call the create action on the Users controller. Because
+the form's remote option is set to true, the request will be posted to the
+users controller as an Ajax request, looking for JavaScript. In order to
+service that request, the create action of your controller would look like
+this:
+
+```ruby
+ # app/controllers/users_controller.rb
+ # ......
+ def create
+ @user = User.new(params[:user])
+
+ respond_to do |format|
+ if @user.save
+ format.html { redirect_to @user, notice: 'User was successfully created.' }
+ format.js {}
+ format.json { render json: @user, status: :created, location: @user }
+ else
+ format.html { render action: "new" }
+ format.json { render json: @user.errors, status: :unprocessable_entity }
+ end
+ end
+ end
+```
+
+Notice the format.js in the `respond_to` block; that allows the controller to
+respond to your Ajax request. You then have a corresponding
+`app/views/users/create.js.erb` view file that generates the actual JavaScript
+code that will be sent and executed on the client side.
+
+```erb
+$("<%= escape_javascript(render @user) %>").appendTo("#users");
+```
+
+Turbolinks
+----------
+
+Rails 4 ships with the [Turbolinks gem](https://github.com/rails/turbolinks).
+This gem uses Ajax to speed up page rendering in most applications.
+
+### How Turbolinks Works
+
+Turbolinks attaches a click handler to all `<a>` on the page. If your browser
+supports
+[PushState](https://developer.mozilla.org/en-US/docs/DOM/Manipulating_the_browser_history#The_pushState(\).C2.A0method),
+Turbolinks will make an Ajax request for the page, parse the response, and
+replace the entire `<body>` of the page with the `<body>` of the response. It
+will then use PushState to change the URL to the correct one, preserving
+refresh semantics and giving you pretty URLs.
+
+The only thing you have to do to enable Turbolinks is have it in your Gemfile,
+and put `//= require turbolinks` in your CoffeeScript manifest, which is usually
+`app/assets/javascripts/application.js`.
+
+If you want to disable Turbolinks for certain links, add a `data-no-turbolink`
+attribute to the tag:
+
+```html
+<a href="..." data-no-turbolink>No turbolinks here</a>.
+```
+
+### Page Change Events
+
+When writing CoffeeScript, you'll often want to do some sort of processing upon
+page load. With jQuery, you'd write something like this:
+
+```coffeescript
+$(document).ready ->
+ alert "page has loaded!"
+```
+
+However, because Turbolinks overrides the normal page loading process, the
+event that this relies on will not be fired. If you have code that looks like
+this, you must change your code to do this instead:
+
+```coffeescript
+$(document).on "page:change", ->
+ alert "page has loaded!"
+```
+
+For more details, including other events you can bind to, check out [the
+Turbolinks
+README](https://github.com/rails/turbolinks/blob/master/README.md).
+
+Other Resources
+---------------
+
+Here are some helpful links to help you learn even more:
+
+* [jquery-ujs wiki](https://github.com/rails/jquery-ujs/wiki)
+* [jquery-ujs list of external articles](https://github.com/rails/jquery-ujs/wiki/External-articles)
+* [Rails 3 Remote Links and Forms: A Definitive Guide](http://www.alfajango.com/blog/rails-3-remote-links-and-forms/)
+* [Railscasts: Unobtrusive JavaScript](http://railscasts.com/episodes/205-unobtrusive-javascript)
+* [Railscasts: Turbolinks](http://railscasts.com/episodes/390-turbolinks)
diff --git a/install.rb b/install.rb
index b87b008c2e..bff8fee934 100644
--- a/install.rb
+++ b/install.rb
@@ -1,11 +1,16 @@
version = ARGV.pop
-%w( activesupport activemodel activerecord actionpack actionmailer railties ).each do |framework|
+if version.nil?
+ puts "Usage: ruby install.rb version"
+ exit(64)
+end
+
+%w( activesupport activemodel activerecord actionpack actionview actionmailer railties ).each do |framework|
puts "Installing #{framework}..."
`cd #{framework} && gem build #{framework}.gemspec && gem install #{framework}-#{version}.gem --no-ri --no-rdoc && rm #{framework}-#{version}.gem`
end
-puts "Installing Rails..."
+puts "Installing rails..."
`gem build rails.gemspec`
`gem install rails-#{version}.gem --no-ri --no-rdoc `
`rm rails-#{version}.gem`
diff --git a/rails.gemspec b/rails.gemspec
index 3600882c5c..d1c199a97a 100644
--- a/rails.gemspec
+++ b/rails.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../RAILS_VERSION",__FILE__)).strip
+version = File.read(File.expand_path('../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -8,22 +8,24 @@ Gem::Specification.new do |s|
s.description = 'Ruby on Rails is a full-stack web framework optimized for programmer happiness and sustainable productivity. It encourages beautiful code by favoring convention over configuration.'
s.required_ruby_version = '>= 1.9.3'
- s.required_rubygems_version = ">= 1.8.11"
- s.license = 'MIT'
+ s.required_rubygems_version = '>= 1.8.11'
+
+ s.license = 'MIT'
s.author = 'David Heinemeier Hansson'
s.email = 'david@loudthinking.com'
s.homepage = 'http://www.rubyonrails.org'
- s.bindir = 'bin'
- s.executables = []
- s.files = Dir['guides/**/*']
+ s.files = ['README.md'] + Dir['guides/**/*']
+
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'actionpack', version
+ s.add_dependency 'actionview', version
+ s.add_dependency 'activemodel', version
+ s.add_dependency 'activerecord', version
+ s.add_dependency 'actionmailer', version
+ s.add_dependency 'railties', version
- s.add_dependency('activesupport', version)
- s.add_dependency('actionpack', version)
- s.add_dependency('activerecord', version)
- s.add_dependency('actionmailer', version)
- s.add_dependency('railties', version)
- s.add_dependency('bundler', '~> 1.2')
- s.add_dependency('sprockets-rails', '~> 1.0')
+ s.add_dependency 'bundler', '>= 1.3.0', '< 2.0'
+ s.add_dependency 'sprockets-rails', '~> 2.0.0'
end
diff --git a/railties/CHANGELOG.md b/railties/CHANGELOG.md
index ba9b24c6c6..8963f6e604 100644
--- a/railties/CHANGELOG.md
+++ b/railties/CHANGELOG.md
@@ -1,114 +1,142 @@
-## Rails 4.0.0 (unreleased) ##
+* Removal of all javascript stuff (gems and files) when generating a new
+ application using the `--skip-javascript` option.
-* Add `.rake` to list of file extensions included by `rake notes` and `rake notes:custom`. *Brent J. Nordquist*
+ *Robin Dupret*
-* New test locations `test/models`, `test/helpers`, `test/controllers`, and
- `test/mailers`. Corresponding rake tasks added as well. *Mike Moore*
+* Make the application name snake cased when it contains spaces
-* Set a different cache per environment for assets pipeline
- through `config.assets.cache`.
+ 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.
- *Guillermo Iguaran*
+ *Robin Dupret*
-* `Rails.public_path` now returns a Pathname object. *Prem Sichanugrist*
+* Added `--model-name` option to `ScaffoldControllerGenerator`.
-* Remove highly uncommon `config.assets.manifest` option for moving the manifest path.
- This option is now unsupported in sprockets-rails.
+ *yalab*
- *Guillermo Iguaran & Dmitry Vorotilin*
+* Expose MiddlewareStack#unshift to environment configuration.
-* Add `config.action_controller.permit_all_parameters` to disable
- StrongParameters protection, it's false by default.
+ *Ben Pickles*
- *Guillermo Iguaran*
+* Include `web-console` into newly generated applications' Gemfile.
-* Remove `config.active_record.whitelist_attributes` and
- `config.active_record.mass_assignment_sanitizer` from new applications since
- MassAssignmentSecurity has been extracted from Rails.
+ *Genadi Samokovarov*
- *Guillermo Iguaran*
+* `rails server` will only extend the logger to output to STDOUT
+ in development environment.
-* Change `rails new` and `rails plugin new` generators to name the `.gitkeep` files
- as `.keep` in a more SCM-agnostic way.
+ *Richard Schneeman*
- Change `--skip-git` option to only skip the `.gitignore` file and still generate
- the `.keep` files.
+* Don't require passing path to app before options in `rails new`
+ and `rails plugin new`
- Add `--skip-keeps` option to skip the `.keep` files.
+ *Piotr Sarnacki*
- *Derek Prior & Francesco Rodriguez*
+* rake notes now searches *.less files
-* Fixed support for DATABASE_URL environment variable for rake db tasks. *Grace Liu*
+ *Josh Crowder*
-* rails dbconsole now can use SSL for MySQL. The database.yml options sslca, sslcert, sslcapath, sslcipher,
- and sslkey now affect rails dbconsole. *Jim Kingdon and Lars Petrus*
+* Generate nested route for namespaced controller generated using
+ `rails g controller`.
+ Fixes #11532.
-* Correctly handle SCRIPT_NAME when generating routes to engine in application
- that's mounted at a sub-uri. With this behavior, you *should not* use
- default_url_options[:script_name] to set proper application's mount point by
- yourself. *Piotr Sarnacki*
+ Example:
-* `config.threadsafe!` is deprecated in favor of `config.eager_load` which provides a more fine grained control on what is eager loaded *José Valim*
+ rails g controller admin/dashboard index
-* The migration generator will now produce AddXXXToYYY/RemoveXXXFromYYY migrations with references statements, for instance
+ # Before:
+ get "dashboard/index"
- rails g migration AddReferencesToProducts user:references supplier:references{polymorphic}
+ # After:
+ namespace :admin do
+ get "dashboard/index"
+ end
- will generate the migration with:
+ *Prathamesh Sonpatki*
- add_reference :products, :user, index: true
- add_reference :products, :supplier, polymorphic: true, index: true
+* Fix the event name of action_dispatch requests.
- *Aleksey Magusev*
+ *Rafael Mendonça França*
-* Allow scaffold/model/migration generators to accept a `polymorphic` modifier
- for `references`/`belongs_to`, for instance
+* Make `config.log_level` work with custom loggers.
- rails g model Product supplier:references{polymorphic}
+ *Max Shytikov*
- will generate the model with `belongs_to :supplier, polymorphic: true`
- association and appropriate migration.
+* Changed stylesheet load order in the stylesheet manifest generator.
+ Fixes #11639.
- *Aleksey Magusev*
+ *Pawel Janiak*
-* Set `config.active_record.migration_error` to `:page_load` for development *Richard Schneeman*
+* Added generated unit test for generator generator using new
+ `test:generators` rake task.
-* Add runner to Rails::Railtie as a hook called just after runner starts. *José Valim & kennyj*
+ *Josef Šimánek*
-* Add `/rails/info/routes` path, displays same information as `rake routes` *Richard Schneeman & Andrew White*
+* Removed `update:application_controller` rake task.
-* Improved `rake routes` output for redirects *Åukasz StrzaÅ‚kowski & Andrew White*
+ *Josef Šimánek*
-* Load all environments available in `config.paths["config/environments"]`. *Piotr Sarnacki*
+* Fix `rake environment` to do not eager load modules
-* Add `config.queue_consumer` to change the job queue consumer from the default `ActiveSupport::ThreadedQueueConsumer`. *Carlos Antonio da Silva*
+ *Paul Nikitochkin*
-* Add `Rails.queue` for processing jobs in the background. *Yehuda Katz*
+* Fix `rake notes` to look into `*.sass` files
-* Remove Rack::SSL in favour of ActionDispatch::SSL. *Rafael Mendonça França*
+ *Yuri Artemev*
-* Remove Active Resource from Rails framework. *Prem Sichangrist*
+* Removed deprecated `Rails.application.railties.engines`.
-* Allow to set class that will be used to run as a console, other than IRB, with `Rails.application.config.console=`. It's best to add it to `console` block. *Piotr Sarnacki*
+ *Arun Agrawal*
- Example:
+* Removed deprecated threadsafe! from Rails Config.
- # it can be added to config/application.rb
- console do
- # this block is called only when running console,
- # so we can safely require pry here
- require "pry"
- config.console = Pry
- end
+ *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
+
+ *Vipul A M*
+
+* 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`.
+
+ *John Wang*
+
+* Clearing autoloaded constants triggers routes reloading.
+ Fixes #10685.
+
+ *Xavier Noria*
+
+* Fixes bug with scaffold generator with `--assets=false --resource-route=false`.
+ Fixes #9525.
+
+ *Arun Agrawal*
-* Add convenience `hide!` method to Rails generators to hide current generator
- namespace from showing when running `rails generate`. *Carlos Antonio da Silva*
+* 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.
-* Scaffold now uses `content_tag_for` in index.html.erb *José Valim*
+ *John Wang*
-* Rails::Plugin has gone. Instead of adding plugins to vendor/plugins use gems or bundler with path or git dependencies. *Santiago Pastorino*
+* Changes repetitive th tags to use colspan attribute in `index.html.erb` template.
-* Set config.action_mailer.async = true to turn on asynchronous
- message delivery *Brian Cardarella*
+ *Sıtkı Bağdat*
-Please check [3-2-stable](https://github.com/rails/rails/blob/3-2-stable/railties/CHANGELOG.md) for previous changes.
+Please check [4-0-stable](https://github.com/rails/rails/blob/4-0-stable/railties/CHANGELOG.md) for previous changes.
diff --git a/railties/MIT-LICENSE b/railties/MIT-LICENSE
index 03bde18130..0d7fb865e2 100644
--- a/railties/MIT-LICENSE
+++ b/railties/MIT-LICENSE
@@ -1,4 +1,4 @@
-Copyright (c) 2004-2012 David Heinemeier Hansson
+Copyright (c) 2004-2013 David Heinemeier Hansson
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
diff --git a/railties/RDOC_MAIN.rdoc b/railties/RDOC_MAIN.rdoc
new file mode 100644
index 0000000000..eccdee7b07
--- /dev/null
+++ b/railties/RDOC_MAIN.rdoc
@@ -0,0 +1,73 @@
+== 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.
+
+Understanding the MVC pattern is key to understanding \Rails. MVC divides your application
+into three layers, each with a specific responsibility.
+
+The View layer is composed of "templates" that are responsible for providing
+appropriate representations of your application's resources. Templates
+can come in a variety of formats, but most view templates are \HTML with embedded Ruby
+code (.erb files).
+
+The Model layer represents your domain model (such as Account, Product, Person, Post)
+and encapsulates the business logic that is specific to your application. In \Rails,
+database-backed model classes are derived from ActiveRecord::Base. Active Record allows
+you to present the data from database rows as objects and embellish these data objects
+with business logic methods. Although most \Rails models are backed by a database, models
+can also be ordinary Ruby classes, or Ruby classes that implement a set of interfaces as
+provided by the ActiveModel module. You can read more about Active Record in its
+{README}[link:files/activerecord/README_rdoc.html].
+
+The Controller layer is responsible for handling incoming HTTP requests and providing a
+suitable response. Usually this means returning \HTML, but \Rails controllers can also
+generate XML, JSON, PDFs, mobile-specific views, and more. Controllers manipulate models
+and render view templates in order to generate the appropriate HTTP response.
+
+In \Rails, the Controller and View layers are handled together by Action Pack.
+These two layers are bundled in a single package due to their heavy interdependence.
+This is unlike the relationship between Active Record and Action Pack, which are
+independent. Each of these packages can be used independently outside of \Rails. You
+can read more about Action Pack in its {README}[link:files/actionpack/README_rdoc.html].
+
+== Getting Started
+
+1. Install \Rails at the command prompt if you haven't yet:
+
+ gem install rails
+
+2. At the command prompt, create a new \Rails application:
+
+ rails new myapp
+
+ where "myapp" is the application name.
+
+3. Change directory to +myapp+ and start the web server:
+
+ cd myapp; rails server
+
+ Run with <tt>--help</tt> or <tt>-h</tt> for options.
+
+4. Go to http://localhost:3000 and you'll see:
+
+ "Welcome aboard: You're riding Ruby on Rails!"
+
+5. Follow the guidelines to start developing your application. You may find the following resources handy:
+
+* The \README file created within your application.
+* {Getting Started with \Rails}[http://guides.rubyonrails.org/getting_started.html].
+* {Ruby on \Rails Tutorial}[http://ruby.railstutorial.org/ruby-on-rails-tutorial-book].
+* {Ruby on \Rails Guides}[http://guides.rubyonrails.org].
+* {The API Documentation}[http://api.rubyonrails.org].
+
+== Contributing
+
+We encourage you to contribute to Ruby on \Rails! Please check out the {Contributing to Rails
+guide}[http://edgeguides.rubyonrails.org/contributing_to_ruby_on_rails.html] for guidelines about how
+to proceed. {Join us}[http://contributors.rubyonrails.org]!
+
+
+== License
+
+Ruby on \Rails is released under the {MIT License}[http://www.opensource.org/licenses/MIT].
diff --git a/railties/Rakefile b/railties/Rakefile
index 8576275aea..a899d069b5 100644
--- a/railties/Rakefile
+++ b/railties/Rakefile
@@ -1,17 +1,16 @@
require 'rake/testtask'
require 'rubygems/package_task'
-require 'date'
-require 'rbconfig'
-
-
task :default => :test
+
+desc "Run all unit tests"
task :test => 'test:isolated'
namespace :test do
task :isolated do
- dir = ENV["TEST_DIR"] || "**"
- Dir["test/#{dir}/*_test.rb"].each do |file|
+ dirs = (ENV["TEST_DIR"] || ENV["TEST_DIRS"] || "**").split(",")
+ test_files = dirs.map { |dir| "test/#{dir}/*_test.rb" }
+ Dir[*test_files].each do |file|
next true if file.include?("fixtures")
dash_i = [
'test',
@@ -20,7 +19,7 @@ namespace :test do
"#{File.dirname(__FILE__)}/../actionpack/lib",
"#{File.dirname(__FILE__)}/../activemodel/lib"
]
- ruby "-I#{dash_i.join ':'}", file
+ ruby "-w", "-I#{dash_i.join ':'}", file
end
end
end
@@ -32,15 +31,6 @@ Rake::TestTask.new('test:regular') do |t|
t.verbose = true
end
-# Update spinoffs -------------------------------------------------------------------
-
-desc "Updates application README to the latest version Railties README"
-task :update_readme do
- readme = "lib/rails/generators/rails/app/templates/README"
- rm readme
- cp "./README.rdoc", readme
-end
-
# Generate GEM ----------------------------------------------------------------------------
spec = eval(File.read('railties.gemspec'))
@@ -51,7 +41,7 @@ end
# Publishing -------------------------------------------------------
-desc "Release to gemcutter"
+desc "Release to rubygems"
task :release => :package do
require 'rake/gemcutter'
Rake::Gemcutter::Tasks.new(spec).define
diff --git a/railties/bin/rails b/railties/bin/rails
index a1c4faaa73..c11d65a08e 100755
--- a/railties/bin/rails
+++ b/railties/bin/rails
@@ -1,6 +1,8 @@
#!/usr/bin/env ruby
-if File.exists?(File.join(File.expand_path('../../..', __FILE__), '.git'))
+git_path = File.join(File.expand_path('../../..', __FILE__), '.git')
+
+if File.exist?(git_path)
railties_path = File.expand_path('../../lib', __FILE__)
$:.unshift(railties_path)
end
diff --git a/railties/lib/rails.rb b/railties/lib/rails.rb
index dd51a6fb01..ea82327365 100644
--- a/railties/lib/rails.rb
+++ b/railties/lib/rails.rb
@@ -3,12 +3,13 @@ require 'rails/ruby_version_check'
require 'pathname'
require 'active_support'
+require 'active_support/dependencies/autoload'
require 'active_support/core_ext/kernel/reporting'
+require 'active_support/core_ext/module/delegation'
require 'active_support/core_ext/array/extract_options'
require 'rails/application'
require 'rails/version'
-require 'rails/deprecation'
require 'active_support/railtie'
require 'action_dispatch/railtie'
@@ -20,58 +21,22 @@ silence_warnings do
end
module Rails
- autoload :Info, 'rails/info'
- autoload :InfoController, 'rails/info_controller'
+ extend ActiveSupport::Autoload
+
+ autoload :Info
+ autoload :InfoController
+ autoload :WelcomeController
class << self
- def application
- @application ||= nil
- end
+ attr_accessor :application, :cache, :logger
- def application=(application)
- @application = application
- end
+ delegate :initialize!, :initialized?, to: :application
# The Configuration instance used to configure the Rails environment
def configuration
application.config
end
- # Rails.queue is the application's queue. You can push a job onto
- # the queue by:
- #
- # Rails.queue.push job
- #
- # A job is an object that responds to +run+. Queue consumers will
- # pop jobs off of the queue and invoke the queue's +run+ method.
- #
- # Note that depending on your queue implementation, jobs may not
- # be executed in the same process as they were created in, and
- # are never executed in the same thread as they were created in.
- #
- # If necessary, a queue implementation may need to serialize your
- # job for distribution to another process. The documentation of
- # your queue will specify the requirements for that serialization.
- def queue
- application.queue
- end
-
- def initialize!
- application.initialize!
- end
-
- def initialized?
- application.initialized?
- end
-
- def logger
- @logger ||= nil
- end
-
- def logger=(logger)
- @logger = logger
- end
-
def backtrace_cleaner
@backtrace_cleaner ||= begin
# Relies on Active Support, so we have to lazy load to postpone definition until AS has been loaded
@@ -92,14 +57,6 @@ module Rails
@_env = ActiveSupport::StringInquirer.new(environment)
end
- def cache
- @cache ||= nil
- end
-
- def cache=(cache)
- @cache = cache
- end
-
# Returns all rails groups for loading based on:
#
# * The Rails environment;
@@ -116,7 +73,7 @@ module Rails
env = Rails.env
groups.unshift(:default, env)
groups.concat ENV["RAILS_GROUPS"].to_s.split(",")
- groups.concat hash.map { |k,v| k if v.map(&:to_s).include?(env) }
+ groups.concat hash.map { |k, v| k if v.map(&:to_s).include?(env) }
groups.compact!
groups.uniq!
groups
diff --git a/railties/lib/rails/all.rb b/railties/lib/rails/all.rb
index eabe566829..2e83c0fe14 100644
--- a/railties/lib/rails/all.rb
+++ b/railties/lib/rails/all.rb
@@ -3,9 +3,10 @@ require "rails"
%w(
active_record
action_controller
+ action_view
action_mailer
rails/test_unit
- sprockets/rails
+ sprockets
).each do |framework|
begin
require "#{framework}/railtie"
diff --git a/railties/lib/rails/api/task.rb b/railties/lib/rails/api/task.rb
new file mode 100644
index 0000000000..3e32576040
--- /dev/null
+++ b/railties/lib/rails/api/task.rb
@@ -0,0 +1,163 @@
+require 'rdoc/task'
+
+module Rails
+ module API
+ class Task < RDoc::Task
+ RDOC_FILES = {
+ 'activesupport' => {
+ :include => %w(
+ README.rdoc
+ lib/active_support/**/*.rb
+ ),
+ :exclude => 'lib/active_support/vendor/*'
+ },
+
+ 'activerecord' => {
+ :include => %w(
+ README.rdoc
+ lib/active_record/**/*.rb
+ )
+ },
+
+ 'activemodel' => {
+ :include => %w(
+ README.rdoc
+ lib/active_model/**/*.rb
+ )
+ },
+
+ 'actionpack' => {
+ :include => %w(
+ README.rdoc
+ lib/abstract_controller/**/*.rb
+ lib/action_controller/**/*.rb
+ lib/action_dispatch/**/*.rb
+ )
+ },
+
+ 'actionview' => {
+ :include => %w(
+ README.rdoc
+ lib/action_view/**/*.rb
+ ),
+ :exclude => 'lib/action_view/vendor/*'
+ },
+
+ 'actionmailer' => {
+ :include => %w(
+ README.rdoc
+ lib/action_mailer/**/*.rb
+ )
+ },
+
+ 'railties' => {
+ :include => %w(
+ README.rdoc
+ lib/**/*.rb
+ ),
+ :exclude => 'lib/rails/generators/rails/**/templates/**/*.rb'
+ }
+ }
+
+ def initialize(name)
+ super
+
+ # Every time rake runs this task is instantiated as all the rest.
+ # Be lazy computing stuff to have as light impact as possible to
+ # the rest of tasks.
+ before_running_rdoc do
+ load_and_configure_sdoc
+ configure_rdoc_files
+ setup_horo_variables
+ end
+ end
+
+ # Hack, ignore the desc calls performed by the original initializer.
+ def desc(description)
+ # no-op
+ end
+
+ def load_and_configure_sdoc
+ require 'sdoc'
+
+ self.title = 'Ruby on Rails API'
+ self.rdoc_dir = api_dir
+
+ options << '-m' << api_main
+ options << '-e' << 'UTF-8'
+
+ options << '-f' << 'sdoc'
+ options << '-T' << 'rails'
+ rescue LoadError
+ $stderr.puts %(Unable to load SDoc, please add\n\n gem 'sdoc', require: false\n\nto the Gemfile.)
+ exit 1
+ end
+
+ def configure_rdoc_files
+ rdoc_files.include(api_main)
+
+ RDOC_FILES.each do |component, cfg|
+ cdr = component_root_dir(component)
+
+ Array(cfg[:include]).each do |pattern|
+ rdoc_files.include("#{cdr}/#{pattern}")
+ end
+
+ Array(cfg[:exclude]).each do |pattern|
+ rdoc_files.exclude("#{cdr}/#{pattern}")
+ end
+ end
+ end
+
+ def setup_horo_variables
+ ENV['HORO_PROJECT_NAME'] = 'Ruby on Rails'
+ ENV['HORO_PROJECT_VERSION'] = rails_version
+ end
+
+ def api_main
+ component_root_dir('railties') + '/RDOC_MAIN.rdoc'
+ end
+ end
+
+ class RepoTask < Task
+ def load_and_configure_sdoc
+ super
+ options << '-g' # link to GitHub, SDoc flag
+ end
+
+ def component_root_dir(component)
+ component
+ end
+
+ def api_dir
+ 'doc/rdoc'
+ end
+ end
+
+ class EdgeTask < RepoTask
+ def rails_version
+ "master@#{`git rev-parse HEAD`[0, 7]}"
+ end
+ end
+
+ class StableTask < RepoTask
+ def rails_version
+ File.read('RAILS_VERSION').strip
+ end
+ end
+
+ class AppTask < Task
+ def component_root_dir(gem_name)
+ $:.grep(%r{#{gem_name}[\w.-]*/lib\z}).first[0..-5]
+ end
+
+ def api_dir
+ 'doc/api'
+ end
+
+ def rails_version
+ Rails::VERSION::STRING
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/app_rails_loader.rb b/railties/lib/rails/app_rails_loader.rb
new file mode 100644
index 0000000000..1610751844
--- /dev/null
+++ b/railties/lib/rails/app_rails_loader.rb
@@ -0,0 +1,61 @@
+require 'pathname'
+
+module Rails
+ module AppRailsLoader
+ RUBY = Gem.ruby
+ EXECUTABLES = ['bin/rails', 'script/rails']
+ BUNDLER_WARNING = <<EOS
+Looks like your app's ./bin/rails is a stub that was generated by Bundler.
+
+In Rails 4, your app's bin/ directory contains executables that are versioned
+like any other source code, rather than stubs that are generated on demand.
+
+Here's how to upgrade:
+
+ bundle config --delete bin # Turn off Bundler's stub generator
+ rake rails:update:bin # Use the new Rails 4 executables
+ git add bin # Add bin/ to source control
+
+You may need to remove bin/ from your .gitignore as well.
+
+When you install a gem whose executable you want to use in your app,
+generate it and add it to source control:
+
+ bundle binstubs some-gem-name
+ git add bin/new-executable
+
+EOS
+
+ def self.exec_app_rails
+ original_cwd = Dir.pwd
+
+ loop do
+ if exe = find_executable
+ contents = File.read(exe)
+
+ if contents =~ /(APP|ENGINE)_PATH/
+ exec RUBY, exe, *ARGV
+ break # non reachable, hack to be able to stub exec in the test suite
+ elsif exe.end_with?('bin/rails') && contents.include?('This file was generated by Bundler')
+ $stderr.puts(BUNDLER_WARNING)
+ Object.const_set(:APP_PATH, File.expand_path('config/application', Dir.pwd))
+ require File.expand_path('../boot', APP_PATH)
+ require 'rails/commands'
+ break
+ end
+ end
+
+ # If we exhaust the search there is no executable, this could be a
+ # call to generate a new application, so restore the original cwd.
+ Dir.chdir(original_cwd) and return if Pathname.new(Dir.pwd).root?
+
+ # Otherwise keep moving upwards in search of an executable.
+ Dir.chdir('..')
+ end
+ end
+
+ def self.find_executable
+ EXECUTABLES.find { |exe| File.exist?(exe) }
+ end
+ end
+end
diff --git a/railties/lib/rails/application.rb b/railties/lib/rails/application.rb
index eff5bbbdd6..d1e88cfafd 100644
--- a/railties/lib/rails/application.rb
+++ b/railties/lib/rails/application.rb
@@ -1,5 +1,6 @@
require 'fileutils'
-require 'active_support/queueing'
+require 'active_support/core_ext/object/blank'
+require 'active_support/key_generator'
require 'rails/engine'
module Rails
@@ -45,43 +46,84 @@ module Rails
# 6) Run config.before_initialize callbacks
# 7) Run Railtie#initializer defined by railties, engines and application.
# One by one, each engine sets up its load paths, routes and runs its config/initializers/* files.
- # 9) Custom Railtie#initializers added by railties, engines and applications are executed
- # 10) Build the middleware stack and run to_prepare callbacks
- # 11) Run config.before_eager_load and eager_load! if eager_load is true
- # 12) Run config.after_initialize callbacks
+ # 8) Custom Railtie#initializers added by railties, engines and applications are executed
+ # 9) Build the middleware stack and run to_prepare callbacks
+ # 10) Run config.before_eager_load and eager_load! if eager_load is true
+ # 11) Run config.after_initialize callbacks
#
+ # == Multiple Applications
+ #
+ # If you decide to define multiple applications, then the first application
+ # that is initialized will be set to +Rails.application+, unless you override
+ # it with a different application.
+ #
+ # To create a new application, you can instantiate a new instance of a class
+ # that has already been created:
+ #
+ # class Application < Rails::Application
+ # end
+ #
+ # first_application = Application.new
+ # second_application = Application.new(config: first_application.config)
+ #
+ # In the above example, the configuration from the first application was used
+ # to initialize the second application. You can also use the +initialize_copy+
+ # on one of the applications to create a copy of the application which shares
+ # the configuration.
+ #
+ # If you decide to define rake tasks, runners, or initializers in an
+ # application other than +Rails.application+, then you must run those
+ # these manually.
class Application < Engine
- autoload :Bootstrap, 'rails/application/bootstrap'
- autoload :Configuration, 'rails/application/configuration'
- autoload :Finisher, 'rails/application/finisher'
- autoload :RoutesReloader, 'rails/application/routes_reloader'
+ autoload :Bootstrap, 'rails/application/bootstrap'
+ autoload :Configuration, 'rails/application/configuration'
+ autoload :DefaultMiddlewareStack, 'rails/application/default_middleware_stack'
+ autoload :Finisher, 'rails/application/finisher'
+ autoload :Railties, 'rails/engine/railties'
+ autoload :RoutesReloader, 'rails/application/routes_reloader'
class << self
def inherited(base)
- raise "You cannot have more than one Rails::Application" if Rails.application
super
- Rails.application = base.instance
- Rails.application.add_lib_to_load_path!
- ActiveSupport.run_load_hooks(:before_configuration, base.instance)
+ Rails.application ||= base.instance
end
+
+ # Makes the +new+ method public.
+ #
+ # Note that Rails::Application inherits from Rails::Engine, which
+ # inherits from Rails::Railtie and the +new+ method on Rails::Railtie is
+ # private
+ public :new
end
- attr_accessor :assets, :sandbox, :queue_consumer
+ attr_accessor :assets, :sandbox
alias_method :sandbox?, :sandbox
attr_reader :reloaders
- attr_writer :queue
delegate :default_url_options, :default_url_options=, to: :routes
- def initialize
- super
+ INITIAL_VARIABLES = [:config, :railties, :routes_reloader, :reloaders,
+ :routes, :helpers, :app_env_config] # :nodoc:
+
+ def initialize(initial_variable_values = {}, &block)
+ super()
@initialized = false
@reloaders = []
@routes_reloader = nil
- @env_config = nil
+ @app_env_config = nil
@ordered_railties = nil
@railties = nil
- @queue = nil
+
+ add_lib_to_load_path!
+ ActiveSupport.run_load_hooks(:before_configuration, self)
+
+ initial_variable_values.each do |variable_name, value|
+ if INITIAL_VARIABLES.include?(variable_name)
+ instance_variable_set("@#{variable_name}", value)
+ end
+ end
+
+ instance_eval(&block) if block_given?
end
# Returns true if the application is initialized.
@@ -93,6 +135,7 @@ module Rails
# dispatches the request to the underlying middleware stack.
def call(env)
env["ORIGINAL_FULLPATH"] = build_original_fullpath(env)
+ env["ORIGINAL_SCRIPT_NAME"] = env["SCRIPT_NAME"]
super(env)
end
@@ -101,37 +144,66 @@ module Rails
routes_reloader.reload!
end
-
# Return the application's KeyGenerator
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
- @key_generator ||= ActiveSupport::KeyGenerator.new(config.secret_token, iterations: 1000)
+ @caching_key_generator ||= begin
+ if config.secret_key_base
+ key_generator = ActiveSupport::KeyGenerator.new(config.secret_key_base, iterations: 1000)
+ ActiveSupport::CachingKeyGenerator.new(key_generator)
+ else
+ ActiveSupport::LegacyKeyGenerator.new(config.secret_token)
+ end
+ end
end
# Stores some of the Rails initial environment parameters which
# will be used by middlewares and engines to configure themselves.
- # Currently stores:
- #
- # * "action_dispatch.parameter_filter" => config.filter_parameters,
- # * "action_dispatch.secret_token" => config.secret_token,
- # * "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
- # * "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
- # * "action_dispatch.logger" => Rails.logger,
- # * "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner
- #
- # These parameters will be used by middlewares and engines to configure themselves
- #
def env_config
- @env_config ||= super.merge({
- "action_dispatch.parameter_filter" => config.filter_parameters,
- "action_dispatch.secret_token" => config.secret_token,
- "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
- "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
- "action_dispatch.logger" => Rails.logger,
- "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner,
- "action_dispatch.key_generator" => key_generator
- })
+ @app_env_config ||= begin
+ validate_secret_key_config!
+
+ super.merge({
+ "action_dispatch.parameter_filter" => config.filter_parameters,
+ "action_dispatch.redirect_filter" => config.filter_redirect,
+ "action_dispatch.secret_token" => config.secret_token,
+ "action_dispatch.secret_key_base" => config.secret_key_base,
+ "action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
+ "action_dispatch.show_detailed_exceptions" => config.consider_all_requests_local,
+ "action_dispatch.logger" => Rails.logger,
+ "action_dispatch.backtrace_cleaner" => Rails.backtrace_cleaner,
+ "action_dispatch.key_generator" => key_generator,
+ "action_dispatch.http_auth_salt" => config.action_dispatch.http_auth_salt,
+ "action_dispatch.signed_cookie_salt" => config.action_dispatch.signed_cookie_salt,
+ "action_dispatch.encrypted_cookie_salt" => config.action_dispatch.encrypted_cookie_salt,
+ "action_dispatch.encrypted_signed_cookie_salt" => config.action_dispatch.encrypted_signed_cookie_salt
+ })
+ end
+ end
+
+ # If you try to define a set of rake tasks on the instance, these will get
+ # passed up to the rake tasks defined on the application's class.
+ def rake_tasks(&block)
+ self.class.rake_tasks(&block)
+ end
+
+ # Sends the initializers to the +initializer+ method defined in the
+ # Rails::Initializable module. Each Rails::Application class has its own
+ # set of initializers, as defined by the Initializable module.
+ def initializer(name, opts={}, &block)
+ self.class.initializer(name, opts, &block)
+ end
+
+ # Sends any runner called in the instance of a new application up
+ # to the +runner+ method defined in Rails::Railtie.
+ def runner(&blk)
+ self.class.runner(&blk)
+ end
+
+ # Sends the +isolate_namespace+ method up to the class method.
+ def isolate_namespace(mod)
+ self.class.isolate_namespace(mod)
end
## Rails internal API
@@ -151,7 +223,9 @@ module Rails
# you need to load files in lib/ during the application configuration as well.
def add_lib_to_load_path! #:nodoc:
path = File.join config.root, 'lib'
- $LOAD_PATH.unshift(path) if File.exists?(path)
+ if File.exist?(path) && !$LOAD_PATH.include?(path)
+ $LOAD_PATH.unshift(path)
+ end
end
def require_environment! #:nodoc:
@@ -177,9 +251,7 @@ module Rails
end
# Initialize the application passing the given group. By default, the
- # group is :default but sprockets precompilation passes group equals
- # to assets if initialize_on_precompile is false to avoid booting the
- # whole app.
+ # group is :default
def initialize!(group=:default) #:nodoc:
raise "Application has been already initialized." if @initialized
run_initializers(group, self)
@@ -197,8 +269,8 @@ module Rails
@config ||= Application::Configuration.new(find_root_with_flag("config.ru", Dir.pwd))
end
- def queue #:nodoc:
- @queue ||= config.queue || ActiveSupport::Queue.new
+ def config=(configuration) #:nodoc:
+ @config = configuration
end
def to_app #:nodoc:
@@ -209,11 +281,6 @@ module Rails
config.helpers_paths
end
- def railties #:nodoc:
- @railties ||= Rails::Railtie.subclasses.map(&:instance) +
- Rails::Engine.subclasses.map(&:instance)
- end
-
protected
alias :build_middleware_stack :app
@@ -222,9 +289,9 @@ module Rails
railties.each { |r| r.run_tasks_blocks(app) }
super
require "rails/tasks"
- config = self.config
task :environment do
- config.eager_load = false
+ ActiveSupport.on_load(:before_initialize) { config.eager_load = false }
+
require_environment!
end
end
@@ -279,78 +346,9 @@ module Rails
initializers
end
- def reload_dependencies? #:nodoc:
- config.reload_classes_only_on_change != true || reloaders.map(&:updated?).any?
- end
-
def default_middleware_stack #:nodoc:
- ActionDispatch::MiddlewareStack.new.tap do |middleware|
- app = self
- if rack_cache = config.action_controller.perform_caching && config.action_dispatch.rack_cache
- begin
- require 'rack/cache'
- rescue LoadError => error
- error.message << ' Be sure to add rack-cache to your Gemfile'
- raise
- end
-
- if rack_cache == true
- rack_cache = {
- metastore: "rails:/",
- entitystore: "rails:/",
- verbose: false
- }
- end
-
- require "action_dispatch/http/rack_cache"
- middleware.use ::Rack::Cache, rack_cache
- end
-
- if config.force_ssl
- middleware.use ::ActionDispatch::SSL, config.ssl_options
- end
-
- if config.action_dispatch.x_sendfile_header.present?
- middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
- end
-
- if config.serve_static_assets
- middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
- end
-
- middleware.use ::Rack::Lock unless config.cache_classes
- middleware.use ::Rack::Runtime
- middleware.use ::Rack::MethodOverride
- middleware.use ::ActionDispatch::RequestId
- middleware.use ::Rails::Rack::Logger, config.log_tags # must come after Rack::MethodOverride to properly log overridden methods
- middleware.use ::ActionDispatch::ShowExceptions, config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
- middleware.use ::ActionDispatch::DebugExceptions, app
- middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
-
- unless config.cache_classes
- middleware.use ::ActionDispatch::Reloader, lambda { app.reload_dependencies? }
- end
-
- middleware.use ::ActionDispatch::Callbacks
- middleware.use ::ActionDispatch::Cookies
-
- if config.session_store
- if config.force_ssl && !config.session_options.key?(:secure)
- config.session_options[:secure] = true
- end
- middleware.use config.session_store, config.session_options
- middleware.use ::ActionDispatch::Flash
- end
-
- middleware.use ::ActionDispatch::ParamsParser
- middleware.use ::Rack::Head
- middleware.use ::Rack::ConditionalGet
- middleware.use ::Rack::ETag, "no-cache"
-
- if config.action_dispatch.best_standards_support
- middleware.use ::ActionDispatch::BestStandardsSupport, config.action_dispatch.best_standards_support
- end
- end
+ default_stack = DefaultMiddlewareStack.new(self, config, paths)
+ default_stack.build_stack
end
def build_original_fullpath(env) #:nodoc:
@@ -364,5 +362,11 @@ module Rails
"#{script_name}#{path_info}"
end
end
+
+ def validate_secret_key_config! #:nodoc:
+ if config.secret_key_base.blank? && config.secret_token.blank?
+ raise "You must set config.secret_key_base in your app's config."
+ end
+ end
end
end
diff --git a/railties/lib/rails/application/bootstrap.rb b/railties/lib/rails/application/bootstrap.rb
index 62d57c0cc6..a26d41c0cf 100644
--- a/railties/lib/rails/application/bootstrap.rb
+++ b/railties/lib/rails/application/bootstrap.rb
@@ -42,7 +42,6 @@ INFO
logger = ActiveSupport::Logger.new f
logger.formatter = config.log_formatter
logger = ActiveSupport::TaggedLogging.new(logger)
- logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
logger
rescue StandardError
logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDERR))
@@ -53,6 +52,8 @@ INFO
)
logger
end
+
+ Rails.logger.level = ActiveSupport::Logger.const_get(config.log_level.to_s.upcase)
end
# Initialize cache early in the stack so railties can make use of it.
diff --git a/railties/lib/rails/application/configuration.rb b/railties/lib/rails/application/configuration.rb
index c3e75a643b..dd0b9c6d70 100644
--- a/railties/lib/rails/application/configuration.rb
+++ b/railties/lib/rails/application/configuration.rb
@@ -1,19 +1,18 @@
require 'active_support/core_ext/kernel/reporting'
require 'active_support/file_update_checker'
-require 'active_support/queueing'
require 'rails/engine/configuration'
module Rails
class Application
class Configuration < ::Rails::Engine::Configuration
- attr_accessor :asset_host, :asset_path, :assets, :autoflush_log,
+ attr_accessor :allow_concurrency, :asset_host, :assets, :autoflush_log,
:cache_classes, :cache_store, :consider_all_requests_local, :console,
:eager_load, :exceptions_app, :file_watcher, :filter_parameters,
:force_ssl, :helpers_paths, :logger, :log_formatter, :log_tags,
- :railties_order, :relative_url_root, :secret_token,
+ :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,
- :queue, :queue_consumer, :beginning_of_week
+ :beginning_of_week, :filter_redirect
attr_writer :log_level
attr_reader :encoding
@@ -21,8 +20,10 @@ module Rails
def initialize(*)
super
self.encoding = "utf-8"
+ @allow_concurrency = nil
@consider_all_requests_local = false
@filter_parameters = []
+ @filter_redirect = []
@helpers_paths = []
@serve_static_assets = true
@static_cache_control = nil
@@ -43,31 +44,26 @@ module Rails
@exceptions_app = nil
@autoflush_log = true
@log_formatter = ActiveSupport::Logger::SimpleFormatter.new
- @queue = ActiveSupport::SynchronousQueue.new
- @queue_consumer = nil
@eager_load = nil
+ @secret_token = nil
+ @secret_key_base = nil
@assets = ActiveSupport::OrderedOptions.new
- @assets.enabled = false
+ @assets.enabled = true
@assets.paths = []
- @assets.precompile = [ Proc.new { |path| !%w(.js .css).include?(File.extname(path)) },
+ @assets.precompile = [ Proc.new { |path, fn| fn =~ /app\/assets/ && !%w(.js .css).include?(File.extname(path)) },
/(?:\/|\\|\A)application\.(css|js)$/ ]
@assets.prefix = "/assets"
- @assets.version = ''
+ @assets.version = '1.0'
@assets.debug = false
@assets.compile = true
@assets.digest = false
@assets.cache_store = [ :file_store, "#{root}/tmp/cache/assets/#{Rails.env}/" ]
@assets.js_compressor = nil
@assets.css_compressor = nil
- @assets.initialize_on_precompile = true
@assets.logger = nil
end
- def compiled_asset_path
- "/"
- end
-
def encoding=(value)
@encoding = value
silence_warnings do
@@ -91,21 +87,21 @@ module Rails
end
end
- def threadsafe!
- ActiveSupport::Deprecation.warn "config.threadsafe! is deprecated. Rails applications " \
- "behave by default as thread safe in production as long as config.cache_classes and " \
- "config.eager_load are set to true"
- @cache_classes = true
- @eager_load = true
- self
- end
-
- # Loads and returns the contents of the #database_configuration_file. The
- # contents of the file are processed via ERB before being sent through
- # YAML::load.
+ # Loads and returns the configuration of the database.
def database_configuration
- require 'erb'
- YAML.load ERB.new(IO.read(paths["config/database"].first)).result
+ yaml = paths["config/database"].first
+ if File.exist?(yaml)
+ require "erb"
+ YAML.load ERB.new(IO.read(yaml)).result
+ elsif ENV['DATABASE_URL']
+ nil
+ else
+ raise "Could not load database configuration. No such file - #{yaml}"
+ end
+ rescue Psych::SyntaxError => e
+ raise "YAML syntax error occurred while parsing #{paths["config/database"].first}. " \
+ "Please note that YAML must be consistently indented using spaces. Tabs are not allowed. " \
+ "Error: #{e.message}"
end
def log_level
@@ -144,10 +140,6 @@ module Rails
end
end
- def whiny_nils=(*)
- ActiveSupport::Deprecation.warn "config.whiny_nils option " \
- "is deprecated and no longer works", caller
- end
end
end
end
diff --git a/railties/lib/rails/application/default_middleware_stack.rb b/railties/lib/rails/application/default_middleware_stack.rb
new file mode 100644
index 0000000000..570ff02c83
--- /dev/null
+++ b/railties/lib/rails/application/default_middleware_stack.rb
@@ -0,0 +1,99 @@
+module Rails
+ class Application
+ class DefaultMiddlewareStack
+ attr_reader :config, :paths, :app
+
+ def initialize(app, config, paths)
+ @app = app
+ @config = config
+ @paths = paths
+ end
+
+ def build_stack
+ ActionDispatch::MiddlewareStack.new.tap do |middleware|
+ if rack_cache = load_rack_cache
+ require "action_dispatch/http/rack_cache"
+ middleware.use ::Rack::Cache, rack_cache
+ end
+
+ if config.force_ssl
+ middleware.use ::ActionDispatch::SSL, config.ssl_options
+ end
+
+ middleware.use ::Rack::Sendfile, config.action_dispatch.x_sendfile_header
+
+ if config.serve_static_assets
+ middleware.use ::ActionDispatch::Static, paths["public"].first, config.static_cache_control
+ end
+
+ middleware.use ::Rack::Lock unless allow_concurrency?
+ middleware.use ::Rack::Runtime
+ middleware.use ::Rack::MethodOverride
+ middleware.use ::ActionDispatch::RequestId
+
+ # Must come after Rack::MethodOverride to properly log overridden methods
+ middleware.use ::Rails::Rack::Logger, config.log_tags
+ middleware.use ::ActionDispatch::ShowExceptions, show_exceptions_app
+ middleware.use ::ActionDispatch::DebugExceptions, app
+ middleware.use ::ActionDispatch::RemoteIp, config.action_dispatch.ip_spoofing_check, config.action_dispatch.trusted_proxies
+
+ unless config.cache_classes
+ middleware.use ::ActionDispatch::Reloader, lambda { reload_dependencies? }
+ end
+
+ middleware.use ::ActionDispatch::Callbacks
+ middleware.use ::ActionDispatch::Cookies
+
+ if config.session_store
+ if config.force_ssl && !config.session_options.key?(:secure)
+ config.session_options[:secure] = true
+ end
+ middleware.use config.session_store, config.session_options
+ middleware.use ::ActionDispatch::Flash
+ end
+
+ middleware.use ::ActionDispatch::ParamsParser
+ middleware.use ::Rack::Head
+ middleware.use ::Rack::ConditionalGet
+ middleware.use ::Rack::ETag, "no-cache"
+ end
+ end
+
+ private
+
+ def reload_dependencies?
+ config.reload_classes_only_on_change != true || app.reloaders.map(&:updated?).any?
+ end
+
+ def allow_concurrency?
+ config.allow_concurrency.nil? ? config.cache_classes : config.allow_concurrency
+ end
+
+ def load_rack_cache
+ rack_cache = config.action_dispatch.rack_cache
+ return unless rack_cache
+
+ begin
+ require 'rack/cache'
+ rescue LoadError => error
+ error.message << ' Be sure to add rack-cache to your Gemfile'
+ raise
+ end
+
+ if rack_cache == true
+ {
+ metastore: "rails:/",
+ entitystore: "rails:/",
+ verbose: false
+ }
+ else
+ rack_cache
+ end
+ end
+
+ def show_exceptions_app
+ config.exceptions_app || ActionDispatch::PublicExceptions.new(Rails.public_path)
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/application/finisher.rb b/railties/lib/rails/application/finisher.rb
index c520f7af9d..7a1bb1e25c 100644
--- a/railties/lib/rails/application/finisher.rb
+++ b/railties/lib/rails/application/finisher.rb
@@ -25,6 +25,7 @@ module Rails
get '/rails/info/properties' => "rails/info#properties"
get '/rails/info/routes' => "rails/info#routes"
get '/rails/info' => "rails/info#index"
+ get '/' => "rails/welcome#index"
end
end
end
@@ -61,17 +62,28 @@ module Rails
ActiveSupport.run_load_hooks(:after_initialize, self)
end
- # Set app reload just after the finisher hook to ensure
- # routes added in the hook are still loaded.
+ # Set routes reload after the finisher hook to ensure routes added in
+ # the hook are taken into account.
initializer :set_routes_reloader_hook do
reloader = routes_reloader
reloader.execute_if_updated
self.reloaders << reloader
- ActionDispatch::Reloader.to_prepare { reloader.execute_if_updated }
+ ActionDispatch::Reloader.to_prepare do
+ # We configure #execute rather than #execute_if_updated because if
+ # autoloaded constants are cleared we need to reload routes also in
+ # case any was used there, as in
+ #
+ # mount MailPreview => 'mail_view'
+ #
+ # This means routes are also reloaded if i18n is updated, which
+ # might not be necessary, but in order to be more precise we need
+ # some sort of reloaders dependency support, to be added.
+ reloader.execute
+ end
end
- # Set app reload just after the finisher hook to ensure
- # paths added in the hook are still loaded.
+ # Set clearing dependencies after the finisher hook to ensure paths
+ # added in the hook are taken into account.
initializer :set_clear_dependencies_hook, group: :all do
callback = lambda do
ActiveSupport::DescendantsTracker.clear
@@ -81,28 +93,21 @@ module Rails
if config.reload_classes_only_on_change
reloader = config.file_watcher.new(*watchable_args, &callback)
self.reloaders << reloader
- # We need to set a to_prepare callback regardless of the reloader result, i.e.
- # models should be reloaded if any of the reloaders (i18n, routes) were updated.
- ActionDispatch::Reloader.to_prepare(prepend: true){ reloader.execute }
+
+ # Prepend this callback to have autoloaded constants cleared before
+ # any other possible reloading, in case they need to autoload fresh
+ # constants.
+ ActionDispatch::Reloader.to_prepare(prepend: true) do
+ # In addition to changes detected by the file watcher, if routes
+ # or i18n have been updated we also need to clear constants,
+ # that's why we run #execute rather than #execute_if_updated, this
+ # callback has to clear autoloaded constants after any update.
+ reloader.execute
+ end
else
ActionDispatch::Reloader.to_cleanup(&callback)
end
end
-
- # Disable dependency loading during request cycle
- initializer :disable_dependency_loading do
- if config.eager_load && config.cache_classes
- ActiveSupport::Dependencies.unhook!
- end
- end
-
- initializer :activate_queue_consumer do |app|
- if config.queue.class == ActiveSupport::Queue
- app.queue_consumer = config.queue_consumer || config.queue.consumer
- app.queue_consumer.start
- at_exit { app.queue_consumer.shutdown }
- end
- end
end
end
end
diff --git a/railties/lib/rails/cli.rb b/railties/lib/rails/cli.rb
index 443d6f47ad..dd70c272c6 100644
--- a/railties/lib/rails/cli.rb
+++ b/railties/lib/rails/cli.rb
@@ -1,16 +1,15 @@
-require 'rbconfig'
-require 'rails/script_rails_loader'
+require 'rails/app_rails_loader'
# If we are inside a Rails application this method performs an exec and thus
# the rest of this script is not run.
-Rails::ScriptRailsLoader.exec_script_rails!
+Rails::AppRailsLoader.exec_app_rails
require 'rails/ruby_version_check'
Signal.trap("INT") { puts; exit(1) }
if ARGV.first == 'plugin'
ARGV.shift
- require 'rails/commands/plugin_new'
+ require 'rails/commands/plugin'
else
require 'rails/commands/application'
end
diff --git a/railties/lib/rails/code_statistics.rb b/railties/lib/rails/code_statistics.rb
index 1aed2796c1..0ae6d2a455 100644
--- a/railties/lib/rails/code_statistics.rb
+++ b/railties/lib/rails/code_statistics.rb
@@ -1,6 +1,14 @@
+require 'rails/code_statistics_calculator'
+
class CodeStatistics #:nodoc:
- TEST_TYPES = %w(Units Functionals Unit\ tests Functional\ tests Integration\ tests)
+ TEST_TYPES = ['Controller tests',
+ 'Helper tests',
+ 'Model tests',
+ 'Mailer tests',
+ 'Integration tests',
+ 'Functional tests (old)',
+ 'Unit tests (old)']
def initialize(*pairs)
@pairs = pairs
@@ -27,64 +35,38 @@ class CodeStatistics #:nodoc:
end
def calculate_directory_statistics(directory, pattern = /.*\.(rb|js|coffee)$/)
- stats = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
+ stats = CodeStatisticsCalculator.new
Dir.foreach(directory) do |file_name|
- if File.directory?(directory + "/" + file_name) and (/^\./ !~ file_name)
- newstats = calculate_directory_statistics(directory + "/" + file_name, pattern)
- stats.each { |k, v| stats[k] += newstats[k] }
+ path = "#{directory}/#{file_name}"
+
+ if File.directory?(path) && (/^\./ !~ file_name)
+ stats.add(calculate_directory_statistics(path, pattern))
end
next unless file_name =~ pattern
- comment_started = false
-
- case file_name
- when /.*\.js$/
- comment_pattern = /^\s*\/\//
- else
- comment_pattern = /^\s*#/
- end
-
- File.open(directory + "/" + file_name) do |f|
- while line = f.gets
- stats["lines"] += 1
- if(comment_started)
- if line =~ /^=end/
- comment_started = false
- end
- next
- else
- if line =~ /^=begin/
- comment_started = true
- next
- end
- end
- stats["classes"] += 1 if line =~ /^\s*class\s+[_A-Z]/
- stats["methods"] += 1 if line =~ /^\s*def\s+[_a-z]/
- stats["codelines"] += 1 unless line =~ /^\s*$/ || line =~ comment_pattern
- end
- end
+ stats.add_by_file_path(path)
end
stats
end
def calculate_total
- total = { "lines" => 0, "codelines" => 0, "classes" => 0, "methods" => 0 }
- @statistics.each_value { |pair| pair.each { |k, v| total[k] += v } }
- total
+ @statistics.each_with_object(CodeStatisticsCalculator.new) do |pair, total|
+ total.add(pair.last)
+ end
end
def calculate_code
code_loc = 0
- @statistics.each { |k, v| code_loc += v['codelines'] unless TEST_TYPES.include? k }
+ @statistics.each { |k, v| code_loc += v.code_lines unless TEST_TYPES.include? k }
code_loc
end
def calculate_tests
test_loc = 0
- @statistics.each { |k, v| test_loc += v['codelines'] if TEST_TYPES.include? k }
+ @statistics.each { |k, v| test_loc += v.code_lines if TEST_TYPES.include? k }
test_loc
end
@@ -99,15 +81,15 @@ class CodeStatistics #:nodoc:
end
def print_line(name, statistics)
- m_over_c = (statistics["methods"] / statistics["classes"]) rescue m_over_c = 0
- loc_over_m = (statistics["codelines"] / statistics["methods"]) - 2 rescue loc_over_m = 0
-
- puts "| #{name.ljust(20)} " +
- "| #{statistics["lines"].to_s.rjust(5)} " +
- "| #{statistics["codelines"].to_s.rjust(5)} " +
- "| #{statistics["classes"].to_s.rjust(7)} " +
- "| #{statistics["methods"].to_s.rjust(7)} " +
- "| #{m_over_c.to_s.rjust(3)} " +
+ m_over_c = (statistics.methods / statistics.classes) rescue m_over_c = 0
+ loc_over_m = (statistics.code_lines / statistics.methods) - 2 rescue loc_over_m = 0
+
+ puts "| #{name.ljust(20)} " \
+ "| #{statistics.lines.to_s.rjust(5)} " \
+ "| #{statistics.code_lines.to_s.rjust(5)} " \
+ "| #{statistics.classes.to_s.rjust(7)} " \
+ "| #{statistics.methods.to_s.rjust(7)} " \
+ "| #{m_over_c.to_s.rjust(3)} " \
"| #{loc_over_m.to_s.rjust(5)} |"
end
diff --git a/railties/lib/rails/code_statistics_calculator.rb b/railties/lib/rails/code_statistics_calculator.rb
new file mode 100644
index 0000000000..60e4aef9b7
--- /dev/null
+++ b/railties/lib/rails/code_statistics_calculator.rb
@@ -0,0 +1,79 @@
+class CodeStatisticsCalculator #:nodoc:
+ attr_reader :lines, :code_lines, :classes, :methods
+
+ PATTERNS = {
+ rb: {
+ line_comment: /^\s*#/,
+ begin_block_comment: /^=begin/,
+ end_block_comment: /^=end/,
+ class: /^\s*class\s+[_A-Z]/,
+ method: /^\s*def\s+[_a-z]/,
+ },
+ js: {
+ line_comment: %r{^\s*//},
+ begin_block_comment: %r{^\s*/\*},
+ end_block_comment: %r{\*/},
+ method: /function(\s+[_a-zA-Z][\da-zA-Z]*)?\s*\(/,
+ },
+ coffee: {
+ line_comment: /^\s*#/,
+ begin_block_comment: /^\s*###/,
+ end_block_comment: /^\s*###/,
+ class: /^\s*class\s+[_A-Z]/,
+ method: /[-=]>/,
+ }
+ }
+
+ def initialize(lines = 0, code_lines = 0, classes = 0, methods = 0)
+ @lines = lines
+ @code_lines = code_lines
+ @classes = classes
+ @methods = methods
+ end
+
+ def add(code_statistics_calculator)
+ @lines += code_statistics_calculator.lines
+ @code_lines += code_statistics_calculator.code_lines
+ @classes += code_statistics_calculator.classes
+ @methods += code_statistics_calculator.methods
+ end
+
+ def add_by_file_path(file_path)
+ File.open(file_path) do |f|
+ self.add_by_io(f, file_type(file_path))
+ end
+ end
+
+ def add_by_io(io, file_type)
+ patterns = PATTERNS[file_type] || {}
+
+ comment_started = false
+
+ while line = io.gets
+ @lines += 1
+
+ if comment_started
+ if patterns[:end_block_comment] && line =~ patterns[:end_block_comment]
+ comment_started = false
+ end
+ next
+ else
+ if patterns[:begin_block_comment] && line =~ patterns[:begin_block_comment]
+ comment_started = true
+ next
+ end
+ end
+
+ @classes += 1 if patterns[:class] && line =~ patterns[:class]
+ @methods += 1 if patterns[:method] && line =~ patterns[:method]
+ if line !~ /^\s*$/ && (patterns[:line_comment].nil? || line !~ patterns[:line_comment])
+ @code_lines += 1
+ end
+ end
+ end
+
+ private
+ def file_type(file_path)
+ File.extname(file_path).sub(/\A\./, '').downcase.to_sym
+ end
+end
diff --git a/railties/lib/rails/commands.rb b/railties/lib/rails/commands.rb
index b0fae13192..f32bf772a5 100644
--- a/railties/lib/rails/commands.rb
+++ b/railties/lib/rails/commands.rb
@@ -9,110 +9,9 @@ aliases = {
"r" => "runner"
}
-help_message = <<-EOT
-Usage: rails COMMAND [ARGS]
-
-The most common rails commands are:
- generate Generate new code (short-cut alias: "g")
- console Start the Rails console (short-cut alias: "c")
- server Start the Rails server (short-cut alias: "s")
- dbconsole Start a console for the database specified in config/database.yml
- (short-cut alias: "db")
- new Create a new Rails application. "rails new my_app" creates a
- new application called MyApp in "./my_app"
-
-In addition to those, there are:
- application Generate the Rails application code
- destroy Undo code generated with "generate" (short-cut alias: "d")
- benchmarker See how fast a piece of code runs
- profiler Get profile information from a piece of code
- plugin new Generates skeleton for developing a Rails plugin
- runner Run a piece of code in the application environment (short-cut alias: "r")
-
-All commands can be run with -h (or --help) for more information.
-EOT
-
-
command = ARGV.shift
command = aliases[command] || command
-case command
-when 'generate', 'destroy', 'plugin'
- require 'rails/generators'
-
- if command == 'plugin' && ARGV.first == 'new'
- require "rails/commands/plugin_new"
- else
- require APP_PATH
- Rails.application.require_environment!
-
- Rails.application.load_generators
-
- require "rails/commands/#{command}"
- end
-
-when 'benchmarker', 'profiler'
- require APP_PATH
- Rails.application.require_environment!
- require "rails/commands/#{command}"
-
-when 'console'
- require 'rails/commands/console'
- options = Rails::Console.parse_arguments(ARGV)
-
- # RAILS_ENV needs to be set before config/application is required
- ENV['RAILS_ENV'] = options[:environment] if options[:environment]
-
- # shift ARGV so IRB doesn't freak
- ARGV.shift if ARGV.first && ARGV.first[0] != '-'
-
- require APP_PATH
- Rails.application.require_environment!
- Rails::Console.start(Rails.application, options)
-
-when 'server'
- # Change to the application's path if there is no config.ru file in current dir.
- # This allows us to run script/rails server from other directories, but still get
- # the main config.ru and properly set the tmp directory.
- Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exists?(File.expand_path("config.ru"))
-
- require 'rails/commands/server'
- Rails::Server.new.tap { |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
- }
-
-when 'dbconsole'
- require 'rails/commands/dbconsole'
- Rails::DBConsole.start
-
-when 'application', 'runner'
- require "rails/commands/#{command}"
-
-when 'new'
- if %w(-h --help).include?(ARGV.first)
- require 'rails/commands/application'
- else
- puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
- puts "Type 'rails' for help."
- exit(1)
- end
-
-when '--version', '-v'
- ARGV.unshift '--version'
- require 'rails/commands/application'
-
-when '-h', '--help'
- puts help_message
+require 'rails/commands/commands_tasks'
-else
- puts "Error: Command '#{command}' not recognized"
- if %x{rake #{command} --dry-run 2>&1 } && $?.success?
- puts "Did you mean: `$ rake #{command}` ?\n\n"
- end
- puts help_message
- exit(1)
-end
+Rails::CommandsTasks.new(ARGV).run_command!(command)
diff --git a/railties/lib/rails/commands/application.rb b/railties/lib/rails/commands/application.rb
index ff0eda3413..c998e6b6a8 100644
--- a/railties/lib/rails/commands/application.rb
+++ b/railties/lib/rails/commands/application.rb
@@ -1,24 +1,3 @@
-require 'rails/version'
-
-if ['--version', '-v'].include?(ARGV.first)
- puts "Rails #{Rails::VERSION::STRING}"
- exit(0)
-end
-
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
- railsrc = File.join(File.expand_path("~"), ".railsrc")
- if File.exist?(railsrc)
- extra_args_string = File.open(railsrc).read
- extra_args = extra_args_string.split(/\n+/).map {|l| l.split}.flatten
- puts "Using #{extra_args.join(" ")} from #{railsrc}"
- ARGV << extra_args
- ARGV.flatten!
- end
-end
-
require 'rails/generators'
require 'rails/generators/rails/app/app_generator'
@@ -34,4 +13,5 @@ module Rails
end
end
-Rails::Generators::AppGenerator.start
+args = Rails::Generators::ARGVScrubber.new(ARGV).prepare!
+Rails::Generators::AppGenerator.start args
diff --git a/railties/lib/rails/commands/benchmarker.rb b/railties/lib/rails/commands/benchmarker.rb
deleted file mode 100644
index b745b45e17..0000000000
--- a/railties/lib/rails/commands/benchmarker.rb
+++ /dev/null
@@ -1,34 +0,0 @@
-require 'optparse'
-require 'rails/test_help'
-require 'rails/performance_test_help'
-
-ARGV.push('--benchmark') # HAX
-require 'active_support/testing/performance'
-ARGV.pop
-
-def options
- options = {}
- defaults = ActiveSupport::Testing::Performance::DEFAULTS
-
- OptionParser.new do |opt|
- opt.banner = "Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]"
- opt.on('-r', '--runs N', Numeric, 'Number of runs.', "Default: #{defaults[:runs]}") { |r| options[:runs] = r }
- opt.on('-o', '--output PATH', String, 'Directory to use when writing the results.', "Default: #{defaults[:output]}") { |o| options[:output] = o }
- opt.on('-m', '--metrics a,b,c', Array, 'Metrics to use.', "Default: #{defaults[:metrics].join(",")}") { |m| options[:metrics] = m.map(&:to_sym) }
- opt.parse!(ARGV)
- end
-
- options
-end
-
-class BenchmarkerTest < ActionDispatch::PerformanceTest #:nodoc:
- self.profile_options = options
-
- ARGV.each do |expression|
- eval <<-RUBY
- def test_#{expression.parameterize('_')}
- #{expression}
- end
- RUBY
- end
-end
diff --git a/railties/lib/rails/commands/commands_tasks.rb b/railties/lib/rails/commands/commands_tasks.rb
new file mode 100644
index 0000000000..de60423784
--- /dev/null
+++ b/railties/lib/rails/commands/commands_tasks.rb
@@ -0,0 +1,174 @@
+module Rails
+ # This is a class which takes in a rails command and initiates the appropriate
+ # initiation sequence.
+ #
+ # Warning: This class mutates ARGV because some commands require manipulating
+ # it before they are run.
+ class CommandsTasks # :nodoc:
+ attr_reader :argv
+
+ HELP_MESSAGE = <<-EOT
+Usage: rails COMMAND [ARGS]
+
+The most common rails commands are:
+ generate Generate new code (short-cut alias: "g")
+ console Start the Rails console (short-cut alias: "c")
+ server Start the Rails server (short-cut alias: "s")
+ dbconsole Start a console for the database specified in config/database.yml
+ (short-cut alias: "db")
+ new Create a new Rails application. "rails new my_app" creates a
+ new application called MyApp in "./my_app"
+
+In addition to those, there are:
+ application Generate the Rails application code
+ destroy Undo code generated with "generate" (short-cut alias: "d")
+ plugin new Generates skeleton for developing a Rails plugin
+ runner Run a piece of code in the application environment (short-cut alias: "r")
+
+All commands can be run with -h (or --help) for more information.
+EOT
+
+ COMMAND_WHITELIST = %(plugin generate destroy console server dbconsole application runner new version help)
+
+ def initialize(argv)
+ @argv = argv
+ end
+
+ def run_command!(command)
+ command = parse_command(command)
+ if COMMAND_WHITELIST.include?(command)
+ send(command)
+ else
+ write_error_message(command)
+ end
+ end
+
+ def plugin
+ require_command!("plugin")
+ end
+
+ def generate
+ generate_or_destroy(:generate)
+ end
+
+ def destroy
+ generate_or_destroy(:destroy)
+ end
+
+ def console
+ require_command!("console")
+ options = Rails::Console.parse_arguments(argv)
+
+ # RAILS_ENV needs to be set before config/application is required
+ ENV['RAILS_ENV'] = options[:environment] if options[:environment]
+
+ # shift ARGV so IRB doesn't freak
+ shift_argv!
+
+ require_application_and_environment!
+ Rails::Console.start(Rails.application, options)
+ end
+
+ def server
+ set_application_directory!
+ require_command!("server")
+
+ Rails::Server.new.tap do |server|
+ # We need to require application after the server sets environment,
+ # otherwise the --environment option given to the server won't propagate.
+ require APP_PATH
+ Dir.chdir(Rails.application.root)
+ server.start
+ end
+ end
+
+ def dbconsole
+ require_command!("dbconsole")
+ Rails::DBConsole.start
+ end
+
+ def application
+ require_command!("application")
+ end
+
+ def runner
+ require_command!("runner")
+ end
+
+ def new
+ if %w(-h --help).include?(argv.first)
+ require_command!("application")
+ else
+ exit_with_initialization_warning!
+ end
+ end
+
+ def version
+ argv.unshift '--version'
+ require_command!("application")
+ end
+
+ def help
+ write_help_message
+ end
+
+ private
+
+ def exit_with_initialization_warning!
+ puts "Can't initialize a new Rails application within the directory of another, please change to a non-Rails directory first.\n"
+ puts "Type 'rails' for help."
+ exit(1)
+ end
+
+ def shift_argv!
+ argv.shift if argv.first && argv.first[0] != '-'
+ end
+
+ def require_command!(command)
+ require "rails/commands/#{command}"
+ end
+
+ def generate_or_destroy(command)
+ require 'rails/generators'
+ require_application_and_environment!
+ Rails.application.load_generators
+ require "rails/commands/#{command}"
+ end
+
+ # Change to the application's path if there is no config.ru file in current directory.
+ # This allows us to run `rails server` from other directories, but still get
+ # the main config.ru and properly set the tmp directory.
+ def set_application_directory!
+ Dir.chdir(File.expand_path('../../', APP_PATH)) unless File.exist?(File.expand_path("config.ru"))
+ end
+
+ def require_application_and_environment!
+ require APP_PATH
+ Rails.application.require_environment!
+ end
+
+ def write_help_message
+ puts HELP_MESSAGE
+ end
+
+ def write_error_message(command)
+ puts "Error: Command '#{command}' not recognized"
+ if %x{rake #{command} --dry-run 2>&1 } && $?.success?
+ puts "Did you mean: `$ rake #{command}` ?\n\n"
+ end
+ write_help_message
+ exit(1)
+ end
+
+ def parse_command(command)
+ case command
+ when '--version', '-v'
+ 'version'
+ when '--help', '-h'
+ 'help'
+ else
+ command
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/commands/console.rb b/railties/lib/rails/commands/console.rb
index 92cee6b638..f6bdf129d6 100644
--- a/railties/lib/rails/commands/console.rb
+++ b/railties/lib/rails/commands/console.rb
@@ -24,11 +24,21 @@ module Rails
if arguments.first && arguments.first[0] != '-'
env = arguments.first
- options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env
+ if available_environments.include? env
+ options[:environment] = env
+ else
+ options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env
+ end
end
options
end
+
+ private
+
+ def available_environments
+ Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') }
+ end
end
attr_reader :options, :app, :console
@@ -36,7 +46,10 @@ module Rails
def initialize(app, options={})
@app = app
@options = options
+
+ app.sandbox = sandbox?
app.load_console
+
@console = app.config.console || IRB
end
@@ -45,7 +58,7 @@ module Rails
end
def environment
- options[:environment] ||= ENV['RAILS_ENV'] || 'development'
+ options[:environment] ||= ENV['RAILS_ENV'] || ENV['RACK_ENV'] || 'development'
end
def environment?
@@ -61,7 +74,6 @@ module Rails
end
def start
- app.sandbox = sandbox?
require_debugger if debugger?
set_environment! if environment?
@@ -79,13 +91,11 @@ module Rails
end
def require_debugger
- begin
- require 'debugger'
- puts "=> Debugger enabled"
- rescue Exception
- puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle, and try again."
- exit
- end
+ 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 c84fa832f5..3e4cc787c4 100644
--- a/railties/lib/rails/commands/dbconsole.rb
+++ b/railties/lib/rails/commands/dbconsole.rb
@@ -5,8 +5,8 @@ require 'rbconfig'
module Rails
class DBConsole
- attr_reader :config, :arguments
-
+ attr_reader :arguments
+
def self.start
new.start
end
@@ -44,7 +44,7 @@ module Rails
find_cmd_and_exec(['mysql', 'mysql5'], *args)
- when "postgresql", "postgres"
+ when "postgresql", "postgres", "postgis"
ENV['PGUSER'] = config["username"] if config["username"]
ENV['PGHOST'] = config["host"] if config["host"]
ENV['PGPORT'] = config["port"].to_s if config["port"]
@@ -59,7 +59,7 @@ module Rails
args << "-#{options['mode']}" if options['mode']
args << "-header" if options['header']
- args << File.expand_path(config['database'], Rails.root)
+ args << File.expand_path(config['database'], Rails.respond_to?(:root) ? Rails.root : nil)
find_cmd_and_exec('sqlite3', *args)
@@ -82,17 +82,13 @@ module Rails
def config
@config ||= begin
cfg = begin
- cfg = YAML.load(ERB.new(IO.read("config/database.yml")).result)
+ YAML.load(ERB.new(IO.read("config/database.yml")).result)
rescue SyntaxError, StandardError
require APP_PATH
Rails.application.config.database_configuration
end
- unless cfg[environment]
- abort "No database is configured for the environment '#{environment}'"
- end
-
- cfg[environment]
+ cfg[environment] || abort("No database is configured for the environment '#{environment}'")
end
end
@@ -108,7 +104,7 @@ module Rails
def parse_arguments(arguments)
options = {}
-
+
OptionParser.new do |opt|
opt.banner = "Usage: rails dbconsole [environment] [options]"
opt.on("-p", "--include-password", "Automatically provide the password from database.yml") do |v|
@@ -123,7 +119,7 @@ module Rails
opt.on("--header") do |h|
options['header'] = h
end
-
+
opt.on("-h", "--help", "Show this help message.") do
puts opt
exit
@@ -140,12 +136,20 @@ module Rails
if arguments.first && arguments.first[0] != '-'
env = arguments.first
- options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env
+ if available_environments.include? env
+ options[:environment] = env
+ else
+ options[:environment] = %w(production development test).detect {|e| e =~ /^#{env}/} || env
+ end
end
-
+
options
end
+ def available_environments
+ Dir['config/environments/*.rb'].map { |fname| File.basename(fname, '.*') }
+ end
+
def find_cmd_and_exec(commands, *args)
commands = Array(commands)
@@ -154,7 +158,7 @@ module Rails
full_path_command = nil
found = commands.detect do |cmd|
- dir = dirs_on_path.detect do |path|
+ dirs_on_path.detect do |path|
full_path_command = File.join(path, cmd)
File.executable? full_path_command
end
diff --git a/railties/lib/rails/commands/plugin.rb b/railties/lib/rails/commands/plugin.rb
new file mode 100644
index 0000000000..837fe0ec10
--- /dev/null
+++ b/railties/lib/rails/commands/plugin.rb
@@ -0,0 +1,9 @@
+if ARGV.first != "new"
+ ARGV[0] = "--help"
+else
+ ARGV.shift
+end
+
+require 'rails/generators'
+require 'rails/generators/rails/plugin/plugin_generator'
+Rails::Generators::PluginGenerator.start
diff --git a/railties/lib/rails/commands/plugin_new.rb b/railties/lib/rails/commands/plugin_new.rb
deleted file mode 100644
index 4d7bf3c9f3..0000000000
--- a/railties/lib/rails/commands/plugin_new.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-if ARGV.first != "new"
- ARGV[0] = "--help"
-else
- ARGV.shift
-end
-
-require 'rails/generators'
-require 'rails/generators/rails/plugin_new/plugin_new_generator'
-Rails::Generators::PluginNewGenerator.start \ No newline at end of file
diff --git a/railties/lib/rails/commands/profiler.rb b/railties/lib/rails/commands/profiler.rb
deleted file mode 100644
index 3f6966b4f0..0000000000
--- a/railties/lib/rails/commands/profiler.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'optparse'
-require 'rails/test_help'
-require 'rails/performance_test_help'
-require 'active_support/testing/performance'
-
-def options
- options = {}
- defaults = ActiveSupport::Testing::Performance::DEFAULTS
-
- OptionParser.new do |opt|
- opt.banner = "Usage: rails benchmarker 'Ruby.code' 'Ruby.more_code' ... [OPTS]"
- opt.on('-r', '--runs N', Numeric, 'Number of runs.', "Default: #{defaults[:runs]}") { |r| options[:runs] = r }
- opt.on('-o', '--output PATH', String, 'Directory to use when writing the results.', "Default: #{defaults[:output]}") { |o| options[:output] = o }
- opt.on('-m', '--metrics a,b,c', Array, 'Metrics to use.', "Default: #{defaults[:metrics].join(",")}") { |m| options[:metrics] = m.map(&:to_sym) }
- opt.on('-f', '--formats x,y,z', Array, 'Formats to output to.', "Default: #{defaults[:formats].join(",")}") { |m| options[:formats] = m.map(&:to_sym) }
- opt.parse!(ARGV)
- end
-
- options
-end
-
-class ProfilerTest < ActionDispatch::PerformanceTest #:nodoc:
- self.profile_options = options
-
- ARGV.each do |expression|
- eval <<-RUBY
- def test_#{expression.parameterize('_')}
- #{expression}
- end
- RUBY
- end
-end
diff --git a/railties/lib/rails/commands/runner.rb b/railties/lib/rails/commands/runner.rb
index 0cc672e01c..2b77d5d387 100644
--- a/railties/lib/rails/commands/runner.rb
+++ b/railties/lib/rails/commands/runner.rb
@@ -1,7 +1,7 @@
require 'optparse'
require 'rbconfig'
-options = { environment: (ENV['RAILS_ENV'] || "development").dup }
+options = { environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup }
code_or_file = nil
if ARGV.first.nil?
@@ -24,7 +24,7 @@ ARGV.clone.options do |opts|
if RbConfig::CONFIG['host_os'] !~ /mswin|mingw/
opts.separator ""
- opts.separator "You can also use runner as a shebang line for your scripts like this:"
+ opts.separator "You can also use runner as a shebang line for your executables:"
opts.separator "-------------------------------------------------------------"
opts.separator "#!/usr/bin/env #{File.expand_path($0)} runner"
opts.separator ""
@@ -41,14 +41,14 @@ ENV["RAILS_ENV"] = options[:environment]
require APP_PATH
Rails.application.require_environment!
- Rails.application.load_runner
+Rails.application.load_runner
if code_or_file.nil?
$stderr.puts "Run '#{$0} -h' for help."
exit 1
elsif File.exist?(code_or_file)
$0 = code_or_file
- eval(File.read(code_or_file), nil, code_or_file)
+ Kernel.load code_or_file
else
eval(code_or_file)
end
diff --git a/railties/lib/rails/commands/server.rb b/railties/lib/rails/commands/server.rb
index 80fdc06cd2..4201dac42f 100644
--- a/railties/lib/rails/commands/server.rb
+++ b/railties/lib/rails/commands/server.rb
@@ -1,6 +1,7 @@
require 'fileutils'
require 'optparse'
require 'action_dispatch'
+require 'rails'
module Rails
class Server < ::Rack::Server
@@ -32,7 +33,8 @@ module Rails
opt_parser.parse! args
- options[:server] = args.shift
+ options[:log_stdout] = options[:daemonize].blank? && (options[:environment] || Rails.env) == "development"
+ options[:server] = args.shift
options
end
end
@@ -42,8 +44,12 @@ module Rails
set_environment
end
+ # TODO: this is no longer required but we keep it for the moment to support older config.ru files.
def app
- @app ||= super.respond_to?(:to_app) ? super.to_app : super
+ @app ||= begin
+ app = super
+ app.respond_to?(:to_app) ? app.to_app : app
+ end
end
def opt_parser
@@ -58,7 +64,10 @@ module Rails
url = "#{options[:SSLEnable] ? 'https' : 'http'}://#{options[:Host]}:#{options[:Port]}"
puts "=> Booting #{ActiveSupport::Inflector.demodulize(server)}"
puts "=> Rails #{Rails.version} application starting in #{Rails.env} on #{url}"
- puts "=> Call with -d to detach" unless options[:daemonize]
+ puts "=> Run `rails server -h` for more startup options"
+ if options[:Host].to_s.match(/0\.0\.0\.0/)
+ puts "=> Notice: server is listening on all interfaces (#{options[:Host]}). Consider using 127.0.0.1 (--binding option)"
+ end
trap(:INT) { exit }
puts "=> Ctrl-C to shutdown server" unless options[:daemonize]
@@ -67,11 +76,12 @@ module Rails
FileUtils.mkdir_p(File.join(Rails.root, 'tmp', dir_to_make))
end
- unless options[:daemonize]
+ if options[:log_stdout]
wrapped_app # touch the app so the logger is set up
console = ActiveSupport::Logger.new($stdout)
console.formatter = Rails.logger.formatter
+ console.level = Rails.logger.level
Rails.logger.extend(ActiveSupport::Logger.broadcast(console))
end
@@ -107,7 +117,7 @@ module Rails
super.merge({
Port: 3000,
DoNotReverseLookup: true,
- environment: (ENV['RAILS_ENV'] || "development").dup,
+ environment: (ENV['RAILS_ENV'] || ENV['RACK_ENV'] || "development").dup,
daemonize: false,
debugger: false,
pid: File.expand_path("tmp/pids/server.pid"),
diff --git a/railties/lib/rails/configuration.rb b/railties/lib/rails/configuration.rb
index 5fa7f043c6..f5d7dede66 100644
--- a/railties/lib/rails/configuration.rb
+++ b/railties/lib/rails/configuration.rb
@@ -1,4 +1,3 @@
-require 'active_support/deprecation'
require 'active_support/ordered_options'
require 'active_support/core_ext/object'
require 'rails/paths'
@@ -27,11 +26,11 @@ module Rails
#
# Middlewares can also be completely swapped out and replaced with others:
#
- # config.middleware.swap ActionDispatch::BestStandardsSupport, Magical::Unicorns
+ # config.middleware.swap ActionDispatch::Flash, Magical::Unicorns
#
# And finally they can also be removed from the stack completely:
#
- # config.middleware.delete ActionDispatch::BestStandardsSupport
+ # config.middleware.delete ActionDispatch::Flash
#
class MiddlewareStackProxy
def initialize
@@ -60,6 +59,10 @@ module Rails
@operations << [__method__, args, block]
end
+ def unshift(*args, &block)
+ @operations << [__method__, args, block]
+ end
+
def merge_into(other) #:nodoc:
@operations.each do |operation, args, block|
other.send(operation, *args, &block)
diff --git a/railties/lib/rails/console/helpers.rb b/railties/lib/rails/console/helpers.rb
index 230d3d9d04..b775f1ff8d 100644
--- a/railties/lib/rails/console/helpers.rb
+++ b/railties/lib/rails/console/helpers.rb
@@ -1,9 +1,15 @@
module Rails
module ConsoleMethods
+ # Gets the helper methods available to the controller.
+ #
+ # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+
def helper
@helper ||= ApplicationController.helpers
end
+ # Gets a new instance of a controller object.
+ #
+ # This method assumes an +ApplicationController+ exists, and it extends +ActionController::Base+
def controller
@controller ||= ApplicationController.new
end
diff --git a/railties/lib/rails/engine.rb b/railties/lib/rails/engine.rb
index 2c2bb1c714..e8adef2fd3 100644
--- a/railties/lib/rails/engine.rb
+++ b/railties/lib/rails/engine.rb
@@ -1,4 +1,5 @@
require 'rails/railtie'
+require 'rails/engine/railties'
require 'active_support/core_ext/module/delegation'
require 'pathname'
require 'rbconfig'
@@ -101,12 +102,12 @@ module Rails
# paths["config"] # => ["config"]
# paths["config/initializers"] # => ["config/initializers"]
# paths["config/locales"] # => ["config/locales"]
- # paths["config/routes"] # => ["config/routes.rb"]
+ # paths["config/routes.rb"] # => ["config/routes.rb"]
# end
#
# The <tt>Application</tt> class adds a couple more paths to this set. And as in your
# <tt>Application</tt>, all folders under +app+ are automatically added to the load path.
- # If you have an <tt>app/observers</tt> folder for example, it will be added by default.
+ # If you have an <tt>app/services</tt> folder for example, it will be added by default.
#
# == Endpoint
#
@@ -123,7 +124,7 @@ module Rails
#
# Now you can mount your engine in application's routes just like that:
#
- # MyRailsApp::Application.routes.draw do
+ # Rails.application.routes.draw do
# mount MyEngine::Engine => "/engine"
# end
#
@@ -153,7 +154,7 @@ module Rails
# Note that now there can be more than one router in your application, and it's better to avoid
# passing requests through many routers. Consider this situation:
#
- # MyRailsApp::Application.routes.draw do
+ # Rails.application.routes.draw do
# mount MyEngine::Engine => "/blog"
# get "/blog/omg" => "main#omg"
# end
@@ -163,7 +164,7 @@ module Rails
# and if there is no such route in +Engine+'s routes, it will be dispatched to <tt>main#omg</tt>.
# It's much better to swap that:
#
- # MyRailsApp::Application.routes.draw do
+ # Rails.application.routes.draw do
# get "/blog/omg" => "main#omg"
# mount MyEngine::Engine => "/blog"
# end
@@ -175,7 +176,7 @@ module Rails
# There are some places where an Engine's name is used:
#
# * routes: when you mount an Engine with <tt>mount(MyEngine::Engine => '/my_engine')</tt>,
- # it's used as default :as option
+ # it's used as default <tt>:as</tt> option
# * rake task for installing migrations <tt>my_engine:install:migrations</tt>
#
# Engine name is set by default based on class name. For <tt>MyEngine::Engine</tt> it will be
@@ -250,7 +251,7 @@ module Rails
# created to allow you to do that. Consider such a scenario:
#
# # config/routes.rb
- # MyApplication::Application.routes.draw do
+ # Rails.application.routes.draw do
# mount MyEngine::Engine => "/my_engine", as: "my_engine"
# get "/foo" => "foo#index"
# end
@@ -350,8 +351,13 @@ module Rails
Rails::Railtie::Configuration.eager_load_namespaces << base
base.called_from = begin
- # Remove the line number from backtraces making sure we don't leave anything behind
- call_stack = caller.map { |p| p.sub(/:\d+.*/, '') }
+ call_stack = if Kernel.respond_to?(:caller_locations)
+ caller_locations.map(&:path)
+ else
+ # Remove the line number from backtraces making sure we don't leave anything behind
+ caller.map { |p| p.sub(/:\d+.*/, '') }
+ end
+
File.dirname(call_stack.detect { |p| p !~ %r[railties[\w.-]*/lib/rails|rack[\w.-]*/lib/rack] })
end
end
@@ -408,7 +414,7 @@ module Rails
end
delegate :middleware, :root, :paths, to: :config
- delegate :engine_name, :isolated?, to: "self.class"
+ delegate :engine_name, :isolated?, to: :class
def initialize
@_all_autoload_paths = nil
@@ -446,7 +452,7 @@ module Rails
self
end
- # Load rails generators and invoke the registered hooks.
+ # Load Rails generators and invoke the registered hooks.
# Check <tt>Rails::Railtie.generators</tt> for more info.
def load_generators(app=self)
require "rails/generators"
@@ -466,6 +472,10 @@ module Rails
end
end
+ def railties
+ @railties ||= Railties.new
+ end
+
# Returns a module with all the helpers defined for the engine.
def helpers
@helpers ||= begin
@@ -548,7 +558,7 @@ module Rails
#
# This needs to be an initializer, since it needs to run once
# per engine and get the engine as a block parameter
- initializer :set_autoload_paths, before: :bootstrap_hook do |app|
+ initializer :set_autoload_paths, before: :bootstrap_hook do
ActiveSupport::Dependencies.autoload_paths.unshift(*_all_autoload_paths)
ActiveSupport::Dependencies.autoload_once_paths.unshift(*_all_autoload_once_paths)
@@ -629,6 +639,10 @@ module Rails
end
end
+ def routes? #:nodoc:
+ @routes
+ end
+
protected
def run_tasks_blocks(*) #:nodoc:
@@ -636,10 +650,6 @@ module Rails
paths["lib/tasks"].existent.sort.each { |ext| load(ext) }
end
- def routes? #:nodoc:
- @routes
- end
-
def has_migrations? #:nodoc:
paths["db/migrate"].existent.any?
end
diff --git a/railties/lib/rails/engine/commands.rb b/railties/lib/rails/engine/commands.rb
index 072d16291b..f39f926109 100644
--- a/railties/lib/rails/engine/commands.rb
+++ b/railties/lib/rails/engine/commands.rb
@@ -27,7 +27,7 @@ else
puts <<-EOT
Usage: rails COMMAND [ARGS]
-The common rails commands available for engines are:
+The common Rails commands available for engines are:
generate Generate new code (short-cut alias: "g")
destroy Undo code generated with "generate" (short-cut alias: "d")
diff --git a/railties/lib/rails/engine/configuration.rb b/railties/lib/rails/engine/configuration.rb
index 22e885a3a6..10d1821709 100644
--- a/railties/lib/rails/engine/configuration.rb
+++ b/railties/lib/rails/engine/configuration.rb
@@ -38,6 +38,7 @@ module Rails
def paths
@paths ||= begin
paths = Rails::Paths::Root.new(@root)
+
paths.add "app", eager_load: true, glob: "*"
paths.add "app/assets", glob: "*"
paths.add "app/controllers", eager_load: true
@@ -45,19 +46,27 @@ module Rails
paths.add "app/models", eager_load: true
paths.add "app/mailers", eager_load: true
paths.add "app/views"
+
+ paths.add "app/controllers/concerns", eager_load: true
+ paths.add "app/models/concerns", eager_load: true
+
paths.add "lib", load_path: true
paths.add "lib/assets", glob: "*"
paths.add "lib/tasks", glob: "**/*.rake"
+
paths.add "config"
paths.add "config/environments", glob: "#{Rails.env}.rb"
paths.add "config/initializers", glob: "**/*.rb"
paths.add "config/locales", glob: "*.{rb,yml}"
paths.add "config/routes.rb"
+
paths.add "db"
paths.add "db/migrate"
paths.add "db/seeds.rb"
+
paths.add "vendor", load_path: true
paths.add "vendor/assets", glob: "*"
+
paths
end
end
diff --git a/railties/lib/rails/engine/railties.rb b/railties/lib/rails/engine/railties.rb
new file mode 100644
index 0000000000..9969a1475d
--- /dev/null
+++ b/railties/lib/rails/engine/railties.rb
@@ -0,0 +1,21 @@
+module Rails
+ class Engine < Railtie
+ class Railties
+ include Enumerable
+ attr_reader :_all
+
+ def initialize
+ @_all ||= ::Rails::Railtie.subclasses.map(&:instance) +
+ ::Rails::Engine.subclasses.map(&:instance)
+ end
+
+ def each(*args, &block)
+ _all.each(*args, &block)
+ end
+
+ def -(others)
+ _all - others
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators.rb b/railties/lib/rails/generators.rb
index 367f9288b8..1c0952cc55 100644
--- a/railties/lib/rails/generators.rb
+++ b/railties/lib/rails/generators.rb
@@ -1,6 +1,8 @@
activesupport_path = File.expand_path('../../../../activesupport/lib', __FILE__)
$:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
+require 'thor/group'
+
require 'active_support'
require 'active_support/core_ext/object/blank'
require 'active_support/core_ext/kernel/singleton_class'
@@ -9,8 +11,6 @@ require 'active_support/core_ext/hash/deep_merge'
require 'active_support/core_ext/module/attribute_accessors'
require 'active_support/core_ext/string/inflections'
-require 'rails/generators/base'
-
module Rails
module Generators
autoload :Actions, 'rails/generators/actions'
@@ -50,7 +50,6 @@ module Rails
javascripts: true,
javascript_engine: :js,
orm: false,
- performance_tool: nil,
resource_controller: :controller,
resource_route: true,
scaffold_controller: :scaffold_controller,
@@ -172,16 +171,13 @@ module Rails
"resource_route",
"#{orm}:migration",
"#{orm}:model",
- "#{orm}:observer",
"#{test}:controller",
"#{test}:helper",
"#{test}:integration",
"#{test}:mailer",
"#{test}:model",
- "#{test}:observer",
"#{test}:scaffold",
"#{test}:view",
- "#{test}:performance",
"#{template}:controller",
"#{template}:scaffold",
"#{template}:mailer",
@@ -229,7 +225,7 @@ module Rails
rails = groups.delete("rails")
rails.map! { |n| n.sub(/^rails:/, '') }
rails.delete("app")
- rails.delete("plugin_new")
+ rails.delete("plugin")
print_list("rails", rails)
hidden_namespaces.each { |n| groups.delete(n.to_s) }
diff --git a/railties/lib/rails/generators/actions.rb b/railties/lib/rails/generators/actions.rb
index 5c4e81431c..366c72ebaa 100644
--- a/railties/lib/rails/generators/actions.rb
+++ b/railties/lib/rails/generators/actions.rb
@@ -4,6 +4,10 @@ require 'rbconfig'
module Rails
module Generators
module Actions
+ def initialize(*) # :nodoc:
+ super
+ @in_group = nil
+ end
# Adds an entry into Gemfile for the supplied gem.
#
@@ -78,11 +82,11 @@ module Rails
# end
#
# environment(nil, env: "development") do
- # "config.active_record.observers = :cacher"
+ # "config.autoload_paths += %W(#{config.root}/extras)"
# end
def environment(data=nil, options={}, &block)
sentinel = /class [a-z_:]+ < Rails::Application/i
- env_file_sentinel = /::Application\.configure do/
+ env_file_sentinel = /Rails\.application\.configure do/
data = block.call if !data && block_given?
in_root do
@@ -186,7 +190,7 @@ module Rails
log :generate, what
argument = args.map {|arg| arg.to_s }.flatten.join(" ")
- in_root { run_ruby_script("script/rails generate #{what} #{argument}", verbose: false) }
+ in_root { run_ruby_script("bin/rails generate #{what} #{argument}", verbose: false) }
end
# Runs the supplied rake task
@@ -211,7 +215,7 @@ module Rails
# Make an entry in Rails routing file config/routes.rb
#
- # route "root :to => 'welcome#index'"
+ # route "root 'welcome#index'"
def route(routing_code)
log :route, routing_code
sentinel = /\.routes\.draw do\s*$/
diff --git a/railties/lib/rails/generators/active_model.rb b/railties/lib/rails/generators/active_model.rb
index 0e51b9c568..6183944bb0 100644
--- a/railties/lib/rails/generators/active_model.rb
+++ b/railties/lib/rails/generators/active_model.rb
@@ -59,8 +59,8 @@ module Rails
end
# PATCH/PUT update
- def update_attributes(params=nil)
- "#{name}.update_attributes(#{params})"
+ def update(params=nil)
+ "#{name}.update(#{params})"
end
# POST create
diff --git a/railties/lib/rails/generators/app_base.rb b/railties/lib/rails/generators/app_base.rb
index e761e26b04..1a270a6c8c 100644
--- a/railties/lib/rails/generators/app_base.rb
+++ b/railties/lib/rails/generators/app_base.rb
@@ -5,6 +5,8 @@ require 'rails/version' unless defined?(Rails::VERSION)
require 'rbconfig'
require 'open-uri'
require 'uri'
+require 'rails/generators/base'
+require 'active_support/core_ext/array/extract_options'
module Rails
module Generators
@@ -18,17 +20,18 @@ module Rails
argument :app_path, type: :string
- def self.add_shared_options_for(name)
- class_option :builder, type: :string, aliases: '-b',
- desc: "Path to a #{name} builder (can be a filesystem path or URL)"
+ def self.strict_args_position
+ false
+ end
+ def self.add_shared_options_for(name)
class_option :template, type: :string, aliases: '-m',
- desc: "Path to an #{name} template (can be a filesystem path or URL)"
+ desc: "Path to some #{name} template (can be a filesystem path or URL)"
class_option :skip_gemfile, type: :boolean, default: false,
desc: "Don't create a Gemfile"
- class_option :skip_bundle, type: :boolean, default: false,
+ class_option :skip_bundle, type: :boolean, aliases: '-B', default: false,
desc: "Don't run bundle install"
class_option :skip_git, type: :boolean, aliases: '-G', default: false,
@@ -40,6 +43,9 @@ module Rails
class_option :skip_active_record, type: :boolean, aliases: '-O', default: false,
desc: 'Skip Active Record files'
+ class_option :skip_action_view, type: :boolean, aliases: '-V', default: false,
+ desc: 'Skip Action View files'
+
class_option :skip_sprockets, type: :boolean, aliases: '-S', default: false,
desc: 'Skip Sprockets files'
@@ -52,9 +58,6 @@ module Rails
class_option :skip_javascript, type: :boolean, aliases: '-J', default: false,
desc: 'Skip JavaScript files'
- class_option :skip_index_html, type: :boolean, aliases: '-I', default: false,
- desc: 'Skip public/index.html and app/assets/images/rails.png files'
-
class_option :dev, type: :boolean, default: false,
desc: "Setup the #{name} with Gemfile pointing to your Rails checkout"
@@ -64,31 +67,61 @@ module Rails
class_option :skip_test_unit, type: :boolean, aliases: '-T', default: false,
desc: 'Skip Test::Unit files'
+ class_option :rc, type: :string, default: false,
+ desc: "Path to file containing extra configuration options for rails command"
+
+ class_option :no_rc, type: :boolean, default: false,
+ desc: 'Skip loading of extra configuration options from .railsrc file'
+
class_option :help, type: :boolean, aliases: '-h', group: :rails,
desc: 'Show this help message and quit'
end
def initialize(*args)
- @original_wd = Dir.pwd
+ @original_wd = Dir.pwd
+ @gem_filter = lambda { |gem| true }
+ @extra_entries = []
super
convert_database_option_for_jruby
end
protected
+ def gemfile_entry(name, *args)
+ options = args.extract_options!
+ version = args.first
+ github = options[:github]
+ path = options[:path]
+
+ if github
+ @extra_entries << GemfileEntry.github(name, github)
+ elsif path
+ @extra_entries << GemfileEntry.path(name, path)
+ else
+ @extra_entries << GemfileEntry.version(name, version)
+ end
+ self
+ end
+
+ def gemfile_entries
+ [ rails_gemfile_entry,
+ database_gemfile_entry,
+ assets_gemfile_entry,
+ javascript_gemfile_entry,
+ jbuilder_gemfile_entry,
+ webconsole_gemfile_entry,
+ sdoc_gemfile_entry,
+ @extra_entries].flatten.find_all(&@gem_filter)
+ end
+
+ def add_gem_entry_filter
+ @gem_filter = lambda { |next_filter,entry|
+ yield(entry) && next_filter.call(entry)
+ }.curry[@gem_filter]
+ end
+
def builder
@builder ||= begin
- if path = options[:builder]
- if URI(path).is_a?(URI::HTTP)
- contents = open(path, "Accept" => "application/x-thor-template") {|io| io.read }
- else
- contents = open(File.expand_path(path, @original_wd)) {|io| io.read }
- end
-
- prok = eval("proc { #{contents} }", TOPLEVEL_BINDING, path, 1)
- instance_eval(&prok)
- end
-
builder_class = get_builder_class
builder_class.send(:include, ActionMethods)
builder_class.new(self)
@@ -100,11 +133,9 @@ module Rails
end
def create_root
- self.destination_root = File.expand_path(app_path, destination_root)
valid_const?
empty_directory '.'
- set_default_accessors!
FileUtils.cd(destination_root) unless options[:pretend]
end
@@ -115,6 +146,7 @@ module Rails
end
def set_default_accessors!
+ self.destination_root = File.expand_path(app_path, destination_root)
self.rails_template = case options[:template]
when /^https?:\/\//
options[:template]
@@ -126,37 +158,52 @@ module Rails
end
def database_gemfile_entry
- options[:skip_active_record] ? "" : "gem '#{gem_for_database}'"
+ return [] if options[:skip_active_record]
+ GemfileEntry.version gem_for_database, nil,
+ "Use #{options[:database]} as the database for Active Record"
end
def include_all_railties?
- !options[:skip_active_record] && !options[:skip_test_unit] && !options[:skip_sprockets]
+ !options[:skip_active_record] && !options[:skip_action_view] && !options[:skip_test_unit] && !options[:skip_sprockets]
end
def comment_if(value)
options[value] ? '# ' : ''
end
+ class GemfileEntry < Struct.new(:name, :version, :comment, :options, :commented_out)
+ def initialize(name, version, comment, options = {}, commented_out = false)
+ super
+ end
+
+ def self.github(name, github, comment = nil)
+ new(name, nil, comment, github: github)
+ end
+
+ def self.version(name, version, comment = nil)
+ new(name, version, comment)
+ end
+
+ def self.path(name, path, comment = nil)
+ new(name, nil, comment, path: path)
+ end
+
+ def padding(max_width)
+ ' ' * (max_width - name.length + 2)
+ end
+ end
+
def rails_gemfile_entry
if options.dev?
- <<-GEMFILE.strip_heredoc
- gem 'rails', path: '#{Rails::Generators::RAILS_DEV_PATH}'
- gem 'journey', github: 'rails/journey'
- gem 'arel', github: 'rails/arel'
- gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders'
- GEMFILE
+ [GemfileEntry.path('rails', Rails::Generators::RAILS_DEV_PATH),
+ GemfileEntry.github('arel', 'rails/arel')]
elsif options.edge?
- <<-GEMFILE.strip_heredoc
- gem 'rails', github: 'rails/rails'
- gem 'journey', github: 'rails/journey'
- gem 'arel', github: 'rails/arel'
- gem 'activerecord-deprecated_finders', github: 'rails/activerecord-deprecated_finders'
- GEMFILE
+ [GemfileEntry.github('rails', 'rails/rails'),
+ GemfileEntry.github('arel', 'rails/arel')]
else
- <<-GEMFILE.strip_heredoc
- # Bundle edge Rails instead: gem 'rails', github: 'rails/rails'
- gem 'rails', '#{Rails::VERSION::STRING}'
- GEMFILE
+ [GemfileEntry.version('rails',
+ Rails::VERSION::STRING,
+ "Bundle edge Rails instead: gem 'rails', github: 'rails/rails'")]
end
end
@@ -188,57 +235,71 @@ module Rails
end
def assets_gemfile_entry
- return if options[:skip_sprockets]
-
- gemfile = if options.dev? || options.edge?
- <<-GEMFILE
- # Gems used only for assets and not required
- # in production environments by default.
- group :assets do
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
- gem 'sass-rails', github: 'rails/sass-rails'
- gem 'coffee-rails', github: 'rails/coffee-rails'
-
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- #{javascript_runtime_gemfile_entry}
- gem 'uglifier', '>= 1.0.3'
- end
- GEMFILE
+ return [] if options[:skip_sprockets]
+
+ gems = []
+ if options.dev? || options.edge?
+ gems << GemfileEntry.github('sprockets-rails', 'rails/sprockets-rails',
+ 'Use edge version of sprockets-rails')
+ gems << GemfileEntry.github('sass-rails', 'rails/sass-rails',
+ 'Use SCSS for stylesheets')
else
- <<-GEMFILE
- # Gems used only for assets and not required
- # in production environments by default.
- group :assets do
- gem 'sprockets-rails', github: 'rails/sprockets-rails'
- gem 'sass-rails', '~> 4.0.0.beta'
- gem 'coffee-rails', '~> 4.0.0.beta'
-
- # See https://github.com/sstephenson/execjs#readme for more supported runtimes
- #{javascript_runtime_gemfile_entry}
- gem 'uglifier', '>= 1.0.3'
- end
- GEMFILE
+ gems << GemfileEntry.version('sass-rails',
+ '~> 4.0.0.rc1',
+ 'Use SCSS for stylesheets')
end
- gemfile.strip_heredoc.gsub(/^[ \t]*$/, '')
+ gems << GemfileEntry.version('uglifier',
+ '>= 1.3.0',
+ 'Use Uglifier as compressor for JavaScript assets')
+
+ gems
+ end
+
+ def jbuilder_gemfile_entry
+ comment = 'Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder'
+ GemfileEntry.version('jbuilder', '~> 1.2', comment)
+ end
+
+ def webconsole_gemfile_entry
+ comment = 'Run `rails console` in the browser. Read more: https://github.com/rails/web-console'
+ GemfileEntry.new('web-console', nil, comment, group: :development)
+ end
+
+ def sdoc_gemfile_entry
+ comment = 'bundle exec rake doc:rails generates the API under doc/api.'
+ GemfileEntry.new('sdoc', nil, comment, { group: :doc, require: false })
+ end
+
+ def coffee_gemfile_entry
+ comment = 'Use CoffeeScript for .js.coffee assets and views'
+ if options.dev? || options.edge?
+ GemfileEntry.github 'coffee-rails', 'rails/coffee-rails', comment
+ else
+ GemfileEntry.version 'coffee-rails', '~> 4.0.0', comment
+ end
end
def javascript_gemfile_entry
- unless options[:skip_javascript]
- <<-GEMFILE.strip_heredoc
- gem '#{options[:javascript]}-rails'
+ if options[:skip_javascript]
+ []
+ else
+ gems = [coffee_gemfile_entry, javascript_runtime_gemfile_entry]
+ gems << GemfileEntry.version("#{options[:javascript]}-rails", nil,
+ "Use #{options[:javascript]} as the JavaScript library")
- # Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks
- gem 'turbolinks'
- GEMFILE
+ gems << GemfileEntry.version("turbolinks", nil,
+ "Turbolinks makes following links in your web application faster. Read more: https://github.com/rails/turbolinks")
+ gems
end
end
def javascript_runtime_gemfile_entry
+ comment = 'See https://github.com/sstephenson/execjs#readme for more supported runtimes'
if defined?(JRUBY_VERSION)
- "gem 'therubyrhino'\n"
+ GemfileEntry.version 'therubyrhino', nil, comment
else
- "# gem 'therubyracer', platforms: :ruby\n"
+ GemfileEntry.new 'therubyracer', nil, comment, { platforms: :ruby }, true
end
end
diff --git a/railties/lib/rails/generators/base.rb b/railties/lib/rails/generators/base.rb
index 7e938fab47..dc1d4fa181 100644
--- a/railties/lib/rails/generators/base.rb
+++ b/railties/lib/rails/generators/base.rb
@@ -7,7 +7,7 @@ rescue LoadError
exit
end
-require 'rails/generators/actions'
+require 'rails/generators'
module Rails
module Generators
@@ -168,15 +168,15 @@ module Rails
as_hook = options.delete(:as) || generator_name
names.each do |name|
- defaults = if options[:type] == :boolean
- { }
- elsif [true, false].include?(default_value_for_option(name, options))
- { banner: "" }
- else
- { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
- end
-
unless class_options.key?(name)
+ defaults = if options[:type] == :boolean
+ { }
+ elsif [true, false].include?(default_value_for_option(name, options))
+ { banner: "" }
+ else
+ { desc: "#{name.to_s.humanize} to be invoked", banner: "NAME" }
+ end
+
class_option(name, defaults.merge!(options))
end
@@ -211,7 +211,7 @@ module Rails
return unless base_name && generator_name
return unless default_generator_root
path = File.join(default_generator_root, 'templates')
- path if File.exists?(path)
+ path if File.exist?(path)
end
# Returns the base root for a common set of generators. This is used to dynamically
@@ -255,12 +255,7 @@ module Rails
# Split the class from its module nesting
nesting = class_name.split('::')
last_name = nesting.pop
-
- # Extract the last Module in the nesting
- last = nesting.inject(Object) do |last_module, nest|
- break unless last_module.const_defined?(nest, false)
- last_module.const_get(nest)
- end
+ last = extract_last_module(nesting)
if last && last.const_defined?(last_name.camelize, false)
raise Error, "The name '#{class_name}' is either already used in your application " <<
@@ -270,6 +265,14 @@ module Rails
end
end
+ # Takes in an array of nested modules and extracts the last module
+ def extract_last_module(nesting)
+ nesting.inject(Object) do |last_module, nest|
+ break unless last_module.const_defined?(nest, false)
+ last_module.const_get(nest)
+ end
+ end
+
# Use Rails default banner.
def self.banner
"rails generate #{namespace.sub(/^rails:/,'')} #{self.arguments.map{ |a| a.usage }.join(' ')} [options]".gsub(/\s+/, ' ')
@@ -365,12 +368,12 @@ module Rails
source_root && File.expand_path("../USAGE", source_root),
default_generator_root && File.join(default_generator_root, "USAGE")
]
- paths.compact.detect { |path| File.exists? path }
+ paths.compact.detect { |path| File.exist? path }
end
def self.default_generator_root
path = File.expand_path(File.join(base_name, generator_name), base_root)
- path if File.exists?(path)
+ path if File.exist?(path)
end
end
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
index 32546936e3..69c10efa47 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/_form.html.erb
@@ -13,8 +13,22 @@
<% attributes.each do |attribute| -%>
<div class="field">
- <%%= f.label :<%= attribute.name %> %><br />
+<% if attribute.password_digest? -%>
+ <%%= f.label :password %><br>
+ <%%= f.password_field :password %>
+ </div>
+ <div>
+ <%%= 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 -%>
<div class="actions">
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 f5182bcc50..814d6fdb0e 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/index.html.erb
@@ -3,27 +3,27 @@
<table>
<thead>
<tr>
- <% attributes.each do |attribute| -%>
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
<th><%= attribute.human_name %></th>
- <% end -%>
- <th></th>
- <th></th>
- <th></th>
+<% end -%>
+ <th colspan="3"></th>
</tr>
</thead>
<tbody>
- <%%= content_tag_for(:tr, @<%= plural_table_name %>) do |<%= singular_table_name %>| %>
- <% attributes.each do |attribute| -%>
- <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
- <% end -%>
- <td><%%= link_to 'Show', <%= singular_table_name %> %></td>
- <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td>
- <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ <%% @<%= plural_table_name %>.each do |<%= singular_table_name %>| %>
+ <tr>
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
+ <td><%%= <%= singular_table_name %>.<%= attribute.name %> %></td>
+<% end -%>
+ <td><%%= link_to 'Show', <%= singular_table_name %> %></td>
+ <td><%%= link_to 'Edit', edit_<%= singular_table_name %>_path(<%= singular_table_name %>) %></td>
+ <td><%%= link_to 'Destroy', <%= singular_table_name %>, method: :delete, data: { confirm: 'Are you sure?' } %></td>
+ </tr>
<%% end %>
</tbody>
</table>
-<br />
+<br>
<%%= link_to 'New <%= human_name %>', new_<%= singular_table_name %>_path %>
diff --git a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
index e15c963971..5e634153be 100644
--- a/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
+++ b/railties/lib/rails/generators/erb/scaffold/templates/show.html.erb
@@ -1,12 +1,11 @@
<p id="notice"><%%= notice %></p>
-<% attributes.each do |attribute| -%>
+<% attributes.reject(&:password_digest?).each do |attribute| -%>
<p>
<strong><%= attribute.human_name %>:</strong>
<%%= @<%= singular_table_name %>.<%= attribute.name %> %>
</p>
<% end -%>
-
<%%= link_to 'Edit', edit_<%= singular_table_name %>_path(@<%= singular_table_name %>) %> |
<%%= link_to 'Back', <%= index_helper %>_path %>
diff --git a/railties/lib/rails/generators/generated_attribute.rb b/railties/lib/rails/generators/generated_attribute.rb
index d8a4f15b4b..5e2784c4b0 100644
--- a/railties/lib/rails/generators/generated_attribute.rb
+++ b/railties/lib/rails/generators/generated_attribute.rb
@@ -99,13 +99,17 @@ module Rails
end
def index_name
- @index_name ||= if reference?
- polymorphic? ? %w(id type).map { |t| "#{name}_#{t}" } : "#{name}_id"
+ @index_name ||= if polymorphic?
+ %w(id type).map { |t| "#{name}_#{t}" }
else
- name
+ column_name
end
end
+ def column_name
+ @column_name ||= reference? ? "#{name}_id" : name
+ end
+
def foreign_key?
!!(name =~ /_id$/)
end
@@ -126,6 +130,10 @@ module Rails
@has_uniq_index
end
+ def password_digest?
+ name == 'password' && type == :digest
+ end
+
def inject_options
"".tap { |s| @attr_options.each { |k,v| s << ", #{k}: #{v.inspect}" } }
end
diff --git a/railties/lib/rails/generators/migration.rb b/railties/lib/rails/generators/migration.rb
index 5bf98bb6e0..3566f96f5e 100644
--- a/railties/lib/rails/generators/migration.rb
+++ b/railties/lib/rails/generators/migration.rb
@@ -1,15 +1,14 @@
+require 'active_support/concern'
+
module Rails
module Generators
# Holds common methods for migrations. It assumes that migrations has the
# [0-9]*_name format and can be used by another frameworks (like Sequel)
# just by implementing the next migration version method.
module Migration
+ extend ActiveSupport::Concern
attr_reader :migration_number, :migration_file_name, :migration_class_name
- def self.included(base) #:nodoc:
- base.extend ClassMethods
- end
-
module ClassMethods
def migration_lookup_at(dirname) #:nodoc:
Dir.glob("#{dirname}/[0-9]*_*.rb")
@@ -52,7 +51,7 @@ module Rails
if destination && options.force?
remove_file(destination)
elsif destination
- raise Error, "Another migration is already named #{@migration_file_name}: #{destination}"
+ raise Error, "Another migration is already named #{@migration_file_name}: #{destination}. Use --force to remove the old migration file and replace it."
end
destination = File.join(migration_dir, "#{@migration_number}_#{@migration_file_name}.rb")
end
diff --git a/railties/lib/rails/generators/named_base.rb b/railties/lib/rails/generators/named_base.rb
index 84f8f76838..e712c747b0 100644
--- a/railties/lib/rails/generators/named_base.rb
+++ b/railties/lib/rails/generators/named_base.rb
@@ -18,6 +18,8 @@ module Rails
parse_attributes! if respond_to?(:attributes)
end
+ # Defines the template that would be used for the migration file.
+ # The arguments include the source template file, the migration filename etc.
no_tasks do
def template(source, *args, &block)
inside_template do
@@ -40,7 +42,7 @@ module Rails
def indent(content, multiplier = 2)
spaces = " " * multiplier
- content = content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join
+ content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join
end
def wrap_with_namespace(content)
@@ -160,6 +162,14 @@ module Rails
end
end
+ def attributes_names
+ @attributes_names ||= attributes.each_with_object([]) do |a, names|
+ names << a.column_name
+ names << 'password_confirmation' if a.password_digest?
+ names << "#{a.name}_type" if a.polymorphic?
+ end
+ end
+
def pluralize_table_names?
!defined?(ActiveRecord::Base) || ActiveRecord::Base.pluralize_table_names
end
@@ -169,10 +179,10 @@ module Rails
#
# ==== Examples
#
- # check_class_collision suffix: "Observer"
+ # check_class_collision suffix: "Decorator"
#
# If the generator is invoked with class name Admin, it will check for
- # the presence of "AdminObserver".
+ # the presence of "AdminDecorator".
#
def self.check_class_collision(options={})
define_method :check_class_collision do
diff --git a/railties/lib/rails/generators/rails/app/app_generator.rb b/railties/lib/rails/generators/rails/app/app_generator.rb
index 18637451ac..3c62e7a4d2 100644
--- a/railties/lib/rails/generators/rails/app/app_generator.rb
+++ b/railties/lib/rails/generators/rails/app/app_generator.rb
@@ -38,7 +38,7 @@ module Rails
end
def readme
- copy_file "README", "README.rdoc"
+ copy_file "README.rdoc", "README.rdoc"
end
def gemfile
@@ -55,8 +55,20 @@ module Rails
def app
directory 'app'
+
+ keep_file 'app/assets/images'
keep_file 'app/mailers'
keep_file 'app/models'
+
+ keep_file 'app/controllers/concerns'
+ keep_file 'app/models/concerns'
+ end
+
+ def bin
+ directory "bin" do |content|
+ "#{shebang}\n" + content
+ end
+ chmod "bin", 0755 & ~File.umask, verbose: false
end
def config
@@ -81,10 +93,6 @@ module Rails
directory "db"
end
- def doc
- directory "doc"
- end
-
def lib
empty_directory 'lib'
empty_directory_with_keep_file 'lib/tasks'
@@ -97,18 +105,6 @@ module Rails
def public_directory
directory "public", "public", recursive: false
- if options[:skip_index_html]
- remove_file "public/index.html"
- remove_file 'app/assets/images/rails.png'
- keep_file 'app/assets/images'
- end
- end
-
- def script
- directory "script" do |content|
- "#{shebang}\n" + content
- end
- chmod "script", 0755, verbose: false
end
def test
@@ -119,7 +115,6 @@ module Rails
empty_directory_with_keep_file 'test/helpers'
empty_directory_with_keep_file 'test/integration'
- template 'test/performance/browsing_test.rb'
template 'test/test_helper.rb'
end
@@ -134,7 +129,9 @@ module Rails
end
def vendor_javascripts
- empty_directory_with_keep_file 'vendor/assets/javascripts'
+ unless options[:skip_javascript]
+ empty_directory_with_keep_file 'vendor/assets/javascripts'
+ end
end
def vendor_stylesheets
@@ -146,7 +143,7 @@ module Rails
# We need to store the RAILS_DEV_PATH in a constant, otherwise the path
# can change in Ruby 1.8.7 when we FileUtils.cd.
RAILS_DEV_PATH = File.expand_path("../../../../../..", File.dirname(__FILE__))
- RESERVED_NAMES = %w[application destroy benchmarker profiler plugin runner test]
+ RESERVED_NAMES = %w[application destroy plugin runner test]
class AppGenerator < AppBase # :nodoc:
add_shared_options_for "application"
@@ -156,15 +153,18 @@ module Rails
desc: "Show Rails version number and quit"
def initialize(*args)
- raise Error, "Options should be given after the application name. For details run: rails --help" if args[0].blank?
-
super
+ unless app_path
+ raise Error, "Application name should be provided in arguments. For details run: rails --help"
+ end
+
if !options[:skip_active_record] && !DATABASES.include?(options[:database])
raise Error, "Invalid value for --database option. Supported for preconfiguration are: #{DATABASES.join(", ")}."
end
end
+ public_task :set_default_accessors!
public_task :create_root
def create_root_files
@@ -179,6 +179,10 @@ module Rails
build(:app)
end
+ def create_bin_files
+ build(:bin)
+ end
+
def create_config_files
build(:config)
end
@@ -196,10 +200,6 @@ module Rails
build(:db)
end
- def create_doc_files
- build(:doc)
- end
-
def create_lib_files
build(:lib)
end
@@ -212,10 +212,6 @@ module Rails
build(:public_directory)
end
- def create_script_files
- build(:script)
- end
-
def create_test_files
build(:test) unless options[:skip_test_unit]
end
@@ -232,6 +228,12 @@ module Rails
build(:leftovers)
end
+ def delete_js_folder_skipping_javascript
+ if options[:skip_javascript]
+ remove_dir 'app/assets/javascripts'
+ end
+ end
+
public_task :apply_rails_template, :run_bundle
protected
@@ -246,7 +248,7 @@ module Rails
end
def app_name
- @app_name ||= defined_app_const_base? ? defined_app_name : File.basename(destination_root)
+ @app_name ||= (defined_app_const_base? ? defined_app_name : File.basename(destination_root)).tr('\\', '').tr(". ", "_")
end
def defined_app_name
@@ -301,5 +303,76 @@ module Rails
defined?(::AppBuilder) ? ::AppBuilder : Rails::AppBuilder
end
end
+
+ # This class handles preparation of the arguments before the AppGenerator is
+ # called. The class provides version or help information if they were
+ # requested, and also constructs the railsrc file (used for extra configuration
+ # options).
+ #
+ # This class should be called before the AppGenerator is required and started
+ # since it configures and mutates ARGV correctly.
+ class ARGVScrubber # :nodoc
+ def initialize(argv = ARGV)
+ @argv = argv
+ end
+
+ def prepare!
+ handle_version_request!(@argv.first)
+ handle_invalid_command!(@argv.first, @argv) do
+ handle_rails_rc!(@argv.drop(1))
+ end
+ end
+
+ def self.default_rc_file
+ File.join(File.expand_path('~'), '.railsrc')
+ end
+
+ private
+
+ def handle_version_request!(argument)
+ if ['--version', '-v'].include?(argument)
+ require 'rails/version'
+ puts "Rails #{Rails::VERSION::STRING}"
+ exit(0)
+ end
+ end
+
+ def handle_invalid_command!(argument, argv)
+ if argument == "new"
+ yield
+ else
+ ['--help'] + argv.drop(1)
+ end
+ end
+
+ def handle_rails_rc!(argv)
+ if argv.find { |arg| arg == '--no-rc' }
+ argv.reject { |arg| arg == '--no-rc' }
+ else
+ railsrc(argv) { |rc_argv, rc| insert_railsrc_into_argv!(rc_argv, rc) }
+ end
+ end
+
+ def railsrc(argv)
+ if (customrc = argv.index{ |x| x.include?("--rc=") })
+ fname = File.expand_path(argv[customrc].gsub(/--rc=/, ""))
+ yield(argv.take(customrc) + argv.drop(customrc + 1), fname)
+ else
+ yield argv, self.class.default_rc_file
+ end
+ end
+
+ def read_rc_file(railsrc)
+ extra_args = File.readlines(railsrc).flat_map(&:split)
+ puts "Using #{extra_args.join(" ")} from #{railsrc}"
+ extra_args
+ end
+
+ def insert_railsrc_into_argv!(argv, railsrc)
+ return argv unless File.exist?(railsrc)
+ extra_args = read_rc_file railsrc
+ argv.take(1) + extra_args + argv.drop(1)
+ end
+ end
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/Gemfile b/railties/lib/rails/generators/rails/app/templates/Gemfile
index 30f8a5f75e..21dcba7947 100644
--- a/railties/lib/rails/generators/rails/app/templates/Gemfile
+++ b/railties/lib/rails/generators/rails/app/templates/Gemfile
@@ -1,25 +1,30 @@
source 'https://rubygems.org'
-<%= rails_gemfile_entry -%>
-
-<%= database_gemfile_entry -%>
-
-<%= "gem 'jruby-openssl'\n" if defined?(JRUBY_VERSION) -%>
-
-<%= assets_gemfile_entry %>
-<%= javascript_gemfile_entry %>
-
-# To use ActiveModel has_secure_password
-# gem 'bcrypt-ruby', '~> 3.0.0'
-
-# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
-# gem 'jbuilder'
+<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%>
+, '<%= gem.version %>'
+<% elsif gem.options.any? -%>
+,<%= gem.padding(max_width) %><%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% else %>
+<% end -%>
+<% end -%>
+
+# Use ActiveModel has_secure_password
+# gem 'bcrypt-ruby', '~> 3.1.2'
# Use unicorn as the app server
# gem 'unicorn'
-# Deploy with Capistrano
-# gem 'capistrano', group: :development
+# Use Capistrano for deployment
+# gem 'capistrano-rails', group: :development
-# To use debugger
-# gem 'debugger'
+<% unless defined?(JRUBY_VERSION) -%>
+# Use debugger
+# gem 'debugger', group: [:development, :test]
+<% end -%>
diff --git a/railties/lib/rails/generators/rails/app/templates/README b/railties/lib/rails/generators/rails/app/templates/README
deleted file mode 100644
index b5d7b6436b..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/README
+++ /dev/null
@@ -1,259 +0,0 @@
-== Welcome to Rails
-
-Rails is a web-application framework that includes everything needed to create
-database-backed web applications according to the Model-View-Control pattern.
-
-This pattern splits the view (also called the presentation) into "dumb"
-templates that are primarily responsible for inserting pre-built data in between
-HTML tags. The model contains the "smart" domain objects (such as Account,
-Product, Person, Post) that holds all the business logic and knows how to
-persist themselves to a database. The controller handles the incoming requests
-(such as Save New Account, Update Product, Show Post) by manipulating the model
-and directing data to the view.
-
-In Rails, the model is handled by what's called an object-relational mapping
-layer entitled Active Record. This layer allows you to present the data from
-database rows as objects and embellish these data objects with business logic
-methods. You can read more about Active Record in
-link:files/vendor/rails/activerecord/README.html.
-
-The controller and view are handled by the Action Pack, which handles both
-layers by its two parts: Action View and Action Controller. These two layers
-are bundled in a single package due to their heavy interdependence. This is
-unlike the relationship between the Active Record and Action Pack that is much
-more separate. Each of these packages can be used independently outside of
-Rails. You can read more about Action Pack in
-link:files/vendor/rails/actionpack/README.html.
-
-
-== Getting Started
-
-1. At the command prompt, create a new Rails application:
- <tt>rails new myapp</tt> (where <tt>myapp</tt> is the application name)
-
-2. Change directory to <tt>myapp</tt> and start the web server:
- <tt>cd myapp; rails server</tt> (run with --help for options)
-
-3. Go to http://localhost:3000/ and you'll see:
- "Welcome aboard: You're riding Ruby on Rails!"
-
-4. Follow the guidelines to start developing your application. You can find
-the following resources handy:
-
-* The Getting Started Guide: http://guides.rubyonrails.org/getting_started.html
-* Ruby on Rails Tutorial Book: http://www.railstutorial.org/
-
-
-== Debugging Rails
-
-Sometimes your application goes wrong. Fortunately there are a lot of tools that
-will help you debug it and get it back on the rails.
-
-First area to check is the application log files. Have "tail -f" commands
-running on the server.log and development.log. Rails will automatically display
-debugging and runtime information to these files. Debugging info will also be
-shown in the browser on requests from 127.0.0.1.
-
-You can also log your own messages directly into the log file from your code
-using the Ruby logger class from inside your controllers. Example:
-
- class WeblogController < ActionController::Base
- def destroy
- @weblog = Weblog.find(params[:id])
- @weblog.destroy
- logger.info("#{Time.now} Destroyed Weblog ID ##{@weblog.id}!")
- end
- end
-
-The result will be a message in your log file along the lines of:
-
- Mon Oct 08 14:22:29 +1000 2007 Destroyed Weblog ID #1!
-
-More information on how to use the logger is at http://www.ruby-doc.org/core/
-
-Also, Ruby documentation can be found at http://www.ruby-lang.org/. There are
-several books available online as well:
-
-* Programming Ruby: http://www.ruby-doc.org/docs/ProgrammingRuby/ (Pickaxe)
-* Learn to Program: http://pine.fm/LearnToProgram/ (a beginners guide)
-
-These two books will bring you up to speed on the Ruby language and also on
-programming in general.
-
-
-== Debugger
-
-Debugger support is available through the debugger command when you start your
-Mongrel or WEBrick server with --debugger. This means that you can break out of
-execution at any point in the code, investigate and change the model, and then,
-resume execution! You need to install the 'debugger' gem to run the server in debugging
-mode. Add gem 'debugger' to your Gemfile and run <tt>bundle</tt> to install it. Example:
-
- class WeblogController < ActionController::Base
- def index
- @posts = Post.all
- debugger
- end
- end
-
-So the controller will accept the action, run the first line, then present you
-with a IRB prompt in the server window. Here you can do things like:
-
- >> @posts.inspect
- => "[#<Post:0x14a6be8
- @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>,
- #<Post:0x14a6620
- @attributes={"title"=>"Rails", "body"=>"Only ten..", "id"=>"2"}>]"
- >> @posts.first.title = "hello from a debugger"
- => "hello from a debugger"
-
-...and even better, you can examine how your runtime objects actually work:
-
- >> f = @posts.first
- => #<Post:0x13630c4 @attributes={"title"=>nil, "body"=>nil, "id"=>"1"}>
- >> f.
- Display all 152 possibilities? (y or n)
-
-Finally, when you're ready to resume execution, you can enter "cont".
-
-
-== Console
-
-The console is a Ruby shell, which allows you to interact with your
-application's domain model. Here you'll have all parts of the application
-configured, just like it is when the application is running. You can inspect
-domain models, change values, and save to the database. Starting the script
-without arguments will launch it in the development environment.
-
-To start the console, run <tt>rails console</tt> from the application
-directory.
-
-Options:
-
-* Passing the <tt>-s, --sandbox</tt> argument will rollback any modifications
- made to the database.
-* Passing an environment name as an argument will load the corresponding
- environment. Example: <tt>rails console production</tt>.
-
-To reload your controllers and models after launching the console run
-<tt>reload!</tt>
-
-More information about irb can be found at:
-link:http://www.rubycentral.org/pickaxe/irb.html
-
-
-== dbconsole
-
-You can go to the command line of your database directly through <tt>rails
-dbconsole</tt>. You would be connected to the database with the credentials
-defined in database.yml. Starting the script without arguments will connect you
-to the development database. Passing an argument will connect you to a different
-database, like <tt>rails dbconsole production</tt>. Currently works for MySQL,
-PostgreSQL and SQLite 3.
-
-== Description of Contents
-
-The default directory structure of a generated Ruby on Rails application:
-
- |-- app
- | |-- assets
- | |-- images
- | |-- javascripts
- | `-- stylesheets
- | |-- controllers
- | |-- helpers
- | |-- mailers
- | |-- models
- | `-- views
- | `-- layouts
- |-- config
- | |-- environments
- | |-- initializers
- | `-- locales
- |-- db
- |-- doc
- |-- lib
- | `-- tasks
- |-- log
- |-- public
- |-- script
- |-- test
- | |-- fixtures
- | |-- functional
- | |-- integration
- | |-- performance
- | `-- unit
- |-- tmp
- | |-- cache
- | |-- pids
- | |-- sessions
- | `-- sockets
- `-- vendor
- |-- assets
- `-- stylesheets
-
-app
- Holds all the code that's specific to this particular application.
-
-app/assets
- Contains subdirectories for images, stylesheets, and JavaScript files.
-
-app/controllers
- Holds controllers that should be named like weblogs_controller.rb for
- automated URL mapping. All controllers should descend from
- ApplicationController which itself descends from ActionController::Base.
-
-app/models
- Holds models that should be named like post.rb. Models descend from
- ActiveRecord::Base by default.
-
-app/views
- Holds the template files for the view that should be named like
- weblogs/index.html.erb for the WeblogsController#index action. All views use
- eRuby syntax by default.
-
-app/views/layouts
- Holds the template files for layouts to be used with views. This models the
- common header/footer method of wrapping views. In your views, define a layout
- using the <tt>layout :default</tt> and create a file named default.html.erb.
- Inside default.html.erb, call <% yield %> to render the view using this
- layout.
-
-app/helpers
- Holds view helpers that should be named like weblogs_helper.rb. These are
- generated for you automatically when using generators for controllers.
- Helpers can be used to wrap functionality for your views into methods.
-
-config
- Configuration files for the Rails environment, the routing map, the database,
- and other dependencies.
-
-db
- Contains the database schema in schema.rb. db/migrate contains all the
- sequence of Migrations for your schema.
-
-doc
- This directory is where your application documentation will be stored when
- generated using <tt>rake doc:app</tt>
-
-lib
- Application specific libraries. Basically, any kind of custom code that
- doesn't belong under controllers, models, or helpers. This directory is in
- the load path.
-
-public
- The directory available for the web server. Also contains the dispatchers and the
- default HTML files. This should be set as the DOCUMENT_ROOT of your web
- server.
-
-script
- Helper scripts for automation and generation.
-
-test
- Unit and functional tests along with fixtures. When using the rails generate
- command, template test files will be generated for you and placed in this
- directory.
-
-vendor
- External libraries that the application depends on. If the app has frozen rails,
- those gems also go here, under vendor/rails/. This directory is in the load path.
diff --git a/railties/lib/rails/generators/rails/app/templates/README.rdoc b/railties/lib/rails/generators/rails/app/templates/README.rdoc
new file mode 100644
index 0000000000..dd4e97e22e
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/README.rdoc
@@ -0,0 +1,28 @@
+== 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/railties/lib/rails/generators/rails/app/templates/Rakefile b/railties/lib/rails/generators/rails/app/templates/Rakefile
index 6eb23f68a3..ba6b733dd2 100644
--- a/railties/lib/rails/generators/rails/app/templates/Rakefile
+++ b/railties/lib/rails/generators/rails/app/templates/Rakefile
@@ -3,4 +3,4 @@
require File.expand_path('../config/application', __FILE__)
-<%= app_const %>.load_tasks
+Rails.application.load_tasks
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png b/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png
deleted file mode 100644
index d5edc04e65..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/images/rails.png
+++ /dev/null
Binary files differ
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
index 7342bffd9d..8b91313e51 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/javascripts/application.js.tt
@@ -7,8 +7,8 @@
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// compiled file.
//
-// WARNING: THE FIRST BLANK LINE MARKS THE END OF WHAT'S TO BE PROCESSED, ANY BLANK LINE SHOULD
-// GO AFTER THE REQUIRES BELOW.
+// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
+// about supported directives.
//
<% unless options[:skip_javascript] -%>
//= require <%= options[:javascript] %>
diff --git a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
index 3192ec897b..a443db3401 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
+++ b/railties/lib/rails/generators/rails/app/templates/app/assets/stylesheets/application.css
@@ -5,9 +5,11 @@
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
- * You're free to add application-wide styles to this file and they'll appear at the top of the
- * compiled file, but it's generally better to create a new file per style scope.
+ * You're free to add application-wide styles to this file and they'll appear at the bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
*
- *= require_self
*= require_tree .
+ *= require_self
*/
diff --git a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
index e0539aa8bb..4cf47bd0a0 100644
--- a/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/app/views/layouts/application.html.erb.tt
@@ -2,8 +2,12 @@
<html>
<head>
<title><%= camelized %></title>
+ <%- if options[:skip_javascript] -%>
<%%= stylesheet_link_tag "application", media: "all" %>
- <%%= javascript_include_tag "application" %>
+ <%- else -%>
+ <%%= stylesheet_link_tag "application", media: "all", "data-turbolinks-track" => true %>
+ <%%= javascript_include_tag "application", "data-turbolinks-track" => true %>
+ <%- end -%>
<%%= csrf_meta_tags %>
</head>
<body>
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/bundle b/railties/lib/rails/generators/rails/app/templates/bin/bundle
new file mode 100644
index 0000000000..1123dcf501
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/bundle
@@ -0,0 +1,2 @@
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
+load Gem.bin_path('bundler', 'bundle')
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rails b/railties/lib/rails/generators/rails/app/templates/bin/rails
new file mode 100644
index 0000000000..6a128b95e5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rails
@@ -0,0 +1,3 @@
+APP_PATH = File.expand_path('../../config/application', __FILE__)
+require_relative '../config/boot'
+require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/bin/rake b/railties/lib/rails/generators/rails/app/templates/bin/rake
new file mode 100644
index 0000000000..d14fc8395b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/bin/rake
@@ -0,0 +1,3 @@
+require_relative '../config/boot'
+require 'rake'
+Rake.application.run
diff --git a/railties/lib/rails/generators/rails/app/templates/config.ru b/railties/lib/rails/generators/rails/app/templates/config.ru
index fcfbc6b07a..5bc2a619e8 100644
--- a/railties/lib/rails/generators/rails/app/templates/config.ru
+++ b/railties/lib/rails/generators/rails/app/templates/config.ru
@@ -1,4 +1,4 @@
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
-run <%= app_const %>
+run Rails.application
diff --git a/railties/lib/rails/generators/rails/app/templates/config/application.rb b/railties/lib/rails/generators/rails/app/templates/config/application.rb
index fb43c90e21..ac41a0cadb 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/application.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/application.rb
@@ -4,15 +4,18 @@ require File.expand_path('../boot', __FILE__)
require 'rails/all'
<% else -%>
# Pick the frameworks you want:
+require "active_model/railtie"
<%= comment_if :skip_active_record %>require "active_record/railtie"
require "action_controller/railtie"
require "action_mailer/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/rails/railtie"
+<%= comment_if :skip_action_view %>require "action_view/railtie"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie"
<% end -%>
-# Assets should be precompiled for production (so we don't need the gems loaded then)
-Bundler.require(*Rails.groups(assets: %w(development test)))
+# Require the gems listed in Gemfile, including any gems
+# you've limited to :test, :development, or :production.
+Bundler.require(:default, Rails.env)
module <%= app_const_base %>
class Application < Rails::Application
@@ -20,23 +23,17 @@ module <%= app_const_base %>
# 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)
+ # 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.
+ # config.time_zone = 'Central Time (US & Canada)'
- # Configure sensitive parameters which will be filtered from the log file.
- config.filter_parameters += [:password]
+ # The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
+ # config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
+ # config.i18n.default_locale = :de
+<% if options.skip_sprockets? -%>
- # Use SQL instead of Active Record's schema dumper when creating the database.
- # This is necessary if your schema can't be completely dumped by the schema dumper,
- # like if you have constraints or database-specific column types.
- # config.active_record.schema_format = :sql
-
-<% unless options.skip_sprockets? -%>
- # Enable the asset pipeline.
- config.assets.enabled = true
-
- # Version of your assets, change this if you want to expire all your assets.
- config.assets.version = '1.0'
+ # Disable the asset pipeline.
+ config.assets.enabled = false
<% end -%>
end
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/boot.rb b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
index 3596736667..5e5f0c1fac 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/boot.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/boot.rb
@@ -1,4 +1,4 @@
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
-require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
diff --git a/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml b/railties/lib/rails/generators/rails/app/templates/config/databases/postgresql.yml
index 22c9194fad..0194dce6f3 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
@@ -26,7 +26,7 @@ development:
# domain socket that doesn't need configuration. Windows does not have
# domain sockets, so uncomment these lines.
#host: localhost
-
+
# The TCP port the server listens on. Defaults to 5432.
# If your server runs on a different port number, change accordingly.
#port: 5432
@@ -55,6 +55,8 @@ production:
adapter: postgresql
encoding: unicode
database: <%= app_name %>_production
+ # For details on connection pooling, see rails configration guide
+ # http://guides.rubyonrails.org/configuring.html#database-pooling
pool: 5
username: <%= app_name %>
password:
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
new file mode 100644
index 0000000000..7ef89d6608
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/databases/sqlserver.yml
@@ -0,0 +1,57 @@
+# SQL Server (2005 or higher recommended)
+#
+# Install the adapters and driver
+# gem install tiny_tds
+# gem install activerecord-sqlserver-adapter
+#
+# Ensure the activerecord adapter and db driver gems are defined in your Gemfile
+# gem 'tiny_tds'
+# gem 'activerecord-sqlserver-adapter'
+#
+# You should make sure freetds is configured correctly first.
+# freetds.conf contains host/port/protocol_versions settings.
+# http://freetds.schemamania.org/userguide/freetdsconf.htm
+#
+# A typical Microsoft server
+# [mssql]
+# host = mssqlserver.yourdomain.com
+# port = 1433
+# tds version = 7.1
+
+# If you can connect with "tsql -S servername", your basic FreeTDS installation is working.
+# 'man tsql' for more info
+# Set timeout to a larger number if valid queries against a live db fail
+
+development:
+ adapter: sqlserver
+ encoding: utf8
+ reconnect: false
+ database: <%= app_name %>_development
+ username: <%= app_name %>
+ password:
+ timeout: 25
+ dataserver: from_freetds.conf
+
+
+# Warning: The database defined as "test" will be erased and
+# re-generated from your development database when you run "rake".
+# Do not set this db to the same as development or production.
+test:
+ adapter: sqlserver
+ encoding: utf8
+ reconnect: false
+ database: <%= app_name %>_test
+ username: <%= app_name %>
+ password:
+ timeout: 25
+ dataserver: from_freetds.conf
+
+production:
+ adapter: sqlserver
+ encoding: utf8
+ reconnect: false
+ database: <%= app_name %>_production
+ username: <%= app_name %>
+ password:
+ timeout: 25
+ dataserver: from_freetds.conf
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environment.rb b/railties/lib/rails/generators/rails/app/templates/config/environment.rb
index e080ebd74e..ee8d90dc65 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environment.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/environment.rb
@@ -1,5 +1,5 @@
-# Load the rails application.
+# Load the Rails application.
require File.expand_path('../application', __FILE__)
-# Initialize the rails application.
-<%= app_const %>.initialize!
+# Initialize the Rails application.
+Rails.application.initialize!
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 6b5b3a0b1f..cff570a631 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
@@ -1,4 +1,4 @@
-<%= app_const %>.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# In the development environment your application's code is reloaded on
@@ -19,23 +19,15 @@
# 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
-
<%- unless options.skip_active_record? -%>
- # Log the query plan for queries taking more than this (works
- # with SQLite, MySQL, and PostgreSQL).
- config.active_record.auto_explain_threshold_in_seconds = 0.5
-
- # Raise an error on page load if there are pending migrations
+ # Raise an error on page load if there are pending migrations.
config.active_record.migration_error = :page_load
<%- end -%>
<%- unless options.skip_sprockets? -%>
- # Do not compress assets.
- config.assets.compress = false
-
# Debug mode disables concatenation and preprocessing of assets.
+ # This option may cause significant delays in view rendering with a large
+ # number of complex assets.
config.assets.debug = true
<%- end -%>
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/production.rb.tt
index 8aaaba628c..1dfc9f136b 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
@@ -1,4 +1,4 @@
-<%= app_const %>.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# Code is not reloaded between requests.
@@ -24,13 +24,17 @@
<%- unless options.skip_sprockets? -%>
# Compress JavaScripts and CSS.
- config.assets.compress = true
+ config.assets.js_compressor = :uglifier
+ # config.assets.css_compressor = :sass
- # Don't fallback to assets pipeline if a precompiled asset is missed.
+ # Do not 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'
<%- end -%>
# Specifies the header that your server uses for sending files.
@@ -56,17 +60,15 @@
# 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 are already added).
+ # 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.
+ # Set this to true and configure the email server for immediate delivery to raise delivery errors.
# config.action_mailer.raise_delivery_errors = false
- # Enable threaded mode.
- # config.threadsafe!
-
# Enable locale fallbacks for I18n (makes lookups for any locale fall back to
# the I18n.default_locale when a translation can not be found).
config.i18n.fallbacks = true
@@ -74,19 +76,9 @@
# Send deprecation notices to registered listeners.
config.active_support.deprecation = :notify
- <%- unless options.skip_active_record? -%>
- # Log the query plan for queries taking more than this (works
- # with SQLite, MySQL, and PostgreSQL).
- # config.active_record.auto_explain_threshold_in_seconds = 0.5
- <%- end -%>
-
# 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
-
- # Default the production mode queue to an synchronous queue. You will probably
- # want to replace this with an out-of-process queueing solution.
- # config.queue = ActiveSupport::SynchronousQueue.new
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
index a5ef0cd9cd..ba0742f97f 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/environments/test.rb.tt
@@ -1,4 +1,4 @@
-<%= app_const %>.configure do
+Rails.application.configure do
# Settings specified here will take precedence over those in config/application.rb.
# The test environment is used exclusively to run your application's
@@ -13,7 +13,7 @@
config.eager_load = false
# Configure static asset server for tests with Cache-Control for performance.
- config.serve_static_assets = true
+ config.serve_static_assets = true
config.static_cache_control = "public, max-age=3600"
# Show full error reports and disable caching.
@@ -33,7 +33,4 @@
# Print deprecation notices to the stderr.
config.active_support.deprecation = :stderr
-
- # Use the synchronous queue to run jobs immediately.
- config.queue = ActiveSupport::SynchronousQueue.new
end
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb
new file mode 100644
index 0000000000..4a994e1e7b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb
@@ -0,0 +1,4 @@
+# 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/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
index 9262c3379f..ac033bf9dc 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/inflections.rb
@@ -9,7 +9,7 @@
# 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'
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/locale.rb b/railties/lib/rails/generators/rails/app/templates/config/initializers/locale.rb
deleted file mode 100644
index a8285f88ca..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/locale.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-# 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/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
index 3c5611ca59..f3cc6098a3 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/secret_token.rb.tt
@@ -1,12 +1,12 @@
# 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.
# You can use `rake secret` to generate a secure secret key.
-# Make sure your secret_token is kept private
+# Make sure your secret_key_base is kept private
# if you're sharing your code publicly.
-<%= app_const %>.config.secret_token = '<%= app_secret %>'
+Rails.application.config.secret_key_base = '<%= app_secret %>'
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
index 4a099a4ce2..2bb9b82c61 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/session_store.rb.tt
@@ -1,3 +1,3 @@
# Be sure to restart your server when you modify this file.
-<%= app_const %>.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
+Rails.application.config.session_store :cookie_store, key: <%= "'_#{app_name}_session'" %>
diff --git a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt
index 280f777cc0..f2110c2c70 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt
+++ b/railties/lib/rails/generators/rails/app/templates/config/initializers/wrap_parameters.rb.tt
@@ -1,5 +1,5 @@
# Be sure to restart your server when you modify this file.
-#
+
# This file contains settings for ActionController::ParamsWrapper which
# is enabled by default.
@@ -7,8 +7,8 @@
ActiveSupport.on_load(:action_controller) do
wrap_parameters format: [:json] if respond_to?(:wrap_parameters)
end
-
<%- unless options.skip_active_record? -%>
+
# To enable root element in JSON for ActiveRecord objects.
# ActiveSupport.on_load(:active_record) do
# self.include_root_in_json = true
diff --git a/railties/lib/rails/generators/rails/app/templates/config/routes.rb b/railties/lib/rails/generators/rails/app/templates/config/routes.rb
index 631543c705..3f66539d54 100644
--- a/railties/lib/rails/generators/rails/app/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/app/templates/config/routes.rb
@@ -1,9 +1,9 @@
-<%= app_const %>.routes.draw do
+Rails.application.routes.draw do
# The priority is based upon order of creation: first created -> highest priority.
# See how all your routes lay out with "rake routes".
- # You can have the root of your site routed with "root" just remember to delete public/index.html.
- # root to: 'welcome#index'
+ # You can have the root of your site routed with "root"
+ # root 'welcome#index'
# Example of regular route:
# get 'products/:id' => 'catalog#view'
@@ -40,6 +40,13 @@
# end
# end
+ # Example resource route with concerns:
+ # concern :toggleable do
+ # post 'toggle'
+ # end
+ # resources :posts, concerns: :toggleable
+ # resources :photos, concerns: :toggleable
+
# Example resource route within a namespace:
# namespace :admin do
# # Directs /admin/products/* to Admin::ProductsController
diff --git a/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP b/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP
deleted file mode 100644
index fe41f5cc24..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/doc/README_FOR_APP
+++ /dev/null
@@ -1,2 +0,0 @@
-Use this README file to introduce your application and point to useful places in the API for learning more.
-Run "rake doc:app" to generate API documentation for your models, controllers, helpers, and libraries.
diff --git a/railties/lib/rails/generators/rails/app/templates/gitignore b/railties/lib/rails/generators/rails/app/templates/gitignore
index 8910bf5a06..6a502e997f 100644
--- a/railties/lib/rails/generators/rails/app/templates/gitignore
+++ b/railties/lib/rails/generators/rails/app/templates/gitignore
@@ -1,10 +1,10 @@
-# See http://help.github.com/ignore-files/ for more about ignoring files.
+# 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
+# git config --global core.excludesfile '~/.gitignore_global'
-# Ignore bundler config
+# Ignore bundler config.
/.bundle
# Ignore the default SQLite database.
diff --git a/railties/lib/rails/generators/rails/app/templates/public/404.html b/railties/lib/rails/generators/rails/app/templates/public/404.html
index 3d875c342e..a0daa0c156 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/404.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/404.html
@@ -3,16 +3,47 @@
<head>
<title>The page you were looking for doesn't exist (404)</title>
<style>
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ 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;
+ }
+
+ 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-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>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/422.html b/railties/lib/rails/generators/rails/app/templates/public/422.html
index 3f1bfb3417..fbb4b84d72 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/422.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/422.html
@@ -3,16 +3,47 @@
<head>
<title>The change you wanted was rejected (422)</title>
<style>
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ 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;
+ }
+
+ 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-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>
@@ -22,5 +53,6 @@
<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/railties/lib/rails/generators/rails/app/templates/public/500.html b/railties/lib/rails/generators/rails/app/templates/public/500.html
index 012977d3d2..e9052d35bf 100644
--- a/railties/lib/rails/generators/rails/app/templates/public/500.html
+++ b/railties/lib/rails/generators/rails/app/templates/public/500.html
@@ -3,16 +3,47 @@
<head>
<title>We're sorry, but something went wrong (500)</title>
<style>
- body { background-color: #fff; color: #666; text-align: center; font-family: arial, sans-serif; }
- div.dialog {
- width: 25em;
- padding: 0 4em;
- margin: 4em auto 0 auto;
- border: 1px solid #ccc;
- border-right-color: #999;
- border-bottom-color: #999;
- }
- h1 { font-size: 100%; color: #f00; line-height: 1.5em; }
+ 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;
+ }
+
+ 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-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>
diff --git a/railties/lib/rails/generators/rails/app/templates/public/index.html b/railties/lib/rails/generators/rails/app/templates/public/index.html
deleted file mode 100644
index dd09a96de9..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/public/index.html
+++ /dev/null
@@ -1,241 +0,0 @@
-<!DOCTYPE html>
-<html>
- <head>
- <title>Ruby on Rails: Welcome aboard</title>
- <style media="screen">
- body {
- margin: 0;
- margin-bottom: 25px;
- padding: 0;
- background-color: #f0f0f0;
- font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
- font-size: 13px;
- color: #333;
- }
-
- h1 {
- font-size: 28px;
- color: #000;
- }
-
- a {color: #03c}
- a:hover {
- background-color: #03c;
- color: white;
- text-decoration: none;
- }
-
-
- #page {
- background-color: #f0f0f0;
- width: 750px;
- margin: 0;
- margin-left: auto;
- margin-right: auto;
- }
-
- #content {
- float: left;
- background-color: white;
- border: 3px solid #aaa;
- border-top: none;
- padding: 25px;
- width: 500px;
- }
-
- #sidebar {
- float: right;
- width: 175px;
- }
-
- #footer {
- clear: both;
- }
-
- #header, #about, #getting-started {
- padding-left: 75px;
- padding-right: 30px;
- }
-
-
- #header {
- background-image: url("assets/rails.png");
- background-repeat: no-repeat;
- background-position: top left;
- height: 64px;
- }
- #header h1, #header h2 {margin: 0}
- #header h2 {
- color: #888;
- font-weight: normal;
- font-size: 16px;
- }
-
-
- #about h3 {
- margin: 0;
- margin-bottom: 10px;
- font-size: 14px;
- }
-
- #about-content {
- background-color: #ffd;
- border: 1px solid #fc0;
- margin-left: -55px;
- margin-right: -10px;
- }
- #about-content table {
- margin-top: 10px;
- margin-bottom: 10px;
- font-size: 11px;
- border-collapse: collapse;
- }
- #about-content td {
- padding: 10px;
- padding-top: 3px;
- padding-bottom: 3px;
- }
- #about-content td.name {color: #555}
- #about-content td.value {color: #000}
-
- #about-content ul {
- padding: 0;
- list-style-type: none;
- }
-
- #about-content.failure {
- background-color: #fcc;
- border: 1px solid #f00;
- }
- #about-content.failure p {
- margin: 0;
- padding: 10px;
- }
-
-
- #getting-started {
- border-top: 1px solid #ccc;
- margin-top: 25px;
- padding-top: 15px;
- }
- #getting-started h1 {
- margin: 0;
- font-size: 20px;
- }
- #getting-started h2 {
- margin: 0;
- font-size: 14px;
- font-weight: normal;
- color: #333;
- margin-bottom: 25px;
- }
- #getting-started ol {
- margin-left: 0;
- padding-left: 0;
- }
- #getting-started li {
- font-size: 18px;
- color: #888;
- margin-bottom: 25px;
- }
- #getting-started li h2 {
- margin: 0;
- font-weight: normal;
- font-size: 18px;
- color: #333;
- }
- #getting-started li p {
- color: #555;
- font-size: 13px;
- }
-
-
- #sidebar ul {
- margin-left: 0;
- padding-left: 0;
- }
- #sidebar ul h3 {
- margin-top: 25px;
- font-size: 16px;
- padding-bottom: 10px;
- border-bottom: 1px solid #ccc;
- }
- #sidebar li {
- list-style-type: none;
- }
- #sidebar ul.links li {
- margin-bottom: 5px;
- }
-
- .filename {
- font-style: italic;
- }
- </style>
- <script>
- function about() {
- info = document.getElementById('about-content');
- if (window.XMLHttpRequest)
- { xhr = new XMLHttpRequest(); }
- else
- { xhr = new ActiveXObject("Microsoft.XMLHTTP"); }
- xhr.open("GET","rails/info/properties",false);
- xhr.send("");
- info.innerHTML = xhr.responseText;
- info.style.display = 'block'
- }
- </script>
- </head>
- <body>
- <div id="page">
- <div id="sidebar">
- <ul id="sidebar-items">
- <li>
- <h3>Browse the documentation</h3>
- <ul class="links">
- <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
- <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
- <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
- <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
- </ul>
- </li>
- </ul>
- </div>
-
- <div id="content">
- <div id="header">
- <h1>Welcome aboard</h1>
- <h2>You&rsquo;re riding Ruby on Rails!</h2>
- </div>
-
- <div id="about">
- <h3><a href="rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
- <div id="about-content" style="display: none"></div>
- </div>
-
- <div id="getting-started">
- <h1>Getting started</h1>
- <h2>Here&rsquo;s how to get rolling:</h2>
-
- <ol>
- <li>
- <h2>Use <code>rails generate</code> to create your models and controllers</h2>
- <p>To see all available options, run it without parameters.</p>
- </li>
-
- <li>
- <h2>Set up a default route and remove <span class="filename">public/index.html</span></h2>
- <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
- </li>
-
- <li>
- <h2>Create your database</h2>
- <p>Run <code>rake db:create</code> to create your database. If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
- </li>
- </ol>
- </div>
- </div>
-
- <div id="footer">&nbsp;</div>
- </div>
- </body>
-</html>
diff --git a/railties/lib/rails/generators/rails/app/templates/script/rails b/railties/lib/rails/generators/rails/app/templates/script/rails
deleted file mode 100644
index 11bc1edde9..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/script/rails
+++ /dev/null
@@ -1,5 +0,0 @@
-# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
-
-APP_PATH = File.expand_path('../../config/application', __FILE__)
-require File.expand_path('../../config/boot', __FILE__)
-require 'rails/commands'
diff --git a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb b/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb
deleted file mode 100644
index d09ce5ad34..0000000000
--- a/railties/lib/rails/generators/rails/app/templates/test/performance/browsing_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class BrowsingTest < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { runs: 5, metrics: [:wall_time, :memory],
- # output: 'tmp/performance', formats: [:flat] }
-
- test "homepage" do
- get '/'
- end
-end
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 9afda2d0df..4fd060341e 100644
--- a/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/app/templates/test/test_helper.rb
@@ -1,4 +1,4 @@
-ENV["RAILS_ENV"] = "test"
+ENV["RAILS_ENV"] ||= "test"
require File.expand_path('../../config/environment', __FILE__)
require 'rails/test_help'
diff --git a/railties/lib/rails/generators/rails/controller/USAGE b/railties/lib/rails/generators/rails/controller/USAGE
index 9def4af65c..de33900e0a 100644
--- a/railties/lib/rails/generators/rails/controller/USAGE
+++ b/railties/lib/rails/generators/rails/controller/USAGE
@@ -6,7 +6,7 @@ Description:
path like 'parent_module/controller_name'.
This generates a controller class in app/controllers and invokes helper,
- template engine and test framework generators.
+ template engine, assets, and test framework generators.
Example:
`rails generate controller CreditCards open debit credit close`
@@ -16,3 +16,4 @@ 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 bae54623c6..ef84447df9 100644
--- a/railties/lib/rails/generators/rails/controller/controller_generator.rb
+++ b/railties/lib/rails/generators/rails/controller/controller_generator.rb
@@ -10,11 +10,45 @@ module Rails
def add_routes
actions.reverse.each do |action|
- route %{get "#{file_name}/#{action}"}
+ route generate_routing_code(action)
end
end
hook_for :template_engine, :test_framework, :helper, :assets
+
+ private
+
+ # This method creates nested route entry for namespaced resources.
+ # For eg. rails g controller foo/bar/baz index
+ # Will generate -
+ # namespace :foo do
+ # namespace :bar do
+ # get "baz/index"
+ # end
+ # end
+ def generate_routing_code(action)
+ depth = class_path.length
+ # Create 'namespace' ladder
+ # namespace :foo do
+ # namespace :bar do
+ namespace_ladder = class_path.each_with_index.map do |ns, i|
+ indent("namespace :#{ns} do\n", i * 2)
+ end.join
+
+ # Create route
+ # get "baz/index"
+ route = indent(%{get "#{file_name}/#{action}"\n}, depth * 2)
+
+ # Create `end` ladder
+ # end
+ # end
+ end_ladder = (1..depth).reverse_each.map do |i|
+ indent("end\n", i * 2)
+ end.join
+
+ # Combine the 3 parts to generate complete route entry
+ namespace_ladder + route + end_ladder
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/generator/USAGE b/railties/lib/rails/generators/rails/generator/USAGE
index d28eb3d7d8..799383050c 100644
--- a/railties/lib/rails/generators/rails/generator/USAGE
+++ b/railties/lib/rails/generators/rails/generator/USAGE
@@ -10,3 +10,4 @@ Example:
lib/generators/awesome/awesome_generator.rb
lib/generators/awesome/USAGE
lib/generators/awesome/templates/
+ test/lib/generators/awesome_generator_test.rb
diff --git a/railties/lib/rails/generators/rails/generator/generator_generator.rb b/railties/lib/rails/generators/rails/generator/generator_generator.rb
index 9a7a516b5b..15d88f06ac 100644
--- a/railties/lib/rails/generators/rails/generator/generator_generator.rb
+++ b/railties/lib/rails/generators/rails/generator/generator_generator.rb
@@ -10,6 +10,8 @@ module Rails
directory '.', generator_dir
end
+ hook_for :test_framework
+
protected
def generator_dir
diff --git a/railties/lib/rails/generators/rails/migration/USAGE b/railties/lib/rails/generators/rails/migration/USAGE
index af74963b01..baf3d9894f 100644
--- a/railties/lib/rails/generators/rails/migration/USAGE
+++ b/railties/lib/rails/generators/rails/migration/USAGE
@@ -15,15 +15,21 @@ Example:
`rails generate migration AddTitleBodyToPost title:string body:text published:boolean`
- This will create the AddTitleBodyToPost in db/migrate/20080514090912_add_title_body_to_post.rb with
- this in the Up migration:
+ This will create the AddTitleBodyToPost in db/migrate/20080514090912_add_title_body_to_post.rb with this in the Change migration:
add_column :posts, :title, :string
add_column :posts, :body, :text
add_column :posts, :published, :boolean
- And this in the Down migration:
+Migration names containing JoinTable will generate join tables for use with
+has_and_belongs_to_many associations.
- remove_column :posts, :published
- remove_column :posts, :body
- remove_column :posts, :title
+Example:
+ `rails g migration CreateMediaJoinTable artists musics:uniq`
+
+ will create the migration
+
+ create_join_table :artists, :musics do |t|
+ # t.index [:artist_id, :music_id]
+ t.index [:music_id, :artist_id], unique: true
+ end
diff --git a/railties/lib/rails/generators/rails/model/USAGE b/railties/lib/rails/generators/rails/model/USAGE
index e29e19490e..833b7beb7f 100644
--- a/railties/lib/rails/generators/rails/model/USAGE
+++ b/railties/lib/rails/generators/rails/model/USAGE
@@ -21,12 +21,12 @@ Description:
Available field types:
- Just after the field name you can specify a type like text or boolean.
+ Just after the field name you can specify a type like text or boolean.
It will generate the column with the associated SQL type. For instance:
`rails generate model post title:string body:text`
- will generate a title column with a varchar type and a body column with a text
+ will generate a title column with a varchar type and a body column with a text
type. You can use the following types:
integer
@@ -46,27 +46,33 @@ Available field types:
`rails generate model photo title:string album:references`
- It will generate an album_id column. You should generate this kind of fields when
- you will use a `belongs_to` association for instance. `references` also support
- the polymorphism, you could enable the polymorphism like this:
+ It will generate an `album_id` column. You should generate these kinds of fields when
+ you will use a `belongs_to` association, for instance. `references` also supports
+ polymorphism, you can enable polymorphism like this:
`rails generate model product supplier:references{polymorphic}`
- You can also specify some options just after the field type. You can use the
- following options:
+ For integer, string, text and binary fields, an integer in curly braces will
+ be set as the limit:
- limit Set the maximum size of the field giving a number between curly braces
- default Set a default value for the field
- precision Defines the precision for the decimal fields
- scale Defines the scale for the decimal fields
- uniq Defines the field values as unique
- index Will add an index on the field
+ `rails generate model user pseudo:string{30}`
- Examples:
+ For decimal, two integers separated by a comma in curly braces will be used
+ for precision and scale:
+
+ `rails generate model product 'price:decimal{10,2}'`
+
+ You can add a `:uniq` or `:index` suffix for unique or standard indexes
+ respectively:
- `rails generate model user pseudo:string{30}`
`rails generate model user pseudo:string:uniq`
-
+ `rails generate model user pseudo:string:index`
+
+ You can combine any single curly brace option with the index options:
+
+ `rails generate model user username:string{30}:uniq`
+ `rails generate model product supplier:references{polymorphic}:index`
+
Examples:
`rails generate model account`
@@ -76,7 +82,7 @@ Examples:
Model: app/models/account.rb
Test: test/models/account_test.rb
Fixtures: test/fixtures/accounts.yml
- Migration: db/migrate/XXX_add_accounts.rb
+ Migration: db/migrate/XXX_create_accounts.rb
`rails generate model post title:string body:text published:boolean`
@@ -90,5 +96,5 @@ Examples:
Model: app/models/admin/account.rb
Test: test/models/admin/account_test.rb
Fixtures: test/fixtures/admin/accounts.yml
- Migration: db/migrate/XXX_add_admin_accounts.rb
+ Migration: db/migrate/XXX_create_admin_accounts.rb
diff --git a/railties/lib/rails/generators/rails/observer/USAGE b/railties/lib/rails/generators/rails/observer/USAGE
deleted file mode 100644
index 177ff49e4a..0000000000
--- a/railties/lib/rails/generators/rails/observer/USAGE
+++ /dev/null
@@ -1,12 +0,0 @@
-Description:
- Stubs out a new observer. Pass the observer name, either CamelCased or
- under_scored, as an argument.
-
- This generator only invokes your ORM and test framework generators.
-
-Example:
- `rails generate observer Account`
-
- For ActiveRecord and TestUnit it creates:
- Observer: app/models/account_observer.rb
- TestUnit: test/models/account_observer_test.rb
diff --git a/railties/lib/rails/generators/rails/observer/observer_generator.rb b/railties/lib/rails/generators/rails/observer/observer_generator.rb
deleted file mode 100644
index 7a4d701ac6..0000000000
--- a/railties/lib/rails/generators/rails/observer/observer_generator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Rails
- module Generators
- class ObserverGenerator < NamedBase # :nodoc:
- hook_for :orm, required: true
- end
- end
-end
diff --git a/railties/lib/rails/generators/rails/performance_test/USAGE b/railties/lib/rails/generators/rails/performance_test/USAGE
deleted file mode 100644
index 9dc799559c..0000000000
--- a/railties/lib/rails/generators/rails/performance_test/USAGE
+++ /dev/null
@@ -1,10 +0,0 @@
-Description:
- Stubs out a new performance test. Pass the name of the test, either
- CamelCased or under_scored, as an argument.
-
- This generator invokes the current performance tool, which defaults to
- TestUnit.
-
-Example:
- `rails generate performance_test GeneralStories` creates a GeneralStories
- performance test in test/performance/general_stories_test.rb
diff --git a/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb b/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb
deleted file mode 100644
index 56cd562f3d..0000000000
--- a/railties/lib/rails/generators/rails/performance_test/performance_test_generator.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Rails
- module Generators
- class PerformanceTestGenerator < NamedBase # :nodoc:
- hook_for :performance_tool, as: :performance
- end
- end
-end
diff --git a/railties/lib/rails/generators/rails/plugin_new/USAGE b/railties/lib/rails/generators/rails/plugin/USAGE
index 9a7bf9f396..9a7bf9f396 100644
--- a/railties/lib/rails/generators/rails/plugin_new/USAGE
+++ b/railties/lib/rails/generators/rails/plugin/USAGE
diff --git a/railties/lib/rails/generators/rails/plugin/plugin_generator.rb b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
new file mode 100644
index 0000000000..f6f529b80a
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/plugin_generator.rb
@@ -0,0 +1,371 @@
+require 'active_support/core_ext/hash/slice'
+require "rails/generators/rails/app/app_generator"
+require 'date'
+
+module Rails
+ # The plugin builder allows you to override elements of the plugin
+ # generator without being forced to reverse the operations of the default
+ # generator.
+ #
+ # This allows you to override entire operations, like the creation of the
+ # Gemfile, README, or JavaScript files, without needing to know exactly
+ # what those operations do so you can create another template action.
+ class PluginBuilder
+ def rakefile
+ template "Rakefile"
+ end
+
+ def app
+ if mountable?
+ directory 'app'
+ empty_directory_with_keep_file "app/assets/images/#{name}"
+ elsif full?
+ empty_directory_with_keep_file 'app/models'
+ empty_directory_with_keep_file 'app/controllers'
+ empty_directory_with_keep_file 'app/views'
+ empty_directory_with_keep_file 'app/helpers'
+ empty_directory_with_keep_file 'app/mailers'
+ empty_directory_with_keep_file "app/assets/images/#{name}"
+ end
+ end
+
+ def readme
+ template "README.rdoc"
+ end
+
+ def gemfile
+ template "Gemfile"
+ end
+
+ def license
+ template "MIT-LICENSE"
+ end
+
+ def gemspec
+ template "%name%.gemspec"
+ end
+
+ def gitignore
+ template "gitignore", ".gitignore"
+ end
+
+ def lib
+ template "lib/%name%.rb"
+ template "lib/tasks/%name%_tasks.rake"
+ template "lib/%name%/version.rb"
+ template "lib/%name%/engine.rb" if engine?
+ end
+
+ def config
+ template "config/routes.rb" if engine?
+ end
+
+ def test
+ template "test/test_helper.rb"
+ template "test/%name%_test.rb"
+ append_file "Rakefile", <<-EOF
+#{rakefile_test_tasks}
+
+task default: :test
+ EOF
+ if engine?
+ template "test/integration/navigation_test.rb"
+ end
+ end
+
+ PASSTHROUGH_OPTIONS = [
+ :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip
+ ]
+
+ def generate_test_dummy(force = false)
+ opts = (options || {}).slice(*PASSTHROUGH_OPTIONS)
+ opts[:force] = force
+ opts[:skip_bundle] = true
+
+ invoke Rails::Generators::AppGenerator,
+ [ File.expand_path(dummy_path, destination_root) ], opts
+ end
+
+ def test_dummy_config
+ template "rails/boot.rb", "#{dummy_path}/config/boot.rb", force: true
+ template "rails/application.rb", "#{dummy_path}/config/application.rb", force: true
+ if mountable?
+ template "rails/routes.rb", "#{dummy_path}/config/routes.rb", force: true
+ end
+ end
+
+ def test_dummy_assets
+ template "rails/javascripts.js", "#{dummy_path}/app/assets/javascripts/application.js", force: true
+ template "rails/stylesheets.css", "#{dummy_path}/app/assets/stylesheets/application.css", force: true
+ end
+
+ def test_dummy_clean
+ inside dummy_path do
+ remove_file ".gitignore"
+ remove_file "db/seeds.rb"
+ remove_file "doc"
+ remove_file "Gemfile"
+ remove_file "lib/tasks"
+ remove_file "public/robots.txt"
+ remove_file "README"
+ remove_file "test"
+ remove_file "vendor"
+ end
+ end
+
+ def stylesheets
+ if mountable?
+ copy_file "rails/stylesheets.css",
+ "app/assets/stylesheets/#{name}/application.css"
+ elsif full?
+ empty_directory_with_keep_file "app/assets/stylesheets/#{name}"
+ end
+ end
+
+ def javascripts
+ return if options.skip_javascript?
+
+ if mountable?
+ template "rails/javascripts.js",
+ "app/assets/javascripts/#{name}/application.js"
+ elsif full?
+ empty_directory_with_keep_file "app/assets/javascripts/#{name}"
+ end
+ end
+
+ def bin(force = false)
+ return unless engine?
+
+ directory "bin", force: force do |content|
+ "#{shebang}\n" + content
+ end
+ chmod "bin", 0755, verbose: false
+ end
+
+ def gemfile_entry
+ return unless inside_application?
+
+ gemfile_in_app_path = File.join(rails_app_path, "Gemfile")
+ if File.exist? gemfile_in_app_path
+ entry = "gem '#{name}', path: '#{relative_path}'"
+ append_file gemfile_in_app_path, entry
+ end
+ end
+ end
+
+ module Generators
+ class PluginGenerator < AppBase # :nodoc:
+ add_shared_options_for "plugin"
+
+ alias_method :plugin_path, :app_path
+
+ class_option :dummy_path, type: :string, default: "test/dummy",
+ desc: "Create dummy application at given path"
+
+ class_option :full, type: :boolean, default: false,
+ desc: "Generate a rails engine with bundled Rails application for testing"
+
+ class_option :mountable, type: :boolean, default: false,
+ desc: "Generate mountable isolated application"
+
+ class_option :skip_gemspec, type: :boolean, default: false,
+ desc: "Skip gemspec file"
+
+ class_option :skip_gemfile_entry, type: :boolean, default: false,
+ desc: "If creating plugin in application's directory " +
+ "skip adding entry to Gemfile"
+
+ def initialize(*args)
+ @dummy_path = nil
+ super
+
+ unless plugin_path
+ raise Error, "Plugin name should be provided in arguments. For details run: rails plugin new --help"
+ end
+ end
+
+ public_task :set_default_accessors!
+ public_task :create_root
+
+ def create_root_files
+ build(:readme)
+ build(:rakefile)
+ build(:gemspec) unless options[:skip_gemspec]
+ build(:license)
+ build(:gitignore) unless options[:skip_git]
+ build(:gemfile) unless options[:skip_gemfile]
+ end
+
+ def create_app_files
+ build(:app)
+ end
+
+ def create_config_files
+ build(:config)
+ end
+
+ def create_lib_files
+ build(:lib)
+ end
+
+ def create_public_stylesheets_files
+ build(:stylesheets)
+ end
+
+ def create_javascript_files
+ build(:javascripts)
+ end
+
+ def create_images_directory
+ build(:images)
+ end
+
+ def create_bin_files
+ build(:bin)
+ end
+
+ def create_test_files
+ build(:test) unless options[:skip_test_unit]
+ end
+
+ def create_test_dummy_files
+ return unless with_dummy_app?
+ create_dummy_app
+ end
+
+ def update_gemfile
+ build(:gemfile_entry) unless options[:skip_gemfile_entry]
+ end
+
+ def finish_template
+ build(:leftovers)
+ end
+
+ public_task :apply_rails_template, :run_bundle
+
+ def name
+ @name ||= begin
+ # same as ActiveSupport::Inflector#underscore except not replacing '-'
+ underscored = original_name.dup
+ underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
+ underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
+ underscored.downcase!
+
+ underscored
+ end
+ end
+
+ protected
+
+ def app_templates_dir
+ "../../app/templates"
+ end
+
+ def create_dummy_app(path = nil)
+ dummy_path(path) if path
+
+ say_status :vendor_app, dummy_path
+ mute do
+ build(:generate_test_dummy)
+ store_application_definition!
+ build(:test_dummy_config)
+ build(:test_dummy_assets)
+ build(:test_dummy_clean)
+ # ensure that bin/rails has proper dummy_path
+ build(:bin, true)
+ end
+ end
+
+ def engine?
+ full? || mountable?
+ end
+
+ def full?
+ options[:full]
+ end
+
+ def mountable?
+ options[:mountable]
+ end
+
+ def with_dummy_app?
+ options[:skip_test_unit].blank? || options[:dummy_path] != 'test/dummy'
+ end
+
+ def self.banner
+ "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]"
+ end
+
+ def original_name
+ @original_name ||= File.basename(destination_root)
+ end
+
+ def camelized
+ @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
+ 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."
+ elsif camelized =~ /^\d/
+ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers."
+ elsif RESERVED_NAMES.include?(name)
+ raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words."
+ elsif Object.const_defined?(camelized)
+ raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name."
+ end
+ end
+
+ def application_definition
+ @application_definition ||= begin
+
+ dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root)
+ unless options[:pretend] || !File.exist?(dummy_application_path)
+ contents = File.read(dummy_application_path)
+ contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1]
+ end
+ end
+ end
+ alias :store_application_definition! :application_definition
+
+ def get_builder_class
+ defined?(::PluginBuilder) ? ::PluginBuilder : Rails::PluginBuilder
+ end
+
+ def rakefile_test_tasks
+ <<-RUBY
+require 'rake/testtask'
+
+Rake::TestTask.new(:test) do |t|
+ t.libs << 'lib'
+ t.libs << 'test'
+ t.pattern = 'test/**/*_test.rb'
+ t.verbose = false
+end
+ RUBY
+ end
+
+ def dummy_path(path = nil)
+ @dummy_path = path if path
+ @dummy_path || options[:dummy_path]
+ end
+
+ def mute(&block)
+ shell.mute(&block)
+ end
+
+ def rails_app_path
+ APP_PATH.sub("/config/application", "") if defined?(APP_PATH)
+ end
+
+ def inside_application?
+ rails_app_path && app_path =~ /^#{rails_app_path}/
+ end
+
+ def relative_path
+ return unless inside_application?
+ app_path.sub(/^#{rails_app_path}\//, '')
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
new file mode 100644
index 0000000000..5fdf0e1554
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/%name%.gemspec
@@ -0,0 +1,27 @@
+$:.push File.expand_path("../lib", __FILE__)
+
+# Maintain your gem's version:
+require "<%= name %>/version"
+
+# Describe your gem and declare its dependencies:
+Gem::Specification.new do |s|
+ s.name = "<%= name %>"
+ s.version = <%= camelized %>::VERSION
+ s.authors = ["TODO: Your name"]
+ s.email = ["TODO: Your email"]
+ s.homepage = "TODO"
+ s.summary = "TODO: Summary of <%= camelized %>."
+ s.description = "TODO: Description of <%= camelized %>."
+ s.license = "MIT"
+
+ s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
+<% unless options.skip_test_unit? -%>
+ s.test_files = Dir["test/**/*"]
+<% end -%>
+
+ <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>"
+<% unless options[:skip_active_record] -%>
+
+ s.add_development_dependency "<%= gem_for_database %>"
+<% end -%>
+end
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Gemfile b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
new file mode 100644
index 0000000000..d576784415
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Gemfile
@@ -0,0 +1,43 @@
+source "https://rubygems.org"
+
+<% if options[:skip_gemspec] -%>
+<%= '# ' 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
+# development dependencies will be added by default to the :development group.
+gemspec
+<% end -%>
+
+<% if options[:skip_gemspec] -%>
+group :development do
+ gem "<%= gem_for_database %>"
+end
+<% else -%>
+# Declare any dependencies that are still in development here instead of in
+# your gemspec. These might include edge Rails or gems from your path or
+# Git. Remember to move these dependencies to your gemspec before releasing
+# your gem to rubygems.org.
+<% end -%>
+
+<% if options.dev? || options.edge? -%>
+# Your gem is dependent on dev or edge Rails. Once you can lock this
+# dependency down to a specific version, move it to your gemspec.
+<% max_width = gemfile_entries.map { |g| g.name.length }.max -%>
+<% gemfile_entries.each do |gem| -%>
+<% if gem.comment -%>
+
+# <%= gem.comment %>
+<% end -%>
+<%= gem.commented_out ? '# ' : '' %>gem '<%= gem.name %>'<% if gem.version -%>
+, '<%= gem.version %>'
+<% elsif gem.options.any? -%>
+,<%= gem.padding(max_width) %><%= gem.options.map { |k,v|
+ "#{k}: #{v.inspect}" }.join(', ') %>
+<% else %>
+<% end -%>
+<% end -%>
+
+<% end -%>
+# To use debugger
+# gem 'debugger'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
index d7a9109894..d7a9109894 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/MIT-LICENSE
+++ b/railties/lib/rails/generators/rails/plugin/templates/MIT-LICENSE
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
index 301d647731..301d647731 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/README.rdoc
+++ b/railties/lib/rails/generators/rails/plugin/templates/README.rdoc
diff --git a/railties/lib/rails/generators/rails/plugin/templates/Rakefile b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
new file mode 100644
index 0000000000..0ba899176c
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/Rakefile
@@ -0,0 +1,25 @@
+begin
+ require 'bundler/setup'
+rescue LoadError
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
+end
+
+require 'rdoc/task'
+
+RDoc::Task.new(:rdoc) do |rdoc|
+ rdoc.rdoc_dir = 'rdoc'
+ rdoc.title = '<%= camelized %>'
+ rdoc.options << '--line-numbers'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+end
+
+<% if engine? && !options[:skip_active_record] && with_dummy_app? -%>
+APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__)
+load 'rails/tasks/engine.rake'
+<% end %>
+
+<% unless options[:skip_gemspec] -%>
+
+Bundler::GemHelper.install_tasks
+<% end %>
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt
index 448ad7f989..448ad7f989 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/controllers/%name%/application_controller.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/controllers/%name%/application_controller.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt
index 40ae9f52c2..40ae9f52c2 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/helpers/%name%/application_helper.rb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/helpers/%name%/application_helper.rb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/mailers/.empty_directory
diff --git a/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
new file mode 100644
index 0000000000..e69de29bb2
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/models/.empty_directory
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt
index 1d380420b4..1d380420b4 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/app/views/layouts/%name%/application.html.erb.tt
+++ b/railties/lib/rails/generators/rails/plugin/templates/app/views/layouts/%name%/application.html.erb.tt
diff --git a/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
new file mode 100644
index 0000000000..c8de9f3e0f
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/bin/rails.tt
@@ -0,0 +1,7 @@
+# This command will automatically be run when you run "rails" with Rails 4 gems installed from the root of your application.
+
+ENGINE_ROOT = File.expand_path('../..', __FILE__)
+ENGINE_PATH = File.expand_path('../../lib/<%= name -%>/engine', __FILE__)
+
+require 'rails/all'
+require 'rails/engine/commands'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
index 8e158d5831..8e158d5831 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/config/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/config/routes.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore b/railties/lib/rails/generators/rails/plugin/templates/gitignore
index 086d87818a..086d87818a 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/gitignore
+++ b/railties/lib/rails/generators/rails/plugin/templates/gitignore
diff --git a/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb
new file mode 100644
index 0000000000..40c074cced
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%.rb
@@ -0,0 +1,6 @@
+<% if engine? -%>
+require "<%= name %>/engine"
+
+<% end -%>
+module <%= camelized %>
+end
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb
index 967668fe66..967668fe66 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/engine.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/engine.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb
index ef07ef2e19..ef07ef2e19 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%/version.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/%name%/version.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake
index 7121f5ae23..7121f5ae23 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/tasks/%name%_tasks.rake
+++ b/railties/lib/rails/generators/rails/plugin/templates/lib/tasks/%name%_tasks.rake
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
new file mode 100644
index 0000000000..5508829f6b
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/application.rb
@@ -0,0 +1,18 @@
+require File.expand_path('../boot', __FILE__)
+
+<% if include_all_railties? -%>
+require 'rails/all'
+<% else -%>
+# Pick the frameworks you want:
+<%= comment_if :skip_active_record %>require "active_record/railtie"
+require "action_controller/railtie"
+require "action_mailer/railtie"
+<%= comment_if :skip_action_view %>require "action_view/railtie"
+<%= comment_if :skip_sprockets %>require "sprockets/railtie"
+<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie"
+<% end -%>
+
+Bundler.require(*Rails.groups)
+require "<%= name %>"
+
+<%= application_definition %>
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
new file mode 100644
index 0000000000..6266cfc509
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/boot.rb
@@ -0,0 +1,5 @@
+# Set up gems listed in the Gemfile.
+ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../../../Gemfile', __FILE__)
+
+require 'bundler/setup' if File.exist?(ENV['BUNDLE_GEMFILE'])
+$LOAD_PATH.unshift File.expand_path('../../../../lib', __FILE__)
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
new file mode 100644
index 0000000000..5bc2e1c8b5
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/javascripts.js
@@ -0,0 +1,13 @@
+// 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.
+//
+// Read Sprockets README (https://github.com/sstephenson/sprockets#sprockets-directives) for details
+// about supported directives.
+//
+//= require_tree .
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
index 730ee31c3d..730ee31c3d 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/routes.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/routes.rb
diff --git a/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
new file mode 100644
index 0000000000..a443db3401
--- /dev/null
+++ b/railties/lib/rails/generators/rails/plugin/templates/rails/stylesheets.css
@@ -0,0 +1,15 @@
+/*
+ * 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 bottom of the
+ * compiled file so the styles you add here take precedence over styles defined in any styles
+ * defined in the other CSS/SCSS files in this directory. It is generally better to create a new
+ * file per style scope.
+ *
+ *= require_tree .
+ *= require_self
+ */
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb
index 0a8bbd4aaf..0a8bbd4aaf 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/%name%_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/%name%_test.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
index 824caecb24..824caecb24 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/integration/navigation_test.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/integration/navigation_test.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
index 1e26a313cd..1e26a313cd 100644
--- a/railties/lib/rails/generators/rails/plugin_new/templates/test/test_helper.rb
+++ b/railties/lib/rails/generators/rails/plugin/templates/test/test_helper.rb
diff --git a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb b/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb
deleted file mode 100644
index 4a0bcc35a4..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/plugin_new_generator.rb
+++ /dev/null
@@ -1,358 +0,0 @@
-require 'active_support/core_ext/hash/slice'
-require "rails/generators/rails/app/app_generator"
-require 'date'
-
-module Rails
- # The plugin builder allows you to override elements of the plugin
- # generator without being forced to reverse the operations of the default
- # generator.
- #
- # This allows you to override entire operations, like the creation of the
- # Gemfile, README, or JavaScript files, without needing to know exactly
- # what those operations do so you can create another template action.
- class PluginBuilder
- def rakefile
- template "Rakefile"
- end
-
- def app
- if mountable?
- directory 'app'
- empty_directory_with_keep_file "app/assets/images/#{name}"
- elsif full?
- empty_directory_with_keep_file 'app/models'
- empty_directory_with_keep_file 'app/controllers'
- empty_directory_with_keep_file 'app/views'
- empty_directory_with_keep_file 'app/helpers'
- empty_directory_with_keep_file 'app/mailers'
- empty_directory_with_keep_file "app/assets/images/#{name}"
- end
- end
-
- def readme
- template "README.rdoc"
- end
-
- def gemfile
- template "Gemfile"
- end
-
- def license
- template "MIT-LICENSE"
- end
-
- def gemspec
- template "%name%.gemspec"
- end
-
- def gitignore
- template "gitignore", ".gitignore"
- end
-
- def lib
- template "lib/%name%.rb"
- template "lib/tasks/%name%_tasks.rake"
- template "lib/%name%/version.rb"
- if full?
- template "lib/%name%/engine.rb"
- end
- end
-
- def config
- template "config/routes.rb" if full?
- end
-
- def test
- template "test/test_helper.rb"
- template "test/%name%_test.rb"
- append_file "Rakefile", <<-EOF
-#{rakefile_test_tasks}
-
-task default: :test
- EOF
- if full?
- template "test/integration/navigation_test.rb"
- end
- end
-
- PASSTHROUGH_OPTIONS = [
- :skip_active_record, :skip_javascript, :database, :javascript, :quiet, :pretend, :force, :skip
- ]
-
- def generate_test_dummy(force = false)
- opts = (options || {}).slice(*PASSTHROUGH_OPTIONS)
- opts[:force] = force
- opts[:skip_bundle] = true
-
- invoke Rails::Generators::AppGenerator,
- [ File.expand_path(dummy_path, destination_root) ], opts
- end
-
- def test_dummy_config
- template "rails/boot.rb", "#{dummy_path}/config/boot.rb", force: true
- template "rails/application.rb", "#{dummy_path}/config/application.rb", force: true
- if mountable?
- template "rails/routes.rb", "#{dummy_path}/config/routes.rb", force: true
- end
- end
-
- def test_dummy_clean
- inside dummy_path do
- remove_file ".gitignore"
- remove_file "db/seeds.rb"
- remove_file "doc"
- remove_file "Gemfile"
- remove_file "lib/tasks"
- remove_file "app/assets/images/rails.png"
- remove_file "public/index.html"
- remove_file "public/robots.txt"
- remove_file "README"
- remove_file "test"
- remove_file "vendor"
- end
- end
-
- def stylesheets
- if mountable?
- copy_file "#{app_templates_dir}/app/assets/stylesheets/application.css",
- "app/assets/stylesheets/#{name}/application.css"
- elsif full?
- empty_directory_with_keep_file "app/assets/stylesheets/#{name}"
- end
- end
-
- def javascripts
- return if options.skip_javascript?
-
- if mountable?
- template "#{app_templates_dir}/app/assets/javascripts/application.js.tt",
- "app/assets/javascripts/#{name}/application.js"
- elsif full?
- empty_directory_with_keep_file "app/assets/javascripts/#{name}"
- end
- end
-
- def script(force = false)
- return unless full?
-
- directory "script", force: force do |content|
- "#{shebang}\n" + content
- end
- chmod "script", 0755, verbose: false
- end
-
- def gemfile_entry
- return unless inside_application?
-
- gemfile_in_app_path = File.join(rails_app_path, "Gemfile")
- if File.exist? gemfile_in_app_path
- entry = "gem '#{name}', path: '#{relative_path}'"
- append_file gemfile_in_app_path, entry
- end
- end
- end
-
- module Generators
- class PluginNewGenerator < AppBase # :nodoc:
- add_shared_options_for "plugin"
-
- alias_method :plugin_path, :app_path
-
- class_option :dummy_path, type: :string, default: "test/dummy",
- desc: "Create dummy application at given path"
-
- class_option :full, type: :boolean, default: false,
- desc: "Generate a rails engine with bundled Rails application for testing"
-
- class_option :mountable, type: :boolean, default: false,
- desc: "Generate mountable isolated application"
-
- class_option :skip_gemspec, type: :boolean, default: false,
- desc: "Skip gemspec file"
-
- class_option :skip_gemfile_entry, type: :boolean, default: false,
- desc: "If creating plugin in application's directory " +
- "skip adding entry to Gemfile"
-
- def initialize(*args)
- raise Error, "Options should be given after the plugin name. For details run: rails plugin --help" if args[0].blank?
-
- @dummy_path = nil
- super
- end
-
- public_task :create_root
-
- def create_root_files
- build(:readme)
- build(:rakefile)
- build(:gemspec) unless options[:skip_gemspec]
- build(:license)
- build(:gitignore) unless options[:skip_git]
- build(:gemfile) unless options[:skip_gemfile]
- end
-
- def create_app_files
- build(:app)
- end
-
- def create_config_files
- build(:config)
- end
-
- def create_lib_files
- build(:lib)
- end
-
- def create_public_stylesheets_files
- build(:stylesheets)
- end
-
- def create_javascript_files
- build(:javascripts)
- end
-
- def create_images_directory
- build(:images)
- end
-
- def create_script_files
- build(:script)
- end
-
- def create_test_files
- build(:test) unless options[:skip_test_unit]
- end
-
- def create_test_dummy_files
- return if options[:skip_test_unit] && options[:dummy_path] == 'test/dummy'
- create_dummy_app
- end
-
- def update_gemfile
- build(:gemfile_entry) unless options[:skip_gemfile_entry]
- end
-
- def finish_template
- build(:leftovers)
- end
-
- public_task :apply_rails_template, :run_bundle
-
- def name
- @name ||= begin
- # same as ActiveSupport::Inflector#underscore except not replacing '-'
- underscored = original_name.dup
- underscored.gsub!(/([A-Z]+)([A-Z][a-z])/,'\1_\2')
- underscored.gsub!(/([a-z\d])([A-Z])/,'\1_\2')
- underscored.downcase!
-
- underscored
- end
- end
-
- protected
-
- def app_templates_dir
- "../../app/templates"
- end
-
- def create_dummy_app(path = nil)
- dummy_path(path) if path
-
- say_status :vendor_app, dummy_path
- mute do
- build(:generate_test_dummy)
- store_application_definition!
- build(:test_dummy_config)
- build(:test_dummy_clean)
- # ensure that script/rails has proper dummy_path
- build(:script, true)
- end
- end
-
- def full?
- options[:full] || options[:mountable]
- end
-
- def mountable?
- options[:mountable]
- end
-
- def self.banner
- "rails plugin new #{self.arguments.map(&:usage).join(' ')} [options]"
- end
-
- def original_name
- @original_name ||= File.basename(destination_root)
- end
-
- def camelized
- @camelized ||= name.gsub(/\W/, '_').squeeze('_').camelize
- 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."
- elsif camelized =~ /^\d/
- raise Error, "Invalid plugin name #{original_name}. Please give a name which does not start with numbers."
- elsif RESERVED_NAMES.include?(name)
- raise Error, "Invalid plugin name #{original_name}. Please give a name which does not match one of the reserved rails words."
- elsif Object.const_defined?(camelized)
- raise Error, "Invalid plugin name #{original_name}, constant #{camelized} is already in use. Please choose another plugin name."
- end
- end
-
- def application_definition
- @application_definition ||= begin
-
- dummy_application_path = File.expand_path("#{dummy_path}/config/application.rb", destination_root)
- unless options[:pretend] || !File.exists?(dummy_application_path)
- contents = File.read(dummy_application_path)
- contents[(contents.index(/module ([\w]+)\n(.*)class Application/m))..-1]
- end
- end
- end
- alias :store_application_definition! :application_definition
-
- def get_builder_class
- defined?(::PluginBuilder) ? ::PluginBuilder : Rails::PluginBuilder
- end
-
- def rakefile_test_tasks
- <<-RUBY
-require 'rake/testtask'
-
-Rake::TestTask.new(:test) do |t|
- t.libs << 'lib'
- t.libs << 'test'
- t.pattern = 'test/**/*_test.rb'
- t.verbose = false
-end
- RUBY
- end
-
- def dummy_path(path = nil)
- @dummy_path = path if path
- @dummy_path || options[:dummy_path]
- end
-
- def mute(&block)
- shell.mute(&block)
- end
-
- def rails_app_path
- APP_PATH.sub("/config/application", "") if defined?(APP_PATH)
- end
-
- def inside_application?
- rails_app_path && app_path =~ /^#{rails_app_path}/
- end
-
- def relative_path
- return unless inside_application?
- app_path.sub(/^#{rails_app_path}\//, '')
- end
- end
- end
-end
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec b/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec
deleted file mode 100644
index 568ed653b7..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/%name%.gemspec
+++ /dev/null
@@ -1,29 +0,0 @@
-$:.push File.expand_path("../lib", __FILE__)
-
-# Maintain your gem's version:
-require "<%= name %>/version"
-
-# Describe your gem and declare its dependencies:
-Gem::Specification.new do |s|
- s.name = "<%= name %>"
- s.version = <%= camelized %>::VERSION
- s.authors = ["TODO: Your name"]
- s.email = ["TODO: Your email"]
- s.homepage = "TODO"
- s.summary = "TODO: Summary of <%= camelized %>."
- s.description = "TODO: Description of <%= camelized %>."
-
- s.files = Dir["{app,config,db,lib}/**/*", "MIT-LICENSE", "Rakefile", "README.rdoc"]
-<% unless options.skip_test_unit? -%>
- s.test_files = Dir["test/**/*"]
-<% end -%>
-
- <%= '# ' if options.dev? || options.edge? -%>s.add_dependency "rails", "~> <%= Rails::VERSION::STRING %>"
-<% if full? && !options[:skip_javascript] -%>
- # s.add_dependency "<%= "#{options[:javascript]}-rails" %>"
-<% end -%>
-<% unless options[:skip_active_record] -%>
-
- s.add_development_dependency "<%= gem_for_database %>"
-<% end -%>
-end
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile b/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile
deleted file mode 100644
index 7448b386c5..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/Gemfile
+++ /dev/null
@@ -1,38 +0,0 @@
-source "http://rubygems.org"
-
-<% if options[:skip_gemspec] -%>
-<%= '# ' if options.dev? || options.edge? -%>gem "rails", "~> <%= Rails::VERSION::STRING %>"
-<% if full? && !options[:skip_javascript] -%>
-# gem "<%= "#{options[:javascript]}-rails" %>"
-<% end -%>
-<% else -%>
-# Declare your gem's dependencies in <%= name %>.gemspec.
-# Bundler will treat runtime dependencies like base dependencies, and
-# development dependencies will be added by default to the :development group.
-gemspec
-<% end -%>
-
-<% unless options[:javascript] == 'jquery' -%>
-# jquery-rails is used by the dummy application
-gem "jquery-rails"
-
-<% end -%>
-<% if options[:skip_gemspec] -%>
-group :development do
- gem "<%= gem_for_database %>"
-end
-<% else -%>
-# Declare any dependencies that are still in development here instead of in
-# your gemspec. These might include edge Rails or gems from your path or
-# Git. Remember to move these dependencies to your gemspec before releasing
-# your gem to rubygems.org.
-<% end -%>
-
-<% if options.dev? || options.edge? -%>
-# Your gem is dependent on dev or edge Rails. Once you can lock this
-# dependency down to a specific version, move it to your gemspec.
-<%= rails_gemfile_entry -%>
-
-<% end -%>
-# To use debugger
-# gem 'debugger'
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile b/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile
deleted file mode 100644
index 1369140537..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/Rakefile
+++ /dev/null
@@ -1,25 +0,0 @@
-begin
- require 'bundler/setup'
-rescue LoadError
- puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
-end
-
-require 'rdoc/task'
-
-RDoc::Task.new(:rdoc) do |rdoc|
- rdoc.rdoc_dir = 'rdoc'
- rdoc.title = '<%= camelized %>'
- rdoc.options << '--line-numbers'
- rdoc.rdoc_files.include('README.rdoc')
- rdoc.rdoc_files.include('lib/**/*.rb')
-end
-
-<% if full? && !options[:skip_active_record] && !options[:skip_test_unit] -%>
-APP_RAKEFILE = File.expand_path("../<%= dummy_path -%>/Rakefile", __FILE__)
-load 'rails/tasks/engine.rake'
-<% end %>
-
-<% unless options[:skip_gemspec] -%>
-
-Bundler::GemHelper.install_tasks
-<% end %>
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb b/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb
deleted file mode 100644
index 2d3bdc510c..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/lib/%name%.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-<% if full? -%>
-require "<%= name %>/engine"
-
-<% end -%>
-module <%= camelized %>
-end
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb
deleted file mode 100644
index 2f9b7fc962..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/application.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-require File.expand_path('../boot', __FILE__)
-
-<% if include_all_railties? -%>
-require 'rails/all'
-<% else -%>
-# Pick the frameworks you want:
-<%= comment_if :skip_active_record %>require "active_record/railtie"
-require "action_controller/railtie"
-require "action_mailer/railtie"
-<%= comment_if :skip_sprockets %>require "sprockets/rails/railtie"
-<%= comment_if :skip_test_unit %>require "rails/test_unit/railtie"
-<% end -%>
-
-Bundler.require
-require "<%= name %>"
-
-<%= application_definition %>
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb b/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb
deleted file mode 100644
index c78bfb7f63..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/rails/boot.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-gemfile = File.expand_path('../../../../Gemfile', __FILE__)
-
-if File.exist?(gemfile)
- ENV['BUNDLE_GEMFILE'] = gemfile
- require 'bundler'
- Bundler.setup
-end
-
-$:.unshift File.expand_path('../../../../lib', __FILE__) \ No newline at end of file
diff --git a/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt b/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt
deleted file mode 100644
index aa87d1b50c..0000000000
--- a/railties/lib/rails/generators/rails/plugin_new/templates/script/rails.tt
+++ /dev/null
@@ -1,7 +0,0 @@
-# This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
-
-ENGINE_ROOT = File.expand_path('../..', __FILE__)
-ENGINE_PATH = File.expand_path('../../lib/<%= name -%>/engine', __FILE__)
-
-require 'rails/all'
-require 'rails/engine/commands'
diff --git a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
index 121205b254..a0e5553e44 100644
--- a/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
+++ b/railties/lib/rails/generators/rails/resource_route/resource_route_generator.rb
@@ -32,7 +32,7 @@ module Rails
# route prepends two spaces onto the front of the string that is passed, this corrects that
route route_string[2..-1]
end
-
+
private
def route_string
@route_string ||= ""
diff --git a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
index b4f466fbd8..e89789e72b 100644
--- a/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
@@ -2,12 +2,19 @@ require 'rails/generators/rails/resource/resource_generator'
module Rails
module Generators
- class ScaffoldGenerator < ResourceGenerator # :nodoc:
+ class ScaffoldGenerator < ResourceGenerator # :nodoc:
remove_hook_for :resource_controller
remove_class_option :actions
class_option :stylesheets, type: :boolean, desc: "Generate Stylesheets"
class_option :stylesheet_engine, desc: "Engine for Stylesheets"
+ class_option :assets, type: :boolean
+ class_option :resource_route, type: :boolean
+
+ def handle_skip
+ @options = @options.merge(stylesheets: false) unless options[:assets]
+ @options = @options.merge(stylesheet_engine: false) unless options[:stylesheets]
+ end
hook_for :scaffold_controller, required: true
@@ -16,7 +23,9 @@ module Rails
end
hook_for :stylesheet_engine do |stylesheet_engine|
- invoke stylesheet_engine, [controller_name] if options[:stylesheets] && behavior == :invoke
+ if behavior == :invoke
+ invoke stylesheet_engine, [controller_name]
+ end
end
end
end
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
index 4f36b612ae..6bf0a33a5f 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/scaffold_controller_generator.rb
@@ -13,7 +13,7 @@ module Rails
argument :attributes, type: :array, default: [], banner: "field:type field:type"
def create_controller_files
- template "controller.rb", File.join('app/controllers', class_path, "#{controller_file_name}_controller.rb")
+ template "controller.rb", File.join('app/controllers', controller_class_path, "#{controller_file_name}_controller.rb")
end
hook_for :template_engine, :test_framework, as: :scaffold
diff --git a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
index d6bce40b0c..0e69aa101f 100644
--- a/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
+++ b/railties/lib/rails/generators/rails/scaffold_controller/templates/controller.rb
@@ -4,97 +4,64 @@ require_dependency "<%= namespaced_file_path %>/application_controller"
<% end -%>
<% module_namespacing do -%>
class <%= controller_class_name %>Controller < ApplicationController
+ before_action :set_<%= singular_table_name %>, only: [:show, :edit, :update, :destroy]
+
# GET <%= route_url %>
- # GET <%= route_url %>.json
def index
@<%= plural_table_name %> = <%= orm_class.all(class_name) %>
-
- respond_to do |format|
- format.html # index.html.erb
- format.json { render json: <%= "@#{plural_table_name}" %> }
- end
end
# GET <%= route_url %>/1
- # GET <%= route_url %>/1.json
def show
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
-
- respond_to do |format|
- format.html # show.html.erb
- format.json { render json: <%= "@#{singular_table_name}" %> }
- end
end
# GET <%= route_url %>/new
- # GET <%= route_url %>/new.json
def new
@<%= singular_table_name %> = <%= orm_class.build(class_name) %>
-
- respond_to do |format|
- format.html # new.html.erb
- format.json { render json: <%= "@#{singular_table_name}" %> }
- end
end
# GET <%= route_url %>/1/edit
def edit
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
end
# POST <%= route_url %>
- # POST <%= route_url %>.json
def create
@<%= singular_table_name %> = <%= orm_class.build(class_name, "#{singular_table_name}_params") %>
- respond_to do |format|
- if @<%= orm_instance.save %>
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %> }
- format.json { render json: <%= "@#{singular_table_name}" %>, status: :created, location: <%= "@#{singular_table_name}" %> }
- else
- format.html { render action: "new" }
- format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
- end
+ if @<%= orm_instance.save %>
+ redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully created.'" %>
+ else
+ render action: 'new'
end
end
# PATCH/PUT <%= route_url %>/1
- # PATCH/PUT <%= route_url %>/1.json
def update
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
-
- respond_to do |format|
- if @<%= orm_instance.update_attributes("#{singular_table_name}_params") %>
- format.html { redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %> }
- format.json { head :no_content }
- else
- format.html { render action: "edit" }
- format.json { render json: <%= "@#{orm_instance.errors}" %>, status: :unprocessable_entity }
- end
+ if @<%= orm_instance.update("#{singular_table_name}_params") %>
+ redirect_to @<%= singular_table_name %>, notice: <%= "'#{human_name} was successfully updated.'" %>
+ else
+ render action: 'edit'
end
end
# DELETE <%= route_url %>/1
- # DELETE <%= route_url %>/1.json
def destroy
- @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
@<%= orm_instance.destroy %>
-
- respond_to do |format|
- format.html { redirect_to <%= index_helper %>_url }
- format.json { head :no_content }
- end
+ redirect_to <%= index_helper %>_url, notice: <%= "'#{human_name} was successfully destroyed.'" %>
end
private
+ # Use callbacks to share common setup or constraints between actions.
+ def set_<%= singular_table_name %>
+ @<%= singular_table_name %> = <%= orm_class.find(class_name, "params[:id]") %>
+ end
- # Use this method to whitelist the permissible parameters. Example: params.require(:person).permit(:name, :age)
- # Also, you can specialize this method with per-user checking of permissible attributes.
+ # Only allow a trusted parameter "white list" through.
def <%= "#{singular_table_name}_params" %>
- <%- if attributes.empty? -%>
- params[<%= ":#{singular_table_name}" %>]
+ <%- if attributes_names.empty? -%>
+ params[:<%= singular_table_name %>]
<%- else -%>
- params.require(<%= ":#{singular_table_name}" %>).permit(<%= attributes.map {|a| ":#{a.name}" }.join(', ') %>)
+ params.require(:<%= singular_table_name %>).permit(<%= attributes_names.map { |name| ":#{name}" }.join(', ') %>)
<%- end -%>
end
end
diff --git a/railties/lib/rails/generators/resource_helpers.rb b/railties/lib/rails/generators/resource_helpers.rb
index 7fd5c00768..a01eb57651 100644
--- a/railties/lib/rails/generators/resource_helpers.rb
+++ b/railties/lib/rails/generators/resource_helpers.rb
@@ -9,11 +9,19 @@ module Rails
def self.included(base) #:nodoc:
base.class_option :force_plural, type: :boolean, desc: "Forces the use of a plural ModelName"
+ base.class_option :model_name, type: :string, desc: "ModelName to be used"
end
# Set controller variables on initialization.
def initialize(*args) #:nodoc:
super
+ if options[:model_name]
+ controller_name = name
+ self.name = options[:model_name]
+ assign_names!(self.name)
+ else
+ controller_name = name
+ end
if name == name.pluralize && name.singularize != name.pluralize && !options[:force_plural]
unless ResourceHelpers.skip_warn
@@ -24,19 +32,26 @@ module Rails
assign_names!(name)
end
- @controller_name = name.pluralize
+ assign_controller_names!(controller_name.pluralize)
end
protected
- attr_reader :controller_name
+ attr_reader :controller_name, :controller_file_name
def controller_class_path
- class_path
+ if options[:model_name]
+ @controller_class_path
+ else
+ class_path
+ end
end
- def controller_file_name
- @controller_file_name ||= file_name.pluralize
+ def assign_controller_names!(name)
+ @controller_name = name
+ @controller_class_path = name.include?('/') ? name.split('/') : name.split('::')
+ @controller_class_path.map! { |m| m.underscore }
+ @controller_file_name = @controller_class_path.pop
end
def controller_file_path
diff --git a/railties/lib/rails/generators/test_case.rb b/railties/lib/rails/generators/test_case.rb
index 24308dcf6c..58592b4f8e 100644
--- a/railties/lib/rails/generators/test_case.rb
+++ b/railties/lib/rails/generators/test_case.rb
@@ -1,8 +1,7 @@
-require 'active_support/core_ext/class/attribute'
-require 'active_support/core_ext/module/delegation'
-require 'active_support/core_ext/hash/reverse_merge'
-require 'active_support/core_ext/kernel/reporting'
require 'rails/generators'
+require 'rails/generators/testing/behaviour'
+require 'rails/generators/testing/setup_and_teardown'
+require 'rails/generators/testing/assertions'
require 'fileutils'
module Rails
@@ -27,215 +26,11 @@ module Rails
# setup :prepare_destination
# end
class TestCase < ActiveSupport::TestCase
+ include Rails::Generators::Testing::Behaviour
+ include Rails::Generators::Testing::SetupAndTeardown
+ include Rails::Generators::Testing::Assertions
include FileUtils
- class_attribute :destination_root, :current_path, :generator_class, :default_arguments
-
- # Generators frequently change the current path using +FileUtils.cd+.
- # So we need to store the path at file load and revert back to it after each test.
- self.current_path = File.expand_path(Dir.pwd)
- self.default_arguments = []
-
- def setup # :nodoc:
- destination_root_is_set?
- ensure_current_path
- super
- end
-
- def teardown # :nodoc:
- ensure_current_path
- super
- end
-
- # Sets which generator should be tested:
- #
- # tests AppGenerator
- def self.tests(klass)
- self.generator_class = klass
- end
-
- # Sets default arguments on generator invocation. This can be overwritten when
- # invoking it.
- #
- # arguments %w(app_name --skip-active-record)
- def self.arguments(array)
- self.default_arguments = array
- end
-
- # Sets the destination of generator files:
- #
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
- def self.destination(path)
- self.destination_root = path
- end
-
- # Asserts a given file exists. You need to supply an absolute path or a path relative
- # to the configured destination:
- #
- # assert_file "config/environment.rb"
- #
- # You can also give extra arguments. If the argument is a regexp, it will check if the
- # regular expression matches the given file content. If it's a string, it compares the
- # file with the given string:
- #
- # assert_file "config/environment.rb", /initialize/
- #
- # Finally, when a block is given, it yields the file content:
- #
- # assert_file "app/controllers/products_controller.rb" do |controller|
- # assert_instance_method :index, controller do |index|
- # assert_match(/Product\.all/, index)
- # end
- # end
- def assert_file(relative, *contents)
- absolute = File.expand_path(relative, destination_root)
- assert File.exists?(absolute), "Expected file #{relative.inspect} to exist, but does not"
-
- read = File.read(absolute) if block_given? || !contents.empty?
- yield read if block_given?
-
- contents.each do |content|
- case content
- when String
- assert_equal content, read
- when Regexp
- assert_match content, read
- end
- end
- end
- alias :assert_directory :assert_file
-
- # Asserts a given file does not exist. You need to supply an absolute path or a
- # path relative to the configured destination:
- #
- # assert_no_file "config/random.rb"
- def assert_no_file(relative)
- absolute = File.expand_path(relative, destination_root)
- assert !File.exists?(absolute), "Expected file #{relative.inspect} to not exist, but does"
- end
- alias :assert_no_directory :assert_no_file
-
- # Asserts a given migration exists. You need to supply an absolute path or a
- # path relative to the configured destination:
- #
- # assert_migration "db/migrate/create_products.rb"
- #
- # This method manipulates the given path and tries to find any migration which
- # matches the migration name. For example, the call above is converted to:
- #
- # assert_file "db/migrate/003_create_products.rb"
- #
- # Consequently, assert_migration accepts the same arguments has assert_file.
- def assert_migration(relative, *contents, &block)
- file_name = migration_file_name(relative)
- assert file_name, "Expected migration #{relative} to exist, but was not found"
- assert_file file_name, *contents, &block
- end
-
- # Asserts a given migration does not exist. You need to supply an absolute path or a
- # path relative to the configured destination:
- #
- # assert_no_migration "db/migrate/create_products.rb"
- def assert_no_migration(relative)
- file_name = migration_file_name(relative)
- assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}"
- end
-
- # Asserts the given class method exists in the given content. This method does not detect
- # class methods inside (class << self), only class methods which starts with "self.".
- # When a block is given, it yields the content of the method.
- #
- # assert_migration "db/migrate/create_products.rb" do |migration|
- # assert_class_method :up, migration do |up|
- # assert_match(/create_table/, up)
- # end
- # end
- def assert_class_method(method, content, &block)
- assert_instance_method "self.#{method}", content, &block
- end
-
- # Asserts the given method exists in the given content. When a block is given,
- # it yields the content of the method.
- #
- # assert_file "app/controllers/products_controller.rb" do |controller|
- # assert_instance_method :index, controller do |index|
- # assert_match(/Product\.all/, index)
- # end
- # end
- def assert_instance_method(method, content)
- assert content =~ /def #{method}(\(.+\))?(.*?)\n end/m, "Expected to have method #{method}"
- yield $2.strip if block_given?
- end
- alias :assert_method :assert_instance_method
-
- # Asserts the given attribute type gets translated to a field type
- # properly:
- #
- # assert_field_type :date, :date_select
- def assert_field_type(attribute_type, field_type)
- assert_equal(field_type, create_generated_attribute(attribute_type).field_type)
- end
-
- # Asserts the given attribute type gets a proper default value:
- #
- # assert_field_default_value :string, "MyString"
- def assert_field_default_value(attribute_type, value)
- assert_equal(value, create_generated_attribute(attribute_type).default)
- end
-
- # Runs the generator configured for this class. The first argument is an array like
- # command line arguments:
- #
- # class AppGeneratorTest < Rails::Generators::TestCase
- # tests AppGenerator
- # destination File.expand_path("../tmp", File.dirname(__FILE__))
- # teardown :cleanup_destination_root
- #
- # test "database.yml is not created when skipping Active Record" do
- # run_generator %w(myapp --skip-active-record)
- # assert_no_file "config/database.yml"
- # end
- # end
- #
- # You can provide a configuration hash as second argument. This method returns the output
- # printed by the generator.
- def run_generator(args=self.default_arguments, config={})
- capture(:stdout) { self.generator_class.start(args, config.reverse_merge(destination_root: destination_root)) }
- end
-
- # Instantiate the generator.
- def generator(args=self.default_arguments, options={}, config={})
- @generator ||= self.generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
- end
-
- # Create a Rails::Generators::GeneratedAttribute by supplying the
- # attribute type and, optionally, the attribute name:
- #
- # create_generated_attribute(:string, 'name')
- def create_generated_attribute(attribute_type, name = 'test', index = nil)
- Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(':'))
- end
-
- protected
-
- def destination_root_is_set? # :nodoc:
- raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root
- end
-
- def ensure_current_path # :nodoc:
- cd current_path
- end
-
- def prepare_destination # :nodoc:
- rm_rf(destination_root)
- mkdir_p(destination_root)
- end
-
- def migration_file_name(relative) # :nodoc:
- absolute = File.expand_path(relative, destination_root)
- dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
- Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
- end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/generator/generator_generator.rb b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
new file mode 100644
index 0000000000..d7307398ce
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/generator/generator_generator.rb
@@ -0,0 +1,26 @@
+require 'rails/generators/test_unit'
+
+module TestUnit # :nodoc:
+ module Generators # :nodoc:
+ class GeneratorGenerator < Base # :nodoc:
+ check_class_collision suffix: "GeneratorTest"
+
+ class_option :namespace, type: :boolean, default: true,
+ desc: "Namespace generator under lib/generators/name"
+
+ def create_generator_files
+ template 'generator_test.rb', File.join('test/lib/generators', class_path, "#{file_name}_generator_test.rb")
+ end
+
+ protected
+
+ def generator_path
+ if options[:namespace]
+ File.join("generators", regular_class_path, file_name, "#{file_name}_generator")
+ else
+ File.join("generators", regular_class_path, "#{file_name}_generator")
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
new file mode 100644
index 0000000000..a7f1fc4fba
--- /dev/null
+++ b/railties/lib/rails/generators/test_unit/generator/templates/generator_test.rb
@@ -0,0 +1,16 @@
+require 'test_helper'
+require '<%= generator_path %>'
+
+<% module_namespacing do -%>
+class <%= class_name %>GeneratorTest < Rails::Generators::TestCase
+ tests <%= class_name %>Generator
+ destination Rails.root.join('tmp/generators')
+ setup :prepare_destination
+
+ # test "generator runs without errors" do
+ # assert_nothing_raised do
+ # run_generator ["arguments"]
+ # end
+ # end
+end
+<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/model/model_generator.rb b/railties/lib/rails/generators/test_unit/model/model_generator.rb
index 2801749ffe..2826a3ffa1 100644
--- a/railties/lib/rails/generators/test_unit/model/model_generator.rb
+++ b/railties/lib/rails/generators/test_unit/model/model_generator.rb
@@ -3,6 +3,9 @@ require 'rails/generators/test_unit'
module TestUnit # :nodoc:
module Generators # :nodoc:
class ModelGenerator < Base # :nodoc:
+
+ RESERVED_YAML_KEYWORDS = %w(y yes n no true false on off null)
+
argument :attributes, type: :array, default: [], banner: "field:type field:type"
class_option :fixture, type: :boolean
@@ -19,6 +22,15 @@ module TestUnit # :nodoc:
template 'fixtures.yml', File.join('test/fixtures', class_path, "#{plural_file_name}.yml")
end
end
+
+ private
+ def yaml_key_value(key, value)
+ if RESERVED_YAML_KEYWORDS.include?(key.downcase)
+ "'#{key}': #{value}"
+ else
+ "#{key}: #{value}"
+ end
+ end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
index 5c8780aa64..f19e9d1d87 100644
--- a/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
+++ b/railties/lib/rails/generators/test_unit/model/templates/fixtures.yml
@@ -1,16 +1,20 @@
-# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/Fixtures.html
-
+# Read about fixtures at http://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html
<% unless attributes.empty? -%>
-one:
+<% %w(one two).each do |name| %>
+<%= name %>:
<% attributes.each do |attribute| -%>
- <%= attribute.name %>: <%= attribute.default %>
+ <%- if attribute.password_digest? -%>
+ password_digest: <%%= BCrypt::Password.create('secret') %>
+ <%- else -%>
+ <%= yaml_key_value(attribute.column_name, attribute.default) %>
+ <%- end -%>
+ <%- if attribute.polymorphic? -%>
+ <%= yaml_key_value("#{attribute.name}_type", attribute.human_name) %>
+ <%- end -%>
<% end -%>
-
-two:
-<% attributes.each do |attribute| -%>
- <%= attribute.name %>: <%= attribute.default %>
<% end -%>
<% else -%>
+
# This model initially had no columns defined. If you add columns to the
# model remove the '{}' from the fixture names and add the columns immediately
# below each fixture, per the syntax in the comments below
diff --git a/railties/lib/rails/generators/test_unit/observer/observer_generator.rb b/railties/lib/rails/generators/test_unit/observer/observer_generator.rb
deleted file mode 100644
index 64fe694a8b..0000000000
--- a/railties/lib/rails/generators/test_unit/observer/observer_generator.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'rails/generators/test_unit'
-
-module TestUnit # :nodoc:
- module Generators # :nodoc:
- class ObserverGenerator < Base # :nodoc:
- check_class_collision suffix: "ObserverTest"
-
- def create_test_files
- template 'unit_test.rb', File.join('test/models', class_path, "#{file_name}_observer_test.rb")
- end
- end
- end
-end
diff --git a/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb b/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb
deleted file mode 100644
index 28aa23626a..0000000000
--- a/railties/lib/rails/generators/test_unit/observer/templates/unit_test.rb
+++ /dev/null
@@ -1,9 +0,0 @@
-require 'test_helper'
-
-<% module_namespacing do -%>
-class <%= class_name %>ObserverTest < ActiveSupport::TestCase
- # test "the truth" do
- # assert true
- # end
-end
-<% end -%>
diff --git a/railties/lib/rails/generators/test_unit/performance/performance_generator.rb b/railties/lib/rails/generators/test_unit/performance/performance_generator.rb
deleted file mode 100644
index 5552edeee4..0000000000
--- a/railties/lib/rails/generators/test_unit/performance/performance_generator.rb
+++ /dev/null
@@ -1,13 +0,0 @@
-require 'rails/generators/test_unit'
-
-module TestUnit # :nodoc:
- module Generators # :nodoc:
- class PerformanceGenerator < Base # :nodoc:
- check_class_collision suffix: "Test"
-
- def create_test_files
- template 'performance_test.rb', File.join('test/performance', class_path, "#{file_name}_test.rb")
- end
- end
- end
-end
diff --git a/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb b/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb
deleted file mode 100644
index 2bcb482d68..0000000000
--- a/railties/lib/rails/generators/test_unit/performance/templates/performance_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'test_helper'
-require 'rails/performance_test_help'
-
-class <%= class_name %>Test < ActionDispatch::PerformanceTest
- # Refer to the documentation for all available options
- # self.profile_options = { runs: 5, metrics: [:wall_time, :memory],
- # output: 'tmp/performance', formats: [:flat] }
-
- test "homepage" do
- get '/'
- end
-end
diff --git a/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb
index c9af2ca832..30a861f09d 100644
--- a/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb
+++ b/railties/lib/rails/generators/test_unit/plugin/templates/test_helper.rb
@@ -1,2 +1,2 @@
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'active_support'
diff --git a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
index 3b4fec2e83..2e1f55f2a6 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/scaffold_generator.rb
@@ -18,17 +18,16 @@ module TestUnit # :nodoc:
private
def attributes_hash
- return if accessible_attributes.empty?
-
- accessible_attributes.map do |a|
- name = a.name
- "#{name}: @#{singular_table_name}.#{name}"
+ return if attributes_names.empty?
+
+ attributes_names.map do |name|
+ if %w(password password_confirmation).include?(name) && attributes.any?(&:password_digest?)
+ "#{name}: 'secret'"
+ else
+ "#{name}: @#{singular_table_name}.#{name}"
+ end
end.sort.join(', ')
end
-
- def accessible_attributes
- attributes.reject(&:reference?)
- end
end
end
end
diff --git a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
index 30e1650555..18bd1ece9d 100644
--- a/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
+++ b/railties/lib/rails/generators/test_unit/scaffold/templates/functional_test.rb
@@ -36,7 +36,7 @@ class <%= controller_class_name %>ControllerTest < ActionController::TestCase
end
test "should update <%= singular_table_name %>" do
- put :update, id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %>
+ patch :update, id: <%= "@#{singular_table_name}" %>, <%= "#{singular_table_name}: { #{attributes_hash} }" %>
assert_redirected_to <%= singular_table_name %>_path(assigns(:<%= singular_table_name %>))
end
diff --git a/railties/lib/rails/generators/testing/assertions.rb b/railties/lib/rails/generators/testing/assertions.rb
new file mode 100644
index 0000000000..2e877f8762
--- /dev/null
+++ b/railties/lib/rails/generators/testing/assertions.rb
@@ -0,0 +1,121 @@
+module Rails
+ module Generators
+ module Testing
+ module Assertions
+ # Asserts a given file exists. You need to supply an absolute path or a path relative
+ # to the configured destination:
+ #
+ # assert_file "config/environment.rb"
+ #
+ # You can also give extra arguments. If the argument is a regexp, it will check if the
+ # regular expression matches the given file content. If it's a string, it compares the
+ # file with the given string:
+ #
+ # assert_file "config/environment.rb", /initialize/
+ #
+ # Finally, when a block is given, it yields the file content:
+ #
+ # assert_file "app/controllers/products_controller.rb" do |controller|
+ # assert_instance_method :index, controller do |index|
+ # assert_match(/Product\.all/, index)
+ # end
+ # end
+ def assert_file(relative, *contents)
+ absolute = File.expand_path(relative, destination_root).shellescape
+ assert File.exist?(absolute), "Expected file #{relative.inspect} to exist, but does not"
+
+ read = File.read(absolute) if block_given? || !contents.empty?
+ yield read if block_given?
+
+ contents.each do |content|
+ case content
+ when String
+ assert_equal content, read
+ when Regexp
+ assert_match content, read
+ end
+ end
+ end
+ alias :assert_directory :assert_file
+
+ # Asserts a given file does not exist. You need to supply an absolute path or a
+ # path relative to the configured destination:
+ #
+ # assert_no_file "config/random.rb"
+ def assert_no_file(relative)
+ absolute = File.expand_path(relative, destination_root)
+ assert !File.exist?(absolute), "Expected file #{relative.inspect} to not exist, but does"
+ end
+ alias :assert_no_directory :assert_no_file
+
+ # Asserts a given migration exists. You need to supply an absolute path or a
+ # path relative to the configured destination:
+ #
+ # assert_migration "db/migrate/create_products.rb"
+ #
+ # This method manipulates the given path and tries to find any migration which
+ # matches the migration name. For example, the call above is converted to:
+ #
+ # assert_file "db/migrate/003_create_products.rb"
+ #
+ # Consequently, assert_migration accepts the same arguments has assert_file.
+ def assert_migration(relative, *contents, &block)
+ file_name = migration_file_name(relative)
+ assert file_name, "Expected migration #{relative} to exist, but was not found"
+ assert_file file_name, *contents, &block
+ end
+
+ # Asserts a given migration does not exist. You need to supply an absolute path or a
+ # path relative to the configured destination:
+ #
+ # assert_no_migration "db/migrate/create_products.rb"
+ def assert_no_migration(relative)
+ file_name = migration_file_name(relative)
+ assert_nil file_name, "Expected migration #{relative} to not exist, but found #{file_name}"
+ end
+
+ # Asserts the given class method exists in the given content. This method does not detect
+ # class methods inside (class << self), only class methods which starts with "self.".
+ # When a block is given, it yields the content of the method.
+ #
+ # assert_migration "db/migrate/create_products.rb" do |migration|
+ # assert_class_method :up, migration do |up|
+ # assert_match(/create_table/, up)
+ # end
+ # end
+ def assert_class_method(method, content, &block)
+ assert_instance_method "self.#{method}", content, &block
+ end
+
+ # Asserts the given method exists in the given content. When a block is given,
+ # it yields the content of the method.
+ #
+ # assert_file "app/controllers/products_controller.rb" do |controller|
+ # assert_instance_method :index, controller do |index|
+ # assert_match(/Product\.all/, index)
+ # end
+ # end
+ def assert_instance_method(method, content)
+ assert content =~ /(\s+)def #{method}(\(.+\))?(.*?)\n\1end/m, "Expected to have method #{method}"
+ yield $3.strip if block_given?
+ end
+ alias :assert_method :assert_instance_method
+
+ # Asserts the given attribute type gets translated to a field type
+ # properly:
+ #
+ # assert_field_type :date, :date_select
+ def assert_field_type(attribute_type, field_type)
+ assert_equal(field_type, create_generated_attribute(attribute_type).field_type)
+ end
+
+ # Asserts the given attribute type gets a proper default value:
+ #
+ # assert_field_default_value :string, "MyString"
+ def assert_field_default_value(attribute_type, value)
+ assert_equal(value, create_generated_attribute(attribute_type).default)
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/testing/behaviour.rb b/railties/lib/rails/generators/testing/behaviour.rb
new file mode 100644
index 0000000000..7576eba6e0
--- /dev/null
+++ b/railties/lib/rails/generators/testing/behaviour.rb
@@ -0,0 +1,106 @@
+require 'active_support/core_ext/class/attribute'
+require 'active_support/core_ext/module/delegation'
+require 'active_support/core_ext/hash/reverse_merge'
+require 'active_support/core_ext/kernel/reporting'
+require 'active_support/concern'
+require 'rails/generators'
+
+module Rails
+ module Generators
+ module Testing
+ module Behaviour
+ extend ActiveSupport::Concern
+
+ included do
+ class_attribute :destination_root, :current_path, :generator_class, :default_arguments
+
+ # Generators frequently change the current path using +FileUtils.cd+.
+ # So we need to store the path at file load and revert back to it after each test.
+ self.current_path = File.expand_path(Dir.pwd)
+ self.default_arguments = []
+ end
+
+ module ClassMethods
+ # Sets which generator should be tested:
+ #
+ # tests AppGenerator
+ def tests(klass)
+ self.generator_class = klass
+ end
+
+ # Sets default arguments on generator invocation. This can be overwritten when
+ # invoking it.
+ #
+ # arguments %w(app_name --skip-active-record)
+ def arguments(array)
+ self.default_arguments = array
+ end
+
+ # Sets the destination of generator files:
+ #
+ # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ def destination(path)
+ self.destination_root = path
+ end
+ end
+
+ # Runs the generator configured for this class. The first argument is an array like
+ # command line arguments:
+ #
+ # class AppGeneratorTest < Rails::Generators::TestCase
+ # tests AppGenerator
+ # destination File.expand_path("../tmp", File.dirname(__FILE__))
+ # teardown :cleanup_destination_root
+ #
+ # test "database.yml is not created when skipping Active Record" do
+ # run_generator %w(myapp --skip-active-record)
+ # assert_no_file "config/database.yml"
+ # end
+ # end
+ #
+ # You can provide a configuration hash as second argument. This method returns the output
+ # printed by the generator.
+ def run_generator(args=self.default_arguments, config={})
+ capture(:stdout) do
+ args += ['--skip-bundle'] unless args.include? '--dev'
+ self.generator_class.start(args, config.reverse_merge(destination_root: destination_root))
+ end
+ end
+
+ # Instantiate the generator.
+ def generator(args=self.default_arguments, options={}, config={})
+ @generator ||= self.generator_class.new(args, options, config.reverse_merge(destination_root: destination_root))
+ end
+
+ # Create a Rails::Generators::GeneratedAttribute by supplying the
+ # attribute type and, optionally, the attribute name:
+ #
+ # create_generated_attribute(:string, 'name')
+ def create_generated_attribute(attribute_type, name = 'test', index = nil)
+ Rails::Generators::GeneratedAttribute.parse([name, attribute_type, index].compact.join(':'))
+ end
+
+ protected
+
+ def destination_root_is_set? # :nodoc:
+ raise "You need to configure your Rails::Generators::TestCase destination root." unless destination_root
+ end
+
+ def ensure_current_path # :nodoc:
+ cd current_path
+ end
+
+ def prepare_destination # :nodoc:
+ rm_rf(destination_root)
+ mkdir_p(destination_root)
+ end
+
+ def migration_file_name(relative) # :nodoc:
+ absolute = File.expand_path(relative, destination_root)
+ dirname, file_name = File.dirname(absolute), File.basename(absolute).sub(/\.rb$/, '')
+ Dir.glob("#{dirname}/[0-9]*_*.rb").grep(/\d+_#{file_name}.rb$/).first
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/generators/testing/setup_and_teardown.rb b/railties/lib/rails/generators/testing/setup_and_teardown.rb
new file mode 100644
index 0000000000..73102a283f
--- /dev/null
+++ b/railties/lib/rails/generators/testing/setup_and_teardown.rb
@@ -0,0 +1,18 @@
+module Rails
+ module Generators
+ module Testing
+ module SetupAndTeardown
+ def setup # :nodoc:
+ destination_root_is_set?
+ ensure_current_path
+ super
+ end
+
+ def teardown # :nodoc:
+ ensure_current_path
+ super
+ end
+ end
+ end
+ end
+end
diff --git a/railties/lib/rails/info.rb b/railties/lib/rails/info.rb
index aacc1be2fc..edadeaca0e 100644
--- a/railties/lib/rails/info.rb
+++ b/railties/lib/rails/info.rb
@@ -23,13 +23,13 @@ module Rails
end
def frameworks
- %w( active_record action_pack action_mailer active_support )
+ %w( active_record action_pack action_view action_mailer active_support )
end
def framework_version(framework)
if Object.const_defined?(framework.classify)
require "#{framework}/version"
- "#{framework.classify}::VERSION::STRING".constantize
+ framework.classify.constantize.version.to_s
end
end
@@ -46,7 +46,7 @@ module Rails
alias inspect to_s
def to_html
- (table = '<table>').tap do
+ '<table>'.tap do |table|
properties.each do |(name, value)|
table << %(<tr><td class="name">#{CGI.escapeHTML(name.to_s)}</td>)
formatted_value = if value.kind_of?(Array)
@@ -61,8 +61,10 @@ module Rails
end
end
- # The Ruby version and platform, e.g. "1.8.2 (powerpc-darwin8.2.0)".
- property 'Ruby version', "#{RUBY_VERSION} (#{RUBY_PLATFORM})"
+ # The Ruby version and platform, e.g. "2.0.0-p247 (x86_64-darwin12.4.0)".
+ property 'Ruby version' do
+ "#{RUBY_VERSION}-p#{RUBY_PATCHLEVEL} (#{RUBY_PLATFORM})"
+ end
# The RubyGems version, if it's installed.
property 'RubyGems version' do
@@ -75,7 +77,7 @@ module Rails
# The Rails version.
property 'Rails version' do
- Rails::VERSION::STRING
+ Rails.version.to_s
end
property 'JavaScript Runtime' do
diff --git a/railties/lib/rails/info_controller.rb b/railties/lib/rails/info_controller.rb
index e94c6a2030..fa5668a5b5 100644
--- a/railties/lib/rails/info_controller.rb
+++ b/railties/lib/rails/info_controller.rb
@@ -1,13 +1,14 @@
require 'action_dispatch/routing/inspector'
-class Rails::InfoController < ActionController::Base
- self.view_paths = File.join(File.dirname(__FILE__), 'templates')
- layout 'application'
+class Rails::InfoController < ActionController::Base # :nodoc:
+ self.view_paths = File.expand_path('../templates', __FILE__)
+ prepend_view_path ActionDispatch::DebugExceptions::RESCUES_TEMPLATE_PATH
+ layout -> { request.xhr? ? nil : 'application' }
before_filter :require_local!
def index
- redirect_to '/rails/info/routes'
+ redirect_to action: :routes
end
def properties
@@ -15,8 +16,7 @@ class Rails::InfoController < ActionController::Base
end
def routes
- inspector = ActionDispatch::Routing::RoutesInspector.new
- @info = inspector.format(_routes.routes).join("\n")
+ @routes_inspector = ActionDispatch::Routing::RoutesInspector.new(_routes.routes)
end
protected
diff --git a/railties/lib/rails/paths.rb b/railties/lib/rails/paths.rb
index 8af4130e87..f512aefb23 100644
--- a/railties/lib/rails/paths.rb
+++ b/railties/lib/rails/paths.rb
@@ -55,9 +55,9 @@ module Rails
add(path, with: value, glob: glob)
end
- def add(path, options={})
- with = options[:with] || path
- @root[path] = Path.new(self, path, [with].flatten, options)
+ def add(path, options = {})
+ with = Array(options.fetch(:with, path))
+ @root[path] = Path.new(self, path, with, options)
end
def [](path)
@@ -81,34 +81,28 @@ module Rails
end
def autoload_once
- filter_by(:autoload_once?)
+ filter_by { |p| p.autoload_once? }
end
def eager_load
- filter_by(:eager_load?)
+ filter_by { |p| p.eager_load? }
end
def autoload_paths
- filter_by(:autoload?)
+ filter_by { |p| p.autoload? }
end
def load_paths
- filter_by(:load_path?)
+ filter_by { |p| p.load_path? }
end
- protected
+ private
- def filter_by(constraint)
- all = []
- all_paths.each do |path|
- if path.send(constraint)
- paths = path.existent
- paths -= path.children.map { |p| p.send(constraint) ? [] : p.existent }.flatten
- all.concat(paths)
- end
- end
- all.uniq!
- all
+ def filter_by(&block)
+ all_paths.find_all(&block).flat_map { |path|
+ paths = path.existent
+ paths - path.children.map { |p| yield(p) ? [] : p.existent }.flatten
+ }.uniq
end
end
@@ -130,8 +124,9 @@ module Rails
end
def children
- keys = @root.keys.select { |k| k.include?(@current) }
- keys.delete(@current)
+ keys = @root.keys.find_all { |k|
+ k.start_with?(@current) && k != @current
+ }
@root.values_at(*keys.sort)
end
@@ -189,9 +184,9 @@ module Rails
path = File.expand_path(p, @root.path)
if @glob && File.directory?(path)
- result.concat Dir.chdir(path) {
- Dir.glob(@glob).map { |file| File.join path, file }.sort
- }
+ Dir.chdir(path) do
+ result.concat(Dir.glob(@glob).map { |file| File.join path, file }.sort)
+ end
else
result << path
end
@@ -203,7 +198,7 @@ module Rails
# Returns all expanded paths but only if they exist in the filesystem.
def existent
- expanded.select { |f| File.exists?(f) }
+ expanded.select { |f| File.exist?(f) }
end
def existent_directories
diff --git a/railties/lib/rails/performance_test_help.rb b/railties/lib/rails/performance_test_help.rb
deleted file mode 100644
index b1285efde2..0000000000
--- a/railties/lib/rails/performance_test_help.rb
+++ /dev/null
@@ -1,3 +0,0 @@
-ActionController::Base.perform_caching = true
-ActiveSupport::Dependencies.mechanism = :require
-Rails.logger.level = ActiveSupport::Logger::INFO
diff --git a/railties/lib/rails/rack/debugger.rb b/railties/lib/rails/rack/debugger.rb
index 902361ce77..f7b77bcb3b 100644
--- a/railties/lib/rails/rack/debugger.rb
+++ b/railties/lib/rails/rack/debugger.rb
@@ -12,8 +12,8 @@ module Rails
::Debugger.settings[:autoeval] = true if ::Debugger.respond_to?(:settings)
puts "=> Debugger enabled"
rescue LoadError
- puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle, and try again."
- exit
+ puts "You're missing the 'debugger' gem. Add it to your Gemfile, bundle it and try again."
+ exit(1)
end
def call(env)
diff --git a/railties/lib/rails/rack/log_tailer.rb b/railties/lib/rails/rack/log_tailer.rb
index 18f22e8089..50d0eb96fc 100644
--- a/railties/lib/rails/rack/log_tailer.rb
+++ b/railties/lib/rails/rack/log_tailer.rb
@@ -7,7 +7,7 @@ module Rails
path = Pathname.new(log || "#{::File.expand_path(Rails.root)}/log/#{Rails.env}.log").cleanpath
@cursor = @file = nil
- if ::File.exists?(path)
+ if ::File.exist?(path)
@cursor = ::File.size(path)
@file = ::File.open(path, 'r')
end
diff --git a/railties/lib/rails/rack/logger.rb b/railties/lib/rails/rack/logger.rb
index 3f59bb8733..3b35798679 100644
--- a/railties/lib/rails/rack/logger.rb
+++ b/railties/lib/rails/rack/logger.rb
@@ -1,19 +1,23 @@
require 'active_support/core_ext/time/conversions'
require 'active_support/core_ext/object/blank'
+require 'active_support/log_subscriber'
+require 'action_dispatch/http/request'
+require 'rack/body_proxy'
module Rails
module Rack
# Sets log tags, logs the request, calls the app, and flushes the logs.
class Logger < ActiveSupport::LogSubscriber
def initialize(app, taggers = nil)
- @app, @taggers = app, taggers || []
+ @app = app
+ @taggers = taggers || []
end
def call(env)
request = ActionDispatch::Request.new(env)
- if Rails.logger.respond_to?(:tagged)
- Rails.logger.tagged(compute_tags(request)) { call_app(request, env) }
+ if logger.respond_to?(:tagged)
+ logger.tagged(compute_tags(request)) { call_app(request, env) }
else
call_app(request, env)
end
@@ -23,13 +27,20 @@ module Rails
def call_app(request, env)
# Put some space between requests in development logs.
- if Rails.env.development?
- Rails.logger.info ''
- Rails.logger.info ''
+ if development?
+ logger.debug ''
+ logger.debug ''
end
- Rails.logger.info started_request_message(request)
- @app.call(env)
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.start 'request.action_dispatch', request: request
+ logger.info started_request_message(request)
+ resp = @app.call(env)
+ resp[2] = ::Rack::BodyProxy.new(resp[2]) { finish(request) }
+ resp
+ rescue Exception
+ finish(request)
+ raise
ensure
ActiveSupport::LogSubscriber.flush_all!
end
@@ -55,6 +66,21 @@ module Rails
end
end
end
+
+ private
+
+ def finish(request)
+ instrumenter = ActiveSupport::Notifications.instrumenter
+ instrumenter.finish 'request.action_dispatch', request: request
+ end
+
+ def development?
+ Rails.env.development?
+ end
+
+ def logger
+ Rails.logger
+ end
end
end
end
diff --git a/railties/lib/rails/railtie.rb b/railties/lib/rails/railtie.rb
index 5b454e7f20..89ca8cbe11 100644
--- a/railties/lib/rails/railtie.rb
+++ b/railties/lib/rails/railtie.rb
@@ -112,7 +112,6 @@ module Rails
# Be sure to look at the documentation of those specific classes for more information.
#
class Railtie
- autoload :Configurable, "rails/railtie/configurable"
autoload :Configuration, "rails/railtie/configuration"
include Initializable
@@ -121,6 +120,7 @@ module Rails
class << self
private :new
+ delegate :config, to: :instance
def subclasses
@subclasses ||= []
@@ -128,7 +128,6 @@ module Rails
def inherited(base)
unless base.abstract_railtie?
- base.send(:include, Railtie::Configurable)
subclasses << base
end
end
@@ -166,13 +165,50 @@ module Rails
@railtie_name ||= generate_railtie_name(self.name)
end
+ # Since Rails::Railtie cannot be instantiated, any methods that call
+ # +instance+ are intended to be called only on subclasses of a Railtie.
+ def instance
+ @instance ||= new
+ end
+
+ def respond_to_missing?(*args)
+ instance.respond_to?(*args) || super
+ end
+
+ # Allows you to configure the railtie. This is the same method seen in
+ # Railtie::Configurable, but this module is no longer required for all
+ # subclasses of Railtie so we provide the class method here.
+ def configure(&block)
+ instance.configure(&block)
+ end
+
protected
def generate_railtie_name(class_or_module)
ActiveSupport::Inflector.underscore(class_or_module).tr("/", "_")
end
+
+ # If the class method does not have a method, then send the method call
+ # to the Railtie instance.
+ def method_missing(name, *args, &block)
+ if instance.respond_to?(name)
+ instance.public_send(name, *args, &block)
+ else
+ super
+ end
+ end
+ end
+
+ delegate :railtie_name, to: :class
+
+ def initialize
+ if self.class.abstract_railtie?
+ raise "#{self.class.name} is abstract, you cannot instantiate it directly."
+ end
end
- delegate :railtie_name, to: "self.class"
+ def configure(&block)
+ instance_eval(&block)
+ end
def config
@config ||= Railtie::Configuration.new
diff --git a/railties/lib/rails/railtie/configuration.rb b/railties/lib/rails/railtie/configuration.rb
index 0cbbf04da2..eb3b2d8ef4 100644
--- a/railties/lib/rails/railtie/configuration.rb
+++ b/railties/lib/rails/railtie/configuration.rb
@@ -80,7 +80,7 @@ module Rails
to_prepare_blocks << blk if blk
end
- def respond_to?(name)
+ def respond_to?(name, include_private = false)
super || @@options.key?(name.to_sym)
end
diff --git a/railties/lib/rails/ruby_version_check.rb b/railties/lib/rails/ruby_version_check.rb
index 4536fedaa3..3b7f358a5b 100644
--- a/railties/lib/rails/ruby_version_check.rb
+++ b/railties/lib/rails/ruby_version_check.rb
@@ -2,12 +2,12 @@ if RUBY_VERSION < '1.9.3'
desc = defined?(RUBY_DESCRIPTION) ? RUBY_DESCRIPTION : "ruby #{RUBY_VERSION} (#{RUBY_RELEASE_DATE})"
abort <<-end_message
- Rails 4 requires Ruby 1.9.3+.
+ Rails 4 prefers to run on Ruby 2.0.
You're running
#{desc}
- Please upgrade to continue.
+ Please upgrade to Ruby 1.9.3 or newer to continue.
end_message
end
diff --git a/railties/lib/rails/script_rails_loader.rb b/railties/lib/rails/script_rails_loader.rb
deleted file mode 100644
index 7054089614..0000000000
--- a/railties/lib/rails/script_rails_loader.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'pathname'
-
-module Rails
- module ScriptRailsLoader
- RUBY = File.join(*RbConfig::CONFIG.values_at("bindir", "ruby_install_name")) + RbConfig::CONFIG["EXEEXT"]
- SCRIPT_RAILS = File.join('script', 'rails')
-
- def self.exec_script_rails!
- cwd = Dir.pwd
- return unless in_rails_application? || in_rails_application_subdirectory?
- exec RUBY, SCRIPT_RAILS, *ARGV if in_rails_application?
- Dir.chdir("..") do
- # Recurse in a chdir block: if the search fails we want to be sure
- # the application is generated in the original working directory.
- exec_script_rails! unless cwd == Dir.pwd
- end
- rescue SystemCallError
- # could not chdir, no problem just return
- end
-
- def self.in_rails_application?
- File.exists?(SCRIPT_RAILS)
- end
-
- def self.in_rails_application_subdirectory?(path = Pathname.new(Dir.pwd))
- File.exists?(File.join(path, SCRIPT_RAILS)) || !path.root? && in_rails_application_subdirectory?(path.parent)
- end
- end
-end \ No newline at end of file
diff --git a/railties/lib/rails/source_annotation_extractor.rb b/railties/lib/rails/source_annotation_extractor.rb
index 3474b02af4..b806b922b7 100644
--- a/railties/lib/rails/source_annotation_extractor.rb
+++ b/railties/lib/rails/source_annotation_extractor.rb
@@ -15,7 +15,7 @@
class SourceAnnotationExtractor
class Annotation < Struct.new(:line, :tag, :text)
def self.directories
- @@directories ||= %w(app config lib script test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
+ @@directories ||= %w(app config db lib test) + (ENV['SOURCE_ANNOTATION_DIRECTORIES'] || '').split(',')
end
# Returns a representation of the annotation that looks like this:
@@ -31,16 +31,25 @@ class SourceAnnotationExtractor
end
end
- # Prints all annotations with tag +tag+ under the root directories +app+, +config+, +lib+,
- # +script+, and +test+ (recursively). Filenames with extension
- # +.builder+, +.rb+, +.erb+, +.haml+, +.slim+, +.css+, +.scss+, +.js+,
- # +.coffee+, and +.rake+ are taken into account. The +options+ hash is passed to each
- # annotation's +to_s+.
+ # Prints all annotations with tag +tag+ under the root directories +app+,
+ # +config+, +db+, +lib+, and +test+ (recursively).
+ #
+ # Additional directories may be added using a comma-delimited list set using
+ # <tt>ENV['SOURCE_ANNOTATION_DIRECTORIES']</tt>.
+ #
+ # Directories may also be explicitly set using the <tt>:dirs</tt> key in +options+.
+ #
+ # SourceAnnotationExtractor.enumerate 'TODO|FIXME', dirs: %w(app lib), tag: true
+ #
+ # If +options+ has a <tt>:tag</tt> flag, it will be passed to each annotation's +to_s+.
+ #
+ # See <tt>#find_in</tt> for a list of file extensions that will be taken into account.
#
# This class method is the single entry point for the rake tasks.
def self.enumerate(tag, options={})
extractor = new(tag)
- extractor.display(extractor.find, options)
+ dirs = options.delete(:dirs) || Annotation.directories
+ extractor.display(extractor.find(dirs), options)
end
attr_reader :tag
@@ -51,7 +60,7 @@ class SourceAnnotationExtractor
# Returns a hash that maps filenames under +dirs+ (recursively) to arrays
# with their annotations.
- def find(dirs = Annotation.directories)
+ def find(dirs)
dirs.inject({}) { |h, dir| h.update(find_in(dir)) }
end
@@ -68,16 +77,22 @@ class SourceAnnotationExtractor
if File.directory?(item)
results.update(find_in(item))
- elsif item =~ /\.(builder|rb|coffee|rake)$/
- results.update(extract_annotations_from(item, /#\s*(#{tag}):?\s*(.*)$/))
- elsif item =~ /\.(css|scss|js)$/
- results.update(extract_annotations_from(item, /\/\/\s*(#{tag}):?\s*(.*)$/))
- elsif item =~ /\.erb$/
- results.update(extract_annotations_from(item, /<%\s*#\s*(#{tag}):?\s*(.*?)\s*%>/))
- elsif item =~ /\.haml$/
- results.update(extract_annotations_from(item, /-\s*#\s*(#{tag}):?\s*(.*)$/))
- elsif item =~ /\.slim$/
- results.update(extract_annotations_from(item, /\/\s*\s*(#{tag}):?\s*(.*)$/))
+ 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
end
end
diff --git a/railties/lib/rails/tasks.rb b/railties/lib/rails/tasks.rb
index 9807000578..af5f2707b1 100644
--- a/railties/lib/rails/tasks.rb
+++ b/railties/lib/rails/tasks.rb
@@ -1,6 +1,4 @@
-$VERBOSE = nil
-
-# Load Rails rakefile extensions
+# Load Rails Rakefile extensions
%w(
annotations
documentation
diff --git a/railties/lib/rails/tasks/documentation.rake b/railties/lib/rails/tasks/documentation.rake
index 2851ca4189..8544890553 100644
--- a/railties/lib/rails/tasks/documentation.rake
+++ b/railties/lib/rails/tasks/documentation.rake
@@ -1,105 +1,70 @@
-require 'rdoc/task'
-
-# Monkey-patch to remove redoc'ing and clobber descriptions to cut down on rake -T noise
-class RDocTaskWithoutDescriptions < RDoc::Task
- include ::Rake::DSL
-
- def define
- task rdoc_task_name
-
- task rerdoc_task_name => [clobber_task_name, rdoc_task_name]
-
- task clobber_task_name do
- rm_r rdoc_dir rescue nil
- end
-
- task :clobber => [clobber_task_name]
+begin
+ require 'rdoc/task'
+rescue LoadError
+ # Rubinius installs RDoc as a gem, and for this interpreter "rdoc/task" is
+ # available only if the application bundle includes "rdoc" (normally as a
+ # dependency of the "sdoc" gem.)
+ #
+ # If RDoc is not available it is fine that we do not generate the tasks that
+ # depend on it. Just be robust to this gotcha and go on.
+else
+ require 'rails/api/task'
+
+ # Monkey-patch to remove redoc'ing and clobber descriptions to cut down on rake -T noise
+ class RDocTaskWithoutDescriptions < RDoc::Task
+ include ::Rake::DSL
+
+ def define
+ task rdoc_task_name
+
+ task rerdoc_task_name => [clobber_task_name, rdoc_task_name]
+
+ task clobber_task_name do
+ rm_r rdoc_dir rescue nil
+ end
- directory @rdoc_dir
- task rdoc_task_name => [rdoc_target]
- file rdoc_target => @rdoc_files + [Rake.application.rakefile] do
- rm_r @rdoc_dir rescue nil
- @before_running_rdoc.call if @before_running_rdoc
- args = option_list + @rdoc_files
- if @external
- argstring = args.join(' ')
- sh %{ruby -Ivendor vendor/rd #{argstring}}
- else
- require 'rdoc/rdoc'
- RDoc::RDoc.new.document(args)
+ task :clobber => [clobber_task_name]
+
+ directory @rdoc_dir
+ task rdoc_task_name => [rdoc_target]
+ file rdoc_target => @rdoc_files + [Rake.application.rakefile] do
+ rm_r @rdoc_dir rescue nil
+ @before_running_rdoc.call if @before_running_rdoc
+ args = option_list + @rdoc_files
+ if @external
+ argstring = args.join(' ')
+ sh %{ruby -Ivendor vendor/rd #{argstring}}
+ else
+ require 'rdoc/rdoc'
+ RDoc::RDoc.new.document(args)
+ end
end
+ self
end
- self
end
-end
-namespace :doc do
- def gem_path(gem_name)
- path = $LOAD_PATH.grep(/#{gem_name}[\w.-]*\/lib$/).first
- yield File.dirname(path) if path
+ namespace :doc do
+ RDocTaskWithoutDescriptions.new("app") { |rdoc|
+ rdoc.rdoc_dir = 'doc/app'
+ rdoc.template = ENV['template'] if ENV['template']
+ rdoc.title = ENV['title'] || "Rails Application Documentation"
+ rdoc.options << '--line-numbers'
+ rdoc.options << '--charset' << 'utf-8'
+ rdoc.rdoc_files.include('README.rdoc')
+ rdoc.rdoc_files.include('app/**/*.rb')
+ rdoc.rdoc_files.include('lib/**/*.rb')
+ }
+ Rake::Task['doc:app'].comment = "Generate docs for the app -- also available doc:rails, doc:guides (options: TEMPLATE=/rdoc-template.rb, TITLE=\"Custom Title\")"
+
+ # desc 'Generate documentation for the Rails framework.'
+ Rails::API::AppTask.new('rails')
end
+end
- RDocTaskWithoutDescriptions.new("app") { |rdoc|
- rdoc.rdoc_dir = 'doc/app'
- rdoc.template = ENV['template'] if ENV['template']
- rdoc.title = ENV['title'] || "Rails Application Documentation"
- rdoc.options << '--line-numbers'
- rdoc.options << '--charset' << 'utf-8'
- rdoc.rdoc_files.include('doc/README_FOR_APP')
- rdoc.rdoc_files.include('app/**/*.rb')
- rdoc.rdoc_files.include('lib/**/*.rb')
- }
- Rake::Task['doc:app'].comment = "Generate docs for the app -- also available doc:rails, doc:guides (options: TEMPLATE=/rdoc-template.rb, TITLE=\"Custom Title\")"
-
- # desc 'Generate documentation for the Rails framework.'
- RDocTaskWithoutDescriptions.new("rails") { |rdoc|
- rdoc.rdoc_dir = 'doc/api'
- rdoc.template = "#{ENV['template']}.rb" if ENV['template']
- rdoc.title = "Rails Framework Documentation"
- rdoc.options << '--line-numbers'
- rdoc.rdoc_files.include('README.rdoc')
-
- gem_path('actionmailer') do |actionmailer|
- %w(README.rdoc CHANGELOG.md MIT-LICENSE lib/action_mailer/base.rb).each do |file|
- rdoc.rdoc_files.include("#{actionmailer}/#{file}")
- end
- end
-
- gem_path('actionpack') do |actionpack|
- %w(README.rdoc CHANGELOG.md MIT-LICENSE lib/action_controller/**/*.rb lib/action_view/**/*.rb).each do |file|
- rdoc.rdoc_files.include("#{actionpack}/#{file}")
- end
- end
-
- gem_path('activemodel') do |activemodel|
- %w(README.rdoc CHANGELOG.md MIT-LICENSE lib/active_model/**/*.rb).each do |file|
- rdoc.rdoc_files.include("#{activemodel}/#{file}")
- end
- end
-
- gem_path('activerecord') do |activerecord|
- %w(README.rdoc CHANGELOG.md lib/active_record/**/*.rb).each do |file|
- rdoc.rdoc_files.include("#{activerecord}/#{file}")
- end
- end
-
- gem_path('activesupport') do |activesupport|
- %w(README.rdoc CHANGELOG.md lib/active_support/**/*.rb).each do |file|
- rdoc.rdoc_files.include("#{activesupport}/#{file}")
- end
- end
-
- gem_path('railties') do |railties|
- %w(README.rdoc CHANGELOG.md lib/{*.rb,commands/*.rb,generators/*.rb}).each do |file|
- rdoc.rdoc_files.include("#{railties}/#{file}")
- end
- end
- }
-
- # desc "Generate Rails Guides"
+namespace :doc do
task :guides do
- # FIXME: Reaching outside lib directory is a bad idea
- require File.expand_path('../../../../guides/rails_guides', __FILE__)
+ rails_gem_dir = Gem::Specification.find_by_name("rails").gem_dir
+ require File.expand_path(File.join(rails_gem_dir, "/guides/rails_guides"))
RailsGuides::Generator.new(Rails.root.join("doc/guides")).generate
end
end
diff --git a/railties/lib/rails/tasks/engine.rake b/railties/lib/rails/tasks/engine.rake
index 70370be3f5..16ad1bfc84 100644
--- a/railties/lib/rails/tasks/engine.rake
+++ b/railties/lib/rails/tasks/engine.rake
@@ -26,7 +26,7 @@ namespace :db do
desc "Display status of migrations"
app_task "migrate:status"
- desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all dbs in the config)'
+ desc 'Create the database from config/database.yml for the current Rails.env (use db:create:all to create all databases in the config)'
app_task "create"
app_task "create:all"
diff --git a/railties/lib/rails/tasks/framework.rake b/railties/lib/rails/tasks/framework.rake
index 50499304cb..e669315934 100644
--- a/railties/lib/rails/tasks/framework.rake
+++ b/railties/lib/rails/tasks/framework.rake
@@ -1,6 +1,6 @@
namespace :rails do
- desc "Update configs and some other initially generated files (or use just update:configs, update:scripts, or update:application_controller)"
- task update: [ "update:configs", "update:scripts", "update:application_controller" ]
+ desc "Update configs and some other initially generated files (or use just update:configs or update:bin)"
+ task update: [ "update:configs", "update:bin" ]
desc "Applies the template supplied by LOCATION=(/path/to/template) or URL"
task :template do
@@ -46,8 +46,8 @@ namespace :rails do
require 'rails/generators/rails/app/app_generator'
gen = Rails::Generators::AppGenerator.new ["rails"], { with_dispatchers: true },
destination_root: Rails.root
- File.exists?(Rails.root.join("config", "application.rb")) ?
- gen.send(:app_const) : gen.send(:valid_app_const?)
+ File.exist?(Rails.root.join("config", "application.rb")) ?
+ gen.send(:app_const) : gen.send(:valid_const?)
gen
end
end
@@ -58,19 +58,9 @@ namespace :rails do
invoke_from_app_generator :create_config_files
end
- # desc "Adds new scripts to the application script/ directory"
- task :scripts do
- invoke_from_app_generator :create_script_files
- end
-
- # desc "Rename application.rb to application_controller.rb"
- task :application_controller do
- old_style = Rails.root + '/app/controllers/application.rb'
- new_style = Rails.root + '/app/controllers/application_controller.rb'
- if File.exists?(old_style) && !File.exists?(new_style)
- FileUtils.mv(old_style, new_style)
- puts "#{old_style} has been renamed to #{new_style}, update your SCM as necessary"
- end
+ # desc "Adds new executables to the application bin/ directory"
+ task :bin do
+ invoke_from_app_generator :create_bin_files
end
end
end
diff --git a/railties/lib/rails/tasks/log.rake b/railties/lib/rails/tasks/log.rake
index 6e1334692e..877f175ef3 100644
--- a/railties/lib/rails/tasks/log.rake
+++ b/railties/lib/rails/tasks/log.rake
@@ -1,9 +1,23 @@
namespace :log do
- desc "Truncates all *.log files in log/ to zero bytes"
+ desc "Truncates all *.log files in log/ to zero bytes (specify which logs with LOGS=test,development)"
task :clear do
- FileList["log/*.log"].each do |log_file|
- f = File.open(log_file, "w")
- f.close
+ log_files.each do |file|
+ clear_log_file(file)
end
end
+
+ def log_files
+ if ENV['LOGS']
+ ENV['LOGS'].split(',')
+ .map { |file| "log/#{file.strip}.log" }
+ .select { |file| File.exist?(file) }
+ else
+ FileList["log/*.log"]
+ end
+ end
+
+ def clear_log_file(file)
+ f = File.open(file, "w")
+ f.close
+ end
end
diff --git a/railties/lib/rails/tasks/routes.rake b/railties/lib/rails/tasks/routes.rake
index 676b475640..1815c2fdc7 100644
--- a/railties/lib/rails/tasks/routes.rake
+++ b/railties/lib/rails/tasks/routes.rake
@@ -2,6 +2,6 @@ desc 'Print out all defined routes in match order, with names. Target specific c
task routes: :environment do
all_routes = Rails.application.routes.routes
require 'action_dispatch/routing/inspector'
- inspector = ActionDispatch::Routing::RoutesInspector.new
- puts inspector.format(all_routes, ENV['CONTROLLER']).join "\n"
+ inspector = ActionDispatch::Routing::RoutesInspector.new(all_routes)
+ puts inspector.format(ActionDispatch::Routing::ConsoleFormatter.new, ENV['CONTROLLER'])
end
diff --git a/railties/lib/rails/templates/layouts/application.html.erb b/railties/lib/rails/templates/layouts/application.html.erb
index 53276d3e7c..7352d48e7b 100644
--- a/railties/lib/rails/templates/layouts/application.html.erb
+++ b/railties/lib/rails/templates/layouts/application.html.erb
@@ -22,6 +22,10 @@
a { color: #000; }
a:visited { color: #666; }
a:hover { color: #fff; background-color:#000; }
+
+ h2 { padding-left: 10px; }
+
+ <%= yield :style %>
</style>
</head>
<body>
diff --git a/railties/lib/rails/templates/rails/info/routes.html.erb b/railties/lib/rails/templates/rails/info/routes.html.erb
index 890f6f5b03..2d8a190986 100644
--- a/railties/lib/rails/templates/rails/info/routes.html.erb
+++ b/railties/lib/rails/templates/rails/info/routes.html.erb
@@ -6,4 +6,4 @@
Routes match in priority from top to bottom
</p>
-<p><pre><%= @info %></pre></p> \ No newline at end of file
+<%= @routes_inspector.format(ActionDispatch::Routing::HtmlTableFormatter.new(self)) %>
diff --git a/railties/lib/rails/templates/rails/welcome/index.html.erb b/railties/lib/rails/templates/rails/welcome/index.html.erb
new file mode 100644
index 0000000000..eb620caa00
--- /dev/null
+++ b/railties/lib/rails/templates/rails/welcome/index.html.erb
@@ -0,0 +1,245 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <title>Ruby on Rails: Welcome aboard</title>
+ <style media="screen">
+ body {
+ margin: 0;
+ margin-bottom: 25px;
+ padding: 0;
+ background-color: #f0f0f0;
+ font-family: "Lucida Grande", "Bitstream Vera Sans", "Verdana";
+ font-size: 13px;
+ color: #333;
+ }
+
+ h1 {
+ font-size: 28px;
+ color: #000;
+ }
+
+ a {color: #03c}
+ a:hover {
+ background-color: #03c;
+ color: white;
+ text-decoration: none;
+ }
+
+
+ #page {
+ background-color: #f0f0f0;
+ width: 750px;
+ margin: 0;
+ margin-left: auto;
+ margin-right: auto;
+ }
+
+ #content {
+ float: left;
+ background-color: white;
+ border: 3px solid #aaa;
+ border-top: none;
+ padding: 25px;
+ width: 500px;
+ }
+
+ #sidebar {
+ float: right;
+ width: 175px;
+ }
+
+ #footer {
+ clear: both;
+ }
+
+ #header, #about, #getting-started {
+ padding-left: 75px;
+ padding-right: 30px;
+ }
+
+
+ #header {
+ background-image: url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAADIAAABACAYAAABY1SR7AAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAGZhJREFUeNqsWwmUXGWV/t5Sr9aurl6qO0l3Z9/DEoJh18gZQGAUxPHIyQHH7eioZ8bjnAFHZ0RndNxxRBhGcUbxoKIHBkTEcUYREIHIGpKQjUDS6U660/tSVV3Lq/fefPf/Xy2dBFGYx3npqvde/e/e/97v3u/e/8e4Lt2L8DCCAFcGwF8ZBjYbgM1rAZoO+WLwZhDMu9y4+YcOozbAqzwXNA3GdzX/5hV+KnKO2+GXFj/AvzmW8e72iG202CYiphbY403f9/k3QHZtJ9oWtyCQe7wGX79TKVb7rP9pXJPDVxf0Rz+oyxm4HNWrahFNixdk3EAJbERMWOm4ulctVODNVeEVK0DeRVDgb1wfJgcqUo6duaKnFOH7bm6JmH+5LOEgZprwRIHAV3JYfLjKM55Noz3bBqdcgt0Wg52Kq/cHHkXns0qIukKBlltk9rU2QaiouiefPQ+RdBuseAJeqYTK1CTH8mE4NsyIpRWu8nssCs+xULWpjGVwTvieKl/sV6mIXzOib/OftzuG8d6l8SiVMODyRb46oazg8YPP2Wnvy9ISNqplzsxYAW6hjGhHEmYiBoPC+hRMfFMrESgrBC5n0KS+lq1nPahZh2OXymg9bSNWX/u3FKyKI//7Exx96B4Y8RiCEseq8t0VznyxjMDidFIJ8QSf3hJEOFbZEAHVhIkFTX54fxtnIW5pJUQIeZ8ooZShkInuDOLpFIX1ldtCBix7KI/k4E7OwbTjcNIdiCQzsONp2LEk7GgUnZsuQN9lW2En45xlukrUghWzeZq8FsXsi8+gND6MSCqD9k3nwulIUShKZxt0LYPWortRSY0NXreC8J6pZNDChEDh53PT1NIPLaEnLbQKNTETEaR7sycA0jD1INXZAnzObjTbiWh7Vr1A3Knn4nciu+lCvstUig09cp96cVCtcELoFpEIFUjjyIM/osWIg+IMXS3DcfNwZ3NQHmmKU9OqroX2jWdgatduuPkpmA4ViZrK9RqKABEBtg9tDeW+oUIyTIYuFaX7eCG4aqbU+hhKocD3UBoZISBLiC9cpAQKyq5SQo6OjVswtec5VHLTiHUuIN4WonXlqUj2riS0DIUXwZlERFHSK+SQGzqI3MHdmNm7CzMvvowF527B8qvejZ3/+iXk9vVTao5tiTKN0OUHISZEGS/8W6UbRdoTSHe3E1f+CRaR3xhBLVJSIQ7qleZQGBigZYoYdR+ElUjBaW3H6JMPIrV0Hdo2bEayZ7my0KsdLctPBS64EuWZMYw/9wTGnvod0mtzWH71Vuz66o10bVpK8FIx6orUMejpCKYBTvfM9HXBJtA8z3/1BKDivaksVJmaYsgsYPDnd6LzzAuw8I1XUIGleC1HtDWLnguv5BiX4+jDD2D4sQeV1bQvNXBi6vAb1MGtrEEHjRPgqfZ0qMRJElYYSudfq12nmzAvtJ2yib69iRadRGnySD0Uv5bDtCPou/gqnPY3N6DnLRczgtHxCf4aVnUeUdgw6i6FqM1w292Ujo/TJdB5wHcJ2iDCaBTRmVfw4rkw4yksuvQyJJf0YvrgNiayvBLESS9AYuFqJLLLCPb4SQWulosojhxmeCeoDeaQSoVuy8lPtSKxYKnC2Bmf+DwtvBgv3/qfTI6uEtGuJV7PCBTIq5zNtt5uxBgyvap30pf55TISfX1Y/PatGPrVvcgPvEyAJ1GenaPZLSy//G2IL+qki43CNCMwk620iovy9FGUJgYwm8gwpK/guRJOS5dyD688h+n9z2L28F4Ujx2ia04jEl8Ad3oGVTePaGcnQ3sKLb1rkD3nIqx594dRIh733n6PmmrrvGj671sjVlxczRWAkxZ0r+rTrhfMJ0uEM8xKUYXONR+5nr57BdpP24TCsX6M/f5F5AYLWPauK9F11htUwjOIL8GNZH1qpKwiyVGELk0OoDj2EtziFOaODSN3aC/v24xmZzAU51TgcJKd/DktHo9jyRXvg0Or7PvejTj22KPKiyafew6zg8MYypVLNsLkJ2bxaZXM4i5EmCBPsEaoWJUUpfeSK7DgvEtQmh4ihTDQdf5FOHDHr7HqPVeh99KL4OVzpE50N18CtqnCdBCY6rsEcTsqIGUGD6rY9e3bMPzIHmTWLsbqa7ai84wL6YrTqEyOqEmwonEExSoO//R7dLcJWiWCueF+7P7mjZAUY8YdJZqySMo24j5zQSybQdeyhdrX5imho4NhEEnkRbkDQyjSRVJLeziCgef/6avIrFuOtR95P2lJNSSshg4l6rdm+Ht9inWsqIOX7voN+u/eRoEM5PvHMbbjGcwcfg7jO3YxbCcRiaaYQOXnpEaFGeahGQaMCidJRidt8RghS6Q344XQIowmFq2QXdLNdwsx8zUFqCOQNIECVqdp8pESB53Fvhdux9T2FxBb1AWX4XbjDX/HFzjEmgedB4XYKT5D4T0VTLRCtIiTwOBvfovpvS8T+Bm4MyW6jw13tIIDt/9G/TTWk8HKvzgbmd4+YldYQIdixgHJYkC82Ul6UDnQSbEGdsFGZlEWyUyLyiEyYwajRVAoAXNlEjR+pjUCUmiDQcKOORwwgpFfP4cg5mPzTZ9FoqePdGVWuZRPYQNPcgrd0/dCpqpdy3DIsQ4fxtiTu7Hxkx8iRXkcB+94iM86/K0Jx4opi5aOzGJs14toWeLAdYXWxFQCtJlkA+LUq+bI7QR3mj3YoqVNgGcXd5NWUOiZAk9GH86S4jK25jWBLVREl1uK5Voywz6WXf1WLHjTm0lPigSyxoUpnEqU8c26Wyk/Y24RMjhw/yMoj+cQbWvH0isuwuijL6BwaJwcyq7XUTaBP7N3HOU3ke7HSONJb8RTBGoGKZPFyTE8saTZyCPtrC2coxOoTuY5+x4UTzHNsNjR6d6Qa8JJ5BIV8ksVtKzpwcr3v5dyOrzHKMWXizsZAnK6k1ImPDmAqjOmdr9AwXcodzr4kwfQfuY6VKbzyhpGU96S75WxIqb2DaPnvNWKklQD4WSuzB+sVILjOYjm/VARSWKTBQQzlZCFmErYeubzVJJR14SlQtVQMjO0xrXvoulXkq3OKnxAXqSsoSmNUbOM/BV35RjDDz9JrBXpnnEM3vsYjj38LLyZihI8QNAgQhITOCmTO46i+6w1MPm86RVIiC09/RJUGcECCe2UU0G6QIyUjEC5hGaCNd4RqHKU6VuDylQlI2N8hfXDWibEdyhCKXREuZUVUX8lyhh2+Jl5Q/6akSgT4izGn3wBFu+JwYOKj8qwtsbJaYmJuYEZ5AYmFOWXPCN1jTodzeuqM0WtSI1rzXrV0LSNKRFuZLYQ2EYVPjEQVuQUMsCya65GvL1HWUwJS+FNUcBsUiZUQv7aLGlndr+I8ug4XUMVAJw4U7FmI8SFETTmUaGK2gas1SeeP8znoizIEso9DaUIy2FWkNU5V0VYs/azWXKncuCHqgQq1CHiY831H8TGr34erRvXKdD6LD3b+HnRn12qGgdqlmxHZe2aRcy6NbQScl8y8dSOfWQE1yK9YYmqXYww3xhNObemUI2IWraF2d1HMTeeh83MbkUiylKiiMdy2wjzXBjxWYdRiSkhfDVVKGSstxM9l16JxZe/E2+848c49bPXK9D2vPUyEsBOVZMINmpCW6HgEOuIQjXF6FYuAV2aHsWyrVfj9C9er5SR5Kms0PTf8QoZtIo7WSJW+mmRJLGSpDK2ipzV2bK6X6fxtWOCicYVqyhGXkXn+WeTcfape5ZDsPGM91C5iy8LI0s445bd9FkrAFHICt1N8DE+gdyeQczs34+uzeei68LNLGfdea50st6VbiyYmHq+nxTFRSSRVsD3ii7xyeQbdt/M5h/MERMT4i6GjlAWeUxh6HCN8+LIz+5H5zlUbtHSOnVp4MCa51JaIQ16i0kwP9CP0uExPP+JL2DggfuYN8jTJClYxnH4aNimdpp0r7nDkyx9h5gE0+RqSVTyZXXTsMz5FaJyMJrrGLNopyWUIImj//1LjPzuUZLCC5gzVqMwPIglV7/rxCaihFaCPCDOxDUl1EoylFP4mUlFCgPDStLKWB47PnUjrSSsNqrJsa/zR02ZwGjYRoVkEZh0ZHzbfmTPXE85SWrnKip6GeFE2I1iKVBCzNK9pmiVhS1x+Axx7myRJesvgHvvR3rNKmQ3n/OKPVGND1MVXTqHiFK6qVFiwlXgTVDhkq+ChhnyJCW9GeaoIGQOdV0M9YhYZWbvUXrIJJ+rKL6lJ9CYj5Fai0iKqyPkx0HcUsJYrBbtREIJ2H72GxTI/2CL1zAbLkZ8WIxYgUvsKebq6Zl3rEZvymx6echo1N+au9XcS3oHsxWMPrGTFH+CLhsmbhMNRWrNB4SZVSwyJ5WDFRb3DAAmaXf2rPP+6BpbkmStkBLAWwkHmdNWKfYqFaZRp2GGdo+mhpv6bBkNhepRzERpdASeW1aKSZ5RidpoUsRAvQ+NJCnJHHl+bcZ80vjkij661vo/rWMQSitWskgnNv7LP+MNN38NadYuCPtYCItIFTjMRgfeqClkhkFZ+FXCQmpFuyKXii7xNI93LT9szdrUMsNZnJkuwZX6zlKdaqRXrESiq/e19kBC3NisLt+Gc/7jW0gtZ51Bl1MCmUaoM//aRv0aapnF0l362KIUnI6EyuhCUOuWrIVfAZcRAj5NJWJ0C5epP19y1awJLWhdt/a1t3KcGF8Yxb5bbsLItoeYmxZRkRWq46IrR9StX/tcw4oKsYH+nlrZpmbcZQ7R1tDPBvMbdIwofLpVKIfcJy5nCa5WRhnDFkVOx+s5kr29GPzpfUxsuxg0zlQUxSZudG/CqNOSIJxYCclGCA7fDRDpiCK6gIVfidVmWXrHRh0fmBd+eSYIIEcWdRhdJJsWp+aQT1vI9nYjnl3wuhSJLuhAJJ1WQWDisadUELCi0bD1WlscMpq6lrV1Ft0riC9tVcFD8odfDVS9bod5pNGgC3+XFnxsXA2rsw25/gHMTcwiRxdbvLgPsY7s61IktWSZinw6l8SbupNGvUlphB1yZY3aIhfZtRmz4XS3oMoA5JP6BywdvBIr24ytMdzsWjHaMcnI0nXRG5FkdCrnS6gy6QzccxeMZDsJW+r1KbJ4pbKAVy6huXoyauVUaAUjRK5WjN9cH05PCiZl84VfsXaSVTKf191C6F61qCXjtjAORtvTSPb0sgYoEi/UmEmnMj6JkpXA6z2cTAbxxV26GdEEZB12DVVV63BrIuwYaWpCGZyuJBWSFSxPLTB5PH1+rhDDKlQbuvajNUzE+UVyRTTdQt+zWIrGWIJOozo8hjmashq8PkXsZAoty1Yqi/gVnq6ru+p1pUKFTM3dENJzu421TiqKKq3hhUp45apSyM1VGMH0xOi+liz0yOxUyijs2w2DlRjI+8tHB3XUIP+fGBxA9+LFr1kRgwV769p1fPkEQ+9KRq+dKE9MsGKc1BmxltEC7W6CEdW0aUtocIvw0tcSt5JGu3R4OA+zIxW1uKoUOUZzFxmxRp/ai+iz+xi9CK5EVJGdqBNBlG4xdvBlRq9eTQteawhm0MgPLsSGj92gVqjKk8ew/TOfxPjjz8BKxhvLFGHjWUBuJh0Cu6pqD7WCTGz4BDqKpE30rIlj05rw6sKFxuCXPP9O8MEjxQqOTuQwNjJLa1mItaRRGB3GLHnO6znaNmxC/nA/cocPKNoS61iEZVdfEy5LBHVKUieCLY5eeKIiXp6RapJuNVJFMCamYGnOUFyslBo0Xronai0dIfXmnZIqtKhgNIaj/F3ULSLx4j60dnXXy8s/OZe0dyGW7cLOL34arevXI9rayWgYhZPtoJtNqsTbyPKUgwzamyCw867MtG5NBUF9bSBXLCkeKOzDroUutaZODax52yUk5sfgsyrL897+PXtQHTmK7vWnomPpCkSTf3pI7j7/Qmz/5HWY3r5LNziYeC3WPlYsovOJJ7VKVbuPENcgXEyvuV3IbKXpPlcqqh0acqGe2S1oq1jzqmZ+b0mGDJNaM2bnjrHuPnYUifZOtDMKda9ah1RnZ30F99WO9jM2MzouZw0vLdJIuCsiUInOz0vbiVNa9DSBtITyWo3VAV/XG/KmPEuBKrmard7rNxKiyCoN7EBnpXlLCiYTmfibuEHSSSkLV4uzGNr5NEYP7EZb31J0rd6AzMIevtf+g4oIg+7e8iYM3H03J5muw9n3ZquqfwU3aGDdMBqdztr+lXBbhyg+R2xYTb5jN7YG6SKnyh870r8Ki6Py0CiO3fcTNWaCBU3E8FVDr7ZPRjbcDLHO30N/TmazdLk+JFMxVoZh6errUrcmnDQp5o4MocrI4o3N6dmXhp1hoHkOFV2R5CXtVwm3Qc0aBip8Z6lY0HtRpJ8GYz5pVFgxgkaHiaCuDE1gfOAhFdNbJIKxplCKNJqqyoqi0CT9tp9/IyyPE2SryYyDKD9LVKxKUqXbuFOM+yVDN/Rq+0ia1mLmtYNqK8rhTiSpLLNbLkDLuZvQ0X8QBoG+//5fIMjP4AQ/kJkuM+vW+sS1wkgiVSTi0Fq2XqoLFfFYMMkyHSFL2mOpHQmy+aU4xXHoLk6rrIkYiE1JNpZOJjO1ivduOLSkZeuk6/YBwR54jaVv6chXpmZQmJnEssveQjwVcPCXv1IWt4//sUVB7K4WpGTREqhvJCrO5MhtGLMTKWU5pUSpDKs1glhbB4W3VCSpTM6gOl2GQzxJt+RQUMFcOoENrXG0FEhESSvMmIVIZ6uaHL9QZn6Y067VNJueV4bdmYDdktJ7pAJNKKfG+pG/cz8GH/gfGLIARF4o9fs8RWSrUmZxN7Z+9za0sooTPiRuI22bsUMHsevWW1B+iFnYdOgqFWTPPxWnXPdxtK5eV8fB9IH92Pn1m1hz7MQh00Xm/C34+K23MiOXsPvLX8bgbXej5bz1OPs7tzIhduHgnXfghX/8OplEsr6U4ZtV9G69HMvf8wEkKUfgaUeWbs4zX/8Sxm/+AbzxCRVF1VpFM9hrvS2ZmZbuRUh2LpxPw7t60EWK8vgHPoCZ5w4i1pvBps99Bu2nbJ73XLyzB4kvLcAPt27F2LFR9MTjSKbb1L1h4mIq4iNL14u2ZRFJysazZCNHqA0DZXRcuBGnf+bz6v4JLDqVgk3r247DnMdJDkOzffJtDfoY2b0dg08/gbZlq7BiyyWk+MuQ2bAGU9v2snTtQnxBj3pu9OnfYXr/Hiq1EZ0bz0ZsUS+sFUvgDB+DFfh1v3X9Kg4xknfLRNZ21h2/RYTX29avU0pUSwUcuP07KLw0oBZrA5bGozt2MlA5updgzGuJnYyp6rt7778HP37fX+OJW77ZaKzKoo5eOdfRhMehO3+EbXzu8H/dXW/SOTwj0gZqeoVck+h3xES9LDjpVp3QXeRdqSVLkDllrepy5oeHMPH0brq2qdteRmNJwj7pYKFVlr75YrztKw6ya9aFTzF8Tk+pBZrmXRGRdCsSLMiQbKlfE7PLrjarCcSSA0QZvQQevGKncnrfXpVwZTde3+XvqN9b8d4PYfuNn8O+b9zO56K6oGpOiMYreNfSc7eoE+FO00P334XJx3fQzM685zd8/Hqs/uCHGGEy9QEslaT0Cm9t7rVyYqnGWogEGIl+nqUTmyxwTj62HTs/91ks3XqN2u8VBLKZoVt14pe/42oc/O6dzB2+qnEMNGHEPHHbSfiSqloakGP7D7+Dpz79BfT6cRXu5rHatk51Nh9aEaOJu2mOZIf36uDu6EDi3PVoIQGV5efiwSG1Rjny8COY3P4sI1WM2HKx4bpPYdEFlzA9RMOlhCAsLJssYqGxRbcZI8//9MfIrliDvjPOwqqL/xwD996P6rY9zGHWPNMNPf8UJl9+Cdm169G9YWOdapjB8auShsJMc85YdekVWL7lQgroKHd68qMfRcAEu+lrX1GdSdmBKjQn0aOrU9lso5bK53uSLiyscNu10tAy66FganAQD9zwD6jM5ZBe2IeLbvoGWs5YofZQyfKXxbpejl133omfXfth7P/Fz8NRLbXgb0nGNe26GhGST5MzFmEYll2oCl+sd2IZCcWtTKxd6rokwdYVpyK9fB1z1KnIrD0NDt1WiNGB738X3kxJVapiWVmR5pCurc2iSaIkmNJ0Hr+9+WYkMu0YfHI7Dv9+J+766Eew8vSNiFP4WGsGBanhh6bw1K3fRjSdwfSel5FikTT67At4+t9vgVssojA0Rp6VwOyhfjx9262qABrfw1KaJW15YprXvsVcEG1sT5eCji40fXSURVyAvTd9TSmv6nTVifQx/uwzmHiU7kb3Clu+GC27MsY247p07+SihN0m/Kgc6EXRIjmMgDvCF9mcsXJxDgniZSnN3xFLIcc6Yormd1mhCX2QpWc7SteolNUpNUQkIUvJpDkUrsrfqy1L8ZjaFSTrJKLsCbvz6BqxaBwdBReWbJmF3kTa2NYRVYFGHEYKqqFKFXtzMg6uUhaJyzZyQ/d/FdUm8LwmAuYwO/vhQBU+m+ddmy+NpBKNWpIzF7EdRSxrOygMMl6LruUw2tQXOTy1akNFk/XtU/V70H3g6YyNNk5GtOIp/DYvlKp9LoJLWuIl2fADfJ/X71PQ8Jo2Vzbv620OAFI9jtIqCQ7tnfC/JxhNT4dShds4UKvB66s1ftPnRqOh/l13hDDqWGhxqUgTsIV1Fzg5Y7TEpKsK+B/w+sdqUWuqv1CxUN8K/MqHLMnhj/g/J/4/juDky9VSg0kh/zQj322897Pao/8nwAC+AZicLeuzngAAAABJRU5ErkJggg==);
+ background-repeat: no-repeat;
+ background-position: top left;
+ height: 64px;
+ }
+ #header h1, #header h2 {margin: 0}
+ #header h2 {
+ color: #888;
+ font-weight: normal;
+ font-size: 16px;
+ }
+
+
+ #about h3 {
+ margin: 0;
+ margin-bottom: 10px;
+ font-size: 14px;
+ }
+
+ #about-content {
+ background-color: #ffd;
+ border: 1px solid #fc0;
+ margin-left: -55px;
+ margin-right: -10px;
+ }
+ #about-content table {
+ margin-top: 10px;
+ margin-bottom: 10px;
+ font-size: 11px;
+ border-collapse: collapse;
+ }
+ #about-content td {
+ padding: 10px;
+ padding-top: 3px;
+ padding-bottom: 3px;
+ }
+ #about-content td.name {color: #555}
+ #about-content td.value {color: #000}
+
+ #about-content ul {
+ padding: 0;
+ list-style-type: none;
+ }
+
+ #about-content.failure {
+ background-color: #fcc;
+ border: 1px solid #f00;
+ }
+ #about-content.failure p {
+ margin: 0;
+ padding: 10px;
+ }
+
+
+ #getting-started {
+ border-top: 1px solid #ccc;
+ margin-top: 25px;
+ padding-top: 15px;
+ }
+ #getting-started h1 {
+ margin: 0;
+ font-size: 20px;
+ }
+ #getting-started h2 {
+ margin: 0;
+ font-size: 14px;
+ font-weight: normal;
+ color: #333;
+ margin-bottom: 25px;
+ }
+ #getting-started ol {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #getting-started li {
+ font-size: 18px;
+ color: #888;
+ margin-bottom: 25px;
+ }
+ #getting-started li h2 {
+ margin: 0;
+ font-weight: normal;
+ font-size: 18px;
+ color: #333;
+ }
+ #getting-started li p {
+ color: #555;
+ font-size: 13px;
+ }
+
+
+ #sidebar ul {
+ margin-left: 0;
+ padding-left: 0;
+ }
+ #sidebar ul h3 {
+ margin-top: 25px;
+ font-size: 16px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #ccc;
+ }
+ #sidebar li {
+ list-style-type: none;
+ }
+ #sidebar ul.links li {
+ margin-bottom: 5px;
+ }
+
+ .filename {
+ font-style: italic;
+ }
+ </style>
+ <script>
+ function about() {
+ var info = document.getElementById('about-content'),
+ xhr;
+
+ if (info.innerHTML === '') {
+ xhr = new XMLHttpRequest();
+ xhr.open("GET", "/rails/info/properties", false);
+ xhr.setRequestHeader("X-Requested-With", "XMLHttpRequest");
+ xhr.send("");
+ info.innerHTML = xhr.responseText;
+ }
+
+ info.style.display = info.style.display === 'none' ? 'block' : 'none';
+ }
+ </script>
+ </head>
+ <body>
+ <div id="page">
+ <div id="sidebar">
+ <ul id="sidebar-items">
+ <li>
+ <h3>Browse the documentation</h3>
+ <ul class="links">
+ <li><a href="http://guides.rubyonrails.org/">Rails Guides</a></li>
+ <li><a href="http://api.rubyonrails.org/">Rails API</a></li>
+ <li><a href="http://www.ruby-doc.org/core/">Ruby core</a></li>
+ <li><a href="http://www.ruby-doc.org/stdlib/">Ruby standard library</a></li>
+ </ul>
+ </li>
+ </ul>
+ </div>
+
+ <div id="content">
+ <div id="header">
+ <h1>Welcome aboard</h1>
+ <h2>You&rsquo;re riding Ruby on Rails!</h2>
+ </div>
+
+ <div id="about">
+ <h3><a href="/rails/info/properties" onclick="about(); return false">About your application&rsquo;s environment</a></h3>
+ <div id="about-content" style="display: none"></div>
+ </div>
+
+ <div id="getting-started">
+ <h1>Getting started</h1>
+ <h2>Here&rsquo;s how to get rolling:</h2>
+
+ <ol>
+ <li>
+ <h2>Use <code>rails generate</code> to create your models and controllers</h2>
+ <p>To see all available options, run it without parameters.</p>
+ </li>
+
+ <li>
+ <h2>Set up a root route to replace this page</h2>
+ <p>You're seeing this page because you're running in development mode and you haven't set a root route yet.</p>
+ <p>Routes are set up in <span class="filename">config/routes.rb</span>.</p>
+ </li>
+
+ <li>
+ <h2>Configure your database</h2>
+ <p>If you're not using SQLite (the default), edit <span class="filename">config/database.yml</span> with your username and password.</p>
+ </li>
+ </ol>
+ </div>
+ </div>
+
+ <div id="footer">&nbsp;</div>
+ </div>
+ </body>
+</html>
diff --git a/railties/lib/rails/test_help.rb b/railties/lib/rails/test_help.rb
index aed7fd4b14..46f7466551 100644
--- a/railties/lib/rails/test_help.rb
+++ b/railties/lib/rails/test_help.rb
@@ -2,25 +2,16 @@
# so fixtures aren't loaded into that environment
abort("Abort testing: Your Rails environment is running in production mode!") if Rails.env.production?
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'active_support/test_case'
require 'action_controller/test_case'
require 'action_dispatch/testing/integration'
+require 'rails/generators/test_case'
# Config Rails backtrace in tests.
require 'rails/backtrace_cleaner'
MiniTest.backtrace_filter = Rails.backtrace_cleaner
-# Enable turn if it is available
-begin
- require 'turn'
-
- Turn.config do |c|
- c.natural = true
- end
-rescue LoadError
-end
-
if defined?(ActiveRecord::Base)
class ActiveSupport::TestCase
include ActiveRecord::TestFixtures
diff --git a/railties/lib/rails/test_unit/railtie.rb b/railties/lib/rails/test_unit/railtie.rb
index ed89ce4128..75180ff978 100644
--- a/railties/lib/rails/test_unit/railtie.rb
+++ b/railties/lib/rails/test_unit/railtie.rb
@@ -1,3 +1,7 @@
+if defined?(Rake.application) && Rake.application.top_level_tasks.grep(/^(default$|test(:|$))/).any?
+ ENV['RAILS_ENV'] ||= 'test'
+end
+
module Rails
class TestUnitRailtie < Rails::Railtie
config.app_generators do |c|
@@ -5,7 +9,6 @@ module Rails
fixture_replacement: nil
c.integration_tool :test_unit
- c.performance_tool :test_unit
end
rake_tasks do
diff --git a/railties/lib/rails/test_unit/sub_test_task.rb b/railties/lib/rails/test_unit/sub_test_task.rb
index 87b6f9b5a4..d9bffba4d7 100644
--- a/railties/lib/rails/test_unit/sub_test_task.rb
+++ b/railties/lib/rails/test_unit/sub_test_task.rb
@@ -1,6 +1,124 @@
+require 'rake/testtask'
+
module Rails
+ class TestTask < Rake::TestTask # :nodoc: all
+ # A utility class which is used primarily in "rails/test_unit/testing.rake"
+ # to help define rake tasks corresponding to <tt>rake test</tt>.
+ #
+ # This class takes a TestInfo class and defines the appropriate rake task
+ # based on the information, then invokes it.
+ class TestCreator
+ def initialize(info)
+ @info = info
+ end
+
+ def invoke_rake_task
+ if @info.files.any?
+ create_and_run_single_test
+ reset_application_tasks
+ else
+ Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke
+ end
+ end
+
+ private
+
+ def create_and_run_single_test
+ Rails::TestTask.new('test:single') { |t|
+ t.test_files = @info.files
+ }
+ ENV['TESTOPTS'] ||= @info.opts
+ Rake::Task['test:single'].invoke
+ end
+
+ def reset_application_tasks
+ Rake.application.top_level_tasks.replace @info.tasks
+ end
+ end
+
+ # This is a utility class used by the <tt>TestTask::TestCreator</tt> class.
+ # This class takes a set of test tasks and checks to see if they correspond
+ # to test files (or can be transformed into test files). Calling <tt>files</tt>
+ # provides the set of test files and is used when initializing tests after
+ # a call to <tt>rake test</tt>.
+ class TestInfo
+ def initialize(tasks)
+ @tasks = tasks
+ @files = nil
+ end
+
+ def files
+ @files ||= @tasks.map { |task|
+ [task, translate(task)].find { |file| test_file?(file) }
+ }.compact
+ end
+
+ def translate(file)
+ if file =~ /^app\/(.*)$/
+ "test/#{$1.sub(/\.rb$/, '')}_test.rb"
+ else
+ "test/#{file}_test.rb"
+ end
+ end
+
+ def tasks
+ @tasks - test_file_tasks - opt_names
+ end
+
+ def opts
+ opts = opt_names
+ if opts.any?
+ "-n #{opts.join ' '}"
+ end
+ end
+
+ private
+
+ def test_file_tasks
+ @tasks.find_all { |task|
+ [task, translate(task)].any? { |file| test_file?(file) }
+ }
+ end
+
+ def test_file?(file)
+ file =~ /^test/ && File.file?(file) && !File.directory?(file)
+ end
+
+ def opt_names
+ (@tasks - test_file_tasks).reject { |t| task_defined? t }
+ end
+
+ def task_defined?(task)
+ Rake::Task.task_defined? task
+ end
+ end
+
+ def self.test_creator(tasks)
+ info = TestInfo.new(tasks)
+ TestCreator.new(info)
+ end
+
+ def initialize(name = :test)
+ super
+ @libs << "test" # lib *and* test seem like a better default
+ end
+
+ def define
+ task @name do
+ if ENV['TESTOPTS']
+ ARGV.replace Shellwords.split ENV['TESTOPTS']
+ end
+ libs = @libs - $LOAD_PATH
+ $LOAD_PATH.unshift(*libs)
+ file_list.each { |fl|
+ FileList[fl].to_a.each { |f| require File.expand_path f }
+ }
+ end
+ end
+ end
+
# Silence the default description to cut down on `rake -T` noise.
- class SubTestTask < Rake::TestTask
+ class SubTestTask < Rake::TestTask # :nodoc:
def desc(string)
# Ignore the description.
end
diff --git a/railties/lib/rails/test_unit/testing.rake b/railties/lib/rails/test_unit/testing.rake
index cd59fbe599..38107e77b2 100644
--- a/railties/lib/rails/test_unit/testing.rake
+++ b/railties/lib/rails/test_unit/testing.rake
@@ -2,52 +2,11 @@ require 'rbconfig'
require 'rake/testtask'
require 'rails/test_unit/sub_test_task'
-TEST_CHANGES_SINCE = Time.now - 600
-
-# Look up tests for recently modified sources.
-def recent_tests(source_pattern, test_path, touched_since = 10.minutes.ago)
- FileList[source_pattern].map do |path|
- if File.mtime(path) > touched_since
- tests = []
- source_dir = File.dirname(path).split("/")
- source_file = File.basename(path, '.rb')
-
- # Support subdirs in app/models and app/controllers
- modified_test_path = source_dir.length > 2 ? "#{test_path}/" << source_dir[1..source_dir.length].join('/') : test_path
-
- # For modified files in app/ run the tests for it. ex. /test/controllers/account_controller.rb
- test = "#{modified_test_path}/#{source_file}_test.rb"
- tests.push test if File.exist?(test)
-
- # For modified files in app, run tests in subdirs too. ex. /test/controllers/account/*_test.rb
- test = "#{modified_test_path}/#{File.basename(path, '.rb').sub("_controller","")}"
- FileList["#{test}/*_test.rb"].each { |f| tests.push f } if File.exist?(test)
-
- return tests
-
- end
- end.flatten.compact
-end
-
-
-# Recreated here from Active Support because :uncommitted needs it before Rails is available
-module Kernel
- remove_method :silence_stderr # Removing old method to prevent method redefined warning
- def silence_stderr
- old_stderr = STDERR.dup
- STDERR.reopen(RbConfig::CONFIG['host_os'] =~ /mswin|mingw/ ? 'NUL:' : '/dev/null')
- STDERR.sync = true
- yield
- ensure
- STDERR.reopen(old_stderr)
- end
-end
-
task default: :test
-desc 'Runs test:units, test:functionals, test:integration together (also available: test:benchmark, test:profile)'
+desc 'Runs test:units, test:functionals, test:generators, test:integration together'
task :test do
- Rake::Task[ENV['TEST'] ? 'test:single' : 'test:run'].invoke
+ Rails::TestTask.test_creator(Rake.application.top_level_tasks).invoke_rake_task
end
namespace :test do
@@ -55,106 +14,36 @@ namespace :test do
# Placeholder task for other Railtie and plugins to enhance. See Active Record for an example.
end
- task :run do
- errors = %w(test:units test:functionals test:integration).collect do |task|
- begin
- Rake::Task[task].invoke
- nil
- rescue => e
- { task: task, exception: e }
- end
- end.compact
+ task :run => ['test:units', 'test:functionals', 'test:generators', 'test:integration']
- if errors.any?
- puts errors.map { |e| "Errors running #{e[:task]}! #{e[:exception].inspect}" }.join("\n")
- abort
- end
+ # Inspired by: http://ngauthier.com/2012/02/quick-tests-with-bash.html
+ desc "Run tests quickly by merging all types and not resetting db"
+ Rails::TestTask.new(:all) do |t|
+ t.pattern = "test/**/*_test.rb"
end
- Rake::TestTask.new(recent: "test:prepare") do |t|
- since = TEST_CHANGES_SINCE
- touched = FileList['test/**/*_test.rb'].select { |path| File.mtime(path) > since } +
- recent_tests('app/models/**/*.rb', 'test/models', since) +
- recent_tests('app/models/**/*.rb', 'test/unit', since) +
- recent_tests('app/controllers/**/*.rb', 'test/controllers', since) +
- recent_tests('app/controllers/**/*.rb', 'test/functional', since)
-
- t.libs << 'test'
- t.test_files = touched.uniq
+ namespace :all do
+ desc "Run tests quickly, but also reset db"
+ task :db => %w[db:test:prepare test:all]
end
- Rake::Task['test:recent'].comment = "Test recent changes"
-
- Rake::TestTask.new(uncommitted: "test:prepare") do |t|
- def t.file_list
- if File.directory?(".svn")
- changed_since_checkin = silence_stderr { `svn status` }.split.map { |path| path.chomp[7 .. -1] }
- elsif File.directory?(".git")
- changed_since_checkin = silence_stderr { `git ls-files --modified --others --exclude-standard` }.split.map { |path| path.chomp }
- else
- abort "Not a Subversion or Git checkout."
- end
- models = changed_since_checkin.select { |path| path =~ /app[\\\/]models[\\\/].*\.rb$/ }
- controllers = changed_since_checkin.select { |path| path =~ /app[\\\/]controllers[\\\/].*\.rb$/ }
+ Rails::TestTask.new(single: "test:prepare")
- unit_tests = models.map { |model| "test/models/#{File.basename(model, '.rb')}_test.rb" } +
- models.map { |model| "test/unit/#{File.basename(model, '.rb')}_test.rb" } +
- functional_tests = controllers.map { |controller| "test/controllers/#{File.basename(controller, '.rb')}_test.rb" } +
- controllers.map { |controller| "test/functional/#{File.basename(controller, '.rb')}_test.rb" }
- (unit_tests + functional_tests).uniq.select { |file| File.exist?(file) }
+ ["models", "helpers", "controllers", "mailers", "integration"].each do |name|
+ Rails::TestTask.new(name => "test:prepare") do |t|
+ t.pattern = "test/#{name}/**/*_test.rb"
end
-
- t.libs << 'test'
- end
- Rake::Task['test:uncommitted'].comment = "Test changes since last checkin (only Subversion and Git)"
-
- Rake::TestTask.new(single: "test:prepare") do |t|
- t.libs << "test"
- end
-
- Rails::SubTestTask.new(models: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/models/**/*_test.rb'
end
- Rails::SubTestTask.new(helpers: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/helpers/**/*_test.rb'
+ Rails::TestTask.new(generators: "test:prepare") do |t|
+ t.pattern = "test/lib/generators/**/*_test.rb"
end
- Rails::SubTestTask.new(units: "test:prepare") do |t|
- t.libs << "test"
+ Rails::TestTask.new(units: "test:prepare") do |t|
t.pattern = 'test/{models,helpers,unit}/**/*_test.rb'
end
- Rails::SubTestTask.new(controllers: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/controllers/**/*_test.rb'
- end
-
- Rails::SubTestTask.new(mailers: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/mailers/**/*_test.rb'
- end
-
- Rails::SubTestTask.new(functionals: "test:prepare") do |t|
- t.libs << "test"
+ Rails::TestTask.new(functionals: "test:prepare") do |t|
t.pattern = 'test/{controllers,mailers,functional}/**/*_test.rb'
end
-
- Rails::SubTestTask.new(integration: "test:prepare") do |t|
- t.libs << "test"
- t.pattern = 'test/integration/**/*_test.rb'
- end
-
- Rails::SubTestTask.new(benchmark: 'test:prepare') do |t|
- t.libs << 'test'
- t.pattern = 'test/performance/**/*_test.rb'
- t.options = '-- --benchmark'
- end
-
- Rails::SubTestTask.new(profile: 'test:prepare') do |t|
- t.libs << 'test'
- t.pattern = 'test/performance/**/*_test.rb'
- end
end
diff --git a/railties/lib/rails/version.rb b/railties/lib/rails/version.rb
index ec878f9dcf..5a6d8d0983 100644
--- a/railties/lib/rails/version.rb
+++ b/railties/lib/rails/version.rb
@@ -1,10 +1,10 @@
module Rails
- module VERSION #:nodoc:
+ module VERSION
MAJOR = 4
- MINOR = 0
+ MINOR = 1
TINY = 0
PRE = "beta"
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end
diff --git a/railties/lib/rails/welcome_controller.rb b/railties/lib/rails/welcome_controller.rb
new file mode 100644
index 0000000000..45b764fa6b
--- /dev/null
+++ b/railties/lib/rails/welcome_controller.rb
@@ -0,0 +1,7 @@
+class Rails::WelcomeController < ActionController::Base # :nodoc:
+ self.view_paths = File.expand_path('../templates', __FILE__)
+ layout nil
+
+ def index
+ end
+end
diff --git a/railties/railties.gemspec b/railties/railties.gemspec
index 6d28947e83..56b8736800 100644
--- a/railties/railties.gemspec
+++ b/railties/railties.gemspec
@@ -1,4 +1,4 @@
-version = File.read(File.expand_path("../../RAILS_VERSION", __FILE__)).strip
+version = File.read(File.expand_path('../../RAILS_VERSION', __FILE__)).strip
Gem::Specification.new do |s|
s.platform = Gem::Platform::RUBY
@@ -6,24 +6,28 @@ Gem::Specification.new do |s|
s.version = version
s.summary = 'Tools for creating, working with, and running Rails applications.'
s.description = 'Rails internals: application bootup, plugins, generators, and rake tasks.'
+
s.required_ruby_version = '>= 1.9.3'
- s.license = 'MIT'
- s.author = 'David Heinemeier Hansson'
- s.email = 'david@loudthinking.com'
- s.homepage = 'http://www.rubyonrails.org'
+ s.license = 'MIT'
+
+ s.author = 'David Heinemeier Hansson'
+ s.email = 'david@loudthinking.com'
+ s.homepage = 'http://www.rubyonrails.org'
- s.files = Dir['CHANGELOG.md', 'README.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}']
- s.require_path = 'lib'
+ s.files = Dir['CHANGELOG.md', 'README.rdoc', 'RDOC_MAIN.rdoc', 'bin/**/*', 'lib/**/{*,.[a-z]*}']
+ s.require_path = 'lib'
- s.bindir = 'bin'
- s.executables = ['rails']
+ s.bindir = 'bin'
+ s.executables = ['rails']
s.rdoc_options << '--exclude' << '.'
- s.add_dependency('rake', '>= 0.8.7')
- s.add_dependency('thor', '>= 0.15.4', '< 2.0')
- s.add_dependency('rdoc', '~> 3.4')
- s.add_dependency('activesupport', version)
- s.add_dependency('actionpack', version)
+ s.add_dependency 'activesupport', version
+ s.add_dependency 'actionpack', version
+
+ s.add_dependency 'rake', '>= 0.8.7'
+ s.add_dependency 'thor', '>= 0.18.1', '< 2.0'
+
+ s.add_development_dependency 'actionview', version
end
diff --git a/railties/test/abstract_unit.rb b/railties/test/abstract_unit.rb
index dfcf5aa27d..643cc6b0ee 100644
--- a/railties/test/abstract_unit.rb
+++ b/railties/test/abstract_unit.rb
@@ -3,16 +3,17 @@ ENV["RAILS_ENV"] ||= "test"
require File.expand_path("../../../load_paths", __FILE__)
require 'stringio'
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'fileutils'
require 'active_support'
-
require 'action_controller'
+require 'action_view'
require 'rails/all'
module TestApp
class Application < Rails::Application
config.root = File.dirname(__FILE__)
+ config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
end
end
diff --git a/railties/test/app_rails_loader_test.rb b/railties/test/app_rails_loader_test.rb
new file mode 100644
index 0000000000..92cb3233d8
--- /dev/null
+++ b/railties/test/app_rails_loader_test.rb
@@ -0,0 +1,68 @@
+require 'tmpdir'
+require 'abstract_unit'
+require 'rails/app_rails_loader'
+
+class AppRailsLoaderTest < ActiveSupport::TestCase
+ 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)
+ end
+
+ setup do
+ @tmp = Dir.mktmpdir('railties-rails-loader-test-suite')
+ @cwd = Dir.pwd
+ Dir.chdir(@tmp)
+ end
+
+ ['bin', 'script'].each do |script_dir|
+ 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(:exist?).with('bin/rails').returns(false)
+ File.stubs(:exist?).with('script/rails').returns(false)
+
+ assert !Rails::AppRailsLoader.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
+
+ 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
+ end
+
+ test "is in a Rails application if parent directory has #{exe} containing #{keyword} and chdirs to the root directory" do
+ write "foo/bar/#{exe}"
+ write "foo/#{exe}", keyword
+
+ Dir.chdir('foo/bar')
+
+ expects_exec exe
+ Rails::AppRailsLoader.exec_app_rails
+
+ # Compare the realpath in case either of them has symlinks.
+ #
+ # This happens in particular in Mac OS X, where @tmp starts
+ # with "/var", and Dir.pwd with "/private/var", due to a
+ # default system symlink var -> private/var.
+ assert_equal File.realpath("#@tmp/foo"), File.realpath(Dir.pwd)
+ end
+ end
+ end
+
+ teardown do
+ Dir.chdir(@cwd)
+ FileUtils.rm_rf(@tmp)
+ end
+end
diff --git a/railties/test/application/asset_debugging_test.rb b/railties/test/application/asset_debugging_test.rb
index 1eddfac664..9a571fac3a 100644
--- a/railties/test/application/asset_debugging_test.rb
+++ b/railties/test/application/asset_debugging_test.rb
@@ -14,7 +14,7 @@ module ApplicationTests
app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>"
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/posts', to: "posts#index"
end
RUBY
@@ -48,7 +48,7 @@ module ApplicationTests
assert_no_match(/<script src="\/assets\/xmlhr-([0-z]+)\.js"><\/script>/, last_response.body)
end
- test "assets aren't concatened when compile is true is on and debug_assets params is true" do
+ test "assets aren't concatenated when compile is true is on and debug_assets params is true" do
add_to_env_config "production", "config.assets.compile = true"
ENV["RAILS_ENV"] = "production"
diff --git a/railties/test/application/assets_test.rb b/railties/test/application/assets_test.rb
index 2506d19e6d..b235b51d90 100644
--- a/railties/test/application/assets_test.rb
+++ b/railties/test/application/assets_test.rb
@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
require 'isolation/abstract_unit'
require 'rack/test'
+require 'active_support/json'
module ApplicationTests
class AssetsTest < ActiveSupport::TestCase
@@ -27,24 +28,25 @@ module ApplicationTests
def clean_assets!
quietly do
- assert Dir.chdir(app_path) { system('bundle exec rake assets:clean') }
+ assert Dir.chdir(app_path) { system('bundle exec rake assets:clobber') }
end
end
def assert_file_exists(filename)
- assert File.exists?(filename), "missing #{filename}"
+ assert Dir[filename].first, "missing #{filename}"
end
def assert_no_file_exists(filename)
- assert !File.exists?(filename), "#{filename} does exist"
+ assert !File.exist?(filename), "#{filename} does exist"
end
test "assets routes have higher priority" do
+ app_file "app/assets/images/rails.png", "notactuallyapng"
app_file "app/assets/javascripts/demo.js.erb", "a = <%= image_path('rails.png').inspect %>;"
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
- get '*path', to: lambda { |env| [200, { "Content-Type" => "text/html" }, "Not an asset"] }
+ Rails.application.routes.draw do
+ get '*path', to: lambda { |env| [200, { "Content-Type" => "text/html" }, ["Not an asset"]] }
end
RUBY
@@ -75,15 +77,39 @@ module ApplicationTests
precompile!
files = Dir["#{app_path}/public/assets/application-*.js"]
- files << Dir["#{app_path}/public/assets/application.js"].first
files << Dir["#{app_path}/public/assets/foo/application-*.js"].first
- files << Dir["#{app_path}/public/assets/foo/application.js"].first
files.each do |file|
assert_not_nil file, "Expected application.js asset to be generated, but none found"
- assert_equal "alert();", File.read(file)
+ assert_equal "alert();\n", File.read(file)
end
end
+ def test_precompile_does_not_hit_the_database
+ app_file "app/assets/javascripts/application.js", "alert();"
+ app_file "app/assets/javascripts/foo/application.js", "alert();"
+ app_file "app/controllers/users_controller.rb", <<-eoruby
+ class UsersController < ApplicationController; end
+ eoruby
+ app_file "app/models/user.rb", <<-eoruby
+ class User < ActiveRecord::Base; raise 'should not be reached'; end
+ eoruby
+
+ ENV['RAILS_ENV'] = 'production'
+ ENV['DATABASE_URL'] = 'postgresql://baduser:badpass@127.0.0.1/dbname'
+
+ precompile!
+
+ files = Dir["#{app_path}/public/assets/application-*.js"]
+ files << Dir["#{app_path}/public/assets/foo/application-*.js"].first
+ files.each do |file|
+ assert_not_nil file, "Expected application.js asset to be generated, but none found"
+ assert_equal "alert();".strip, File.read(file).strip
+ end
+ ensure
+ ENV.delete 'RAILS_ENV'
+ ENV.delete 'DATABASE_URL'
+ end
+
test "precompile application.js and application.css and all other non JS/CSS files" do
app_file "app/assets/javascripts/application.js", "alert();"
app_file "app/assets/stylesheets/application.css", "body{}"
@@ -108,21 +134,26 @@ module ApplicationTests
precompile!
+ images_should_compile = ["a-*.png", "happyface-*.png", "happy_face-*.png", "happy.face-*.png",
+ "happy-face-*.png", "happy.happy_face-*.png", "happy_happy.face-*.png",
+ "happy.happy.face-*.png", "-happy-*.png", "-happy.face-*.png",
+ "_happy.face-*.png", "_happy-*.png"]
+
images_should_compile.each do |filename|
assert_file_exists("#{app_path}/public/assets/#{filename}")
end
- assert_file_exists("#{app_path}/public/assets/application.js")
- assert_file_exists("#{app_path}/public/assets/application.css")
+ assert_file_exists("#{app_path}/public/assets/application-*.js")
+ assert_file_exists("#{app_path}/public/assets/application-*.css")
- assert_no_file_exists("#{app_path}/public/assets/someapplication.js")
- assert_no_file_exists("#{app_path}/public/assets/someapplication.css")
+ assert_no_file_exists("#{app_path}/public/assets/someapplication-*.js")
+ assert_no_file_exists("#{app_path}/public/assets/someapplication-*.css")
- assert_no_file_exists("#{app_path}/public/assets/something.min.js")
- assert_no_file_exists("#{app_path}/public/assets/something.min.css")
+ assert_no_file_exists("#{app_path}/public/assets/something.min-*.js")
+ assert_no_file_exists("#{app_path}/public/assets/something.min-*.css")
- assert_no_file_exists("#{app_path}/public/assets/something.else.js")
- assert_no_file_exists("#{app_path}/public/assets/something.else.css")
+ assert_no_file_exists("#{app_path}/public/assets/something.else-*.js")
+ assert_no_file_exists("#{app_path}/public/assets/something.else-*.css")
end
test "precompile something.js for directory containing index file" do
@@ -131,7 +162,30 @@ module ApplicationTests
precompile!
- assert_file_exists("#{app_path}/public/assets/something.js")
+ assert_file_exists("#{app_path}/public/assets/something-*.js")
+ end
+
+ test 'precompile use assets defined in app env config' do
+ add_to_env_config 'production', 'config.assets.precompile = [ "something.js" ]'
+
+ app_file 'app/assets/javascripts/something.js.erb', 'alert();'
+
+ precompile! 'RAILS_ENV=production'
+
+ assert_file_exists("#{app_path}/public/assets/something-*.js")
+ end
+
+ test 'precompile use assets defined in app config and reassigned in app env config' do
+ add_to_config 'config.assets.precompile = [ "something.js" ]'
+ add_to_env_config 'production', 'config.assets.precompile += [ "another.js" ]'
+
+ app_file 'app/assets/javascripts/something.js.erb', 'alert();'
+ app_file 'app/assets/javascripts/another.js.erb', 'alert();'
+
+ precompile! 'RAILS_ENV=production'
+
+ assert_file_exists("#{app_path}/public/assets/something-*.js")
+ assert_file_exists("#{app_path}/public/assets/another-*.js")
end
test "asset pipeline should use a Sprockets::Index when config.assets.digest is true" do
@@ -151,11 +205,11 @@ module ApplicationTests
add_to_config "config.assets.digest = true"
precompile!
- manifest = "#{app_path}/public/assets/manifest.yml"
+ manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
- assets = YAML.load_file(manifest)
- assert_match(/application-([0-z]+)\.js/, assets["application.js"])
- assert_match(/application-([0-z]+)\.css/, assets["application.css"])
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
+ assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"])
+ assert_match(/application-([0-z]+)\.css/, assets["assets"]["application.css"])
end
test "the manifest file should be saved by default in the same assets folder" do
@@ -166,26 +220,9 @@ module ApplicationTests
precompile!
- manifest = "#{app_path}/public/x/manifest.yml"
- assets = YAML.load_file(manifest)
- assert_match(/application-([0-z]+)\.js/, assets["application.js"])
- end
-
- test "precompile does not append asset digests when config.assets.digest is false" do
- app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
- app_file "app/assets/javascripts/application.js", "alert();"
- add_to_config "config.assets.digest = false"
-
- precompile!
-
- assert_file_exists("#{app_path}/public/assets/application.js")
- assert_file_exists("#{app_path}/public/assets/application.css")
-
- manifest = "#{app_path}/public/assets/manifest.yml"
-
- assets = YAML.load_file(manifest)
- assert_equal "application.js", assets["application.js"]
- assert_equal "application.css", assets["application.css"]
+ manifest = Dir["#{app_path}/public/x/manifest-*.json"].first
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
+ assert_match(/application-([0-z]+)\.js/, assets["assets"]["application.js"])
end
test "assets do not require any assets group gem when manifest file is present" do
@@ -195,9 +232,9 @@ module ApplicationTests
ENV["RAILS_ENV"] = "production"
precompile!
- manifest = "#{app_path}/public/assets/manifest.yml"
- assets = YAML.load_file(manifest)
- asset_path = assets["application.js"]
+ manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
+ asset_path = assets["assets"]["application.js"]
require "#{app_path}/config/environment"
@@ -208,92 +245,43 @@ module ApplicationTests
assert !defined?(Uglifier)
end
- test "assets raise AssetNotPrecompiledError when manifest file is present and requested file isn't precompiled" do
- app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'app' %>"
-
- app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
- get '/posts', :to => "posts#index"
- end
- RUBY
-
- ENV["RAILS_ENV"] = "production"
- precompile!
-
- # Create file after of precompile
- app_file "app/assets/javascripts/app.js", "alert();"
-
- require "#{app_path}/config/environment"
- class ::PostsController < ActionController::Base
- def show_detailed_exceptions?() true end
- end
-
- get '/posts'
- assert_match(/AssetNotPrecompiledError/, last_response.body)
- assert_match(/app\.js isn&#39;t precompiled/, last_response.body)
- end
-
- test "assets raise AssetNotPrecompiledError when manifest file is present and requested file isn't precompiled if digest is disabled" do
- app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'app' %>"
- add_to_config "config.assets.compile = false"
- add_to_config "config.assets.digest = false"
-
- app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
- get '/posts', :to => "posts#index"
- end
- RUBY
-
- ENV["RAILS_ENV"] = "production"
- precompile!
-
- # Create file after of precompile
- app_file "app/assets/javascripts/app.js", "alert();"
-
- require "#{app_path}/config/environment"
- class ::PostsController < ActionController::Base
- def show_detailed_exceptions?() true end
- end
-
- get '/posts'
- assert_match(/AssetNotPrecompiledError/, last_response.body)
- assert_match(/app\.js isn&#39;t precompiled/, last_response.body)
- end
-
- test "precompile properly refers files referenced with asset_path and and run in the provided RAILS_ENV" do
+ 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')
- file = Dir["#{app_path}/public/assets/application.css"].first
- assert_match(/\/assets\/rails\.png/, File.read(file))
file = Dir["#{app_path}/public/assets/application-*.css"].first
assert_match(/\/assets\/rails-([0-z]+)\.png/, File.read(file))
end
- test "precompile shouldn't use the digests present in manifest.yml" do
- app_file "app/assets/stylesheets/application.css.erb", "<%= asset_path('rails.png') %>"
+ 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') %> }"
ENV["RAILS_ENV"] = "production"
precompile!
- manifest = "#{app_path}/public/assets/manifest.yml"
- assets = YAML.load_file(manifest)
- asset_path = assets["application.css"]
+ manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
+ asset_path = assets["assets"]["application.css"]
- app_file "app/assets/images/rails.png", "image changed"
+ app_file "app/assets/images/rails.png", "p { url: change }"
precompile!
- assets = YAML.load_file(manifest)
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
- assert_not_equal asset_path, assets["application.css"]
+ assert_not_equal asset_path, assets["assets"]["application.css"]
end
- test "precompile appends the md5 hash to files referenced with asset_path and run in production as default even using RAILS_GROUPS=assets" do
+ 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
@@ -305,15 +293,20 @@ module ApplicationTests
test "precompile should handle utf8 filenames" do
filename = "レイルズ.png"
- app_file "app/assets/images/#{filename}", "not a image really"
+ app_file "app/assets/images/#{filename}", "not an image really"
add_to_config "config.assets.precompile = [ /\.png$/, /application.(css|js)$/ ]"
precompile!
+
+ manifest = Dir["#{app_path}/public/assets/manifest-*.json"].first
+ assets = ActiveSupport::JSON.decode(File.read(manifest))
+ assert asset_path = assets["assets"].find { |(k, _)| k && k =~ /.png/ }[1]
+
require "#{app_path}/config/environment"
- get "/assets/#{URI.parser.escape(filename)}"
- assert_match "not a image really", last_response.body
- assert_file_exists("#{app_path}/public/assets/#{filename}")
+ get "/assets/#{URI.parser.escape(asset_path)}"
+ assert_match "not an image really", last_response.body
+ assert_file_exists("#{app_path}/public/assets/#{asset_path}")
end
test "assets are cleaned up properly" do
@@ -343,7 +336,7 @@ module ApplicationTests
app_file "app/assets/javascripts/demo.js.erb", "<%= :alert %>();"
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/omg', :to => "omg#index"
end
RUBY
@@ -403,19 +396,7 @@ module ApplicationTests
add_to_config "config.assets.digest = false"
precompile!
- assert_equal "Post;\n", File.read("#{app_path}/public/assets/application.js")
- end
-
- test "assets can't access model information when precompiling if not initializing the app" do
- app_file "app/models/post.rb", "class Post; end"
- app_file "app/assets/javascripts/application.js", "//= require_tree ."
- app_file "app/assets/javascripts/xmlhr.js.erb", "<%= defined?(Post) || :NoPost %>"
-
- add_to_config "config.assets.digest = false"
- add_to_config "config.assets.initialize_on_precompile = false"
-
- precompile!
- assert_equal "NoPost;\n", File.read("#{app_path}/public/assets/application.js")
+ assert_equal "Post;\n", File.read(Dir["#{app_path}/public/assets/application-*.js"].first)
end
test "initialization on the assets group should set assets_dir" do
@@ -449,7 +430,7 @@ module ApplicationTests
clean_assets!
- files = Dir["#{app_path}/public/production_assets/application.js"]
+ files = Dir["#{app_path}/public/production_assets/application-*.js"]
assert_equal 0, files.length, "Expected application.js asset to be removed, but still exists"
end
@@ -466,34 +447,24 @@ module ApplicationTests
end
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}"
add_to_config "config.asset_host = 'example.com'"
precompile!
- assert_match 'src="//example.com/assets/rails.png"', File.read("#{app_path}/public/assets/image_loader.js")
+ 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}"
precompile!
- assert_match 'src="/sub/uri/assets/rails.png"', File.read("#{app_path}/public/assets/app.js")
- end
-
- test "html assets are compiled when executing precompile" do
- app_file "app/assets/pages/page.html.erb", "<%= javascript_include_tag :application %>"
- ENV["RAILS_ENV"] = "production"
- ENV["RAILS_GROUP"] = "assets"
-
- quietly do
- Dir.chdir(app_path){ `bundle exec rake assets:precompile` }
- end
-
- assert_file_exists("#{app_path}/public/assets/page.html")
+ 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
@@ -501,11 +472,10 @@ module ApplicationTests
precompile!
quietly do
- Dir.chdir(app_path){ `bundle exec rake assets:cache:clean` }
+ Dir.chdir(app_path){ `bundle exec rake assets:clobber` }
end
- require "#{app_path}/config/environment"
- assert_equal 0, Dir.entries(Rails.application.assets.cache.cache_path).size - 2 # reject [".", ".."]
+ assert !File.exist?("#{app_path}/tmp/cache/assets")
end
private
@@ -516,7 +486,7 @@ module ApplicationTests
app_file "app/views/posts/index.html.erb", "<%= javascript_include_tag 'application' %>"
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/posts', :to => "posts#index"
end
RUBY
diff --git a/railties/test/application/configuration_test.rb b/railties/test/application/configuration_test.rb
index 7a120410ba..03a735b1c1 100644
--- a/railties/test/application/configuration_test.rb
+++ b/railties/test/application/configuration_test.rb
@@ -1,5 +1,6 @@
require "isolation/abstract_unit"
require 'rack/test'
+require 'env_helpers'
class ::MyMailInterceptor
def self.delivering_email(email); email; end
@@ -17,6 +18,7 @@ module ApplicationTests
class ConfigurationTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
include Rack::Test::Methods
+ include EnvHelpers
def new_app
File.expand_path("#{app_path}/../new_app")
@@ -41,6 +43,16 @@ module ApplicationTests
FileUtils.rm_rf(new_app) if File.directory?(new_app)
end
+ test "Rails.env does not set the RAILS_ENV environment variable which would leak out into rake tasks" do
+ require "rails"
+
+ switch_env "RAILS_ENV", nil do
+ Rails.env = "development"
+ assert_equal "development", Rails.env
+ assert_nil ENV['RAILS_ENV']
+ end
+ end
+
test "a renders exception on pending migration" do
add_to_config <<-RUBY
config.active_record.migration_error = :page_load
@@ -50,6 +62,7 @@ module ApplicationTests
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
@@ -146,17 +159,12 @@ module ApplicationTests
RUBY
require "#{app_path}/config/application"
- assert AppTemplate::Application.initialize!
+ assert Rails.application.initialize!
end
test "application is always added to eager_load namespaces" do
require "#{app_path}/config/application"
- assert AppTemplate::Application, AppTemplate::Application.config.eager_load_namespaces
- end
-
- test "asset_path defaults to nil for application" do
- require "#{app_path}/config/environment"
- assert_equal nil, AppTemplate::Application.config.asset_path
+ assert Rails.application, Rails.application.config.eager_load_namespaces
end
test "the application can be eager loaded even when there are no frameworks" do
@@ -185,6 +193,16 @@ module ApplicationTests
end
end
+ test "filter_parameters should be able to set via config.filter_parameters in an initializer" do
+ app_file 'config/initializers/filter_parameters_logging.rb', <<-RUBY
+ Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ]
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ assert_equal [:password, :foo, 'bar'], Rails.application.env_config['action_dispatch.parameter_filter']
+ end
+
test "config.to_prepare is forwarded to ActionDispatch" do
$prepared = false
@@ -230,21 +248,24 @@ module ApplicationTests
assert_equal Pathname.new(app_path).join("somewhere"), Rails.public_path
end
- test "config.secret_token is sent in env" do
+ test "Use key_generator when secret_key_base is set" do
make_basic_app do |app|
- app.config.secret_token = 'b3c631c314c0bbca50c1b2843150fe33'
+ app.config.secret_key_base = 'b3c631c314c0bbca50c1b2843150fe33'
app.config.session_store :disabled
end
class ::OmgController < ActionController::Base
def index
cookies.signed[:some_key] = "some_value"
- render text: env["action_dispatch.secret_token"]
+ render text: cookies[:some_key]
end
end
get "/"
- assert_equal 'b3c631c314c0bbca50c1b2843150fe33', last_response.body
+
+ secret = app.key_generator.generate_key('signed cookie')
+ verifier = ActiveSupport::MessageVerifier.new(secret)
+ assert_equal 'some_value', verifier.verify(last_response.body)
end
test "protect from forgery is the default in a new app" do
@@ -433,21 +454,6 @@ module ApplicationTests
end
end
- test "config.asset_path is not passed through env" do
- make_basic_app do |app|
- app.config.asset_path = "/omg%s"
- end
-
- class ::OmgController < ActionController::Base
- def index
- render inline: "<%= image_path('foo.jpg') %>"
- end
- end
-
- get "/"
- assert_equal "/omg/images/foo.jpg", last_response.body
- end
-
test "config.action_view.cache_template_loading with cache_classes default" do
add_to_config "config.cache_classes = true"
require "#{app_path}/config/environment"
@@ -562,6 +568,54 @@ module ApplicationTests
assert_equal 'permitted', last_response.body
end
+ test "config.action_controller.action_on_unpermitted_parameters = :raise" do
+ app_file 'app/controllers/posts_controller.rb', <<-RUBY
+ class PostsController < ActionController::Base
+ def create
+ render text: params.require(:post).permit(:name)
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ routes.prepend do
+ resources :posts
+ end
+ 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"}}
+ assert_match "We're sorry, but something went wrong", last_response.body
+ end
+
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default on development" do
+ ENV["RAILS_ENV"] = "development"
+
+ require "#{app_path}/config/environment"
+
+ assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
+ end
+
+ test "config.action_controller.action_on_unpermitted_parameters is :log by default on test" do
+ ENV["RAILS_ENV"] = "test"
+
+ require "#{app_path}/config/environment"
+
+ assert_equal :log, ActionController::Parameters.action_on_unpermitted_parameters
+ end
+
+ test "config.action_controller.action_on_unpermitted_parameters is false by default on production" do
+ ENV["RAILS_ENV"] = "production"
+
+ require "#{app_path}/config/environment"
+
+ assert_equal false, ActionController::Parameters.action_on_unpermitted_parameters
+ end
+
test "config.action_dispatch.ignore_accept_header" do
make_basic_app do |app|
app.config.action_dispatch.ignore_accept_header = true
@@ -588,7 +642,6 @@ module ApplicationTests
assert_respond_to app, :env_config
assert_equal app.env_config['action_dispatch.parameter_filter'], app.config.filter_parameters
- assert_equal app.env_config['action_dispatch.secret_token'], app.config.secret_token
assert_equal app.env_config['action_dispatch.show_exceptions'], app.config.action_dispatch.show_exceptions
assert_equal app.env_config['action_dispatch.logger'], Rails.logger
assert_equal app.env_config['action_dispatch.backtrace_cleaner'], Rails.backtrace_cleaner
@@ -600,27 +653,6 @@ module ApplicationTests
assert app.config.colorize_logging
end
- test "config.active_record.observers" do
- add_to_config <<-RUBY
- config.active_record.observers = :foo_observer
- RUBY
-
- app_file 'app/models/foo.rb', <<-RUBY
- class Foo < ActiveRecord::Base
- end
- RUBY
-
- app_file 'app/models/foo_observer.rb', <<-RUBY
- class FooObserver < ActiveRecord::Observer
- end
- RUBY
-
- require "#{app_path}/config/environment"
-
- ActiveRecord::Base
- assert defined?(FooObserver)
- end
-
test "config.session_store with :active_record_store with activerecord-session_store gem" do
begin
make_basic_app do |app|
@@ -639,5 +671,20 @@ module ApplicationTests
end
end
end
+
+ test "config.log_level with custom logger" do
+ make_basic_app do |app|
+ app.config.logger = Logger.new(STDOUT)
+ app.config.log_level = :info
+ end
+ assert_equal Logger::INFO, Rails.logger.level
+ end
+
+ test "respond_to? accepts include_private" do
+ make_basic_app
+
+ assert_not Rails.configuration.respond_to?(:method_missing)
+ assert Rails.configuration.respond_to?(:method_missing, true)
+ end
end
end
diff --git a/railties/test/application/console_test.rb b/railties/test/application/console_test.rb
index f372afa51c..31bc003dcb 100644
--- a/railties/test/application/console_test.rb
+++ b/railties/test/application/console_test.rb
@@ -81,35 +81,73 @@ class ConsoleTest < ActiveSupport::TestCase
assert_equal 'Once upon a time in a world...',
helper.truncate('Once upon a time in a world far far away')
end
+end
+
+begin
+ require "pty"
+rescue LoadError
+end
- def test_with_sandbox
- require 'rails/all'
- value = false
+class FullStackConsoleTest < ActiveSupport::TestCase
+ def setup
+ skip "PTY unavailable" unless defined?(PTY) && PTY.respond_to?(:open)
- Class.new(Rails::Railtie) do
- console do |app|
- value = app.sandbox?
+ build_app
+ app_file 'app/models/post.rb', <<-CODE
+ class Post < ActiveRecord::Base
end
- end
+ CODE
+ system "#{app_path}/bin/rails runner 'Post.connection.create_table :posts'"
- load_environment(true)
- assert value
+ @master, @slave = PTY.open
end
- def test_active_record_does_not_panic_when_referencing_an_observed_constant
- add_to_config "config.active_record.observers = :user_observer"
+ def teardown
+ teardown_app
+ end
- app_file "app/models/user.rb", <<-MODEL
- class User < ActiveRecord::Base
- end
- MODEL
+ def assert_output(expected, timeout = 1)
+ timeout = Time.now + timeout
- app_file "app/models/user_observer.rb", <<-MODEL
- class UserObserver < ActiveRecord::Observer
+ output = ""
+ until output.include?(expected) || Time.now > timeout
+ if IO.select([@master], [], [], 0.1)
+ output << @master.read(1)
end
- MODEL
+ end
- load_environment
- assert_nothing_raised { User }
+ assert output.include?(expected), "#{expected.inspect} expected, but got:\n\n#{output}"
+ end
+
+ def write_prompt(command, expected_output = nil)
+ @master.puts command
+ assert_output command
+ assert_output expected_output if expected_output
+ assert_output "> "
+ end
+
+ def spawn_console
+ Process.spawn(
+ "#{app_path}/bin/rails console --sandbox",
+ in: @slave, out: @slave, err: @slave
+ )
+
+ assert_output "> ", 30
+ end
+
+ def test_sandbox
+ spawn_console
+
+ write_prompt "Post.count", "=> 0"
+ write_prompt "Post.create"
+ write_prompt "Post.count", "=> 1"
+ @master.puts "quit"
+
+ spawn_console
+
+ write_prompt "Post.count", "=> 0"
+ write_prompt "Post.transaction { Post.create; raise }"
+ write_prompt "Post.count", "=> 0"
+ @master.puts "quit"
end
end
diff --git a/railties/test/application/generators_test.rb b/railties/test/application/generators_test.rb
index bc0af499c1..78ada58ec8 100644
--- a/railties/test/application/generators_test.rb
+++ b/railties/test/application/generators_test.rb
@@ -30,7 +30,7 @@ module ApplicationTests
end
test "allow running plugin new generator inside Rails app directory" do
- FileUtils.cd(rails_root){ `ruby script/rails plugin new vendor/plugins/bukkits` }
+ FileUtils.cd(rails_root){ `ruby bin/rails plugin new vendor/plugins/bukkits` }
assert File.exist?(File.join(rails_root, "vendor/plugins/bukkits/test/dummy/config/application.rb"))
end
diff --git a/railties/test/application/initializers/boot_test.rb b/railties/test/application/initializers/boot_test.rb
deleted file mode 100644
index 7eda15894e..0000000000
--- a/railties/test/application/initializers/boot_test.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-require "isolation/abstract_unit"
-
-module ApplicationTests
- class BootTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
-
- def setup
- # build_app
- # boot_rails
- end
-
- def teardown
- # teardown_app
- end
-
- test "booting rails sets the load paths correctly" do
- # This test is pending reworking the boot process
- end
- end
-end
diff --git a/railties/test/application/initializers/frameworks_test.rb b/railties/test/application/initializers/frameworks_test.rb
index 528bec58ef..83104acf3c 100644
--- a/railties/test/application/initializers/frameworks_test.rb
+++ b/railties/test/application/initializers/frameworks_test.rb
@@ -41,7 +41,7 @@ module ApplicationTests
test "allows me to configure default url options for ActionMailer" do
app_file "config/environments/development.rb", <<-RUBY
- AppTemplate::Application.configure do
+ Rails.application.configure do
config.action_mailer.default_url_options = { :host => "test.rails" }
end
RUBY
@@ -50,25 +50,9 @@ module ApplicationTests
assert_equal "test.rails", ActionMailer::Base.default_url_options[:host]
end
- test "uses the default queue for ActionMailer" do
- require "#{app_path}/config/environment"
- assert_kind_of ActiveSupport::Queue, ActionMailer::Base.queue
- end
-
- test "allows me to configure queue for ActionMailer" do
- app_file "config/environments/development.rb", <<-RUBY
- AppTemplate::Application.configure do
- config.action_mailer.queue = ActiveSupport::TestQueue.new
- end
- RUBY
-
- require "#{app_path}/config/environment"
- assert_kind_of ActiveSupport::TestQueue, ActionMailer::Base.queue
- end
-
test "does not include url helpers as action methods" do
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get "/foo", :to => lambda { |env| [200, {}, []] }, :as => :foo
end
RUBY
@@ -131,7 +115,7 @@ module ApplicationTests
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get "/:controller(/:action)"
end
RUBY
@@ -198,7 +182,7 @@ module ApplicationTests
end
require "#{app_path}/config/environment"
ActiveRecord::Base.connection.drop_table("posts") # force drop posts table for test.
- assert ActiveRecord::Base.connection.schema_cache.tables["posts"]
+ assert ActiveRecord::Base.connection.schema_cache.tables("posts")
end
test "expire schema cache dump" do
@@ -208,7 +192,7 @@ module ApplicationTests
end
silence_warnings {
require "#{app_path}/config/environment"
- assert !ActiveRecord::Base.connection.schema_cache.tables["posts"]
+ assert !ActiveRecord::Base.connection.schema_cache.tables("posts")
}
end
@@ -219,7 +203,7 @@ module ApplicationTests
orig_rails_env, Rails.env = Rails.env, 'development'
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
- assert_match /#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database]
+ assert_match(/#{ActiveRecord::Base.configurations[Rails.env]['database']}/, ActiveRecord::Base.connection_config[:database])
ensure
ActiveRecord::Base.remove_connection
ENV["DATABASE_URL"] = orig_database_url if orig_database_url
@@ -236,7 +220,7 @@ module ApplicationTests
ENV["DATABASE_URL"] = "sqlite3://:@localhost/#{database_url_db_name}"
ActiveRecord::Base.establish_connection
assert ActiveRecord::Base.connection
- assert_match /#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database]
+ assert_match(/#{database_url_db_name}/, ActiveRecord::Base.connection_config[:database])
ensure
ActiveRecord::Base.remove_connection
ENV["DATABASE_URL"] = orig_database_url if orig_database_url
diff --git a/railties/test/application/initializers/i18n_test.rb b/railties/test/application/initializers/i18n_test.rb
index 489b7ddb92..97df073ec7 100644
--- a/railties/test/application/initializers/i18n_test.rb
+++ b/railties/test/application/initializers/i18n_test.rb
@@ -45,7 +45,7 @@ module ApplicationTests
end
# Load paths
- test "no config locales dir present should return empty load path" do
+ test "no config locales directory present should return empty load path" do
FileUtils.rm_rf "#{app_path}/config/locales"
load_app
assert_equal [], Rails.application.config.i18n.load_path
@@ -84,7 +84,7 @@ en:
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/i18n', :to => lambda { |env| [200, {}, [Foo.instance_variable_get('@foo')]] }
end
RUBY
@@ -108,7 +108,7 @@ en:
YAML
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/i18n', :to => lambda { |env| [200, {}, [I18n.t(:foo)]] }
end
RUBY
diff --git a/railties/test/application/initializers/load_path_test.rb b/railties/test/application/initializers/load_path_test.rb
index 31811e7f92..b36628ee37 100644
--- a/railties/test/application/initializers/load_path_test.rb
+++ b/railties/test/application/initializers/load_path_test.rb
@@ -23,7 +23,7 @@ module ApplicationTests
assert $:.include?("#{app_path}/app/models")
end
- test "initializing an application allows to load code on lib path inside application class definitation" do
+ test "initializing an application allows to load code on lib path inside application class definition" do
app_file "lib/foo.rb", <<-RUBY
module Foo; end
RUBY
@@ -72,9 +72,10 @@ module ApplicationTests
end
test "load environment with global" do
+ $initialize_test_set_from_env = nil
app_file "config/environments/development.rb", <<-RUBY
$initialize_test_set_from_env = 'success'
- AppTemplate::Application.configure do
+ Rails.application.configure do
config.cache_classes = true
config.time_zone = "Brasilia"
end
@@ -88,8 +89,8 @@ module ApplicationTests
require "#{app_path}/config/environment"
assert_equal "success", $initialize_test_set_from_env
- assert AppTemplate::Application.config.cache_classes
- assert_equal "Brasilia", AppTemplate::Application.config.time_zone
+ assert Rails.application.config.cache_classes
+ assert_equal "Brasilia", Rails.application.config.time_zone
end
end
end
diff --git a/railties/test/application/loading_test.rb b/railties/test/application/loading_test.rb
index ad7172c514..4f30f30f95 100644
--- a/railties/test/application/loading_test.rb
+++ b/railties/test/application/loading_test.rb
@@ -36,7 +36,7 @@ class LoadingTest < ActiveSupport::TestCase
test "models without table do not panic on scope definitions when loaded" do
app_file "app/models/user.rb", <<-MODEL
class User < ActiveRecord::Base
- default_scope where(published: true)
+ default_scope { where(published: true) }
end
MODEL
@@ -48,7 +48,7 @@ class LoadingTest < ActiveSupport::TestCase
test "load config/environments/environment before Bootstrap initializers" do
app_file "config/environments/development.rb", <<-RUBY
- AppTemplate::Application.configure do
+ Rails.application.configure do
config.development_environment_loaded = true
end
RUBY
@@ -60,7 +60,7 @@ class LoadingTest < ActiveSupport::TestCase
RUBY
require "#{app_path}/config/environment"
- assert ::AppTemplate::Application.config.loaded
+ assert ::Rails.application.config.loaded
end
test "descendants loaded after framework initialization are cleaned on each request without cache classes" do
@@ -75,7 +75,7 @@ class LoadingTest < ActiveSupport::TestCase
MODEL
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/load', to: lambda { |env| [200, {}, Post.all] }
get '/unload', to: lambda { |env| [200, {}, []] }
end
@@ -96,7 +96,7 @@ class LoadingTest < ActiveSupport::TestCase
test "initialize cant be called twice" do
require "#{app_path}/config/environment"
- assert_raise(RuntimeError) { ::AppTemplate::Application.initialize! }
+ assert_raise(RuntimeError) { Rails.application.initialize! }
end
test "reload constants on development" do
@@ -105,7 +105,7 @@ class LoadingTest < ActiveSupport::TestCase
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] }
end
RUBY
@@ -144,7 +144,7 @@ class LoadingTest < ActiveSupport::TestCase
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/c', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [User.counter.to_s]] }
end
RUBY
@@ -179,8 +179,8 @@ class LoadingTest < ActiveSupport::TestCase
RUBY
app_file 'config/routes.rb', <<-RUBY
- $counter = 0
- AppTemplate::Application.routes.draw do
+ $counter ||= 0
+ Rails.application.routes.draw do
get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] }
end
RUBY
@@ -205,13 +205,46 @@ class LoadingTest < ActiveSupport::TestCase
assert_equal "2", last_response.body
end
+ test "dependencies reloading is followed by routes reloading" do
+ add_to_config <<-RUBY
+ config.cache_classes = false
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ $counter ||= 1
+ $counter *= 2
+ Rails.application.routes.draw do
+ get '/c', to: lambda { |env| User; [200, {"Content-Type" => "text/plain"}, [$counter.to_s]] }
+ end
+ RUBY
+
+ app_file "app/models/user.rb", <<-MODEL
+ class User
+ $counter += 1
+ end
+ MODEL
+
+ require 'rack/test'
+ extend Rack::Test::Methods
+
+ require "#{rails_root}/config/environment"
+
+ get "/c"
+ assert_equal "3", last_response.body
+
+ app_file "db/schema.rb", ""
+
+ get "/c"
+ assert_equal "7", last_response.body
+ end
+
test "columns migrations also trigger reloading" do
add_to_config <<-RUBY
config.cache_classes = false
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/title', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.title]] }
get '/body', to: lambda { |env| [200, {"Content-Type" => "text/plain"}, [Post.new.body]] }
end
@@ -270,7 +303,7 @@ class LoadingTest < ActiveSupport::TestCase
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get "/:controller(/:action)"
end
RUBY
@@ -288,10 +321,10 @@ class LoadingTest < ActiveSupport::TestCase
require "#{app_path}/config/application"
assert !Rails.initialized?
- assert !AppTemplate::Application.initialized?
+ assert !Rails.application.initialized?
Rails.initialize!
assert Rails.initialized?
- assert AppTemplate::Application.initialized?
+ assert Rails.application.initialized?
end
protected
diff --git a/railties/test/application/middleware/best_practices_test.rb b/railties/test/application/middleware/best_practices_test.rb
deleted file mode 100644
index f6783c6ca2..0000000000
--- a/railties/test/application/middleware/best_practices_test.rb
+++ /dev/null
@@ -1,30 +0,0 @@
-require 'isolation/abstract_unit'
-
-module ApplicationTests
- class BestPracticesTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
-
- def setup
- build_app
- boot_rails
- require 'rack/test'
- extend Rack::Test::Methods
- simple_controller
- end
-
- def teardown
- teardown_app
- end
-
- test "simple controller in production mode returns best standards" do
- get '/foo'
- assert_equal "IE=Edge,chrome=1", last_response.headers["X-UA-Compatible"]
- end
-
- test "simple controller in development mode leaves out Chrome" do
- app("development")
- get "/foo"
- assert_equal "IE=Edge", last_response.headers["X-UA-Compatible"]
- end
- end
-end
diff --git a/railties/test/application/middleware/cache_test.rb b/railties/test/application/middleware/cache_test.rb
index 9b6c76e7aa..b4db840e68 100644
--- a/railties/test/application/middleware/cache_test.rb
+++ b/railties/test/application/middleware/cache_test.rb
@@ -45,12 +45,10 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
-
- add_to_config "config.action_dispatch.rack_cache = true"
end
def test_cache_keeps_if_modified_since
@@ -80,9 +78,11 @@ module ApplicationTests
def test_cache_works_with_expires
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_header"
- assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
- assert_equal "max-age=10, public", last_response.headers["Cache-Control"]
+ assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"]
+ assert_equal "max-age=10, public", last_response.headers["Cache-Control"]
body = last_response.body
@@ -96,6 +96,8 @@ module ApplicationTests
def test_cache_works_with_expires_private
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_header", private: true
assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_equal "private, max-age=10", last_response.headers["Cache-Control"]
@@ -110,9 +112,11 @@ module ApplicationTests
def test_cache_works_with_etags
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_etag"
- assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
- assert_equal "public", last_response.headers["Cache-Control"]
+ assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"]
+ assert_equal "public", last_response.headers["Cache-Control"]
body = last_response.body
etag = last_response.headers["ETag"]
@@ -125,6 +129,8 @@ module ApplicationTests
def test_cache_works_with_etags_private
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_etag", private: true
assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_equal "must-revalidate, private, max-age=0", last_response.headers["Cache-Control"]
@@ -140,9 +146,11 @@ module ApplicationTests
def test_cache_works_with_last_modified
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_last_modified"
- assert_equal "miss, store", last_response.headers["X-Rack-Cache"]
- assert_equal "public", last_response.headers["Cache-Control"]
+ assert_equal "miss, ignore, store", last_response.headers["X-Rack-Cache"]
+ assert_equal "public", last_response.headers["Cache-Control"]
body = last_response.body
last = last_response.headers["Last-Modified"]
@@ -155,6 +163,8 @@ module ApplicationTests
def test_cache_works_with_last_modified_private
simple_controller
+ add_to_config "config.action_dispatch.rack_cache = true"
+
get "/expires/expires_last_modified", private: true
assert_equal "miss", last_response.headers["X-Rack-Cache"]
assert_equal "must-revalidate, private, max-age=0", last_response.headers["Cache-Control"]
diff --git a/railties/test/application/middleware/cookies_test.rb b/railties/test/application/middleware/cookies_test.rb
index 18af7abafc..bbb7627be9 100644
--- a/railties/test/application/middleware/cookies_test.rb
+++ b/railties/test/application/middleware/cookies_test.rb
@@ -33,7 +33,7 @@ module ApplicationTests
assert_equal false, ActionDispatch::Cookies::CookieJar.always_write_cookie
end
- test 'always_write_cookie can be overrided' do
+ test 'always_write_cookie can be overridden' do
add_to_config <<-RUBY
config.action_dispatch.always_write_cookie = false
RUBY
diff --git a/railties/test/application/middleware/remote_ip_test.rb b/railties/test/application/middleware/remote_ip_test.rb
index 9d97bae9ae..946b82eeb3 100644
--- a/railties/test/application/middleware/remote_ip_test.rb
+++ b/railties/test/application/middleware/remote_ip_test.rb
@@ -1,4 +1,5 @@
require 'isolation/abstract_unit'
+require 'active_support/key_generator'
module ApplicationTests
class RemoteIpTest < ActiveSupport::TestCase
@@ -8,7 +9,7 @@ module ApplicationTests
remote_ip = nil
env = Rack::MockRequest.env_for("/").merge(env).merge!(
'action_dispatch.show_exceptions' => false,
- 'action_dispatch.secret_token' => 'b3c631c314c0bbca50c1b2843150fe33'
+ 'action_dispatch.key_generator' => ActiveSupport::LegacyKeyGenerator.new('b3c631c314c0bbca50c1b2843150fe33')
)
endpoint = Proc.new do |e|
@@ -32,13 +33,23 @@ module ApplicationTests
end
end
+ test "works with both headers individually" do
+ make_basic_app
+ assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1")
+ end
+ assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
+ assert_equal "1.1.1.2", remote_ip("HTTP_CLIENT_IP" => "1.1.1.2")
+ end
+ end
+
test "can disable IP spoofing check" do
make_basic_app do |app|
app.config.action_dispatch.ip_spoofing_check = false
end
assert_nothing_raised(ActionDispatch::RemoteIp::IpSpoofAttackError) do
- assert_equal "1.1.1.2", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
+ assert_equal "1.1.1.1", remote_ip("HTTP_X_FORWARDED_FOR" => "1.1.1.1", "HTTP_CLIENT_IP" => "1.1.1.2")
end
end
diff --git a/railties/test/application/middleware/session_test.rb b/railties/test/application/middleware/session_test.rb
index 5ce41caf61..14a56176f5 100644
--- a/railties/test/application/middleware/session_test.rb
+++ b/railties/test/application/middleware/session_test.rb
@@ -49,7 +49,7 @@ module ApplicationTests
test "session is empty and isn't saved on unverified request when using :null_session protect method" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
post ':controller(/:action)'
end
@@ -90,7 +90,7 @@ module ApplicationTests
test "cookie jar is empty and isn't saved on unverified request when using :null_session protect method" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
post ':controller(/:action)'
end
@@ -128,5 +128,216 @@ module ApplicationTests
get '/foo/read_cookie' # Cookie shouldn't be changed
assert_equal '"1"', last_response.body
end
+
+ test "session using encrypted cookie store" do
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def write_session
+ session[:foo] = 1
+ render nothing: true
+ end
+
+ def read_session
+ render text: session[:foo]
+ end
+
+ def read_encrypted_cookie
+ render text: cookies.encrypted[:_myapp_session]['foo']
+ end
+
+ def read_raw_cookie
+ render text: cookies[:_myapp_session]
+ end
+ end
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get '/foo/write_session'
+ get '/foo/read_session'
+ assert_equal '1', last_response.body
+
+ get '/foo/read_encrypted_cookie'
+ assert_equal '1', last_response.body
+
+ secret = app.key_generator.generate_key('encrypted cookie')
+ sign_secret = app.key_generator.generate_key('signed encrypted cookie')
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+
+ get '/foo/read_raw_cookie'
+ assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
+ end
+
+ test "session upgrading signature to encryption cookie store works the same way as encrypted cookie store" do
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def write_session
+ session[:foo] = 1
+ render nothing: true
+ end
+
+ def read_session
+ render text: session[:foo]
+ end
+
+ def read_encrypted_cookie
+ render text: cookies.encrypted[:_myapp_session]['foo']
+ end
+
+ def read_raw_cookie
+ render text: cookies[:_myapp_session]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get '/foo/write_session'
+ get '/foo/read_session'
+ assert_equal '1', last_response.body
+
+ get '/foo/read_encrypted_cookie'
+ assert_equal '1', last_response.body
+
+ secret = app.key_generator.generate_key('encrypted cookie')
+ sign_secret = app.key_generator.generate_key('signed encrypted cookie')
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+
+ get '/foo/read_raw_cookie'
+ assert_equal 1, encryptor.decrypt_and_verify(last_response.body)['foo']
+ end
+
+ test "session upgrading signature to encryption cookie store upgrades session to encrypted mode" do
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def write_raw_session
+ # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1}
+ cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749"
+ render nothing: true
+ end
+
+ def write_session
+ session[:foo] = session[:foo] + 1
+ render nothing: true
+ end
+
+ def read_session
+ render text: session[:foo]
+ end
+
+ def read_encrypted_cookie
+ render text: cookies.encrypted[:_myapp_session]['foo']
+ end
+
+ def read_raw_cookie
+ render text: cookies[:_myapp_session]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get '/foo/write_raw_session'
+ get '/foo/read_session'
+ assert_equal '1', last_response.body
+
+ get '/foo/write_session'
+ get '/foo/read_session'
+ assert_equal '2', last_response.body
+
+ get '/foo/read_encrypted_cookie'
+ assert_equal '2', last_response.body
+
+ secret = app.key_generator.generate_key('encrypted cookie')
+ sign_secret = app.key_generator.generate_key('signed encrypted cookie')
+ encryptor = ActiveSupport::MessageEncryptor.new(secret, sign_secret)
+
+ get '/foo/read_raw_cookie'
+ assert_equal 2, encryptor.decrypt_and_verify(last_response.body)['foo']
+ end
+
+ test "session upgrading legacy signed cookies to new signed cookies" do
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':controller(/:action)'
+ end
+ RUBY
+
+ controller :foo, <<-RUBY
+ class FooController < ActionController::Base
+ def write_raw_session
+ # {"session_id"=>"1965d95720fffc123941bdfb7d2e6870", "foo"=>1}
+ cookies[:_myapp_session] = "BAh7B0kiD3Nlc3Npb25faWQGOgZFRkkiJTE5NjVkOTU3MjBmZmZjMTIzOTQxYmRmYjdkMmU2ODcwBjsAVEkiCGZvbwY7AEZpBg==--315fb9931921a87ae7421aec96382f0294119749"
+ render nothing: true
+ end
+
+ def write_session
+ session[:foo] = session[:foo] + 1
+ render nothing: true
+ end
+
+ def read_session
+ render text: session[:foo]
+ end
+
+ def read_signed_cookie
+ render text: cookies.signed[:_myapp_session]['foo']
+ end
+
+ def read_raw_cookie
+ render text: cookies[:_myapp_session]
+ end
+ end
+ RUBY
+
+ add_to_config <<-RUBY
+ config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ config.secret_key_base = nil
+ RUBY
+
+ require "#{app_path}/config/environment"
+
+ get '/foo/write_raw_session'
+ get '/foo/read_session'
+ assert_equal '1', last_response.body
+
+ get '/foo/write_session'
+ get '/foo/read_session'
+ assert_equal '2', last_response.body
+
+ get '/foo/read_signed_cookie'
+ assert_equal '2', last_response.body
+
+ verifier = ActiveSupport::MessageVerifier.new(app.config.secret_token)
+
+ get '/foo/read_raw_cookie'
+ assert_equal 2, verifier.verify(last_response.body)['foo']
+ end
end
end
diff --git a/railties/test/application/middleware/static_test.rb b/railties/test/application/middleware/static_test.rb
new file mode 100644
index 0000000000..0a793f8f60
--- /dev/null
+++ b/railties/test/application/middleware/static_test.rb
@@ -0,0 +1,31 @@
+# encoding: utf-8
+require 'isolation/abstract_unit'
+require 'rack/test'
+
+module ApplicationTests
+ class MiddlewareStaticTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ FileUtils.rm_rf "#{app_path}/config/environments"
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ # Regression test to #8907
+ # See https://github.com/rails/rails/commit/9cc82b77196d21a5c7021f6dca59ab9b2b158a45#commitcomment-2416514
+ test "doesn't set Cache-Control header when it is nil" do
+ app_file "public/foo.html", 'static'
+
+ require "#{app_path}/config/environment"
+
+ get 'foo'
+
+ assert_not last_response.headers.has_key?('Cache-Control'), "Cache-Control should not be set"
+ end
+ end
+end
diff --git a/railties/test/application/middleware_test.rb b/railties/test/application/middleware_test.rb
index c03d35e926..20d1d76d78 100644
--- a/railties/test/application/middleware_test.rb
+++ b/railties/test/application/middleware_test.rb
@@ -19,7 +19,7 @@ module ApplicationTests
end
test "default middleware stack" do
- add_to_config "config.action_dispatch.x_sendfile_header = 'X-Sendfile'"
+ add_to_config "config.active_record.migration_error = :page_load"
boot!
@@ -37,6 +37,7 @@ module ApplicationTests
"ActionDispatch::RemoteIp",
"ActionDispatch::Reloader",
"ActionDispatch::Callbacks",
+ "ActiveRecord::Migration::CheckPending",
"ActiveRecord::ConnectionAdapters::ConnectionManagement",
"ActiveRecord::QueryCache",
"ActionDispatch::Cookies",
@@ -45,32 +46,30 @@ module ApplicationTests
"ActionDispatch::ParamsParser",
"Rack::Head",
"Rack::ConditionalGet",
- "Rack::ETag",
- "ActionDispatch::BestStandardsSupport"
+ "Rack::ETag"
], middleware
end
- test "Rack::Sendfile is not included by default" do
+ test "Rack::Cache is not included by default" do
boot!
- assert !middleware.include?("Rack::Sendfile"), "Rack::Sendfile is not included in the default stack unless you set config.action_dispatch.x_sendfile_header"
+ assert !middleware.include?("Rack::Cache"), "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache"
end
- test "Rack::Cache is not included by default" do
- add_to_config "config.action_controller.perform_caching = true"
+ test "Rack::Cache is present when action_dispatch.rack_cache is set" do
+ add_to_config "config.action_dispatch.rack_cache = true"
boot!
- assert !middleware.include?("Rack::Cache"), "Rack::Cache is not included in the default stack unless you set config.action_dispatch.rack_cache"
+ assert_equal "Rack::Cache", middleware.first
end
- test "Rack::Cache is present when action_controller.perform_caching is set and action_dispatch.rack_cache is set" do
- add_to_config "config.action_controller.perform_caching = true"
- add_to_config "config.action_dispatch.rack_cache = true"
+ test "ActiveRecord::Migration::CheckPending is present when active_record.migration_error is set to :page_load" do
+ add_to_config "config.active_record.migration_error = :page_load"
boot!
- assert_equal "Rack::Cache", middleware.first
+ assert middleware.include?("ActiveRecord::Migration::CheckPending")
end
test "ActionDispatch::SSL is present when force_ssl is set" do
@@ -84,7 +83,7 @@ module ApplicationTests
add_to_config "config.ssl_options = { host: 'example.com' }"
boot!
- assert_equal AppTemplate::Application.middleware.first.args, [{host: 'example.com'}]
+ assert_equal Rails.application.middleware.first.args, [{host: 'example.com'}]
end
test "removing Active Record omits its middleware" do
@@ -92,6 +91,7 @@ module ApplicationTests
boot!
assert !middleware.include?("ActiveRecord::ConnectionAdapters::ConnectionManagement")
assert !middleware.include?("ActiveRecord::QueryCache")
+ assert !middleware.include?("ActiveRecord::Migration::CheckPending")
end
test "removes lock if cache classes is set" do
@@ -100,6 +100,12 @@ module ApplicationTests
assert !middleware.include?("Rack::Lock")
end
+ test "removes lock if allow concurrency is set" do
+ add_to_config "config.allow_concurrency = true"
+ boot!
+ assert !middleware.include?("Rack::Lock")
+ end
+
test "removes static asset server if serve_static_assets is disabled" do
add_to_config "config.serve_static_assets = false"
boot!
@@ -133,24 +139,30 @@ module ApplicationTests
end
test "insert middleware after" do
- add_to_config "config.middleware.insert_after ActionDispatch::Static, Rack::Config"
+ add_to_config "config.middleware.insert_after Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.second
end
+ test 'unshift middleware' do
+ add_to_config 'config.middleware.unshift Rack::Config'
+ boot!
+ assert_equal 'Rack::Config', middleware.first
+ end
+
test "Rails.cache does not respond to middleware" do
add_to_config "config.cache_store = :memory_store"
boot!
- assert_equal "Rack::Runtime", middleware.third
+ assert_equal "Rack::Runtime", middleware.fourth
end
test "Rails.cache does respond to middleware" do
boot!
- assert_equal "Rack::Runtime", middleware.fourth
+ assert_equal "Rack::Runtime", middleware.fifth
end
test "insert middleware before" do
- add_to_config "config.middleware.insert_before ActionDispatch::Static, Rack::Config"
+ add_to_config "config.middleware.insert_before Rack::Sendfile, Rack::Config"
boot!
assert_equal "Rack::Config", middleware.first
end
@@ -215,7 +227,7 @@ module ApplicationTests
end
def middleware
- AppTemplate::Application.middleware.map(&:klass).map(&:name)
+ Rails.application.middleware.map(&:klass).map(&:name)
end
end
end
diff --git a/railties/test/application/multiple_applications_test.rb b/railties/test/application/multiple_applications_test.rb
new file mode 100644
index 0000000000..5bfea599e0
--- /dev/null
+++ b/railties/test/application/multiple_applications_test.rb
@@ -0,0 +1,148 @@
+require 'isolation/abstract_unit'
+
+module ApplicationTests
+ class MultipleApplicationsTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app(initializers: true)
+ boot_rails
+ require "#{rails_root}/config/environment"
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def test_cloning_an_application_makes_a_shallow_copy_of_config
+ clone = Rails.application.clone
+
+ assert_equal Rails.application.config, clone.config, "The cloned application should get a copy of the config"
+ assert_equal Rails.application.config.secret_key_base, clone.config.secret_key_base, "The base secret key on the config should be the same"
+ end
+
+ def test_initialization_of_multiple_copies_of_same_application
+ application1 = AppTemplate::Application.new
+ application2 = AppTemplate::Application.new
+
+ assert_not_equal Rails.application.object_id, application1.object_id, "New applications should not be the same as the original application"
+ assert_not_equal Rails.application.object_id, application2.object_id, "New applications should not be the same as the original application"
+ end
+
+ def test_initialization_of_application_with_previous_config
+ application1 = AppTemplate::Application.new(config: Rails.application.config)
+ application2 = AppTemplate::Application.new
+
+ assert_equal Rails.application.config, application1.config, "Creating a new application while setting an initial config should result in the same config"
+ assert_not_equal Rails.application.config, application2.config, "New applications without setting an initial config should not have the same config"
+ end
+
+ def test_initialization_of_application_with_previous_railties
+ application1 = AppTemplate::Application.new(railties: Rails.application.railties)
+ application2 = AppTemplate::Application.new
+
+ assert_equal Rails.application.railties, application1.railties
+ assert_not_equal Rails.application.railties, application2.railties
+ end
+
+ def test_initialize_new_application_with_all_previous_initialization_variables
+ application1 = AppTemplate::Application.new(
+ config: Rails.application.config,
+ railties: Rails.application.railties,
+ routes_reloader: Rails.application.routes_reloader,
+ reloaders: Rails.application.reloaders,
+ routes: Rails.application.routes,
+ helpers: Rails.application.helpers,
+ app_env_config: Rails.application.env_config
+ )
+
+ assert_equal Rails.application.config, application1.config
+ assert_equal Rails.application.railties, application1.railties
+ assert_equal Rails.application.routes_reloader, application1.routes_reloader
+ assert_equal Rails.application.reloaders, application1.reloaders
+ assert_equal Rails.application.routes, application1.routes
+ assert_equal Rails.application.helpers, application1.helpers
+ assert_equal Rails.application.env_config, application1.env_config
+ end
+
+ def test_rake_tasks_defined_on_different_applications_go_to_the_same_class
+ $run_count = 0
+
+ application1 = AppTemplate::Application.new
+ application1.rake_tasks do
+ $run_count += 1
+ end
+
+ application2 = AppTemplate::Application.new
+ application2.rake_tasks do
+ $run_count += 1
+ end
+
+ require "#{app_path}/config/environment"
+
+ assert_equal 0, $run_count, "The count should stay at zero without any calls to the rake tasks"
+ require 'rake'
+ require 'rake/testtask'
+ require 'rdoc/task'
+ Rails.application.load_tasks
+ assert_equal 2, $run_count, "Calling a rake task should result in two increments to the count"
+ end
+
+ def test_multiple_applications_can_be_initialized
+ assert_nothing_raised { AppTemplate::Application.new }
+ end
+
+ def test_initializers_run_on_different_applications_go_to_the_same_class
+ application1 = AppTemplate::Application.new
+ $run_count = 0
+
+ AppTemplate::Application.initializer :init0 do
+ $run_count += 1
+ end
+
+ application1.initializer :init1 do
+ $run_count += 1
+ end
+
+ AppTemplate::Application.new.initializer :init2 do
+ $run_count += 1
+ end
+
+ assert_equal 0, $run_count, "Without loading the initializers, the count should be 0"
+
+ # Set config.eager_load to false so that an eager_load warning doesn't pop up
+ AppTemplate::Application.new { config.eager_load = false }.initialize!
+
+ assert_equal 3, $run_count, "There should have been three initializers that incremented the count"
+ end
+
+ def test_runners_run_on_different_applications_go_to_the_same_class
+ $run_count = 0
+ AppTemplate::Application.runner { $run_count += 1 }
+ AppTemplate::Application.new.runner { $run_count += 1 }
+
+ assert_equal 0, $run_count, "Without loading the runners, the count should be 0"
+ Rails.application.load_runner
+ assert_equal 2, $run_count, "There should have been two runners that increment the count"
+ end
+
+ def test_isolate_namespace_on_an_application
+ assert_nil Rails.application.railtie_namespace, "Before isolating namespace, the railtie namespace should be nil"
+ Rails.application.isolate_namespace(AppTemplate)
+ assert_equal Rails.application.railtie_namespace, AppTemplate, "After isolating namespace, we should have a namespace"
+ end
+
+ def test_inserting_configuration_into_application
+ app = AppTemplate::Application.new(config: Rails.application.config)
+ new_config = Rails::Application::Configuration.new("root_of_application")
+ new_config.secret_key_base = "some_secret_key_dude"
+ app.config.secret_key_base = "a_different_secret_key"
+
+ assert_equal "a_different_secret_key", app.config.secret_key_base, "The configuration's secret key should be set."
+ app.config = new_config
+ assert_equal "some_secret_key_dude", app.config.secret_key_base, "The configuration's secret key should have changed."
+ assert_equal "root_of_application", app.config.root, "The root should have changed to the new config's root."
+ assert_equal new_config, app.config, "The application's config should have changed to the new config."
+ end
+ end
+end
diff --git a/railties/test/application/queue_test.rb b/railties/test/application/queue_test.rb
deleted file mode 100644
index b0b3cf18e9..0000000000
--- a/railties/test/application/queue_test.rb
+++ /dev/null
@@ -1,153 +0,0 @@
-require 'isolation/abstract_unit'
-
-module ApplicationTests
- class QueueTest < ActiveSupport::TestCase
- include ActiveSupport::Testing::Isolation
-
- def setup
- build_app
- boot_rails
- end
-
- def teardown
- teardown_app
- end
-
- def app_const
- @app_const ||= Class.new(Rails::Application)
- end
-
- test "the queue is a SynchronousQueue in test mode" do
- app("test")
- assert_kind_of ActiveSupport::SynchronousQueue, Rails.application.queue
- assert_kind_of ActiveSupport::SynchronousQueue, Rails.queue
- end
-
- test "the queue is a SynchronousQueue in development mode" do
- app("development")
- assert_kind_of ActiveSupport::SynchronousQueue, Rails.application.queue
- assert_kind_of ActiveSupport::SynchronousQueue, Rails.queue
- end
-
- class ThreadTrackingJob
- def initialize
- @origin = Thread.current.object_id
- end
-
- def run
- @target = Thread.current.object_id
- end
-
- def ran_in_different_thread?
- @origin != @target
- end
-
- def ran?
- @target
- end
- end
-
- test "in development mode, an enqueued job will be processed in the same thread" do
- app("development")
-
- job = ThreadTrackingJob.new
- Rails.queue.push job
- sleep 0.1
-
- assert job.ran?, "Expected job to be run"
- refute job.ran_in_different_thread?, "Expected job to run in the same thread"
- end
-
- test "in test mode, an enqueued job will be processed in the same thread" do
- app("test")
-
- job = ThreadTrackingJob.new
- Rails.queue.push job
- sleep 0.1
-
- assert job.ran?, "Expected job to be run"
- refute job.ran_in_different_thread?, "Expected job to run in the same thread"
- end
-
- test "in production, automatically spawn a queue consumer in a background thread" do
- add_to_env_config "production", <<-RUBY
- config.queue = ActiveSupport::Queue.new
- RUBY
-
- app("production")
-
- assert_nil Rails.application.config.queue_consumer
- assert_kind_of ActiveSupport::ThreadedQueueConsumer, Rails.application.queue_consumer
- end
-
- test "attempting to marshal a queue will raise an exception" do
- app("test")
- assert_raises TypeError do
- Marshal.dump Rails.queue
- end
- end
-
- def setup_custom_queue
- add_to_env_config "production", <<-RUBY
- require "my_queue"
- config.queue = MyQueue.new
- RUBY
-
- app_file "lib/my_queue.rb", <<-RUBY
- class MyQueue
- def push(job)
- job.run
- end
- end
- RUBY
-
- app("production")
- end
-
- test "a custom queue implementation can be provided" do
- setup_custom_queue
-
- assert_kind_of MyQueue, Rails.queue
-
- job = Struct.new(:id, :ran) do
- def run
- self.ran = true
- end
- end
-
- job1 = job.new(1)
- Rails.queue.push job1
-
- assert_equal true, job1.ran
- end
-
- test "a custom consumer implementation can be provided" do
- add_to_env_config "production", <<-RUBY
- require "my_queue_consumer"
- config.queue = ActiveSupport::Queue.new
- config.queue_consumer = MyQueueConsumer.new
- RUBY
-
- app_file "lib/my_queue_consumer.rb", <<-RUBY
- class MyQueueConsumer
- attr_reader :started
-
- def start
- @started = true
- end
- end
- RUBY
-
- app("production")
-
- assert_kind_of MyQueueConsumer, Rails.application.queue_consumer
- assert Rails.application.queue_consumer.started
- end
-
- test "default consumer is not used with custom queue implementation" do
- setup_custom_queue
-
- assert_nil Rails.application.queue_consumer
- end
- end
-end
diff --git a/railties/test/application/rake/dbs_test.rb b/railties/test/application/rake/dbs_test.rb
index 03798d572a..4d348c6b4b 100644
--- a/railties/test/application/rake/dbs_test.rb
+++ b/railties/test/application/rake/dbs_test.rb
@@ -21,6 +21,8 @@ module ApplicationTests
def set_database_url
ENV['DATABASE_URL'] = "sqlite3://:@localhost/#{database_url_db_name}"
+ # ensure it's using the DATABASE_URL
+ FileUtils.rm_rf("#{app_path}/config/database.yml")
end
def expected
@@ -31,12 +33,12 @@ module ApplicationTests
Dir.chdir(app_path) do
output = `bundle exec rake db:create`
assert_equal output, ""
- assert File.exists?(expected[:database])
+ assert File.exist?(expected[:database])
assert_equal expected[:database],
ActiveRecord::Base.connection_config[:database]
output = `bundle exec rake db:drop`
assert_equal output, ""
- assert !File.exists?(expected[:database])
+ assert !File.exist?(expected[:database])
end
end
@@ -55,8 +57,8 @@ module ApplicationTests
def db_migrate_and_status
Dir.chdir(app_path) do
- `rails generate model book title:string`
- `bundle exec rake db:migrate`
+ `rails generate model book title:string;
+ bundle exec rake db:migrate`
output = `bundle exec rake db:migrate:status`
assert_match(/database:\s+\S+#{expected[:database]}/, output)
assert_match(/up\s+\d{14}\s+Create books/, output)
@@ -78,9 +80,8 @@ module ApplicationTests
def db_schema_dump
Dir.chdir(app_path) do
- `rails generate model book title:string`
- `rake db:migrate`
- `rake db:schema:dump`
+ `rails generate model book title:string;
+ rake db:migrate db:schema:dump`
schema_dump = File.read("db/schema.rb")
assert_match(/create_table \"books\"/, schema_dump)
end
@@ -97,9 +98,8 @@ module ApplicationTests
def db_fixtures_load
Dir.chdir(app_path) do
- `rails generate model book title:string`
- `bundle exec rake db:migrate`
- `bundle exec rake db:fixtures:load`
+ `rails generate model book title:string;
+ bundle exec rake db:migrate db:fixtures:load`
assert_match(/#{expected[:database]}/,
ActiveRecord::Base.connection_config[:database])
require "#{app_path}/app/models/book"
@@ -122,13 +122,11 @@ module ApplicationTests
def db_structure_dump_and_load
Dir.chdir(app_path) do
- `rails generate model book title:string`
- `bundle exec rake db:migrate`
- `bundle exec rake db:structure:dump`
+ `rails generate model book title:string;
+ bundle exec rake db:migrate db:structure:dump`
structure_dump = File.read("db/structure.sql")
assert_match(/CREATE TABLE \"books\"/, structure_dump)
- `bundle exec rake db:drop`
- `bundle exec rake db:structure:load`
+ `bundle exec rake db:drop db:structure:load`
assert_match(/#{expected[:database]}/,
ActiveRecord::Base.connection_config[:database])
require "#{app_path}/app/models/book"
@@ -152,10 +150,8 @@ module ApplicationTests
def db_test_load_structure
Dir.chdir(app_path) do
- `rails generate model book title:string`
- `bundle exec rake db:migrate`
- `bundle exec rake db:structure:dump`
- `bundle exec rake db:test:load_structure`
+ `rails generate model book title:string;
+ bundle exec rake db:migrate db:structure:dump db:test:load_structure`
ActiveRecord::Base.configurations = Rails.application.config.database_configuration
ActiveRecord::Base.establish_connection 'test'
require "#{app_path}/app/models/book"
@@ -170,12 +166,6 @@ module ApplicationTests
require "#{app_path}/config/environment"
db_test_load_structure
end
-
- test 'db:test:load_structure with database_url' do
- require "#{app_path}/config/environment"
- set_database_url
- db_test_load_structure
- end
end
end
-end \ No newline at end of file
+end
diff --git a/railties/test/application/rake/migrations_test.rb b/railties/test/application/rake/migrations_test.rb
index 0a47fd014c..33c753868c 100644
--- a/railties/test/application/rake/migrations_test.rb
+++ b/railties/test/application/rake/migrations_test.rb
@@ -50,9 +50,9 @@ module ApplicationTests
assert_match(/AddEmailToUsers: migrated/, output)
output = `rake db:rollback STEP=2`
- assert_match(/drop_table\("users"\)/, output)
+ assert_match(/drop_table\(:users\)/, output)
assert_match(/CreateUsers: reverted/, output)
- assert_match(/remove_column\("users", :email\)/, output)
+ assert_match(/remove_column\(:users, :email, :string\)/, output)
assert_match(/AddEmailToUsers: reverted/, output)
end
end
diff --git a/railties/test/application/rake/notes_test.rb b/railties/test/application/rake/notes_test.rb
index 7a227098ba..01d751e822 100644
--- a/railties/test/application/rake/notes_test.rb
+++ b/railties/test/application/rake/notes_test.rb
@@ -24,6 +24,8 @@ module ApplicationTests
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"
@@ -46,9 +48,11 @@ module ApplicationTests
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_equal 9, lines.size
+ assert_equal 11, lines.size
lines.each do |line|
assert_equal 4, line[0].size
@@ -60,8 +64,8 @@ module ApplicationTests
test 'notes finds notes in default directories' do
app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
+ app_file "db/some_seeds.rb", "# TODO: note in db directory"
app_file "lib/some_file.rb", "# TODO: note in lib directory"
- app_file "script/run_something.rb", "# TODO: note in script directory"
app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory"
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
@@ -80,8 +84,8 @@ module ApplicationTests
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
+ assert_match(/note in db directory/, output)
assert_match(/note in lib directory/, output)
- assert_match(/note in script directory/, output)
assert_match(/note in test directory/, output)
assert_no_match(/note in some_other directory/, output)
@@ -96,8 +100,8 @@ module ApplicationTests
test 'notes finds notes in custom directories' do
app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
app_file "config/initializers/some_initializer.rb", "# TODO: note in config directory"
+ app_file "db/some_seeds.rb", "# TODO: note in db directory"
app_file "lib/some_file.rb", "# TODO: note in lib directory"
- app_file "script/run_something.rb", "# TODO: note in script directory"
app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory"
app_file "some_other_dir/blah.rb", "# TODO: note in some_other directory"
@@ -116,8 +120,8 @@ module ApplicationTests
assert_match(/note in app directory/, output)
assert_match(/note in config directory/, output)
+ assert_match(/note in db directory/, output)
assert_match(/note in lib directory/, output)
- assert_match(/note in script directory/, output)
assert_match(/note in test directory/, output)
assert_match(/note in some_other directory/, output)
@@ -130,6 +134,45 @@ module ApplicationTests
end
end
+ test 'custom rake task finds specific notes in specific directories' do
+ app_file "app/controllers/some_controller.rb", "# TODO: note in app directory"
+ app_file "lib/some_file.rb", "# OPTIMIZE: note in lib directory\n" << "# FIXME: note in lib directory"
+ app_file "test/some_test.rb", 1000.times.map { "" }.join("\n") << "# TODO: note in test directory"
+
+ app_file "lib/tasks/notes_custom.rake", <<-EOS
+ require 'rails/source_annotation_extractor'
+ task :notes_custom do
+ tags = 'TODO|FIXME'
+ opts = { dirs: %w(lib test), tag: true }
+ SourceAnnotationExtractor.enumerate(tags, opts)
+ 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
+
+ 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
+
+ lines.each do |line_number|
+ assert_equal 4, line_number.size
+ end
+ end
+ end
+
private
def boot_rails
super
diff --git a/railties/test/application/rake_test.rb b/railties/test/application/rake_test.rb
index c5a68a5152..2169304aa4 100644
--- a/railties/test/application/rake_test.rb
+++ b/railties/test/application/rake_test.rb
@@ -1,5 +1,6 @@
# coding:utf-8
require "isolation/abstract_unit"
+require "active_support/core_ext/string/strip"
module ApplicationTests
class RakeTest < ActiveSupport::TestCase
@@ -8,7 +9,6 @@ module ApplicationTests
def setup
build_app
boot_rails
- FileUtils.rm_rf("#{app_path}/config/environments")
end
def teardown
@@ -29,11 +29,11 @@ module ApplicationTests
app_file "config/environment.rb", <<-RUBY
SuperMiddleware = Struct.new(:app)
- AppTemplate::Application.configure do
+ Rails.application.configure do
config.middleware.use SuperMiddleware
end
- AppTemplate::Application.initialize!
+ Rails.application.initialize!
RUBY
assert_match("SuperMiddleware", Dir.chdir(app_path){ `rake middleware` })
@@ -55,10 +55,8 @@ module ApplicationTests
assert_match "Doing something...", output
end
- def test_does_not_explode_when_accessing_a_model_with_eager_load
+ def test_does_not_explode_when_accessing_a_model
add_to_config <<-RUBY
- config.eager_load = true
-
rake_tasks do
task do_nothing: :environment do
Hello.new.world
@@ -66,16 +64,38 @@ module ApplicationTests
end
RUBY
- app_file "app/models/hello.rb", <<-RUBY
- class Hello
- def world
- puts "Hello world"
+ app_file 'app/models/hello.rb', <<-RUBY
+ class Hello
+ def world
+ puts 'Hello world'
+ end
end
- end
RUBY
- output = Dir.chdir(app_path){ `rake do_nothing` }
- assert_match "Hello world", output
+ output = Dir.chdir(app_path) { `rake do_nothing` }
+ assert_match 'Hello world', output
+ end
+
+ def test_should_not_eager_load_model_for_rake
+ add_to_config <<-RUBY
+ rake_tasks do
+ task do_nothing: :environment do
+ end
+ end
+ RUBY
+
+ add_to_env_config 'production', <<-RUBY
+ config.eager_load = true
+ RUBY
+
+ app_file 'app/models/hello.rb', <<-RUBY
+ raise 'should not be pre-required for rake even eager_load=true'
+ RUBY
+
+ Dir.chdir(app_path) do
+ assert system('rake do_nothing RAILS_ENV=production'),
+ 'should not be pre-required for rake even eager_load=true'
+ end
end
def test_code_statistics_sanity
@@ -83,36 +103,43 @@ module ApplicationTests
Dir.chdir(app_path){ `rake stats` }
end
- def test_rake_test_error_output
- Dir.chdir(app_path){ `rake db:migrate` }
-
- app_file "test/models/one_model_test.rb", <<-RUBY
- raise 'models'
+ def test_rake_routes_calls_the_route_inspector
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ end
RUBY
- app_file "test/controllers/one_controller_test.rb", <<-RUBY
- raise 'controllers'
- RUBY
+ output = Dir.chdir(app_path){ `rake routes` }
+ assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output
+ end
- app_file "test/integration/one_integration_test.rb", <<-RUBY
- raise 'integration'
+ def test_rake_routes_with_controller_environment
+ app_file "config/routes.rb", <<-RUBY
+ Rails.application.routes.draw do
+ get '/cart', to: 'cart#show'
+ get '/basketball', to: 'basketball#index'
+ end
RUBY
- silence_stderr do
- output = Dir.chdir(app_path) { `rake test 2>&1` }
- assert_match 'models', output
- assert_match 'controllers', output
- assert_match 'integration', output
- end
+ ENV['CONTROLLER'] = 'cart'
+ output = Dir.chdir(app_path){ `rake routes` }
+ assert_equal "Prefix Verb URI Pattern Controller#Action\ncart GET /cart(.:format) cart#show\n", output
end
- def test_rake_routes_calls_the_route_inspector
+ def test_rake_routes_displays_message_when_no_routes_are_defined
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
- get '/cart', to: 'cart#show'
+ Rails.application.routes.draw do
end
RUBY
- assert_equal "cart GET /cart(.:format) cart#show\n", Dir.chdir(app_path){ `rake routes` }
+
+ assert_equal <<-MESSAGE.strip_heredoc, Dir.chdir(app_path){ `rake routes` }
+ You don't have any routes defined!
+
+ Please add some routes in config/routes.rb.
+
+ For more information about routes, see the Rails guide: http://guides.rubyonrails.org/routing.html.
+ MESSAGE
end
def test_logger_is_flushed_when_exiting_production_rake_tasks
@@ -163,7 +190,17 @@ module ApplicationTests
bundle exec rake db:migrate db:test:clone test`
end
- assert_match(/7 tests, 13 assertions, 0 failures, 0 errors/, output)
+ assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)
+ assert_no_match(/Errors running/, output)
+ end
+
+ def test_scaffold_with_references_columns_tests_pass_by_default
+ output = Dir.chdir(app_path) do
+ `rails generate scaffold LineItems product:references cart:belongs_to;
+ bundle exec rake db:migrate db:test:clone test`
+ end
+
+ assert_match(/7 runs, 13 assertions, 0 failures, 0 errors/, output)
assert_no_match(/Errors running/, output)
end
@@ -190,7 +227,7 @@ module ApplicationTests
# ensure we have a schema_migrations table to dump
`bundle exec rake db:migrate db:structure:dump DB_STRUCTURE=db/my_structure.sql`
end
- assert File.exists?(File.join(app_path, 'db', 'my_structure.sql'))
+ assert File.exist?(File.join(app_path, 'db', 'my_structure.sql'))
end
def test_rake_dump_structure_should_be_called_twice_when_migrate_redo
@@ -211,46 +248,24 @@ module ApplicationTests
rails generate model product name:string;
bundle exec rake db:migrate db:schema:cache:dump`
end
- assert File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
+ assert File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_rake_clear_schema_cache
Dir.chdir(app_path) do
`bundle exec rake db:schema:cache:dump db:schema:cache:clear`
end
- assert !File.exists?(File.join(app_path, 'db', 'schema_cache.dump'))
- end
-
- def test_load_activerecord_base_when_we_use_observers
- Dir.chdir(app_path) do
- `bundle exec rails g model user;
- bundle exec rake db:migrate;
- bundle exec rails g observer user;`
-
- add_to_config "config.active_record.observers = :user_observer"
-
- assert_equal "0", `bundle exec rails r "puts User.count"`.strip
-
- app_file "lib/tasks/count_user.rake", <<-RUBY
- namespace :user do
- task count: :environment do
- puts User.count
- end
- end
- RUBY
-
- assert_equal "0", `bundle exec rake user:count`.strip
- end
+ assert !File.exist?(File.join(app_path, 'db', 'schema_cache.dump'))
end
def test_copy_templates
Dir.chdir(app_path) do
`bundle exec rake rails:templates:copy`
%w(controller mailer scaffold).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'erb', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'erb', dir))
end
%w(controller helper scaffold_controller assets).each do |dir|
- assert File.exists?(File.join(app_path, 'lib', 'templates', 'rails', dir))
+ assert File.exist?(File.join(app_path, 'lib', 'templates', 'rails', dir))
end
end
end
diff --git a/railties/test/application/rendering_test.rb b/railties/test/application/rendering_test.rb
new file mode 100644
index 0000000000..b01febd768
--- /dev/null
+++ b/railties/test/application/rendering_test.rb
@@ -0,0 +1,45 @@
+require 'isolation/abstract_unit'
+require 'rack/test'
+
+module ApplicationTests
+ class RoutingTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+ include Rack::Test::Methods
+
+ def setup
+ build_app
+ boot_rails
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ test "Unknown format falls back to HTML template" do
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get 'pages/:id', to: 'pages#show'
+ end
+ RUBY
+
+ app_file 'app/controllers/pages_controller.rb', <<-RUBY
+ class PagesController < ApplicationController
+ layout false
+
+ def show
+ end
+ end
+ RUBY
+
+ app_file 'app/views/pages/show.html.erb', <<-RUBY
+ <%= params[:id] %>
+ RUBY
+
+ get '/pages/foo'
+ assert_equal 200, last_response.status
+
+ get '/pages/foo.bar'
+ assert_equal 200, last_response.status
+ end
+ end
+end
diff --git a/railties/test/application/routing_test.rb b/railties/test/application/routing_test.rb
index ffcdeac7f0..8576a2b738 100644
--- a/railties/test/application/routing_test.rb
+++ b/railties/test/application/routing_test.rb
@@ -15,6 +15,12 @@ module ApplicationTests
teardown_app
end
+ test "rails/welcome in development" do
+ app("development")
+ get "/"
+ assert_equal 200, last_response.status
+ end
+
test "rails/info/routes in development" do
app("development")
get "/rails/info/routes"
@@ -27,6 +33,36 @@ module ApplicationTests
assert_equal 200, last_response.status
end
+ test "root takes precedence over internal welcome controller" do
+ app("development")
+
+ get '/'
+ assert_match %r{<h1>Getting started</h1>} , last_response.body
+
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render text: "foo"
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ root to: "foo#index"
+ end
+ RUBY
+
+ get '/'
+ assert_equal 'foo', last_response.body
+ end
+
+ test "rails/welcome in production" do
+ app("production")
+ get "/"
+ assert_equal 404, last_response.status
+ end
+
test "rails/info/routes in production" do
app("production")
get "/rails/info/routes"
@@ -64,7 +100,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -75,7 +111,7 @@ module ApplicationTests
test "mount rack app" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
mount lambda { |env| [200, {}, [env["PATH_INFO"]]] }, at: "/blog"
# The line below is required because mount sometimes
# fails when a resource route is added.
@@ -105,7 +141,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -137,7 +173,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'admin/foo', to: 'admin/foo#index'
get 'foo', to: 'foo#index'
end
@@ -152,7 +188,7 @@ module ApplicationTests
test "routes appending blocks" do
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller/:action'
end
RUBY
@@ -169,7 +205,7 @@ module ApplicationTests
assert_equal 'WIN', last_response.body
app_file 'config/routes.rb', <<-R
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'lol' => 'hello#index'
end
R
@@ -193,7 +229,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'foo', to: 'foo#bar'
end
RUBY
@@ -204,7 +240,7 @@ module ApplicationTests
assert_equal 'bar', last_response.body
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'foo', to: 'foo#baz'
end
RUBY
@@ -225,7 +261,7 @@ module ApplicationTests
end
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'foo', to: ::InitializeRackApp
end
RUBY
@@ -241,6 +277,146 @@ module ApplicationTests
end
end
+ def test_root_path
+ app('development')
+
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render :text => "foo"
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', :to => 'foo#index'
+ root :to => 'foo#index'
+ end
+ RUBY
+
+ remove_file 'public/index.html'
+
+ get '/'
+ assert_equal 'foo', last_response.body
+ end
+
+ test 'routes are added and removed when reloading' do
+ app('development')
+
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render text: "foo"
+ end
+ end
+ RUBY
+
+ controller :bar, <<-RUBY
+ class BarController < ApplicationController
+ def index
+ render text: "bar"
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index'
+ end
+ RUBY
+
+ get '/foo'
+ assert_equal 'foo', last_response.body
+ assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+
+ get '/bar'
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ end
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index'
+ get 'bar', to: 'bar#index'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get '/foo'
+ assert_equal 'foo', last_response.body
+ assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+
+ get '/bar'
+ assert_equal 'bar', last_response.body
+ assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get 'foo', to: 'foo#index'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get '/foo'
+ assert_equal 'foo', last_response.body
+ assert_equal '/foo', Rails.application.routes.url_helpers.foo_path
+
+ get '/bar'
+ assert_equal 404, last_response.status
+ assert_raises NoMethodError do
+ assert_equal '/bar', Rails.application.routes.url_helpers.bar_path
+ end
+ end
+
+ test 'named routes are cleared when reloading' do
+ app('development')
+
+ controller :foo, <<-RUBY
+ class FooController < ApplicationController
+ def index
+ render text: "foo"
+ end
+ end
+ RUBY
+
+ controller :bar, <<-RUBY
+ class BarController < ApplicationController
+ def index
+ render text: "bar"
+ end
+ end
+ RUBY
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':locale/foo', to: 'foo#index', as: 'foo'
+ end
+ RUBY
+
+ get '/en/foo'
+ assert_equal 'foo', last_response.body
+ assert_equal '/en/foo', Rails.application.routes.url_helpers.foo_path(:locale => 'en')
+
+ app_file 'config/routes.rb', <<-RUBY
+ Rails.application.routes.draw do
+ get ':locale/bar', to: 'bar#index', as: 'foo'
+ end
+ RUBY
+
+ Rails.application.reload_routes!
+
+ get '/en/foo'
+ assert_equal 404, last_response.status
+
+ get '/en/bar'
+ assert_equal 'bar', last_response.body
+ assert_equal '/en/bar', Rails.application.routes.url_helpers.foo_path(:locale => 'en')
+ end
+
test 'resource routing with irregular inflection' do
app_file 'config/initializers/inflection.rb', <<-RUBY
ActiveSupport::Inflector.inflections do |inflect|
@@ -249,7 +425,7 @@ module ApplicationTests
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
resources :yazilar
end
RUBY
diff --git a/railties/test/application/runner_test.rb b/railties/test/application/runner_test.rb
index 81ed5873a5..6595c40f8b 100644
--- a/railties/test/application/runner_test.rb
+++ b/railties/test/application/runner_test.rb
@@ -1,8 +1,10 @@
require 'isolation/abstract_unit'
+require 'env_helpers'
module ApplicationTests
class RunnerTest < ActiveSupport::TestCase
include ActiveSupport::Testing::Isolation
+ include EnvHelpers
def setup
build_app
@@ -35,27 +37,27 @@ module ApplicationTests
end
def test_should_run_file
- app_file "script/count_users.rb", <<-SCRIPT
+ app_file "bin/count_users.rb", <<-SCRIPT
puts User.count
SCRIPT
- assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "script/count_users.rb"` }
+ assert_match "42", Dir.chdir(app_path) { `bundle exec rails runner "bin/count_users.rb"` }
end
def test_should_set_dollar_0_to_file
- app_file "script/dollar0.rb", <<-SCRIPT
+ app_file "bin/dollar0.rb", <<-SCRIPT
puts $0
SCRIPT
- assert_match "script/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/dollar0.rb"` }
+ assert_match "bin/dollar0.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/dollar0.rb"` }
end
def test_should_set_dollar_program_name_to_file
- app_file "script/program_name.rb", <<-SCRIPT
+ app_file "bin/program_name.rb", <<-SCRIPT
puts $PROGRAM_NAME
SCRIPT
- assert_match "script/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "script/program_name.rb"` }
+ assert_match "bin/program_name.rb", Dir.chdir(app_path) { `bundle exec rails runner "bin/program_name.rb"` }
end
def test_with_hook
@@ -67,5 +69,21 @@ module ApplicationTests
assert_match "true", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.application.config.ran"` }
end
+
+ def test_default_environment
+ assert_match "development", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` }
+ end
+
+ def test_environment_with_rails_env
+ with_rails_env "production" do
+ assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` }
+ end
+ end
+
+ def test_environment_with_rack_env
+ with_rack_env "production" do
+ assert_match "production", Dir.chdir(app_path) { `bundle exec rails runner "puts Rails.env"` }
+ end
+ end
end
end
diff --git a/railties/test/application/test_runner_test.rb b/railties/test/application/test_runner_test.rb
new file mode 100644
index 0000000000..118f22995e
--- /dev/null
+++ b/railties/test/application/test_runner_test.rb
@@ -0,0 +1,320 @@
+require 'isolation/abstract_unit'
+require 'active_support/core_ext/string/strip'
+
+module ApplicationTests
+ class TestRunnerTest < ActiveSupport::TestCase
+ include ActiveSupport::Testing::Isolation
+
+ def setup
+ build_app
+ ENV['RAILS_ENV'] = nil
+ create_schema
+ end
+
+ def teardown
+ teardown_app
+ end
+
+ def test_run_in_test_environment
+ app_file 'test/unit/env_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class EnvTest < ActiveSupport::TestCase
+ def test_env
+ puts "Current Environment: \#{Rails.env}"
+ end
+ end
+ RUBY
+
+ assert_match "Current Environment: test", run_test_command('test/unit/env_test.rb')
+ end
+
+ def test_run_single_file
+ create_test_file :models, 'foo'
+ create_test_file :models, 'bar'
+ assert_match "1 runs, 1 assertions, 0 failures", run_test_command("test/models/foo_test.rb")
+ end
+
+ def test_run_multiple_files
+ create_test_file :models, 'foo'
+ create_test_file :models, 'bar'
+ assert_match "2 runs, 2 assertions, 0 failures", run_test_command("test/models/foo_test.rb test/models/bar_test.rb")
+ end
+
+ def test_run_file_with_syntax_error
+ app_file 'test/models/error_test.rb', <<-RUBY
+ require 'test_helper'
+ def; end
+ RUBY
+
+ error_stream = Tempfile.new('error')
+ redirect_stderr(error_stream) { run_test_command('test/models/error_test.rb') }
+ assert_match "syntax error", error_stream.read
+ end
+
+ def test_run_models
+ create_test_file :models, 'foo'
+ create_test_file :models, 'bar'
+ create_test_file :controllers, 'foobar_controller'
+ run_test_models_command.tap do |output|
+ assert_match "FooTest", output
+ assert_match "BarTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_helpers
+ create_test_file :helpers, 'foo_helper'
+ create_test_file :helpers, 'bar_helper'
+ create_test_file :controllers, 'foobar_controller'
+ run_test_helpers_command.tap do |output|
+ assert_match "FooHelperTest", output
+ assert_match "BarHelperTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_units
+ create_test_file :models, 'foo'
+ create_test_file :helpers, 'bar_helper'
+ create_test_file :unit, 'baz_unit'
+ create_test_file :controllers, 'foobar_controller'
+ run_test_units_command.tap do |output|
+ assert_match "FooTest", output
+ assert_match "BarHelperTest", output
+ assert_match "BazUnitTest", output
+ assert_match "3 runs, 3 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_controllers
+ create_test_file :controllers, 'foo_controller'
+ create_test_file :controllers, 'bar_controller'
+ create_test_file :models, 'foo'
+ run_test_controllers_command.tap do |output|
+ assert_match "FooControllerTest", output
+ assert_match "BarControllerTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_mailers
+ create_test_file :mailers, 'foo_mailer'
+ create_test_file :mailers, 'bar_mailer'
+ create_test_file :models, 'foo'
+ run_test_mailers_command.tap do |output|
+ assert_match "FooMailerTest", output
+ assert_match "BarMailerTest", output
+ assert_match "2 runs, 2 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_functionals
+ create_test_file :mailers, 'foo_mailer'
+ create_test_file :controllers, 'bar_controller'
+ create_test_file :functional, 'baz_functional'
+ create_test_file :models, 'foo'
+ run_test_functionals_command.tap do |output|
+ assert_match "FooMailerTest", output
+ assert_match "BarControllerTest", output
+ assert_match "BazFunctionalTest", output
+ assert_match "3 runs, 3 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_integration
+ create_test_file :integration, 'foo_integration'
+ create_test_file :models, 'foo'
+ run_test_integration_command.tap do |output|
+ assert_match "FooIntegration", output
+ assert_match "1 runs, 1 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_all_suites
+ suites = [:models, :helpers, :unit, :controllers, :mailers, :functional, :integration]
+ suites.each { |suite| create_test_file suite, "foo_#{suite}" }
+ run_test_command('') .tap do |output|
+ suites.each { |suite| assert_match "Foo#{suite.to_s.camelize}Test", output }
+ assert_match "7 runs, 7 assertions, 0 failures", output
+ end
+ end
+
+ def test_run_named_test
+ app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class Chu2KoiTest < ActiveSupport::TestCase
+ def test_rikka
+ puts 'Rikka'
+ end
+
+ def test_sanae
+ puts 'Sanae'
+ end
+ end
+ RUBY
+
+ run_test_command('test/unit/chu_2_koi_test.rb test_rikka').tap do |output|
+ assert_match "Rikka", output
+ assert_no_match "Sanae", output
+ end
+ end
+
+ def test_run_matched_test
+ app_file 'test/unit/chu_2_koi_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class Chu2KoiTest < ActiveSupport::TestCase
+ def test_rikka
+ puts 'Rikka'
+ end
+
+ def test_sanae
+ puts 'Sanae'
+ end
+ end
+ RUBY
+
+ run_test_command('test/unit/chu_2_koi_test.rb /rikka/').tap do |output|
+ assert_match "Rikka", output
+ assert_no_match "Sanae", output
+ end
+ end
+
+ def test_load_fixtures_when_running_test_suites
+ create_model_with_fixture
+ suites = [:models, :helpers, [:units, :unit], :controllers, :mailers,
+ [:functionals, :functional], :integration]
+
+ suites.each do |suite, directory|
+ directory ||= suite
+ create_fixture_test directory
+ assert_match "3 users", run_task(["test:#{suite}"])
+ Dir.chdir(app_path) { FileUtils.rm_f "test/#{directory}" }
+ end
+ end
+
+ def test_run_with_model
+ create_model_with_fixture
+ create_fixture_test 'models', 'user'
+ assert_match "3 users", run_task(["test models/user"])
+ assert_match "3 users", run_task(["test app/models/user.rb"])
+ end
+
+ def test_run_different_environment_using_env_var
+ app_file 'test/unit/env_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class EnvTest < ActiveSupport::TestCase
+ def test_env
+ puts Rails.env
+ end
+ end
+ RUBY
+
+ ENV['RAILS_ENV'] = 'development'
+ assert_match "development", run_test_command('test/unit/env_test.rb')
+ end
+
+ def test_run_different_environment_using_e_tag
+ env = "development"
+ app_file 'test/unit/env_test.rb', <<-RUBY
+ require 'test_helper'
+
+ class EnvTest < ActiveSupport::TestCase
+ def test_env
+ puts Rails.env
+ end
+ end
+ RUBY
+
+ assert_match env, run_test_command("test/unit/env_test.rb RAILS_ENV=#{env}")
+ end
+
+ def test_generated_scaffold_works_with_rails_test
+ create_scaffold
+ assert_match "0 failures, 0 errors, 0 skips", run_test_command('')
+ end
+
+ private
+ def run_task(tasks)
+ Dir.chdir(app_path) { `bundle exec rake #{tasks.join ' '}` }
+ end
+
+ def run_test_command(arguments = 'test/unit/test_test.rb')
+ run_task ['test', arguments]
+ end
+ %w{ mailers models helpers units controllers functionals integration }.each do |type|
+ define_method("run_test_#{type}_command") do
+ run_task ["test:#{type}"]
+ end
+ end
+
+ def create_model_with_fixture
+ script 'generate model user name:string'
+
+ app_file 'test/fixtures/users.yml', <<-YAML.strip_heredoc
+ vampire:
+ id: 1
+ name: Koyomi Araragi
+ crab:
+ id: 2
+ name: Senjougahara Hitagi
+ cat:
+ id: 3
+ name: Tsubasa Hanekawa
+ YAML
+
+ run_migration
+ end
+
+ def create_fixture_test(path = :unit, name = 'test')
+ app_file "test/#{path}/#{name}_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class #{name.camelize}Test < ActiveSupport::TestCase
+ def test_fixture
+ puts "\#{User.count} users (\#{__FILE__})"
+ end
+ end
+ RUBY
+ end
+
+ def create_schema
+ app_file 'db/schema.rb', ''
+ end
+
+ def redirect_stderr(target_stream)
+ previous_stderr = STDERR.dup
+ $stderr.reopen(target_stream)
+ yield
+ target_stream.rewind
+ ensure
+ $stderr = previous_stderr
+ end
+
+ def create_test_file(path = :unit, name = 'test')
+ app_file "test/#{path}/#{name}_test.rb", <<-RUBY
+ require 'test_helper'
+
+ class #{name.camelize}Test < ActiveSupport::TestCase
+ def test_truth
+ puts "#{name.camelize}Test"
+ assert true
+ end
+ end
+ RUBY
+ end
+
+ def create_scaffold
+ script 'generate scaffold user name:string'
+ Dir.chdir(app_path) { File.exist?('app/models/user.rb') }
+ run_migration
+ end
+
+ def run_migration
+ Dir.chdir(app_path) { `bundle exec rake db:migrate` }
+ end
+ end
+end
diff --git a/railties/test/application/test_test.rb b/railties/test/application/test_test.rb
index f6bcaa466a..c7ad2fba8f 100644
--- a/railties/test/application/test_test.rb
+++ b/railties/test/application/test_test.rb
@@ -52,31 +52,6 @@ module ApplicationTests
run_test_file 'integration/posts_test.rb'
end
- test "performance test" do
- controller 'posts', <<-RUBY
- class PostsController < ActionController::Base
- end
- RUBY
-
- app_file 'app/views/posts/index.html.erb', <<-HTML
- Posts#index
- HTML
-
- app_file 'test/performance/posts_test.rb', <<-RUBY
- require 'test_helper'
- require 'rails/performance_test_help'
-
- class PostsTest < ActionDispatch::PerformanceTest
- def test_index
- get '/posts'
- assert_response :success
- end
- end
- RUBY
-
- run_test_file 'performance/posts_test.rb'
- end
-
private
def run_test_file(name)
result = ruby '-Itest', "#{app_path}/test/#{name}"
diff --git a/railties/test/application/url_generation_test.rb b/railties/test/application/url_generation_test.rb
index 4ecb94b65e..efbc853d7b 100644
--- a/railties/test/application/url_generation_test.rb
+++ b/railties/test/application/url_generation_test.rb
@@ -12,14 +12,16 @@ module ApplicationTests
boot_rails
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
class MyApp < Rails::Application
- config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
+ config.eager_load = false
end
- MyApp.initialize!
+ Rails.application.initialize!
class ::ApplicationController < ActionController::Base
end
diff --git a/railties/test/code_statistics_calculator_test.rb b/railties/test/code_statistics_calculator_test.rb
new file mode 100644
index 0000000000..b3eabf5024
--- /dev/null
+++ b/railties/test/code_statistics_calculator_test.rb
@@ -0,0 +1,288 @@
+require 'abstract_unit'
+require 'rails/code_statistics_calculator'
+
+class CodeStatisticsCalculatorTest < ActiveSupport::TestCase
+ def setup
+ @code_statistics_calculator = CodeStatisticsCalculator.new
+ end
+
+ test 'add statistics to another using #add' do
+ code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4)
+ @code_statistics_calculator.add(code_statistics_calculator_1)
+
+ assert_equal 1, @code_statistics_calculator.lines
+ assert_equal 2, @code_statistics_calculator.code_lines
+ assert_equal 3, @code_statistics_calculator.classes
+ assert_equal 4, @code_statistics_calculator.methods
+
+ code_statistics_calculator_2 = CodeStatisticsCalculator.new(2, 3, 4, 5)
+ @code_statistics_calculator.add(code_statistics_calculator_2)
+
+ assert_equal 3, @code_statistics_calculator.lines
+ assert_equal 5, @code_statistics_calculator.code_lines
+ assert_equal 7, @code_statistics_calculator.classes
+ assert_equal 9, @code_statistics_calculator.methods
+ end
+
+ test 'accumulate statistics using #add_by_io' do
+ code_statistics_calculator_1 = CodeStatisticsCalculator.new(1, 2, 3, 4)
+ @code_statistics_calculator.add(code_statistics_calculator_1)
+
+ code = <<-'CODE'
+ def foo
+ puts 'foo'
+ end
+
+ def bar; end
+ class A; end
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rb)
+
+ assert_equal 7, @code_statistics_calculator.lines
+ assert_equal 7, @code_statistics_calculator.code_lines
+ assert_equal 4, @code_statistics_calculator.classes
+ assert_equal 6, @code_statistics_calculator.methods
+ end
+
+ test 'calculate statistics using #add_by_file_path' do
+ tmp_path = File.expand_path(File.join(File.dirname(__FILE__), 'fixtures', 'tmp'))
+ FileUtils.mkdir_p(tmp_path)
+
+ code = <<-'CODE'
+ def foo
+ puts 'foo'
+ # bar
+ end
+ CODE
+
+ file_path = "#{tmp_path}/stats.rb"
+ File.open(file_path, 'w') { |f| f.write(code) }
+
+ @code_statistics_calculator.add_by_file_path(file_path)
+
+ assert_equal 4, @code_statistics_calculator.lines
+ assert_equal 3, @code_statistics_calculator.code_lines
+ assert_equal 0, @code_statistics_calculator.classes
+ assert_equal 1, @code_statistics_calculator.methods
+
+ FileUtils.rm_rf(tmp_path)
+ end
+
+ test 'calculate number of Ruby methods' do
+ code = <<-'CODE'
+ def foo
+ puts 'foo'
+ end
+
+ def bar; end
+
+ class Foo
+ def bar(abc)
+ end
+ end
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rb)
+
+ assert_equal 3, @code_statistics_calculator.methods
+ end
+
+ test 'calculate Ruby LOCs' do
+ code = <<-'CODE'
+ def foo
+ puts 'foo'
+ end
+
+ # def bar; end
+
+ class A < B
+ end
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rb)
+
+ assert_equal 8, @code_statistics_calculator.lines
+ assert_equal 5, @code_statistics_calculator.code_lines
+ end
+
+ test 'calculate number of Ruby classes' do
+ code = <<-'CODE'
+ class Foo < Bar
+ def foo
+ puts 'foo'
+ end
+ end
+
+ class Z; end
+
+ # class A
+ # end
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rb)
+
+ assert_equal 2, @code_statistics_calculator.classes
+ end
+
+ test 'skip Ruby comments' do
+ code = <<-'CODE'
+=begin
+ class Foo
+ def foo
+ puts 'foo'
+ end
+ end
+=end
+
+ # class A
+ # end
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :rb)
+
+ assert_equal 10, @code_statistics_calculator.lines
+ assert_equal 0, @code_statistics_calculator.code_lines
+ assert_equal 0, @code_statistics_calculator.classes
+ assert_equal 0, @code_statistics_calculator.methods
+ end
+
+ test 'calculate number of JS methods' do
+ code = <<-'CODE'
+ function foo(x, y, z) {
+ doX();
+ }
+
+ $(function () {
+ bar();
+ })
+
+ var baz = function ( x ) {
+ }
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :js)
+
+ assert_equal 3, @code_statistics_calculator.methods
+ end
+
+ test 'calculate JS LOCs' do
+ code = <<-'CODE'
+ function foo()
+ alert('foo');
+ end
+
+ // var b = 2;
+
+ var a = 1;
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :js)
+
+ assert_equal 7, @code_statistics_calculator.lines
+ assert_equal 4, @code_statistics_calculator.code_lines
+ end
+
+ test 'skip JS comments' do
+ code = <<-'CODE'
+ /*
+ * var f = function () {
+ 1 / 2;
+ }
+ */
+
+ // call();
+ //
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :js)
+
+ assert_equal 8, @code_statistics_calculator.lines
+ assert_equal 0, @code_statistics_calculator.code_lines
+ assert_equal 0, @code_statistics_calculator.classes
+ assert_equal 0, @code_statistics_calculator.methods
+ end
+
+ test 'calculate number of CoffeeScript methods' do
+ code = <<-'CODE'
+ square = (x) -> x * x
+
+ math =
+ cube: (x) -> x * square x
+
+ fill = (container, liquid = "coffee") ->
+ "Filling the #{container} with #{liquid}..."
+
+ $('.shopping_cart').bind 'click', (event) =>
+ @customer.purchase @cart
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee)
+
+ assert_equal 4, @code_statistics_calculator.methods
+ end
+
+ test 'calculate CoffeeScript LOCs' do
+ code = <<-'CODE'
+ # Assignment:
+ number = 42
+ opposite = true
+
+ ###
+ CoffeeScript Compiler v1.4.0
+ Released under the MIT License
+ ###
+
+ # Conditions:
+ number = -42 if opposite
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee)
+
+ assert_equal 11, @code_statistics_calculator.lines
+ assert_equal 3, @code_statistics_calculator.code_lines
+ end
+
+ test 'calculate number of CoffeeScript classes' do
+ code = <<-'CODE'
+ class Animal
+ constructor: (@name) ->
+
+ move: (meters) ->
+ alert @name + " moved #{meters}m."
+
+ class Snake extends Animal
+ move: ->
+ alert "Slithering..."
+ super 5
+
+ # class Horse
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee)
+
+ assert_equal 2, @code_statistics_calculator.classes
+ end
+
+ test 'skip CoffeeScript comments' do
+ code = <<-'CODE'
+###
+class Animal
+ constructor: (@name) ->
+
+ move: (meters) ->
+ alert @name + " moved #{meters}m."
+ ###
+
+ # class Horse
+ alert 'hello'
+ CODE
+
+ @code_statistics_calculator.add_by_io(StringIO.new(code), :coffee)
+
+ assert_equal 10, @code_statistics_calculator.lines
+ assert_equal 1, @code_statistics_calculator.code_lines
+ assert_equal 0, @code_statistics_calculator.classes
+ assert_equal 0, @code_statistics_calculator.methods
+ end
+end
diff --git a/railties/test/commands/console_test.rb b/railties/test/commands/console_test.rb
index e047d4882d..a34beaedb3 100644
--- a/railties/test/commands/console_test.rb
+++ b/railties/test/commands/console_test.rb
@@ -1,14 +1,14 @@
require 'abstract_unit'
+require 'env_helpers'
require 'rails/commands/console'
class Rails::ConsoleTest < ActiveSupport::TestCase
+ include EnvHelpers
+
class FakeConsole
def self.start; end
end
- def setup
- end
-
def test_sandbox_option
console = Rails::Console.new(app, parse_arguments(["--sandbox"]))
assert console.sandbox?
@@ -58,10 +58,7 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
end
def test_console_defaults_to_IRB
- config = mock("config", console: nil)
- app = mock("app", config: config)
- app.expects(:load_console).returns(nil)
-
+ app = build_app(console: nil)
assert_equal IRB, Rails::Console.new(app).console
end
@@ -78,7 +75,14 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert_match(/\sspecial-production\s/, output)
end
end
-
+
+ def test_default_environment_with_rack_env
+ with_rack_env 'production' do
+ start
+ assert_match(/\sproduction\s/, output)
+ end
+ end
+
def test_e_option
start ['-e', 'special-production']
assert_match(/\sspecial-production\s/, output)
@@ -104,34 +108,35 @@ class Rails::ConsoleTest < ActiveSupport::TestCase
assert_match(/\sdevelopment\s/, output)
end
- private
+ 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'])
+ assert_match('dev', options[:environment])
+ end
attr_reader :output
+ private :output
+
+ private
def start(argv = [])
rails_console = Rails::Console.new(app, parse_arguments(argv))
- @output = output = capture(:stdout) { rails_console.start }
+ @output = capture(:stdout) { rails_console.start }
end
def app
- @app ||= begin
- config = mock("config", console: FakeConsole)
- app = mock("app", config: config)
- app.stubs(:sandbox=).returns(nil)
- app.expects(:load_console)
- app
- end
+ @app ||= build_app(console: FakeConsole)
end
- def parse_arguments(args)
- Rails::Console.parse_arguments(args)
+ def build_app(config)
+ config = mock("config", config)
+ app = mock("app", config: config)
+ app.stubs(:sandbox=).returns(nil)
+ app.expects(:load_console)
+ app
end
- def with_rails_env(env)
- original_rails_env = ENV['RAILS_ENV']
- ENV['RAILS_ENV'] = env
- yield
- ensure
- ENV['RAILS_ENV'] = original_rails_env
+ def parse_arguments(args)
+ Rails::Console.parse_arguments(args)
end
end
diff --git a/railties/test/commands/dbconsole_test.rb b/railties/test/commands/dbconsole_test.rb
index d45bdaabf5..edb92b3aa2 100644
--- a/railties/test/commands/dbconsole_test.rb
+++ b/railties/test/commands/dbconsole_test.rb
@@ -45,6 +45,18 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
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])
+ 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])
+ end
+
def test_mysql
dbconsole.expects(:find_cmd_and_exec).with(%w[mysql mysql5], 'db')
start(adapter: 'mysql', database: 'db')
@@ -116,6 +128,14 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
assert !aborted
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
+ end
+
def test_oracle
dbconsole.expects(:find_cmd_and_exec).with('sqlplus', 'user@db')
start(adapter: 'oracle', database: 'db', username: 'user', password: 'secret')
@@ -152,8 +172,10 @@ class Rails::DBConsoleTest < ActiveSupport::TestCase
assert_match(/Usage:.*dbconsole/, stdout)
end
- private
attr_reader :aborted, :output
+ private :aborted, :output
+
+ private
def dbconsole
@dbconsole ||= Rails::DBConsole.new(nil)
diff --git a/railties/test/commands/server_test.rb b/railties/test/commands/server_test.rb
index 4a3ea82e3d..ba688f1e9e 100644
--- a/railties/test/commands/server_test.rb
+++ b/railties/test/commands/server_test.rb
@@ -1,7 +1,9 @@
require 'abstract_unit'
+require 'env_helpers'
require 'rails/commands/server'
class Rails::ServerTest < ActiveSupport::TestCase
+ include EnvHelpers
def test_environment_with_server_option
args = ["thin", "-e", "production"]
@@ -23,4 +25,64 @@ class Rails::ServerTest < ActiveSupport::TestCase
assert_nil options[:environment]
assert_equal 'thin', options[:server]
end
+
+ def test_environment_with_rails_env
+ with_rack_env nil do
+ with_rails_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
+ end
+ end
+
+ def test_environment_with_rack_env
+ with_rails_env nil do
+ with_rack_env 'production' do
+ server = Rails::Server.new
+ assert_equal 'production', server.options[:environment]
+ end
+ end
+ end
+
+ def test_log_stdout
+ with_rack_env nil do
+ with_rails_env nil do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "development"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+
+ args = ["-e", "production"]
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+
+ with_rack_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rack_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+
+ with_rails_env 'development' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal true, options[:log_stdout]
+ end
+
+ with_rails_env 'production' do
+ args = []
+ options = Rails::Server::Options.new.parse!(args)
+ assert_equal false, options[:log_stdout]
+ end
+ end
+ end
+ end
end
diff --git a/railties/test/configuration/middleware_stack_proxy_test.rb b/railties/test/configuration/middleware_stack_proxy_test.rb
index 5984c0b425..2442cb995d 100644
--- a/railties/test/configuration/middleware_stack_proxy_test.rb
+++ b/railties/test/configuration/middleware_stack_proxy_test.rb
@@ -1,6 +1,7 @@
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'rails/configuration'
require 'active_support/test_case'
+require 'minitest/mock'
module Rails
module Configuration
diff --git a/railties/test/engine_test.rb b/railties/test/engine_test.rb
index addf49cdb6..7970913d21 100644
--- a/railties/test/engine_test.rb
+++ b/railties/test/engine_test.rb
@@ -1,7 +1,7 @@
require 'abstract_unit'
class EngineTest < ActiveSupport::TestCase
- it "reports routes as available only if they're actually present" do
+ test "reports routes as available only if they're actually present" do
engine = Class.new(Rails::Engine) do
def initialize(*args)
@routes = nil
diff --git a/railties/test/env_helpers.rb b/railties/test/env_helpers.rb
new file mode 100644
index 0000000000..330fe150ca
--- /dev/null
+++ b/railties/test/env_helpers.rb
@@ -0,0 +1,30 @@
+require 'rails'
+
+module EnvHelpers
+ private
+
+ def with_rails_env(env)
+ Rails.instance_variable_set :@_env, nil
+ switch_env 'RAILS_ENV', env do
+ switch_env 'RACK_ENV', nil do
+ yield
+ end
+ end
+ end
+
+ def with_rack_env(env)
+ Rails.instance_variable_set :@_env, nil
+ switch_env 'RACK_ENV', env do
+ switch_env 'RAILS_ENV', nil do
+ yield
+ end
+ end
+ end
+
+ def switch_env(key, value)
+ old, ENV[key] = ENV[key], value
+ yield
+ ensure
+ ENV[key] = old
+ end
+end
diff --git a/railties/test/fixtures/lib/app_builders/empty_builder.rb b/railties/test/fixtures/lib/app_builders/empty_builder.rb
deleted file mode 100644
index babd9c2461..0000000000
--- a/railties/test/fixtures/lib/app_builders/empty_builder.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class AppBuilder
-end \ No newline at end of file
diff --git a/railties/test/fixtures/lib/app_builders/simple_builder.rb b/railties/test/fixtures/lib/app_builders/simple_builder.rb
deleted file mode 100644
index 993d3a2aa2..0000000000
--- a/railties/test/fixtures/lib/app_builders/simple_builder.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class AppBuilder
- def gitignore
- create_file ".gitignore", <<-R.strip
-foobar
- R
- end
-end
diff --git a/railties/test/fixtures/lib/app_builders/tweak_builder.rb b/railties/test/fixtures/lib/app_builders/tweak_builder.rb
deleted file mode 100644
index cb50be01cb..0000000000
--- a/railties/test/fixtures/lib/app_builders/tweak_builder.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class AppBuilder < Rails::AppBuilder
- def gitignore
- create_file ".gitignore", <<-R.strip
-foobar
- R
- end
-end
diff --git a/railties/test/fixtures/lib/plugin_builders/empty_builder.rb b/railties/test/fixtures/lib/plugin_builders/empty_builder.rb
deleted file mode 100644
index 5c5607621c..0000000000
--- a/railties/test/fixtures/lib/plugin_builders/empty_builder.rb
+++ /dev/null
@@ -1,2 +0,0 @@
-class PluginBuilder
-end
diff --git a/railties/test/fixtures/lib/plugin_builders/simple_builder.rb b/railties/test/fixtures/lib/plugin_builders/simple_builder.rb
deleted file mode 100644
index 08f6c5535d..0000000000
--- a/railties/test/fixtures/lib/plugin_builders/simple_builder.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class PluginBuilder
- def gitignore
- create_file ".gitignore", <<-R.strip
-foobar
- R
- end
-end
diff --git a/railties/test/fixtures/lib/plugin_builders/spec_builder.rb b/railties/test/fixtures/lib/plugin_builders/spec_builder.rb
deleted file mode 100644
index 2721429599..0000000000
--- a/railties/test/fixtures/lib/plugin_builders/spec_builder.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-class PluginBuilder < Rails::PluginBuilder
- def test
- create_file "spec/spec_helper.rb"
- append_file "Rakefile", <<-EOF
-# spec tasks in rakefile
-
-task default: :spec
- EOF
- end
-
- def generate_test_dummy
- dummy_path("spec/dummy")
- super
- end
-
- def skip_test_unit?
- true
- end
-end
diff --git a/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb b/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb
deleted file mode 100644
index 1e801409a4..0000000000
--- a/railties/test/fixtures/lib/plugin_builders/tweak_builder.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-class PluginBuilder < Rails::PluginBuilder
- def gitignore
- create_file ".gitignore", <<-R.strip
-foobar
- R
- end
-end
diff --git a/railties/test/generators/actions_test.rb b/railties/test/generators/actions_test.rb
index 8af92479c3..0db40c1d32 100644
--- a/railties/test/generators/actions_test.rb
+++ b/railties/test/generators/actions_test.rb
@@ -1,8 +1,11 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/app/app_generator'
+require 'env_helpers'
class ActionsTest < Rails::Generators::TestCase
include GeneratorsTestHelper
+ include EnvHelpers
+
tests Rails::Generators::AppGenerator
arguments [destination_root]
@@ -100,7 +103,7 @@ class ActionsTest < Rails::Generators::TestCase
run_generator
autoload_paths = 'config.autoload_paths += %w["#{Rails.root}/app/extras"]'
action :environment, autoload_paths, env: 'development'
- assert_file "config/environments/development.rb", /Application\.configure do\n #{Regexp.escape(autoload_paths)}/
+ assert_file "config/environments/development.rb", /Rails\.application\.configure do\n #{Regexp.escape(autoload_paths)}/
end
def test_environment_with_block_should_include_block_contents_in_environment_initializer_block
@@ -148,16 +151,15 @@ class ActionsTest < Rails::Generators::TestCase
end
def test_generate_should_run_script_generate_with_argument_and_options
- generator.expects(:run_ruby_script).once.with('script/rails generate model MyModel', verbose: false)
+ generator.expects(:run_ruby_script).once.with('bin/rails generate model MyModel', verbose: false)
action :generate, 'model', 'MyModel'
end
def test_rake_should_run_rake_command_with_default_env
generator.expects(:run).once.with("rake log:clear RAILS_ENV=development", verbose: false)
- old_env, ENV['RAILS_ENV'] = ENV["RAILS_ENV"], nil
- action :rake, 'log:clear'
- ensure
- ENV["RAILS_ENV"] = old_env
+ with_rails_env nil do
+ action :rake, 'log:clear'
+ end
end
def test_rake_with_env_option_should_run_rake_command_in_env
@@ -167,26 +169,23 @@ class ActionsTest < Rails::Generators::TestCase
def test_rake_with_rails_env_variable_should_run_rake_command_in_env
generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false)
- old_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "production"
- action :rake, 'log:clear'
- ensure
- ENV["RAILS_ENV"] = old_env
+ with_rails_env "production" do
+ action :rake, 'log:clear'
+ end
end
def test_env_option_should_win_over_rails_env_variable_when_running_rake
generator.expects(:run).once.with('rake log:clear RAILS_ENV=production', verbose: false)
- old_env, ENV["RAILS_ENV"] = ENV["RAILS_ENV"], "staging"
- action :rake, 'log:clear', env: 'production'
- ensure
- ENV["RAILS_ENV"] = old_env
+ with_rails_env "staging" do
+ action :rake, 'log:clear', env: 'production'
+ end
end
def test_rake_with_sudo_option_should_run_rake_command_with_sudo
generator.expects(:run).once.with("sudo rake log:clear RAILS_ENV=development", verbose: false)
- old_env, ENV['RAILS_ENV'] = ENV["RAILS_ENV"], nil
- action :rake, 'log:clear', sudo: true
- ensure
- ENV["RAILS_ENV"] = old_env
+ with_rails_env nil do
+ action :rake, 'log:clear', sudo: true
+ end
end
def test_capify_should_run_the_capify_command
@@ -204,14 +203,14 @@ class ActionsTest < Rails::Generators::TestCase
def test_readme
run_generator
Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root)
- assert_match(/Welcome to Rails/, action(:readme, "README.rdoc"))
+ assert_match "application up and running", action(:readme, "README.rdoc")
end
def test_readme_with_quiet
generator(default_arguments, quiet: true)
run_generator
Rails::Generators::AppGenerator.expects(:source_root).times(2).returns(destination_root)
- assert_no_match(/Welcome to Rails/, action(:readme, "README.rdoc"))
+ assert_no_match "application up and running", action(:readme, "README.rdoc")
end
def test_log
diff --git a/railties/test/generators/app_generator_test.rb b/railties/test/generators/app_generator_test.rb
index e5397bf4f3..39d4acb7b2 100644
--- a/railties/test/generators/app_generator_test.rb
+++ b/railties/test/generators/app_generator_test.rb
@@ -4,6 +4,7 @@ require 'generators/shared_generator_tests'
DEFAULT_APP_FILES = %w(
.gitignore
+ README.rdoc
Gemfile
Rakefile
config.ru
@@ -11,27 +12,29 @@ DEFAULT_APP_FILES = %w(
app/assets/stylesheets
app/assets/images
app/controllers
+ app/controllers/concerns
app/helpers
app/mailers
app/models
+ app/models/concerns
app/views/layouts
+ bin/bundle
+ bin/rails
+ bin/rake
config/environments
config/initializers
config/locales
db
- doc
lib
lib/tasks
lib/assets
log
- script/rails
test/fixtures
test/controllers
test/models
test/helpers
test/mailers
test/integration
- test/performance
vendor
vendor/assets
tmp/cache
@@ -51,11 +54,10 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_assets
run_generator
- assert_file "app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application"/
- assert_file "app/views/layouts/application.html.erb", /javascript_include_tag\s+"application"/
- assert_file "app/assets/stylesheets/application.css"
- assert_file "config/application.rb", /config\.assets\.enabled = true/
- assert_file "public/index.html", /url\("assets\/rails.png"\);/
+
+ assert_file("app/views/layouts/application.html.erb", /stylesheet_link_tag\s+"application", media: "all", "data-turbolinks-track" => true/)
+ assert_file("app/views/layouts/application.html.erb", /javascript_include_tag\s+"application", "data-turbolinks-track" => true/)
+ assert_file("app/assets/stylesheets/application.css")
end
def test_invalid_application_name_raises_an_error
@@ -65,12 +67,12 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_invalid_application_name_is_fixed
run_generator [File.join(destination_root, "things-43")]
- assert_file "things-43/config/environment.rb", /Things43::Application\.initialize!/
+ assert_file "things-43/config/environment.rb", /Rails\.application\.initialize!/
assert_file "things-43/config/application.rb", /^module Things43$/
end
def test_application_new_exits_with_non_zero_code_on_invalid_application_name
- quietly { system 'rails new test' }
+ quietly { system 'rails new test --no-rc' }
assert_equal false, $?.success?
end
@@ -111,7 +113,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
destination_root: app_moved_root, shell: @shell
generator.send(:app_const)
quietly { generator.send(:create_config_files) }
- assert_file "myapp_moved/config/environment.rb", /Myapp::Application\.initialize!/
+ assert_file "myapp_moved/config/environment.rb", /Rails\.application\.initialize!/
assert_file "myapp_moved/config/initializers/session_store.rb", /_myapp_session/
end
@@ -131,7 +133,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_application_names_are_not_singularized
run_generator [File.join(destination_root, "hats")]
- assert_file "hats/config/environment.rb", /Hats::Application\.initialize!/
+ assert_file "hats/config/environment.rb", /Rails\.application\.initialize!/
end
def test_gemfile_has_no_whitespace_errors
@@ -164,6 +166,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_config_database_app_name_with_period
+ run_generator [File.join(destination_root, "common.usage.com"), "-d", "postgresql"]
+ assert_file "common.usage.com/config/database.yml", /common_usage_com/
+ end
+
def test_config_postgresql_database
run_generator([destination_root, "-d", "postgresql"])
assert_file "config/database.yml", /postgresql/
@@ -178,9 +185,6 @@ class AppGeneratorTest < Rails::Generators::TestCase
run_generator([destination_root, "-d", "jdbcmysql"])
assert_file "config/database.yml", /mysql/
assert_gem "activerecord-jdbcmysql-adapter"
- # TODO: When the JRuby guys merge jruby-openssl in
- # jruby this will be removed
- assert_gem "jruby-openssl" if defined?(JRUBY_VERSION)
end
def test_config_jdbcsqlite3_database
@@ -217,28 +221,33 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "test/test_helper.rb" do |helper_content|
assert_no_match(/fixtures :all/, helper_content)
end
- assert_file "test/performance/browsing_test.rb"
+ end
+
+ def test_generator_if_skip_action_view_is_given
+ run_generator [destination_root, "--skip-action-view"]
+ assert_file "config/application.rb", /#\s+require\s+["']action_view\/railtie["']/
end
def test_generator_if_skip_sprockets_is_given
run_generator [destination_root, "--skip-sprockets"]
assert_file "config/application.rb" do |content|
- assert_match(/#\s+require\s+["']sprockets\/rails\/railtie["']/, content)
- assert_no_match(/config\.assets\.enabled = true/, content)
+ assert_match(/#\s+require\s+["']sprockets\/railtie["']/, content)
+ assert_match(/config\.assets\.enabled = false/, content)
end
assert_file "Gemfile" do |content|
assert_no_match(/sass-rails/, content)
- assert_no_match(/coffee-rails/, content)
assert_no_match(/uglifier/, content)
+ assert_match(/coffee-rails/, content)
end
assert_file "config/environments/development.rb" do |content|
assert_no_match(/config\.assets\.debug = true/, content)
end
assert_file "config/environments/production.rb" do |content|
assert_no_match(/config\.assets\.digest = true/, content)
- assert_no_match(/config\.assets\.compress = 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
- assert_file "test/performance/browsing_test.rb"
end
def test_inclusion_of_javascript_runtime
@@ -246,22 +255,20 @@ class AppGeneratorTest < Rails::Generators::TestCase
if defined?(JRUBY_VERSION)
assert_gem "therubyrhino"
else
- assert_file "Gemfile", /# gem\s+["']therubyracer["']+, platforms: :ruby$/
+ assert_file "Gemfile", /# gem\s+["']therubyracer["']+, \s+platforms: :ruby$/
end
end
- def test_generator_if_skip_index_html_is_given
- run_generator [destination_root, '--skip-index-html']
- assert_no_file 'public/index.html'
- assert_no_file 'app/assets/images/rails.png'
- assert_file 'app/assets/images/.keep'
- end
-
def test_creation_of_a_test_directory
run_generator
assert_file 'test'
end
+ def test_creation_of_app_assets_images_directory
+ run_generator
+ assert_file "app/assets/images"
+ end
+
def test_creation_of_vendor_assets_javascripts_directory
run_generator
assert_file "vendor/assets/javascripts"
@@ -278,7 +285,7 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_match %r{^//= require jquery}, contents
assert_match %r{^//= require jquery_ujs}, contents
end
- assert_gem "jquery-rails"
+ assert_file "Gemfile", /^gem 'jquery-rails'/
end
def test_other_javascript_libraries
@@ -292,9 +299,28 @@ class AppGeneratorTest < Rails::Generators::TestCase
def test_javascript_is_skipped_if_required
run_generator [destination_root, "--skip-javascript"]
- assert_file "app/assets/javascripts/application.js" do |contents|
- assert_no_match %r{^//=\s+require\s}, contents
+
+ assert_no_file "app/assets/javascripts"
+ assert_no_file "vendor/assets/javascripts"
+
+ assert_file "app/views/layouts/application.html.erb" do |contents|
+ assert_match(/stylesheet_link_tag\s+"application", media: "all" %>/, contents)
+ assert_no_match(/javascript_include_tag\s+"application" \%>/, contents)
end
+
+ assert_file "Gemfile" do |content|
+ assert_no_match(/coffee-rails/, content)
+ end
+ end
+
+ def test_inclusion_of_web_console
+ run_generator
+ assert_file "Gemfile", /gem 'web-console', \s+group: :development/
+ end
+
+ def test_inclusion_of_jbuilder
+ run_generator
+ assert_file "Gemfile", /gem 'jbuilder'/
end
def test_inclusion_of_debugger
@@ -302,6 +328,11 @@ class AppGeneratorTest < Rails::Generators::TestCase
assert_file "Gemfile", /# gem 'debugger'/
end
+ def test_inclusion_of_lazy_loaded_sdoc
+ run_generator
+ assert_file 'Gemfile', /gem 'sdoc', \s+group: :doc, require: false/
+ end
+
def test_template_from_dir_pwd
FileUtils.cd(Rails.root)
assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]))
@@ -344,52 +375,28 @@ class AppGeneratorTest < Rails::Generators::TestCase
end
end
- def test_generated_environments_file_for_auto_explain
- run_generator [destination_root, "--skip-active-record"]
- %w(development production).each do |env|
- assert_file "config/environments/#{env}.rb" do |file|
- assert_no_match %r(auto_explain_threshold_in_seconds), file
- end
- end
- end
-
def test_pretend_option
output = run_generator [File.join(destination_root, "myapp"), "--pretend"]
assert_no_match(/run bundle install/, output)
end
-protected
+ def test_application_name_with_spaces
+ path = File.join(destination_root, "foo bar".shellescape)
- def action(*args, &block)
- silence(:stdout) { generator.send(*args, &block) }
- end
+ # This also applies to MySQL apps but not with SQLite
+ run_generator [path, "-d", 'postgresql']
- def assert_gem(gem)
- assert_file "Gemfile", /^gem\s+["']#{gem}["']$/
+ assert_file "foo bar/config/database.yml", /database: foo_bar_development/
+ assert_file "foo bar/config/initializers/session_store.rb", /key: '_foo_bar/
end
-end
-
-class CustomAppGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- tests Rails::Generators::AppGenerator
-
- arguments [destination_root]
- include SharedCustomGeneratorTests
protected
- def default_files
- ::DEFAULT_APP_FILES
- end
-
- def builders_dir
- "app_builders"
- end
-
- def builder_class
- :AppBuilder
- end
def action(*args, &block)
silence(:stdout) { generator.send(*args, &block) }
end
+
+ def assert_gem(gem)
+ assert_file "Gemfile", /^gem\s+["']#{gem}["']$/
+ end
end
diff --git a/railties/test/generators/argv_scrubber_test.rb b/railties/test/generators/argv_scrubber_test.rb
new file mode 100644
index 0000000000..a94350cbd7
--- /dev/null
+++ b/railties/test/generators/argv_scrubber_test.rb
@@ -0,0 +1,136 @@
+require 'active_support/test_case'
+require 'active_support/testing/autorun'
+require 'rails/generators/rails/app/app_generator'
+require 'tempfile'
+
+module Rails
+ module Generators
+ class ARGVScrubberTest < ActiveSupport::TestCase # :nodoc:
+ # Future people who read this... These tests are just to surround the
+ # current behavior of the ARGVScrubber, they do not mean that the class
+ # *must* act this way, I just want to prevent regressions.
+
+ def test_version
+ ['-v', '--version'].each do |str|
+ scrubber = ARGVScrubber.new [str]
+ output = nil
+ exit_code = nil
+ scrubber.extend(Module.new {
+ define_method(:puts) { |str| output = str }
+ define_method(:exit) { |code| exit_code = code }
+ })
+ scrubber.prepare!
+ assert_equal "Rails #{Rails::VERSION::STRING}", output
+ assert_equal 0, exit_code
+ end
+ end
+
+ def test_default_help
+ argv = ['zomg', 'how', 'are', 'you']
+ scrubber = ARGVScrubber.new argv
+ args = scrubber.prepare!
+ assert_equal ['--help'] + argv.drop(1), args
+ end
+
+ def test_prepare_returns_args
+ scrubber = ARGVScrubber.new ['hi mom']
+ args = scrubber.prepare!
+ assert_equal '--help', args.first
+ end
+
+ def test_no_mutations
+ scrubber = ARGVScrubber.new ['hi mom'].freeze
+ args = scrubber.prepare!
+ assert_equal '--help', args.first
+ end
+
+ def test_new_command_no_rc
+ scrubber = Class.new(ARGVScrubber) {
+ def self.default_rc_file
+ File.join(Dir.tmpdir, 'whatever')
+ end
+ }.new ['new']
+ args = scrubber.prepare!
+ assert_equal [], args
+ end
+
+ def test_new_homedir_rc
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello-world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_singleton_method(:default_rc_file) do
+ file.path
+ end
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new']
+ args = scrubber.prepare!
+ assert_equal ['--hello-world'], args
+ assert_match 'hello-world', message
+ assert_match file.path, message
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_rc_whitespace_separated
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello --world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new', "--rc=#{file.path}"]
+ args = scrubber.prepare!
+ assert_equal ['--hello', '--world'], args
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_new_rc_option
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello-world'
+ file.flush
+
+ message = nil
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| message = msg }
+ }.new ['new', "--rc=#{file.path}"]
+ args = scrubber.prepare!
+ assert_equal ['--hello-world'], args
+ assert_match 'hello-world', message
+ assert_match file.path, message
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_new_rc_option_and_custom_options
+ file = Tempfile.new 'myrcfile'
+ file.puts '--hello'
+ file.puts '--world'
+ file.flush
+
+ scrubber = Class.new(ARGVScrubber) {
+ define_method(:puts) { |msg| }
+ }.new ['new', 'tenderapp', '--love', "--rc=#{file.path}"]
+
+ args = scrubber.prepare!
+ assert_equal ["tenderapp", "--hello", "--world", "--love"], args
+ ensure
+ file.close
+ file.unlink
+ end
+
+ def test_no_rc
+ scrubber = ARGVScrubber.new ['new', '--no-rc']
+ args = scrubber.prepare!
+ assert_equal [], args
+ end
+ end
+ end
+end
diff --git a/railties/test/generators/controller_generator_test.rb b/railties/test/generators/controller_generator_test.rb
index 5205deafd9..9c664a903a 100644
--- a/railties/test/generators/controller_generator_test.rb
+++ b/railties/test/generators/controller_generator_test.rb
@@ -82,4 +82,9 @@ class ControllerGeneratorTest < Rails::Generators::TestCase
assert_instance_method :bar, controller
end
end
+
+ def test_namespaced_routes_are_created_in_routes
+ run_generator ["admin/dashboard", "index"]
+ assert_file "config/routes.rb", /namespace :admin do\n\s+get "dashboard\/index"\n/
+ end
end
diff --git a/railties/test/generators/generated_attribute_test.rb b/railties/test/generators/generated_attribute_test.rb
index 6ab1cd58c7..c48bc20899 100644
--- a/railties/test/generators/generated_attribute_test.rb
+++ b/railties/test/generators/generated_attribute_test.rb
@@ -117,13 +117,13 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
assert create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
end
end
-
+
def test_polymorphic_reference_is_false
%w(foo bar baz).each do |attribute_type|
assert !create_generated_attribute("#{attribute_type}{polymorphic}").polymorphic?
end
end
-
+
def test_blank_type_defaults_to_string_raises_exception
assert_equal :string, create_generated_attribute(nil, 'title').type
assert_equal :string, create_generated_attribute("", 'title').type
@@ -132,6 +132,13 @@ class GeneratedAttributeTest < Rails::Generators::TestCase
def test_handles_index_names_for_references
assert_equal "post", create_generated_attribute('string', 'post').index_name
assert_equal "post_id", create_generated_attribute('references', 'post').index_name
+ assert_equal "post_id", create_generated_attribute('belongs_to', 'post').index_name
assert_equal ["post_id", "post_type"], create_generated_attribute('references{polymorphic}', 'post').index_name
end
+
+ def test_handles_column_names_for_references
+ assert_equal "post", create_generated_attribute('string', 'post').column_name
+ assert_equal "post_id", create_generated_attribute('references', 'post').column_name
+ assert_equal "post_id", create_generated_attribute('belongs_to', 'post').column_name
+ end
end
diff --git a/railties/test/generators/generator_generator_test.rb b/railties/test/generators/generator_generator_test.rb
index f4c975fc18..dcfeaaa8e0 100644
--- a/railties/test/generators/generator_generator_test.rb
+++ b/railties/test/generators/generator_generator_test.rb
@@ -16,6 +16,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/awesome/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/awesome_generator_test.rb",
+ /class AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/awesome\/awesome_generator'/
end
def test_namespaced_generator_skeleton
@@ -29,6 +32,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/rails/awesome/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/rails/awesome_generator_test.rb",
+ /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/rails\/awesome\/awesome_generator'/
end
def test_generator_skeleton_is_created_without_file_name_namespace
@@ -42,6 +48,9 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/awesome_generator.rb",
/class AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/awesome_generator_test.rb",
+ /class AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/awesome_generator'/
end
def test_namespaced_generator_skeleton_without_file_name_namespace
@@ -55,5 +64,8 @@ class GeneratorGeneratorTest < Rails::Generators::TestCase
assert_file "lib/generators/rails/awesome_generator.rb",
/class Rails::AwesomeGenerator < Rails::Generators::NamedBase/
+ assert_file "test/lib/generators/rails/awesome_generator_test.rb",
+ /class Rails::AwesomeGeneratorTest < Rails::Generators::TestCase/,
+ /require 'generators\/rails\/awesome_generator'/
end
end
diff --git a/railties/test/generators/generator_test.rb b/railties/test/generators/generator_test.rb
new file mode 100644
index 0000000000..7871399dd7
--- /dev/null
+++ b/railties/test/generators/generator_test.rb
@@ -0,0 +1,85 @@
+require 'active_support/test_case'
+require 'active_support/testing/autorun'
+require 'rails/generators/app_base'
+
+module Rails
+ module Generators
+ class GeneratorTest < ActiveSupport::TestCase
+ def make_builder_class
+ Class.new(AppBase) do
+ add_shared_options_for "application"
+
+ # include a module to get around thor's method_added hook
+ include(Module.new {
+ def gemfile_entries; super; end
+ def invoke_all; super; self; end
+ def add_gem_entry_filter; super; end
+ def gemfile_entry(*args); super; end
+ })
+ end
+ end
+
+ def test_construction
+ klass = make_builder_class
+ assert klass.start(['new', 'blah'])
+ end
+
+ def test_add_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove'
+ assert_includes generator.gemfile_entries.map(&:name), 'tenderlove'
+ end
+
+ def test_add_gem_with_version
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', '2.0.0'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.version == '2.0.0'
+ }
+ end
+
+ def test_add_github_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', github: 'hello world'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.options[:github] == 'hello world'
+ }
+ end
+
+ def test_add_path_gem
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ generator.gemfile_entry 'tenderlove', path: 'hello world'
+ assert generator.gemfile_entries.find { |gfe|
+ gfe.name == 'tenderlove' && gfe.options[:path] == 'hello world'
+ }
+ end
+
+ def test_filter
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ gems = generator.gemfile_entries
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems.first.name
+ }
+ assert_equal gems.drop(1), generator.gemfile_entries
+ end
+
+ def test_two_filters
+ klass = make_builder_class
+ generator = klass.start(['new', 'blah'])
+ gems = generator.gemfile_entries
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems.first.name
+ }
+ generator.add_gem_entry_filter { |gem|
+ gem.name != gems[1].name
+ }
+ assert_equal gems.drop(2), generator.gemfile_entries
+ end
+ end
+ end
+end
diff --git a/railties/test/generators/generators_test_helper.rb b/railties/test/generators/generators_test_helper.rb
index 7fdd54fc30..32a3d072c9 100644
--- a/railties/test/generators/generators_test_helper.rb
+++ b/railties/test/generators/generators_test_helper.rb
@@ -16,6 +16,7 @@ Rails.application.load_generators
require 'active_record'
require 'action_dispatch'
+require 'action_view'
module GeneratorsTestHelper
def self.included(base)
diff --git a/railties/test/generators/migration_generator_test.rb b/railties/test/generators/migration_generator_test.rb
index 15e5a0b92b..d876597944 100644
--- a/railties/test/generators/migration_generator_test.rb
+++ b/railties/test/generators/migration_generator_test.rb
@@ -28,7 +28,7 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration]
assert_migration "db/migrate/change_title_body_from_posts.rb", /class #{migration} < ActiveRecord::Migration/
end
-
+
def test_migration_with_invalid_file_name
migration = "add_something:datetime"
assert_raise ActiveRecord::IllegalMigrationNameError do
@@ -41,9 +41,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_column :posts, :title, :string/, up)
- assert_match(/add_column :posts, :body, :text/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_column :posts, :title, :string/, change)
+ assert_match(/add_column :posts, :body, :text/, change)
end
end
end
@@ -53,15 +53,10 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string:index", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :up, content do |up|
- assert_match(/remove_column :posts, :title/, up)
- assert_match(/remove_column :posts, :body/, up)
- end
-
- assert_method :down, content do |down|
- assert_match(/add_column :posts, :title, :string/, down)
- assert_match(/add_column :posts, :body, :text/, down)
- assert_match(/add_index :posts, :title/, down)
+ assert_method :change, content do |change|
+ assert_match(/remove_column :posts, :title, :string/, change)
+ assert_match(/remove_column :posts, :body, :text/, change)
+ assert_match(/remove_index :posts, :title/, change)
end
end
end
@@ -71,14 +66,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string", "body:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :up, content do |up|
- assert_match(/remove_column :posts, :title/, up)
- assert_match(/remove_column :posts, :body/, up)
- end
-
- assert_method :down, content do |down|
- assert_match(/add_column :posts, :title, :string/, down)
- assert_match(/add_column :posts, :body, :text/, down)
+ assert_method :change, content do |change|
+ assert_match(/remove_column :posts, :title, :string/, change)
+ assert_match(/remove_column :posts, :body, :text/, change)
end
end
end
@@ -88,14 +78,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :up, content do |up|
- assert_match(/remove_reference :books, :author/, up)
- assert_match(/remove_reference :books, :distributor, polymorphic: true/, up)
- end
-
- assert_method :down, content do |down|
- assert_match(/add_reference :books, :author, index: true/, down)
- assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, down)
+ assert_method :change, content do |change|
+ assert_match(/remove_reference :books, :author, index: true/, change)
+ assert_match(/remove_reference :books, :distributor, polymorphic: true, index: true/, change)
end
end
end
@@ -105,13 +90,13 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string:index", "body:text", "user_id:integer:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_column :posts, :title, :string/, up)
- assert_match(/add_column :posts, :body, :text/, up)
- assert_match(/add_column :posts, :user_id, :integer/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_column :posts, :title, :string/, change)
+ assert_match(/add_column :posts, :body, :text/, change)
+ assert_match(/add_column :posts, :user_id, :integer/, change)
+ assert_match(/add_index :posts, :title/, change)
+ assert_match(/add_index :posts, :user_id, unique: true/, change)
end
- assert_match(/add_index :posts, :title/, content)
- assert_match(/add_index :posts, :user_id, unique: true/, content)
end
end
@@ -120,10 +105,10 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string:inex", "content:text", "user_id:integer:unik"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_column :books, :title, :string/, up)
- assert_match(/add_column :books, :content, :text/, up)
- assert_match(/add_column :books, :user_id, :integer/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_column :books, :title, :string/, change)
+ assert_match(/add_column :books, :content, :text/, change)
+ assert_match(/add_column :books, :user_id, :integer/, change)
end
assert_no_match(/add_index :books, :title/, content)
assert_no_match(/add_index :books, :user_id/, content)
@@ -135,13 +120,13 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:index", "body:text", "user_uuid:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_column :posts, :title, :string/, up)
- assert_match(/add_column :posts, :body, :text/, up)
- assert_match(/add_column :posts, :user_uuid, :string/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_column :posts, :title, :string/, change)
+ assert_match(/add_column :posts, :body, :text/, change)
+ assert_match(/add_column :posts, :user_uuid, :string/, change)
+ assert_match(/add_index :posts, :title/, change)
+ assert_match(/add_index :posts, :user_uuid, unique: true/, change)
end
- assert_match(/add_index :posts, :title/, content)
- assert_match(/add_index :posts, :user_uuid, unique: true/, content)
end
end
@@ -150,11 +135,11 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "title:string{40}:index", "content:string{255}", "price:decimal{1,2}:index", "discount:decimal{3.4}:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_column :books, :title, :string, limit: 40/, up)
- assert_match(/add_column :books, :content, :string, limit: 255/, up)
- assert_match(/add_column :books, :price, :decimal, precision: 1, scale: 2/, up)
- assert_match(/add_column :books, :discount, :decimal, precision: 3, scale: 4/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_column :books, :title, :string, limit: 40/, change)
+ assert_match(/add_column :books, :content, :string, limit: 255/, change)
+ assert_match(/add_column :books, :price, :decimal, precision: 1, scale: 2/, change)
+ assert_match(/add_column :books, :discount, :decimal, precision: 3, scale: 4/, change)
end
assert_match(/add_index :books, :title/, content)
assert_match(/add_index :books, :price/, content)
@@ -167,9 +152,9 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "author:belongs_to", "distributor:references{polymorphic}"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/add_reference :books, :author, index: true/, up)
- assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, up)
+ assert_method :change, content do |change|
+ assert_match(/add_reference :books, :author, index: true/, change)
+ assert_match(/add_reference :books, :distributor, polymorphic: true, index: true/, change)
end
end
end
@@ -179,29 +164,36 @@ class MigrationGeneratorTest < Rails::Generators::TestCase
run_generator [migration, "artist_id", "musics:uniq"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :change, content do |up|
- assert_match(/create_join_table :artists, :musics/, up)
- assert_match(/# t.index \[:artist_id, :music_id\]/, up)
- assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, up)
+ assert_method :change, content do |change|
+ assert_match(/create_join_table :artists, :musics/, change)
+ assert_match(/# t.index \[:artist_id, :music_id\]/, change)
+ assert_match(/ t.index \[:music_id, :artist_id\], unique: true/, change)
end
end
end
- def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove
- migration = "create_books"
+ def test_create_table_migration
+ run_generator ["create_books", "title:string", "content:text"]
+ assert_migration "db/migrate/create_books.rb" do |content|
+ assert_method :change, content do |change|
+ assert_match(/create_table :books/, change)
+ assert_match(/ t\.string :title/, change)
+ assert_match(/ t\.text :content/, change)
+ end
+ end
+ end
+
+ def test_should_create_empty_migrations_if_name_not_start_with_add_or_remove_or_create
+ migration = "delete_books"
run_generator [migration, "title:string", "content:text"]
assert_migration "db/migrate/#{migration}.rb" do |content|
- assert_method :up, content do |up|
- assert_match(/^\s*$/, up)
- end
-
- assert_method :down, content do |down|
- assert_match(/^\s*$/, down)
+ assert_method :change, content do |change|
+ assert_match(/^\s*$/, change)
end
end
end
-
+
def test_properly_identifies_usage_file
assert generator_class.send(:usage_path)
end
diff --git a/railties/test/generators/model_generator_test.rb b/railties/test/generators/model_generator_test.rb
index 0c7ff0ebe7..01ab77ee20 100644
--- a/railties/test/generators/model_generator_test.rb
+++ b/railties/test/generators/model_generator_test.rb
@@ -264,13 +264,40 @@ class ModelGeneratorTest < Rails::Generators::TestCase
error = capture(:stderr) { run_generator ["Account", "--force"] }
assert_no_match(/Another migration is already named create_accounts/, error)
assert_no_file old_migration
- assert_migration 'db/migrate/create_accounts.rb'
+ assert_migration "db/migrate/create_accounts.rb"
end
def test_invokes_default_test_framework
run_generator
assert_file "test/models/account_test.rb", /class AccountTest < ActiveSupport::TestCase/
+
assert_file "test/fixtures/accounts.yml", /name: MyString/, /age: 1/
+ assert_generated_fixture("test/fixtures/accounts.yml",
+ {"one"=>{"name"=>"MyString", "age"=>1}, "two"=>{"name"=>"MyString", "age"=>1}})
+ end
+
+ def test_fixtures_use_the_references_ids
+ run_generator ["LineItem", "product:references", "cart:belongs_to"]
+
+ assert_file "test/fixtures/line_items.yml", /product_id: \n cart_id: /
+ assert_generated_fixture("test/fixtures/line_items.yml",
+ {"one"=>{"product_id"=>nil, "cart_id"=>nil}, "two"=>{"product_id"=>nil, "cart_id"=>nil}})
+ end
+
+ def test_fixtures_use_the_references_ids_and_type
+ run_generator ["LineItem", "product:references{polymorphic}", "cart:belongs_to"]
+
+ assert_file "test/fixtures/line_items.yml", /product_id: \n product_type: Product\n cart_id: /
+ assert_generated_fixture("test/fixtures/line_items.yml",
+ {"one"=>{"product_id"=>nil, "product_type"=>"Product", "cart_id"=>nil},
+ "two"=>{"product_id"=>nil, "product_type"=>"Product", "cart_id"=>nil}})
+ end
+
+ def test_fixtures_respect_reserved_yml_keywords
+ run_generator ["LineItem", "no:integer", "Off:boolean", "ON:boolean"]
+
+ assert_generated_fixture("test/fixtures/line_items.yml",
+ {"one"=>{"no"=>1, "Off"=>false, "ON"=>false}, "two"=>{"no"=>1, "Off"=>false, "ON"=>false}})
end
def test_fixture_is_skipped
@@ -328,4 +355,10 @@ class ModelGeneratorTest < Rails::Generators::TestCase
end
end
end
+
+ private
+ def assert_generated_fixture(path, parsed_contents)
+ fixture_file = File.new File.expand_path(path, destination_root)
+ assert_equal(parsed_contents, YAML.load(fixture_file))
+ end
end
diff --git a/railties/test/generators/named_base_test.rb b/railties/test/generators/named_base_test.rb
index 2bc2c33a72..ac5cfff229 100644
--- a/railties/test/generators/named_base_test.rb
+++ b/railties/test/generators/named_base_test.rb
@@ -117,6 +117,25 @@ class NamedBaseTest < Rails::Generators::TestCase
assert Rails::Generators.hidden_namespaces.include?('hidden')
end
+ def test_scaffold_plural_names_with_model_name_option
+ g = generator ['Admin::Foo'], model_name: 'User'
+ assert_name g, 'user', :singular_name
+ assert_name g, 'User', :name
+ assert_name g, 'user', :file_path
+ assert_name g, 'User', :class_name
+ assert_name g, 'user', :file_name
+ assert_name g, 'User', :human_name
+ assert_name g, 'users', :plural_name
+ assert_name g, 'user', :i18n_scope
+ assert_name g, 'users', :table_name
+ assert_name g, 'Admin::Foos', :controller_name
+ assert_name g, %w(admin), :controller_class_path
+ assert_name g, 'Admin::Foos', :controller_class_name
+ assert_name g, 'admin/foos', :controller_file_path
+ assert_name g, 'foos', :controller_file_name
+ assert_name g, 'admin.foos', :controller_i18n_scope
+ end
+
protected
def assert_name(generator, value, method)
diff --git a/railties/test/generators/namespaced_generators_test.rb b/railties/test/generators/namespaced_generators_test.rb
index 4b168ae110..e17925ff65 100644
--- a/railties/test/generators/namespaced_generators_test.rb
+++ b/railties/test/generators/namespaced_generators_test.rb
@@ -1,18 +1,19 @@
require 'generators/generators_test_helper'
require 'rails/generators/rails/controller/controller_generator'
require 'rails/generators/rails/model/model_generator'
-require 'rails/generators/rails/observer/observer_generator'
require 'rails/generators/mailer/mailer_generator'
require 'rails/generators/rails/scaffold/scaffold_generator'
class NamespacedGeneratorTestCase < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+
def setup
+ super
Rails::Generators.namespace = TestApp
end
end
class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
- include GeneratorsTestHelper
arguments %w(Account foo bar)
tests Rails::Generators::ControllerGenerator
@@ -43,7 +44,7 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
end
end
- def test_helpr_is_also_namespaced
+ def test_helper_is_also_namespaced
run_generator
assert_file "app/helpers/test_app/account_helper.rb", /module TestApp/, / module AccountHelper/
assert_file "test/helpers/test_app/account_helper_test.rb", /module TestApp/, / class AccountHelperTest/
@@ -81,7 +82,6 @@ class NamespacedControllerGeneratorTest < NamespacedGeneratorTestCase
end
class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
- include GeneratorsTestHelper
arguments %w(Account name:string age:integer)
tests Rails::Generators::ModelGenerator
@@ -141,29 +141,7 @@ class NamespacedModelGeneratorTest < NamespacedGeneratorTestCase
end
end
-class NamespacedObserverGeneratorTest < NamespacedGeneratorTestCase
- include GeneratorsTestHelper
- arguments %w(account)
- tests Rails::Generators::ObserverGenerator
-
- def test_invokes_default_orm
- run_generator
- assert_file "app/models/test_app/account_observer.rb", /module TestApp/, / class AccountObserver < ActiveRecord::Observer/
- end
-
- def test_invokes_default_orm_with_class_path
- run_generator ["admin/account"]
- assert_file "app/models/test_app/admin/account_observer.rb", /module TestApp/, / class Admin::AccountObserver < ActiveRecord::Observer/
- end
-
- def test_invokes_default_test_framework
- run_generator
- assert_file "test/models/test_app/account_observer_test.rb", /module TestApp/, / class AccountObserverTest < ActiveSupport::TestCase/
- end
-end
-
class NamespacedMailerGeneratorTest < NamespacedGeneratorTestCase
- include GeneratorsTestHelper
arguments %w(notifier foo bar)
tests Rails::Generators::MailerGenerator
@@ -244,13 +222,9 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
/module TestApp\n class ProductLinesControllerTest < ActionController::TestCase/
# Views
- %w(
- index
- edit
- new
- show
- _form
- ).each { |view| assert_file "app/views/test_app/product_lines/#{view}.html.erb" }
+ %w(index edit new show _form).each do |view|
+ assert_file "app/views/test_app/product_lines/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/test_app/product_lines.html.erb"
# Helpers
@@ -316,13 +290,9 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
/module TestApp\n class Admin::RolesControllerTest < ActionController::TestCase/
# Views
- %w(
- index
- edit
- new
- show
- _form
- ).each { |view| assert_file "app/views/test_app/admin/roles/#{view}.html.erb" }
+ %w(index edit new show _form).each do |view|
+ assert_file "app/views/test_app/admin/roles/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/admin/roles.html.erb"
# Helpers
@@ -389,13 +359,9 @@ class NamespacedScaffoldGeneratorTest < NamespacedGeneratorTestCase
/module TestApp\n class Admin::User::Special::RolesControllerTest < ActionController::TestCase/
# Views
- %w(
- index
- edit
- new
- show
- _form
- ).each { |view| assert_file "app/views/test_app/admin/user/special/roles/#{view}.html.erb" }
+ %w(index edit new show _form).each do |view|
+ assert_file "app/views/test_app/admin/user/special/roles/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/admin/user/special/roles.html.erb"
# Helpers
diff --git a/railties/test/generators/observer_generator_test.rb b/railties/test/generators/observer_generator_test.rb
deleted file mode 100644
index 1231827466..0000000000
--- a/railties/test/generators/observer_generator_test.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/observer/observer_generator'
-
-class ObserverGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- arguments %w(account)
-
- def test_invokes_default_orm
- run_generator
- assert_file "app/models/account_observer.rb", /class AccountObserver < ActiveRecord::Observer/
- end
-
- def test_invokes_default_orm_with_class_path
- run_generator ["admin/account"]
- assert_file "app/models/admin/account_observer.rb", /class Admin::AccountObserver < ActiveRecord::Observer/
- end
-
- def test_invokes_default_test_framework
- run_generator
- assert_file "test/models/account_observer_test.rb", /class AccountObserverTest < ActiveSupport::TestCase/
- end
-
- def test_logs_if_the_test_framework_cannot_be_found
- content = run_generator ["account", "--test-framework=rspec"]
- assert_match(/rspec \[not found\]/, content)
- end
-end
diff --git a/railties/test/generators/performance_test_generator_test.rb b/railties/test/generators/performance_test_generator_test.rb
deleted file mode 100644
index 37f9857193..0000000000
--- a/railties/test/generators/performance_test_generator_test.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/performance_test/performance_test_generator'
-
-class PerformanceTestGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- arguments %w(performance)
-
- def test_performance_test_skeleton_is_created
- run_generator
- assert_file "test/performance/performance_test.rb", /class PerformanceTest < ActionDispatch::PerformanceTest/
- end
-end
diff --git a/railties/test/generators/plugin_generator_test.rb b/railties/test/generators/plugin_generator_test.rb
new file mode 100644
index 0000000000..068eb66bc6
--- /dev/null
+++ b/railties/test/generators/plugin_generator_test.rb
@@ -0,0 +1,350 @@
+require 'generators/generators_test_helper'
+require 'rails/generators/rails/plugin/plugin_generator'
+require 'generators/shared_generator_tests'
+
+DEFAULT_PLUGIN_FILES = %w(
+ .gitignore
+ Gemfile
+ Rakefile
+ README.rdoc
+ bukkits.gemspec
+ MIT-LICENSE
+ lib
+ lib/bukkits.rb
+ lib/tasks/bukkits_tasks.rake
+ lib/bukkits/version.rb
+ test/bukkits_test.rb
+ test/test_helper.rb
+ test/dummy
+)
+
+class PluginGeneratorTest < Rails::Generators::TestCase
+ include GeneratorsTestHelper
+ destination File.join(Rails.root, "tmp/bukkits")
+ arguments [destination_root]
+
+ # brings setup, teardown, and some tests
+ include SharedGeneratorTests
+
+ def test_invalid_plugin_name_raises_an_error
+ content = capture(:stderr){ run_generator [File.join(destination_root, "things-43")] }
+ assert_equal "Invalid plugin name things-43. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] }
+ assert_equal "Invalid plugin name things4.3. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content
+
+ content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] }
+ assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content
+ end
+
+ def test_camelcase_plugin_name_underscores_filenames
+ run_generator [File.join(destination_root, "CamelCasedName")]
+ assert_no_file "CamelCasedName/lib/CamelCasedName.rb"
+ assert_file "CamelCasedName/lib/camel_cased_name.rb", /module CamelCasedName/
+ end
+
+ def test_generating_without_options
+ run_generator
+ assert_file "README.rdoc", /Bukkits/
+ assert_no_file "config/routes.rb"
+ assert_file "test/test_helper.rb"
+ assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/
+ end
+
+ def test_generating_test_files_in_full_mode
+ run_generator [destination_root, "--full"]
+ assert_directory "test/integration/"
+
+ assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/
+ end
+
+ def test_generating_test_files_in_full_mode_without_unit_test_files
+ run_generator [destination_root, "-T", "--full"]
+
+ assert_no_directory "test/integration/"
+ assert_no_file "test"
+ assert_file "Rakefile" do |contents|
+ assert_no_match(/APP_RAKEFILE/, contents)
+ end
+ end
+
+ def test_generating_adds_dummy_app_rake_tasks_without_unit_test_files
+ run_generator [destination_root, "-T", "--mountable", '--dummy-path', 'my_dummy_app']
+ assert_file "Rakefile", /APP_RAKEFILE/
+ end
+
+ def test_generating_adds_dummy_app_without_javascript_and_assets_deps
+ run_generator [destination_root]
+
+ assert_file "test/dummy/app/assets/stylesheets/application.css"
+
+ assert_file "test/dummy/app/assets/javascripts/application.js" do |contents|
+ assert_no_match(/jquery/, contents)
+ end
+ end
+
+ def test_ensure_that_plugin_options_are_not_passed_to_app_generator
+ FileUtils.cd(Rails.root)
+ assert_no_match(/It works from file!.*It works_from_file/, run_generator([destination_root, "-m", "lib/template.rb"]))
+ end
+
+ def test_ensure_that_test_dummy_can_be_generated_from_a_template
+ FileUtils.cd(Rails.root)
+ run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test-unit"])
+ assert_file "spec/dummy"
+ assert_no_file "test"
+ end
+
+ def test_database_entry_is_generated_for_sqlite3_by_default_in_full_mode
+ run_generator([destination_root, "--full"])
+ assert_file "test/dummy/config/database.yml", /sqlite/
+ assert_file "bukkits.gemspec", /sqlite3/
+ end
+
+ def test_config_another_database
+ run_generator([destination_root, "-d", "mysql", "--full"])
+ assert_file "test/dummy/config/database.yml", /mysql/
+ assert_file "bukkits.gemspec", /mysql/
+ end
+
+ def test_dont_generate_development_dependency
+ run_generator [destination_root, "--skip-active-record"]
+
+ assert_file "bukkits.gemspec" do |contents|
+ assert_no_match(/s.add_development_dependency "sqlite3"/, contents)
+ end
+ end
+
+ def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given
+ run_generator [destination_root, "--skip-active-record"]
+ assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
+ end
+
+ def test_ensure_that_skip_active_record_option_is_passed_to_app_generator
+ run_generator [destination_root, "--skip_active_record"]
+ assert_no_file "test/dummy/config/database.yml"
+ assert_file "test/test_helper.rb" do |contents|
+ assert_no_match(/ActiveRecord/, contents)
+ end
+ end
+
+ def test_ensure_that_database_option_is_passed_to_app_generator
+ run_generator [destination_root, "--database", "postgresql"]
+ assert_file "test/dummy/config/database.yml", /postgres/
+ end
+
+ def test_generation_runs_bundle_install_with_full_and_mountable
+ result = run_generator [destination_root, "--mountable", "--full", "--dev"]
+ assert_file "#{destination_root}/Gemfile.lock" do |contents|
+ assert_match(/bukkits/, contents)
+ end
+ assert_match(/run bundle install/, 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
+
+ def test_skipping_javascripts_without_mountable_option
+ run_generator
+ assert_no_file "app/assets/javascripts/bukkits/application.js"
+ end
+
+ def test_javascripts_generation
+ run_generator [destination_root, "--mountable"]
+ assert_file "app/assets/javascripts/bukkits/application.js"
+ end
+
+ def test_skip_javascripts
+ run_generator [destination_root, "--skip-javascript", "--mountable"]
+ assert_no_file "app/assets/javascripts/bukkits/application.js"
+ end
+
+ def test_template_from_dir_pwd
+ FileUtils.cd(Rails.root)
+ assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]))
+ end
+
+ def test_ensure_that_tests_work
+ run_generator
+ FileUtils.cd destination_root
+ quietly { system 'bundle install' }
+ assert_match(/1 runs, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`)
+ 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`)
+ 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
+ end
+
+ def test_creating_engine_in_full_mode
+ run_generator [destination_root, "--full"]
+ assert_file "app/assets/javascripts/bukkits"
+ assert_file "app/assets/stylesheets/bukkits"
+ assert_file "app/assets/images/bukkits"
+ assert_file "app/models"
+ assert_file "app/controllers"
+ assert_file "app/views"
+ assert_file "app/helpers"
+ assert_file "app/mailers"
+ assert_file "bin/rails"
+ assert_file "config/routes.rb", /Rails.application.routes.draw do/
+ assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/
+ assert_file "lib/bukkits.rb", /require "bukkits\/engine"/
+ end
+
+ def test_being_quiet_while_creating_dummy_application
+ assert_no_match(/create\s+config\/application.rb/, run_generator)
+ end
+
+ def test_create_mountable_application_with_mountable_option
+ run_generator [destination_root, "--mountable"]
+ assert_file "app/assets/javascripts/bukkits"
+ assert_file "app/assets/stylesheets/bukkits"
+ assert_file "app/assets/images/bukkits"
+ assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/
+ assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/
+ assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/
+ assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/
+ assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/
+ assert_file "app/views/layouts/bukkits/application.html.erb" do |contents|
+ assert_match "<title>Bukkits</title>", contents
+ assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents)
+ assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
+ end
+ end
+
+ def test_creating_gemspec
+ run_generator
+ assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/
+ assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/
+ assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/
+ assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/
+ end
+
+ def test_usage_of_engine_commands
+ run_generator [destination_root, "--full"]
+ assert_file "bin/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/
+ assert_file "bin/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/
+ assert_file "bin/rails", /require 'rails\/all'/
+ assert_file "bin/rails", /require 'rails\/engine\/commands'/
+ end
+
+ def test_shebang
+ run_generator [destination_root, "--full"]
+ assert_file "bin/rails", /#!\/usr\/bin\/env ruby/
+ end
+
+ def test_passing_dummy_path_as_a_parameter
+ run_generator [destination_root, "--dummy_path", "spec/dummy"]
+ assert_file "spec/dummy"
+ assert_file "spec/dummy/config/application.rb"
+ assert_no_file "test/dummy"
+ end
+
+ def test_creating_dummy_application_with_different_name
+ run_generator [destination_root, "--dummy_path", "spec/fake"]
+ assert_file "spec/fake"
+ assert_file "spec/fake/config/application.rb"
+ assert_no_file "test/dummy"
+ end
+
+ def test_creating_dummy_without_tests_but_with_dummy_path
+ run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"]
+ assert_file "spec/dummy"
+ assert_file "spec/dummy/config/application.rb"
+ assert_no_file "test"
+ assert_file '.gitignore' do |contents|
+ assert_match(/spec\/dummy/, contents)
+ end
+ end
+
+ def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path
+ FileUtils.cd(Rails.root)
+ run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"])
+ assert_file ".gitignore" do |contents|
+ assert_match(/spec\/dummy/, contents)
+ end
+ end
+
+ def test_skipping_test_unit
+ run_generator [destination_root, "--skip-test-unit"]
+ assert_no_file "test"
+ assert_file "bukkits.gemspec" do |contents|
+ assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents)
+ end
+ assert_file '.gitignore' do |contents|
+ assert_no_match(/test\dummy/, contents)
+ end
+ end
+
+ def test_skipping_gemspec
+ run_generator [destination_root, "--skip-gemspec"]
+ assert_no_file "bukkits.gemspec"
+ assert_file "Gemfile" do |contents|
+ assert_no_match('gemspec', contents)
+ assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
+ assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ assert_no_match(/# gem "jquery-rails"/, contents)
+ end
+ end
+
+ def test_skipping_gemspec_in_full_mode
+ run_generator [destination_root, "--skip-gemspec", "--full"]
+ assert_no_file "bukkits.gemspec"
+ assert_file "Gemfile" do |contents|
+ assert_no_match('gemspec', contents)
+ assert_match(/gem "rails", "~> #{Rails.version}"/, contents)
+ assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
+ end
+ end
+
+ def test_creating_plugin_in_app_directory_adds_gemfile_entry
+ # simulate application existance
+ gemfile_path = "#{Rails.root}/Gemfile"
+ Object.const_set('APP_PATH', Rails.root)
+ FileUtils.touch gemfile_path
+
+ run_generator [destination_root]
+
+ assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/
+ ensure
+ Object.send(:remove_const, 'APP_PATH')
+ FileUtils.rm gemfile_path
+ end
+
+ def test_skipping_gemfile_entry
+ # simulate application existance
+ gemfile_path = "#{Rails.root}/Gemfile"
+ Object.const_set('APP_PATH', Rails.root)
+ FileUtils.touch gemfile_path
+
+ run_generator [destination_root, "--skip-gemfile-entry"]
+
+ assert_file gemfile_path do |contents|
+ assert_no_match(/gem 'bukkits', path: 'tmp\/bukkits'/, contents)
+ end
+ ensure
+ Object.send(:remove_const, 'APP_PATH')
+ FileUtils.rm gemfile_path
+ end
+
+
+protected
+ def action(*args, &block)
+ silence(:stdout){ generator.send(*args, &block) }
+ end
+
+ def default_files
+ ::DEFAULT_PLUGIN_FILES
+ end
+end
diff --git a/railties/test/generators/plugin_new_generator_test.rb b/railties/test/generators/plugin_new_generator_test.rb
deleted file mode 100644
index 6974db5751..0000000000
--- a/railties/test/generators/plugin_new_generator_test.rb
+++ /dev/null
@@ -1,402 +0,0 @@
-require 'generators/generators_test_helper'
-require 'rails/generators/rails/plugin_new/plugin_new_generator'
-require 'generators/shared_generator_tests'
-
-DEFAULT_PLUGIN_FILES = %w(
- .gitignore
- Gemfile
- Rakefile
- README.rdoc
- bukkits.gemspec
- MIT-LICENSE
- lib
- lib/bukkits.rb
- lib/tasks/bukkits_tasks.rake
- lib/bukkits/version.rb
- test/bukkits_test.rb
- test/test_helper.rb
- test/dummy
-)
-
-class PluginNewGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- destination File.join(Rails.root, "tmp/bukkits")
- arguments [destination_root]
-
- # brings setup, teardown, and some tests
- include SharedGeneratorTests
-
- def test_invalid_plugin_name_raises_an_error
- content = capture(:stderr){ run_generator [File.join(destination_root, "things-43")] }
- assert_equal "Invalid plugin name things-43. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content
-
- content = capture(:stderr){ run_generator [File.join(destination_root, "things4.3")] }
- assert_equal "Invalid plugin name things4.3. Please give a name which use only alphabetic or numeric or \"_\" characters.\n", content
-
- content = capture(:stderr){ run_generator [File.join(destination_root, "43things")] }
- assert_equal "Invalid plugin name 43things. Please give a name which does not start with numbers.\n", content
- end
-
- def test_camelcase_plugin_name_underscores_filenames
- run_generator [File.join(destination_root, "CamelCasedName")]
- assert_no_file "CamelCasedName/lib/CamelCasedName.rb"
- assert_file "CamelCasedName/lib/camel_cased_name.rb", /module CamelCasedName/
- end
-
- def test_generating_without_options
- run_generator
- assert_file "README.rdoc", /Bukkits/
- assert_no_file "config/routes.rb"
- assert_file "test/test_helper.rb"
- assert_file "test/bukkits_test.rb", /assert_kind_of Module, Bukkits/
- end
-
- def test_generating_test_files_in_full_mode
- run_generator [destination_root, "--full"]
- assert_directory "test/integration/"
-
- assert_file "test/integration/navigation_test.rb", /ActionDispatch::IntegrationTest/
- end
-
- def test_generating_test_files_in_full_mode_without_unit_test_files
- run_generator [destination_root, "-T", "--full"]
-
- assert_no_directory "test/integration/"
- assert_no_file "test"
- assert_no_match(/APP_RAKEFILE/, File.read(File.join(destination_root, "Rakefile")))
- end
-
- def test_ensure_that_plugin_options_are_not_passed_to_app_generator
- FileUtils.cd(Rails.root)
- assert_no_match(/It works from file!.*It works_from_file/, run_generator([destination_root, "-m", "lib/template.rb"]))
- end
-
- def test_ensure_that_test_dummy_can_be_generated_from_a_template
- FileUtils.cd(Rails.root)
- run_generator([destination_root, "-m", "lib/create_test_dummy_template.rb", "--skip-test-unit"])
- assert_file "spec/dummy"
- assert_no_file "test"
- end
-
- def test_database_entry_is_generated_for_sqlite3_by_default_in_full_mode
- run_generator([destination_root, "--full"])
- assert_file "test/dummy/config/database.yml", /sqlite/
- assert_file "bukkits.gemspec", /sqlite3/
- end
-
- def test_config_another_database
- run_generator([destination_root, "-d", "mysql", "--full"])
- assert_file "test/dummy/config/database.yml", /mysql/
- assert_file "bukkits.gemspec", /mysql/
- end
-
- def test_dont_generate_development_dependency
- run_generator [destination_root, "--skip-active-record"]
-
- assert_file "bukkits.gemspec" do |contents|
- assert_no_match(/s.add_development_dependency "sqlite3"/, contents)
- end
- end
-
- def test_active_record_is_removed_from_frameworks_if_skip_active_record_is_given
- run_generator [destination_root, "--skip-active-record"]
- assert_file "test/dummy/config/application.rb", /#\s+require\s+["']active_record\/railtie["']/
- end
-
- def test_ensure_that_skip_active_record_option_is_passed_to_app_generator
- run_generator [destination_root, "--skip_active_record"]
- assert_no_file "test/dummy/config/database.yml"
- assert_no_match(/ActiveRecord/, File.read(File.join(destination_root, "test/test_helper.rb")))
- end
-
- def test_ensure_that_database_option_is_passed_to_app_generator
- run_generator [destination_root, "--database", "postgresql"]
- assert_file "test/dummy/config/database.yml", /postgres/
- end
-
- def test_generation_runs_bundle_install_with_full_and_mountable
- result = run_generator [destination_root, "--mountable", "--full", "--dev"]
- assert_file "#{destination_root}/Gemfile.lock" do |contents|
- assert_match(/bukkits/, contents)
- end
- assert_match(/run bundle install/, 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
-
- def test_skipping_javascripts_without_mountable_option
- run_generator
- assert_no_file "app/assets/javascripts/bukkits/application.js"
- assert_no_file "vendor/assets/javascripts/jquery.js"
- assert_no_file "vendor/assets/javascripts/jquery_ujs.js"
- end
-
- def test_javascripts_generation
- run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits/application.js"
- end
-
- def test_jquery_is_the_default_javascript_library
- run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits/application.js" do |contents|
- assert_match %r{^//= require jquery}, contents
- assert_match %r{^//= require jquery_ujs}, contents
- end
- assert_file 'bukkits.gemspec' do |contents|
- assert_match(/jquery-rails/, contents)
- end
- end
-
- def test_other_javascript_libraries
- run_generator [destination_root, "--mountable", '-j', 'prototype']
- assert_file "app/assets/javascripts/bukkits/application.js" do |contents|
- assert_match %r{^//= require prototype}, contents
- assert_match %r{^//= require prototype_ujs}, contents
- end
- assert_file 'bukkits.gemspec' do |contents|
- assert_match(/prototype-rails/, contents)
- end
- end
-
- def test_skip_javascripts
- run_generator [destination_root, "--skip-javascript", "--mountable"]
- assert_no_file "app/assets/javascripts/bukkits/application.js"
- assert_no_file "vendor/assets/javascripts/jquery.js"
- assert_no_file "vendor/assets/javascripts/jquery_ujs.js"
- end
-
- def test_template_from_dir_pwd
- FileUtils.cd(Rails.root)
- assert_match(/It works from file!/, run_generator([destination_root, "-m", "lib/template.rb"]))
- end
-
- def test_ensure_that_tests_work
- run_generator
- FileUtils.cd destination_root
- quietly { system 'bundle install' }
- assert_match(/1 tests, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`)
- 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 tests, 1 assertions, 0 failures, 0 errors/, `bundle exec rake test`)
- 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
- end
-
- def test_creating_engine_in_full_mode
- run_generator [destination_root, "--full"]
- assert_file "app/assets/javascripts/bukkits"
- assert_file "app/assets/stylesheets/bukkits"
- assert_file "app/assets/images/bukkits"
- assert_file "app/models"
- assert_file "app/controllers"
- assert_file "app/views"
- assert_file "app/helpers"
- assert_file "app/mailers"
- assert_file "config/routes.rb", /Rails.application.routes.draw do/
- assert_file "lib/bukkits/engine.rb", /module Bukkits\n class Engine < ::Rails::Engine\n end\nend/
- assert_file "lib/bukkits.rb", /require "bukkits\/engine"/
- assert_file "script/rails"
- end
-
- def test_being_quiet_while_creating_dummy_application
- assert_no_match(/create\s+config\/application.rb/, run_generator)
- end
-
- def test_create_mountable_application_with_mountable_option
- run_generator [destination_root, "--mountable"]
- assert_file "app/assets/javascripts/bukkits"
- assert_file "app/assets/stylesheets/bukkits"
- assert_file "app/assets/images/bukkits"
- assert_file "config/routes.rb", /Bukkits::Engine.routes.draw do/
- assert_file "lib/bukkits/engine.rb", /isolate_namespace Bukkits/
- assert_file "test/dummy/config/routes.rb", /mount Bukkits::Engine => "\/bukkits"/
- assert_file "app/controllers/bukkits/application_controller.rb", /module Bukkits\n class ApplicationController < ActionController::Base/
- assert_file "app/helpers/bukkits/application_helper.rb", /module Bukkits\n module ApplicationHelper/
- assert_file "app/views/layouts/bukkits/application.html.erb" do |contents|
- assert_match "<title>Bukkits</title>", contents
- assert_match(/stylesheet_link_tag\s+['"]bukkits\/application['"]/, contents)
- assert_match(/javascript_include_tag\s+['"]bukkits\/application['"]/, contents)
- end
- end
-
- def test_creating_gemspec
- run_generator
- assert_file "bukkits.gemspec", /s.name\s+= "bukkits"/
- assert_file "bukkits.gemspec", /s.files = Dir\["\{app,config,db,lib\}\/\*\*\/\*", "MIT-LICENSE", "Rakefile", "README\.rdoc"\]/
- assert_file "bukkits.gemspec", /s.test_files = Dir\["test\/\*\*\/\*"\]/
- assert_file "bukkits.gemspec", /s.version\s+ = Bukkits::VERSION/
- end
-
- def test_usage_of_engine_commands
- run_generator [destination_root, "--full"]
- assert_file "script/rails", /ENGINE_PATH = File.expand_path\('..\/..\/lib\/bukkits\/engine', __FILE__\)/
- assert_file "script/rails", /ENGINE_ROOT = File.expand_path\('..\/..', __FILE__\)/
- assert_file "script/rails", /require 'rails\/all'/
- assert_file "script/rails", /require 'rails\/engine\/commands'/
- end
-
- def test_shebang
- run_generator [destination_root, "--full"]
- assert_file "script/rails", /#!\/usr\/bin\/env ruby/
- end
-
- def test_passing_dummy_path_as_a_parameter
- run_generator [destination_root, "--dummy_path", "spec/dummy"]
- assert_file "spec/dummy"
- assert_file "spec/dummy/config/application.rb"
- assert_no_file "test/dummy"
- end
-
- def test_creating_dummy_application_with_different_name
- run_generator [destination_root, "--dummy_path", "spec/fake"]
- assert_file "spec/fake"
- assert_file "spec/fake/config/application.rb"
- assert_no_file "test/dummy"
- end
-
- def test_creating_dummy_without_tests_but_with_dummy_path
- run_generator [destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"]
- assert_file "spec/dummy"
- assert_file "spec/dummy/config/application.rb"
- assert_no_file "test"
- assert_file '.gitignore' do |contents|
- assert_match(/spec\/dummy/, contents)
- end
- end
-
- def test_ensure_that_gitignore_can_be_generated_from_a_template_for_dummy_path
- FileUtils.cd(Rails.root)
- run_generator([destination_root, "--dummy_path", "spec/dummy", "--skip-test-unit"])
- assert_file ".gitignore" do |contents|
- assert_match(/spec\/dummy/, contents)
- end
- end
-
- def test_skipping_test_unit
- run_generator [destination_root, "--skip-test-unit"]
- assert_no_file "test"
- assert_file "bukkits.gemspec" do |contents|
- assert_no_match(/s.test_files = Dir\["test\/\*\*\/\*"\]/, contents)
- end
- assert_file '.gitignore' do |contents|
- assert_no_match(/test\dummy/, contents)
- end
- end
-
- def test_skipping_gemspec
- run_generator [destination_root, "--skip-gemspec"]
- assert_no_file "bukkits.gemspec"
- assert_file "Gemfile" do |contents|
- assert_no_match('gemspec', contents)
- assert_match(/gem "rails", "~> #{Rails::VERSION::STRING}"/, contents)
- assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
- assert_no_match(/# gem "jquery-rails"/, contents)
- end
- end
-
- def test_skipping_gemspec_in_full_mode
- run_generator [destination_root, "--skip-gemspec", "--full"]
- assert_no_file "bukkits.gemspec"
- assert_file "Gemfile" do |contents|
- assert_no_match('gemspec', contents)
- assert_match(/gem "rails", "~> #{Rails::VERSION::STRING}"/, contents)
- assert_match(/group :development do\n gem "sqlite3"\nend/, contents)
- assert_match(/# gem "jquery-rails"/, contents)
- assert_no_match(/# jquery-rails is used by the dummy application\ngem "jquery-rails"/, contents)
- end
- end
-
- def test_skipping_gemspec_in_full_mode_with_javascript_option
- run_generator [destination_root, "--skip-gemspec", "--full", "--javascript=prototype"]
- assert_file "Gemfile" do |contents|
- assert_match(/# gem "prototype-rails"/, contents)
- assert_match(/# jquery-rails is used by the dummy application\ngem "jquery-rails"/, contents)
- end
- end
-
- def test_creating_plugin_in_app_directory_adds_gemfile_entry
- # simulate application existance
- gemfile_path = "#{Rails.root}/Gemfile"
- Object.const_set('APP_PATH', Rails.root)
- FileUtils.touch gemfile_path
-
- run_generator [destination_root]
-
- assert_file gemfile_path, /gem 'bukkits', path: 'tmp\/bukkits'/
- ensure
- Object.send(:remove_const, 'APP_PATH')
- FileUtils.rm gemfile_path
- end
-
- def test_skipping_gemfile_entry
- # simulate application existance
- gemfile_path = "#{Rails.root}/Gemfile"
- Object.const_set('APP_PATH', Rails.root)
- FileUtils.touch gemfile_path
-
- run_generator [destination_root, "--skip-gemfile-entry"]
-
- assert_file gemfile_path do |contents|
- assert_no_match(/gem 'bukkits', path: 'tmp\/bukkits'/, contents)
- end
- ensure
- Object.send(:remove_const, 'APP_PATH')
- FileUtils.rm gemfile_path
- end
-
-
-protected
- def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
- end
-
- def default_files
- ::DEFAULT_PLUGIN_FILES
- end
-end
-
-class CustomPluginGeneratorTest < Rails::Generators::TestCase
- include GeneratorsTestHelper
- tests Rails::Generators::PluginNewGenerator
-
- destination File.join(Rails.root, "tmp/bukkits")
- arguments [destination_root]
- include SharedCustomGeneratorTests
-
- def test_overriding_test_framework
- FileUtils.cd(destination_root)
- run_generator([destination_root, "-b", "#{Rails.root}/lib/plugin_builders/spec_builder.rb"])
- assert_file 'spec/spec_helper.rb'
- assert_file 'spec/dummy'
- assert_file 'Rakefile', /task default: :spec/
- assert_file 'Rakefile', /# spec tasks in rakefile/
- end
-
-protected
- def default_files
- ::DEFAULT_PLUGIN_FILES
- end
-
- def builder_class
- :PluginBuilder
- end
-
- def builders_dir
- "plugin_builders"
- end
-
- def action(*args, &block)
- silence(:stdout){ generator.send(*args, &block) }
- end
-end
diff --git a/railties/test/generators/scaffold_controller_generator_test.rb b/railties/test/generators/scaffold_controller_generator_test.rb
index 38454dfb8b..26e56a162c 100644
--- a/railties/test/generators/scaffold_controller_generator_test.rb
+++ b/railties/test/generators/scaffold_controller_generator_test.rb
@@ -20,33 +20,30 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/@users = User\.all/, m)
end
- assert_instance_method :show, content do |m|
- assert_match(/@user = User\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :show, content
assert_instance_method :new, content do |m|
assert_match(/@user = User\.new/, m)
end
- assert_instance_method :edit, content do |m|
- assert_match(/@user = User\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :edit, content
assert_instance_method :create, content do |m|
assert_match(/@user = User\.new\(user_params\)/, m)
assert_match(/@user\.save/, m)
- assert_match(/@user\.errors/, m)
end
assert_instance_method :update, content do |m|
- assert_match(/@user = User\.find\(params\[:id\]\)/, m)
- assert_match(/@user\.update_attributes\(user_params\)/, m)
- assert_match(/@user\.errors/, m)
+ assert_match(/@user\.update\(user_params\)/, m)
end
assert_instance_method :destroy, content do |m|
- assert_match(/@user = User\.find\(params\[:id\]\)/, m)
assert_match(/@user\.destroy/, m)
+ assert_match(/User was successfully destroyed/, m)
+ end
+
+ assert_instance_method :set_user, content do |m|
+ assert_match(/@user = User\.find\(params\[:id\]\)/, m)
end
assert_match(/def user_params/, content)
@@ -54,6 +51,33 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
end
end
+ def test_dont_use_require_or_permit_if_there_are_no_attributes
+ run_generator ["User"]
+
+ assert_file "app/controllers/users_controller.rb" do |content|
+ assert_match(/def user_params/, content)
+ assert_match(/params\[:user\]/, content)
+ end
+ end
+
+ def test_controller_permit_references_attributes
+ run_generator ["LineItem", "product:references", "cart:belongs_to"]
+
+ assert_file "app/controllers/line_items_controller.rb" do |content|
+ assert_match(/def line_item_params/, content)
+ assert_match(/params\.require\(:line_item\)\.permit\(:product_id, :cart_id\)/, content)
+ end
+ end
+
+ def test_controller_permit_polymorphic_references_attributes
+ run_generator ["LineItem", "product:references{polymorphic}"]
+
+ assert_file "app/controllers/line_items_controller.rb" do |content|
+ assert_match(/def line_item_params/, content)
+ assert_match(/params\.require\(:line_item\)\.permit\(:product_id, :product_type\)/, content)
+ end
+ end
+
def test_helper_are_invoked_with_a_pluralized_name
run_generator
assert_file "app/helpers/users_helper.rb", /module UsersHelper/
@@ -63,23 +87,20 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
def test_views_are_generated
run_generator
- %w(
- index
- edit
- new
- show
- ).each { |view| assert_file "app/views/users/#{view}.html.erb" }
+ %w(index edit new show).each do |view|
+ assert_file "app/views/users/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/users.html.erb"
end
def test_functional_tests
- run_generator
+ run_generator ["User", "name:string", "age:integer", "organization:references{polymorphic}"]
assert_file "test/controllers/users_controller_test.rb" do |content|
assert_match(/class UsersControllerTest < ActionController::TestCase/, content)
assert_match(/test "should get index"/, content)
- assert_match(/post :create, user: \{ age: @user.age, name: @user.name \}/, content)
- assert_match(/put :update, id: @user, user: \{ age: @user.age, name: @user.name \}/, content)
+ assert_match(/post :create, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \}/, content)
+ assert_match(/patch :update, id: @user, user: \{ age: @user\.age, name: @user\.name, organization_id: @user\.organization_id, organization_type: @user\.organization_type \}/, content)
end
end
@@ -90,7 +111,7 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
assert_match(/class UsersControllerTest < ActionController::TestCase/, content)
assert_match(/test "should get index"/, content)
assert_match(/post :create, user: \{ \}/, content)
- assert_match(/put :update, id: @user, user: \{ \}/, content)
+ assert_match(/patch :update, id: @user, user: \{ \}/, content)
end
end
@@ -142,7 +163,16 @@ class ScaffoldControllerGeneratorTest < Rails::Generators::TestCase
def test_new_hash_style
run_generator
assert_file "app/controllers/users_controller.rb" do |content|
- assert_match(/\{ render action: "new" \}/, 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|
+ assert_instance_method :index, content do |m|
+ assert_match("@users = User.all", m)
+ end
end
end
end
diff --git a/railties/test/generators/scaffold_generator_test.rb b/railties/test/generators/scaffold_generator_test.rb
index efe47cdfcb..524bbde2b7 100644
--- a/railties/test/generators/scaffold_generator_test.rb
+++ b/railties/test/generators/scaffold_generator_test.rb
@@ -30,50 +30,42 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/@product_lines = ProductLine\.all/, m)
end
- assert_instance_method :show, content do |m|
- assert_match(/@product_line = ProductLine\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :show, content
assert_instance_method :new, content do |m|
assert_match(/@product_line = ProductLine\.new/, m)
end
- assert_instance_method :edit, content do |m|
- assert_match(/@product_line = ProductLine\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :edit, content
assert_instance_method :create, content do |m|
assert_match(/@product_line = ProductLine\.new\(product_line_params\)/, m)
assert_match(/@product_line\.save/, m)
- assert_match(/@product_line\.errors/, m)
end
assert_instance_method :update, content do |m|
- assert_match(/@product_line = ProductLine\.find\(params\[:id\]\)/, m)
- assert_match(/@product_line\.update_attributes\(product_line_params\)/, m)
- assert_match(/@product_line\.errors/, m)
+ assert_match(/@product_line\.update\(product_line_params\)/, m)
end
assert_instance_method :destroy, content do |m|
- assert_match(/@product_line = ProductLine\.find\(params\[:id\]\)/, m)
assert_match(/@product_line\.destroy/, m)
end
+
+ assert_instance_method :set_product_line, content do |m|
+ assert_match(/@product_line = ProductLine\.find\(params\[:id\]\)/, m)
+ end
end
assert_file "test/controllers/product_lines_controller_test.rb" do |test|
assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, test)
- assert_match(/post :create, product_line: \{ title: @product_line.title \}/, test)
- assert_match(/put :update, id: @product_line, product_line: \{ title: @product_line.title \}/, test)
+ assert_match(/post :create, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \}/, test)
+ assert_match(/patch :update, id: @product_line, product_line: \{ product_id: @product_line\.product_id, title: @product_line\.title, user_id: @product_line\.user_id \}/, test)
end
# Views
- %w(
- index
- edit
- new
- show
- _form
- ).each { |view| assert_file "app/views/product_lines/#{view}.html.erb" }
+ %w(index edit new show _form).each do |view|
+ assert_file "app/views/product_lines/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/product_lines.html.erb"
# Helpers
@@ -93,7 +85,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/class ProductLinesControllerTest < ActionController::TestCase/, content)
assert_match(/test "should get index"/, content)
assert_match(/post :create, product_line: \{ \}/, content)
- assert_match(/put :update, id: @product_line, product_line: \{ \}/, content)
+ assert_match(/patch :update, id: @product_line, product_line: \{ \}/, content)
end
end
@@ -153,47 +145,39 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_match(/@admin_roles = Admin::Role\.all/, m)
end
- assert_instance_method :show, content do |m|
- assert_match(/@admin_role = Admin::Role\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :show, content
assert_instance_method :new, content do |m|
assert_match(/@admin_role = Admin::Role\.new/, m)
end
- assert_instance_method :edit, content do |m|
- assert_match(/@admin_role = Admin::Role\.find\(params\[:id\]\)/, m)
- end
+ assert_instance_method :edit, content
assert_instance_method :create, content do |m|
assert_match(/@admin_role = Admin::Role\.new\(admin_role_params\)/, m)
assert_match(/@admin_role\.save/, m)
- assert_match(/@admin_role\.errors/, m)
end
assert_instance_method :update, content do |m|
- assert_match(/@admin_role = Admin::Role\.find\(params\[:id\]\)/, m)
- assert_match(/@admin_role\.update_attributes\(admin_role_params\)/, m)
- assert_match(/@admin_role\.errors/, m)
+ assert_match(/@admin_role\.update\(admin_role_params\)/, m)
end
assert_instance_method :destroy, content do |m|
- assert_match(/@admin_role = Admin::Role\.find\(params\[:id\]\)/, m)
assert_match(/@admin_role\.destroy/, m)
end
+
+ assert_instance_method :set_admin_role, content do |m|
+ assert_match(/@admin_role = Admin::Role\.find\(params\[:id\]\)/, m)
+ end
end
assert_file "test/controllers/admin/roles_controller_test.rb",
/class Admin::RolesControllerTest < ActionController::TestCase/
# Views
- %w(
- index
- edit
- new
- show
- _form
- ).each { |view| assert_file "app/views/admin/roles/#{view}.html.erb" }
+ %w(index edit new show _form).each do |view|
+ assert_file "app/views/admin/roles/#{view}.html.erb"
+ end
assert_no_file "app/views/layouts/admin/roles.html.erb"
# Helpers
@@ -211,7 +195,7 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
run_generator [ "admin/role" ], :behavior => :revoke
# Model
- assert_file "app/models/admin.rb" # ( should not be remove )
+ assert_file "app/models/admin.rb" # ( should not be remove )
assert_no_file "app/models/admin/role.rb"
assert_no_file "test/models/admin/role_test.rb"
assert_no_file "test/fixtures/admin/roles.yml"
@@ -255,13 +239,27 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
assert_file "config/routes.rb", /\.routes\.draw do\s*\|map\|\s*$/
end
- def test_scaffold_generator_no_assets
+ def test_scaffold_generator_no_assets_with_switch_no_assets
run_generator [ "posts", "--no-assets" ]
- assert_file "app/assets/stylesheets/scaffold.css"
+ assert_no_file "app/assets/stylesheets/scaffold.css"
+ assert_no_file "app/assets/javascripts/posts.js"
+ assert_no_file "app/assets/stylesheets/posts.css"
+ end
+
+ def test_scaffold_generator_no_assets_with_switch_assets_false
+ run_generator [ "posts", "--assets=false" ]
+ assert_no_file "app/assets/stylesheets/scaffold.css"
assert_no_file "app/assets/javascripts/posts.js"
assert_no_file "app/assets/stylesheets/posts.css"
end
+ def test_scaffold_generator_no_assets_with_switch_resource_route_false
+ run_generator [ "posts", "--resource-route=false" ]
+ assert_file "config/routes.rb" do |route|
+ assert_no_match(/resources :posts$/, route)
+ end
+ end
+
def test_scaffold_generator_no_stylesheets
run_generator [ "posts", "--no-stylesheets" ]
assert_no_file "app/assets/stylesheets/scaffold.css"
@@ -287,4 +285,69 @@ class ScaffoldGeneratorTest < Rails::Generators::TestCase
end
end
end
+
+ def test_scaffold_generator_belongs_to
+ run_generator ["account", "name", "currency:belongs_to"]
+
+ assert_file "app/models/account.rb", /belongs_to :currency/
+
+ assert_migration "db/migrate/create_accounts.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.belongs_to :currency/, up)
+ end
+ end
+
+ assert_file "app/controllers/accounts_controller.rb" do |content|
+ assert_instance_method :account_params, content do |m|
+ assert_match(/permit\(:name, :currency_id\)/, m)
+ end
+ end
+
+ assert_file "app/views/accounts/_form.html.erb" do |content|
+ assert_match(/^\W{4}<%= f\.text_field :name %>/, content)
+ assert_match(/^\W{4}<%= f\.text_field :currency_id %>/, content)
+ end
+ end
+
+ def test_scaffold_generator_password_digest
+ run_generator ["user", "name", "password:digest"]
+
+ assert_file "app/models/user.rb", /has_secure_password/
+
+ assert_migration "db/migrate/create_users.rb" do |m|
+ assert_method :change, m do |up|
+ assert_match(/t\.string :name/, up)
+ assert_match(/t\.string :password_digest/, up)
+ end
+ end
+
+ assert_file "app/controllers/users_controller.rb" do |content|
+ assert_instance_method :user_params, content do |m|
+ assert_match(/permit\(:name, :password, :password_confirmation\)/, m)
+ end
+ end
+
+ assert_file "app/views/users/_form.html.erb" do |content|
+ assert_match(/<%= f\.password_field :password %>/, content)
+ assert_match(/<%= f\.password_field :password_confirmation %>/, content)
+ end
+
+ assert_file "app/views/users/index.html.erb" do |content|
+ assert_no_match(/password/, content)
+ end
+
+ assert_file "app/views/users/show.html.erb" do |content|
+ assert_no_match(/password/, content)
+ end
+
+ assert_file "test/controllers/users_controller_test.rb" do |content|
+ assert_match(/password: 'secret'/, content)
+ assert_match(/password_confirmation: 'secret'/, content)
+ end
+
+ assert_file "test/fixtures/users.yml" do |content|
+ assert_match(/password_digest: <%= BCrypt::Password.create\('secret'\) %>/, content)
+ end
+ end
end
diff --git a/railties/test/generators/shared_generator_tests.rb b/railties/test/generators/shared_generator_tests.rb
index 1e5a4545a1..7184639d23 100644
--- a/railties/test/generators/shared_generator_tests.rb
+++ b/railties/test/generators/shared_generator_tests.rb
@@ -46,11 +46,6 @@ module SharedGeneratorTests
assert_no_file "test"
end
- def test_options_before_application_name_raises_an_error
- content = capture(:stderr){ run_generator(["--pretend", destination_root]) }
- assert_match(/Options should be given after the \w+ name. For details run: rails( plugin)? --help\n/, content)
- end
-
def test_name_collision_raises_an_error
reserved_words = %w[application destroy plugin runner test]
reserved_words.each do |reserved|
@@ -68,22 +63,22 @@ module SharedGeneratorTests
def test_shebang_is_added_to_rails_file
run_generator [destination_root, "--ruby", "foo/bar/baz", "--full"]
- assert_file "script/rails", /#!foo\/bar\/baz/
+ assert_file "bin/rails", /#!foo\/bar\/baz/
end
def test_shebang_when_is_the_same_as_default_use_env
run_generator [destination_root, "--ruby", Thor::Util.ruby_command, "--full"]
- assert_file "script/rails", /#!\/usr\/bin\/env/
+ assert_file "bin/rails", /#!\/usr\/bin\/env/
end
def test_template_raises_an_error_with_invalid_path
- content = capture(:stderr){ run_generator([destination_root, "-m", "non/existant/path"]) }
+ content = capture(:stderr){ run_generator([destination_root, "-m", "non/existent/path"]) }
assert_match(/The template \[.*\] could not be loaded/, content)
- assert_match(/non\/existant\/path/, content)
+ assert_match(/non\/existent\/path/, content)
end
def test_template_is_executed_when_supplied
- path = "http://gist.github.com/103208.txt"
+ path = "https://gist.github.com/josevalim/103208/raw/"
template = %{ say "It works!" }
template.instance_eval "def read; self; end" # Make the string respond to read
@@ -92,7 +87,7 @@ module SharedGeneratorTests
end
def test_template_is_executed_when_supplied_an_https_path
- path = "https://gist.github.com/103208.txt"
+ path = "https://gist.github.com/josevalim/103208/raw/"
template = %{ say "It works!" }
template.instance_eval "def read; self; end" # Make the string respond to read
@@ -140,61 +135,3 @@ module SharedGeneratorTests
assert_no_file('app/mailers/.keep')
end
end
-
-module SharedCustomGeneratorTests
- def setup
- Rails.application = TestApp::Application
- super
- Rails::Generators::AppGenerator.instance_variable_set('@desc', nil)
- end
-
- def teardown
- super
- Rails::Generators::AppGenerator.instance_variable_set('@desc', nil)
- Object.class_eval do
- remove_const :AppBuilder if const_defined?(:AppBuilder)
- remove_const :PluginBuilder if const_defined?(:PluginBuilder)
- end
- Rails.application = TestApp::Application.instance
- end
-
- def test_builder_option_with_empty_app_builder
- FileUtils.cd(destination_root)
- run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/empty_builder.rb"])
- default_files.each{ |path| assert_no_file path }
- end
-
- def test_builder_option_with_simple_plugin_builder
- FileUtils.cd(destination_root)
- run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/simple_builder.rb"])
- (default_files - ['.gitignore']).each{ |path| assert_no_file path }
- assert_file ".gitignore", "foobar"
- end
-
- def test_builder_option_with_relative_path
- here = File.expand_path(File.dirname(__FILE__))
- FileUtils.cd(here)
- run_generator([destination_root, "-b", "../fixtures/lib/#{builders_dir}/simple_builder.rb"])
- FileUtils.cd(destination_root)
- (default_files - ['.gitignore']).each{ |path| assert_no_file path }
- assert_file ".gitignore", "foobar"
- end
-
- def test_builder_option_with_tweak_plugin_builder
- FileUtils.cd(destination_root)
- run_generator([destination_root, "-b", "#{Rails.root}/lib/#{builders_dir}/tweak_builder.rb"])
- default_files.each{ |path| assert_file path }
- assert_file ".gitignore", "foobar"
- end
-
- def test_builder_option_with_http
- url = "http://gist.github.com/103208.txt"
- template = "class #{builder_class}; end"
- template.instance_eval "def read; self; end" # Make the string respond to read
-
- generator([destination_root], builder: url).expects(:open).with(url, 'Accept' => 'application/x-thor-template').returns(template)
- quietly { generator.invoke_all }
-
- default_files.each{ |path| assert_no_file(path) }
- end
-end
diff --git a/railties/test/generators/task_generator_test.rb b/railties/test/generators/task_generator_test.rb
index f810a21d1e..9399be9510 100644
--- a/railties/test/generators/task_generator_test.rb
+++ b/railties/test/generators/task_generator_test.rb
@@ -5,7 +5,7 @@ class TaskGeneratorTest < Rails::Generators::TestCase
include GeneratorsTestHelper
arguments %w(feeds foo bar)
- def test_controller_skeleton_is_created
+ def test_task_is_created
run_generator
assert_file "lib/tasks/feeds.rake", /namespace :feeds/
end
diff --git a/railties/test/generators_test.rb b/railties/test/generators_test.rb
index 9953aa929b..eac28badfe 100644
--- a/railties/test/generators_test.rb
+++ b/railties/test/generators_test.rb
@@ -15,7 +15,7 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_simple_invoke
- assert File.exists?(File.join(@path, 'generators', 'model_generator.rb'))
+ assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
TestUnit::Generators::ModelGenerator.expects(:start).with(["Account"], {})
Rails::Generators.invoke("test_unit:model", ["Account"])
end
@@ -31,7 +31,7 @@ class GeneratorsTest < Rails::Generators::TestCase
end
def test_should_give_higher_preference_to_rails_generators
- assert File.exists?(File.join(@path, 'generators', 'model_generator.rb'))
+ assert File.exist?(File.join(@path, 'generators', 'model_generator.rb'))
Rails::Generators::ModelGenerator.expects(:start).with(["Account"], {})
warnings = capture(:stderr){ Rails::Generators.invoke :model, ["Account"] }
assert warnings.empty?
@@ -106,7 +106,7 @@ class GeneratorsTest < Rails::Generators::TestCase
def test_rails_generators_help_does_not_include_app_nor_plugin_new
output = capture(:stdout){ Rails::Generators.help }
assert_no_match(/app/, output)
- assert_no_match(/plugin_new/, output)
+ assert_no_match(/[^:]plugin/, output)
end
def test_rails_generators_with_others_information
@@ -164,7 +164,7 @@ class GeneratorsTest < Rails::Generators::TestCase
Rails::Generators.invoke "super_shoulda:model", ["Account"]
end
- def test_developer_options_are_overwriten_by_user_options
+ def test_developer_options_are_overwritten_by_user_options
Rails::Generators.options[:with_options] = { generate: false }
self.class.class_eval(<<-end_eval, __FILE__, __LINE__ + 1)
diff --git a/railties/test/initializable_test.rb b/railties/test/initializable_test.rb
index 16e259be5d..ed9573453b 100644
--- a/railties/test/initializable_test.rb
+++ b/railties/test/initializable_test.rb
@@ -20,14 +20,6 @@ module InitializableTests
end
end
- module Word
- include Rails::Initializable
-
- initializer :word do
- $word = "bird"
- end
- end
-
class Parent
include Rails::Initializable
@@ -235,4 +227,4 @@ module InitializableTests
assert_equal [1, 2, 3, 4], $arr
end
end
-end \ No newline at end of file
+end
diff --git a/railties/test/isolation/abstract_unit.rb b/railties/test/isolation/abstract_unit.rb
index e59488f97d..913e2b5e29 100644
--- a/railties/test/isolation/abstract_unit.rb
+++ b/railties/test/isolation/abstract_unit.rb
@@ -9,7 +9,7 @@
require 'fileutils'
require 'bundler/setup' unless defined?(Bundler)
-require 'minitest/autorun'
+require 'active_support/testing/autorun'
require 'active_support/test_case'
RAILS_FRAMEWORK_ROOT = File.expand_path("#{File.dirname(__FILE__)}/../../..")
@@ -119,7 +119,7 @@ module TestHelpers
add_to_config <<-RUBY
config.eager_load = false
- config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
config.session_store :cookie_store, key: "_myapp_session"
config.active_support.deprecation = :log
config.action_controller.allow_forgery_protection = false
@@ -135,10 +135,11 @@ module TestHelpers
def make_basic_app
require "rails"
require "action_controller/railtie"
+ require "action_view/railtie"
app = Class.new(Rails::Application)
app.config.eager_load = false
- app.config.secret_token = "3b7cd727ee24e8444053437c36cc66c4"
+ app.config.secret_key_base = "3b7cd727ee24e8444053437c36cc66c4"
app.config.session_store :cookie_store, key: "_myapp_session"
app.config.active_support.deprecation = :log
@@ -163,7 +164,7 @@ module TestHelpers
RUBY
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get ':controller(/:action)'
end
RUBY
@@ -206,7 +207,7 @@ module TestHelpers
def script(script)
Dir.chdir(app_path) do
- `#{Gem.ruby} #{app_path}/script/rails #{script}`
+ `#{Gem.ruby} #{app_path}/bin/rails #{script}`
end
end
@@ -242,6 +243,12 @@ module TestHelpers
end
end
+ def gsub_app_file(path, regexp, *args, &block)
+ path = "#{app_path}/#{path}"
+ content = File.read(path).gsub(regexp, *args, &block)
+ File.open(path, 'wb') { |f| f.write(content) }
+ end
+
def remove_file(path)
FileUtils.rm_rf "#{app_path}/#{path}"
end
@@ -279,7 +286,7 @@ Module.new do
environment = File.expand_path('../../../../load_paths', __FILE__)
require_environment = "-r #{environment}"
- `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile`
+ `#{Gem.ruby} #{require_environment} #{RAILS_FRAMEWORK_ROOT}/railties/bin/rails new #{app_template_path} --skip-gemfile --no-rc`
File.open("#{app_template_path}/config/boot.rb", 'w') do |f|
f.puts "require '#{environment}'"
f.puts "require 'rails/all'"
diff --git a/railties/test/paths_test.rb b/railties/test/paths_test.rb
index 12f18b9dbf..ed4559ec6f 100644
--- a/railties/test/paths_test.rb
+++ b/railties/test/paths_test.rb
@@ -3,7 +3,7 @@ require 'rails/paths'
class PathsTest < ActiveSupport::TestCase
def setup
- File.stubs(:exists?).returns(true)
+ File.stubs(:exist?).returns(true)
@root = Rails::Paths::Root.new("/foo/bar")
end
@@ -180,7 +180,7 @@ class PathsTest < ActiveSupport::TestCase
assert_equal 1, @root.eager_load.select {|p| p == @root["app"].expanded.first }.size
end
- test "paths added to a eager_load path should be added to the eager_load collection" do
+ test "paths added to an eager_load path should be added to the eager_load collection" do
@root["app"] = "/app"
@root["app"].eager_load!
@root["app"] << "/app2"
diff --git a/railties/test/rack_logger_test.rb b/railties/test/rack_logger_test.rb
new file mode 100644
index 0000000000..6ebd47fff9
--- /dev/null
+++ b/railties/test/rack_logger_test.rb
@@ -0,0 +1,75 @@
+require 'abstract_unit'
+require 'active_support/testing/autorun'
+require 'active_support/test_case'
+require 'rails/rack/logger'
+require 'logger'
+
+module Rails
+ module Rack
+ class LoggerTest < ActiveSupport::TestCase
+ class TestLogger < Rails::Rack::Logger
+ NULL = ::Logger.new File::NULL
+
+ attr_reader :logger
+
+ def initialize(logger = NULL, taggers = nil, &block)
+ super(->(_) { block.call; [200, {}, []] }, taggers)
+ @logger = logger
+ end
+
+ def development?; false; end
+ end
+
+ class Subscriber < Struct.new(:starts, :finishes)
+ def initialize(starts = [], finishes = [])
+ super
+ end
+
+ def start(name, id, payload)
+ starts << [name, id, payload]
+ end
+
+ def finish(name, id, payload)
+ finishes << [name, id, payload]
+ end
+ end
+
+ attr_reader :subscriber, :notifier
+
+ def setup
+ @subscriber = Subscriber.new
+ @notifier = ActiveSupport::Notifications.notifier
+ notifier.subscribe 'request.action_dispatch', subscriber
+ end
+
+ def teardown
+ notifier.unsubscribe subscriber
+ end
+
+ def test_notification
+ logger = TestLogger.new { }
+
+ assert_difference('subscriber.starts.length') do
+ assert_difference('subscriber.finishes.length') do
+ logger.call('REQUEST_METHOD' => 'GET').last.close
+ end
+ end
+ end
+
+ def test_notification_on_raise
+ logger = TestLogger.new do
+ # using an exception class that is not a StandardError subclass on purpose
+ raise NotImplementedError
+ end
+
+ assert_difference('subscriber.starts.length') do
+ assert_difference('subscriber.finishes.length') do
+ assert_raises(NotImplementedError) do
+ logger.call 'REQUEST_METHOD' => 'GET'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/railties/test/rails_info_controller_test.rb b/railties/test/rails_info_controller_test.rb
index 08fcddd4bf..e45a5228a1 100644
--- a/railties/test/rails_info_controller_test.rb
+++ b/railties/test/rails_info_controller_test.rb
@@ -1,5 +1,7 @@
require 'abstract_unit'
+ActionController::Base.superclass.send(:include, ActionView::Layouts)
+
module ActionController
class Base
include ActionController::Testing
@@ -50,7 +52,7 @@ class InfoControllerTest < ActionController::TestCase
test "info controller renders with routes" do
get :routes
- assert_select 'pre'
+ assert_response :success
end
end
diff --git a/railties/test/rails_info_test.rb b/railties/test/rails_info_test.rb
index b9fb071d23..44a5fd1904 100644
--- a/railties/test/rails_info_test.rb
+++ b/railties/test/rails_info_test.rb
@@ -37,8 +37,13 @@ class InfoTest < ActiveSupport::TestCase
assert_property 'Goodbye', 'World'
end
+ def test_rails_version
+ assert_property 'Rails version',
+ File.read(File.realpath('../../../RAILS_VERSION', __FILE__)).chomp
+ end
+
def test_framework_version
- assert_property 'Active Support version', ActiveSupport::VERSION::STRING
+ assert_property 'Active Support version', ActiveSupport.version.to_s
end
def test_frameworks_exist
diff --git a/railties/test/railties/engine_test.rb b/railties/test/railties/engine_test.rb
index fcbe7b6cda..3075ffee0d 100644
--- a/railties/test/railties/engine_test.rb
+++ b/railties/test/railties/engine_test.rb
@@ -90,8 +90,8 @@ module RailtiesTest
Dir.chdir(app_path) do
output = `bundle exec rake bukkits:install:migrations`
- assert File.exists?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
- assert File.exists?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/2_create_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/3_add_last_name_to_users.bukkits.rb")
assert_match(/Copied migration 2_create_users.bukkits.rb from bukkits/, output)
assert_match(/Copied migration 3_add_last_name_to_users.bukkits.rb from bukkits/, output)
assert_match(/NOTE: Migration 3_create_sessions.rb from bukkits has been skipped/, output)
@@ -105,7 +105,7 @@ module RailtiesTest
assert_not_nil bukkits_migration_order, "Expected migration to be skipped"
migrations_count = Dir["#{app_path}/db/migrate/*.rb"].length
- output = `bundle exec rake railties:install:migrations`
+ `bundle exec rake railties:install:migrations`
assert_equal migrations_count, Dir["#{app_path}/db/migrate/*.rb"].length
end
@@ -136,7 +136,7 @@ module RailtiesTest
Dir.chdir(@plugin.path) do
output = `bundle exec rake app:bukkits:install:migrations`
- assert File.exists?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
+ assert File.exist?("#{app_path}/db/migrate/0_add_first_name_to_users.bukkits.rb")
assert_match(/Copied migration 0_add_first_name_to_users.bukkits.rb from bukkits/, output)
assert_equal 1, Dir["#{app_path}/db/migrate/*.rb"].length
end
@@ -187,7 +187,7 @@ module RailtiesTest
assert_equal "Hello bukkits\n", response[2].body
end
- test "adds its views to view paths with lower proriority than app ones" do
+ test "adds its views to view paths with lower priority than app ones" do
@plugin.write "app/controllers/bukkit_controller.rb", <<-RUBY
class BukkitController < ActionController::Base
def index
@@ -270,7 +270,7 @@ module RailtiesTest
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get 'foo', :to => 'foo#index'
end
RUBY
@@ -345,7 +345,7 @@ YAML
#{RAILS_FRAMEWORK_ROOT}/activesupport/lib/active_support/locale/en.yml
#{RAILS_FRAMEWORK_ROOT}/activemodel/lib/active_model/locale/en.yml
#{RAILS_FRAMEWORK_ROOT}/activerecord/lib/active_record/locale/en.yml
- #{RAILS_FRAMEWORK_ROOT}/actionpack/lib/action_view/locale/en.yml
+ #{RAILS_FRAMEWORK_ROOT}/actionview/lib/action_view/locale/en.yml
#{@plugin.path}/config/locales/en.yml
#{app_path}/config/locales/en.yml
#{app_path}/app/locales/en.yml
@@ -416,11 +416,6 @@ YAML
boot_rails
end
- test "Rails::Engine itself does not respond to config" do
- boot_rails
- assert !Rails::Engine.respond_to?(:config)
- end
-
test "initializers are executed after application configuration initializers" do
@plugin.write "lib/bukkits.rb", <<-RUBY
module Bukkits
@@ -472,7 +467,7 @@ YAML
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
mount(Bukkits::Engine => "/bukkits")
end
RUBY
@@ -568,7 +563,7 @@ YAML
@plugin.write "lib/bukkits.rb", <<-RUBY
module Bukkits
class Engine < ::Rails::Engine
- endpoint lambda { |env| [200, {'Content-Type' => 'text/html'}, 'hello'] }
+ endpoint lambda { |env| [200, {'Content-Type' => 'text/html'}, ['hello']] }
end
end
RUBY
@@ -607,7 +602,7 @@ YAML
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get "/bar" => "bar#index", as: "bar"
mount Bukkits::Engine => "/bukkits", as: "bukkits"
end
@@ -725,7 +720,7 @@ YAML
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
mount Bukkits::Engine => "/bukkits", as: "bukkits"
end
RUBY
@@ -769,7 +764,7 @@ YAML
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
mount Bukkits::Awesome::Engine => "/bukkits", :as => "bukkits"
end
RUBY
@@ -833,7 +828,7 @@ YAML
add_to_config "isolate_namespace AppTemplate"
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do end
+ Rails.application.routes.draw do end
RUBY
boot_rails
@@ -1211,7 +1206,7 @@ YAML
RUBY
app_file "config/routes.rb", <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
get '/bar' => 'bar#index', :as => 'bar'
mount Bukkits::Engine => "/bukkits", :as => "bukkits"
end
diff --git a/railties/test/railties/generators_test.rb b/railties/test/railties/generators_test.rb
index 0abb2b7578..7348d70c56 100644
--- a/railties/test/railties/generators_test.rb
+++ b/railties/test/railties/generators_test.rb
@@ -44,7 +44,7 @@ module RailtiesTests
Dir.chdir(engine_path) do
File.open("Gemfile", "w") do |f|
f.write <<-GEMFILE.gsub(/^ {12}/, '')
- source "http://rubygems.org"
+ source "https://rubygems.org"
gem 'rails', path: '#{RAILS_FRAMEWORK_ROOT}'
gem 'sqlite3'
diff --git a/railties/test/railties/mounted_engine_test.rb b/railties/test/railties/mounted_engine_test.rb
index a1c52f01dc..0ef2ff2e2e 100644
--- a/railties/test/railties/mounted_engine_test.rb
+++ b/railties/test/railties/mounted_engine_test.rb
@@ -13,9 +13,10 @@ module ApplicationTests
@simple_plugin = engine "weblog"
@plugin = engine "blog"
+ @metrics_plugin = engine "metrics"
app_file 'config/routes.rb', <<-RUBY
- AppTemplate::Application.routes.draw do
+ Rails.application.routes.draw do
mount Weblog::Engine, :at => '/', :as => 'weblog'
resources :posts
get "/engine_route" => "application_generating#engine_route"
@@ -28,6 +29,7 @@ module ApplicationTests
scope "/:user", :user => "anonymous" do
mount Blog::Engine => "/blog"
end
+ mount Metrics::Engine => "/metrics"
root :to => 'main#index'
end
RUBY
@@ -54,6 +56,34 @@ module ApplicationTests
end
RUBY
+ @metrics_plugin.write "lib/metrics.rb", <<-RUBY
+ module Metrics
+ class Engine < ::Rails::Engine
+ isolate_namespace(Metrics)
+ end
+ end
+ RUBY
+
+ @metrics_plugin.write "config/routes.rb", <<-RUBY
+ Metrics::Engine.routes.draw do
+ get '/generate_blog_route', to: 'generating#generate_blog_route'
+ get '/generate_blog_route_in_view', to: 'generating#generate_blog_route_in_view'
+ end
+ RUBY
+
+ @metrics_plugin.write "app/controllers/metrics/generating_controller.rb", <<-RUBY
+ module Metrics
+ class GeneratingController < ActionController::Base
+ def generate_blog_route
+ render text: blog.post_path(1)
+ end
+
+ def generate_blog_route_in_view
+ render inline: "<%= blog.post_path(1) -%>"
+ end
+ end
+ end
+ RUBY
@plugin.write "app/models/blog/post.rb", <<-RUBY
module Blog
@@ -89,6 +119,7 @@ module ApplicationTests
get '/generate_application_route', to: 'posts#generate_application_route'
get '/application_route_in_view', to: 'posts#application_route_in_view'
get '/engine_polymorphic_path', to: 'posts#engine_polymorphic_path'
+ get '/engine_asset_path', to: 'posts#engine_asset_path'
end
RUBY
@@ -113,6 +144,10 @@ module ApplicationTests
def engine_polymorphic_path
render text: polymorphic_path(Post.new)
end
+
+ def engine_asset_path
+ render inline: "<%= asset_path 'images/foo.png' %>"
+ end
end
end
RUBY
@@ -196,6 +231,21 @@ module ApplicationTests
get "/somone/blog/application_route_in_view"
assert_equal "/", last_response.body
+ # test generating engine's route from other engine
+ get "/metrics/generate_blog_route"
+ assert_equal '/anonymous/blog/posts/1', last_response.body
+
+ get "/metrics/generate_blog_route_in_view"
+ assert_equal '/anonymous/blog/posts/1', last_response.body
+
+ # test generating engine's route from other engine with default_url_options
+ get "/metrics/generate_blog_route", {}, 'SCRIPT_NAME' => '/foo'
+ assert_equal '/foo/anonymous/blog/posts/1', last_response.body
+
+ get "/metrics/generate_blog_route_in_view", {}, 'SCRIPT_NAME' => '/foo'
+ assert_equal '/foo/anonymous/blog/posts/1', last_response.body
+
+
# test generating application's route from engine with default_url_options
get "/someone/blog/generate_application_route", {}, 'SCRIPT_NAME' => '/foo'
assert_equal "/foo/", last_response.body
@@ -211,6 +261,10 @@ module ApplicationTests
# and in an application
get "/application_polymorphic_path"
assert_equal "/posts/44", last_response.body
+
+ # test that asset path will not get script_name when generated in the engine
+ get "/someone/blog/engine_asset_path"
+ assert_equal "/images/foo.png", last_response.body
end
test "route path for controller action when engine is mounted at root" do
diff --git a/railties/test/railties/railtie_test.rb b/railties/test/railties/railtie_test.rb
index c80b0f63af..4cbd4822be 100644
--- a/railties/test/railties/railtie_test.rb
+++ b/railties/test/railties/railtie_test.rb
@@ -19,33 +19,26 @@ module RailtiesTest
@app ||= Rails.application
end
- test "Rails::Railtie itself does not respond to config" do
- assert !Rails::Railtie.respond_to?(:config)
+ test "cannot instantiate a Railtie object" do
+ assert_raise(RuntimeError) { Rails::Railtie.new }
end
test "Railtie provides railtie_name" do
begin
class ::FooBarBaz < Rails::Railtie ; end
- assert_equal "foo_bar_baz", ::FooBarBaz.railtie_name
+ assert_equal "foo_bar_baz", FooBarBaz.railtie_name
ensure
Object.send(:remove_const, :"FooBarBaz")
end
end
- test "railtie_name can be set manualy" do
+ test "railtie_name can be set manually" do
class Foo < Rails::Railtie
railtie_name "bar"
end
assert_equal "bar", Foo.railtie_name
end
- test "cannot inherit from a railtie" do
- class Foo < Rails::Railtie ; end
- assert_raise RuntimeError do
- class Bar < Foo; end
- end
- end
-
test "config is available to railtie" do
class Foo < Rails::Railtie ; end
assert_nil Foo.config.action_controller.foo
@@ -65,7 +58,7 @@ module RailtiesTest
config.foo.greetings = "hello"
end
require "#{app_path}/config/application"
- assert_equal "hello", AppTemplate::Application.config.foo.greetings
+ assert_equal "hello", Rails.application.config.foo.greetings
end
test "railtie can add to_prepare callbacks" do
@@ -103,7 +96,7 @@ module RailtiesTest
require 'rake/testtask'
require 'rdoc/task'
- AppTemplate::Application.load_tasks
+ Rails.application.load_tasks
assert $ran_block
end
@@ -127,7 +120,7 @@ module RailtiesTest
require 'rake/testtask'
require 'rdoc/task'
- AppTemplate::Application.load_tasks
+ Rails.application.load_tasks
assert $ran_block.include?("my_tie")
end
@@ -143,7 +136,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
assert !$ran_block
- AppTemplate::Application.load_generators
+ Rails.application.load_generators
assert $ran_block
end
@@ -159,7 +152,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
assert !$ran_block
- AppTemplate::Application.load_console
+ Rails.application.load_console
assert $ran_block
end
@@ -175,7 +168,7 @@ module RailtiesTest
require "#{app_path}/config/environment"
assert !$ran_block
- AppTemplate::Application.load_runner
+ Rails.application.load_runner
assert $ran_block
end
diff --git a/railties/test/script_rails_loader_test.rb b/railties/test/script_rails_loader_test.rb
deleted file mode 100644
index 3ccc147749..0000000000
--- a/railties/test/script_rails_loader_test.rb
+++ /dev/null
@@ -1,22 +0,0 @@
-require 'abstract_unit'
-require 'rails/script_rails_loader'
-
-class ScriptRailsLoaderTest < ActiveSupport::TestCase
-
- test "is in a rails application if script/rails exists" do
- File.stubs(:exists?).returns(true)
- assert Rails::ScriptRailsLoader.in_rails_application?
- end
-
- test "is in a rails application if parent directory has script/rails" do
- File.stubs(:exists?).with("/foo/bar/script/rails").returns(false)
- File.stubs(:exists?).with("/foo/script/rails").returns(true)
- assert Rails::ScriptRailsLoader.in_rails_application_subdirectory?(Pathname.new("/foo/bar"))
- end
-
- test "is not in a rails application if at the root directory and doesn't have script/rails" do
- Pathname.any_instance.stubs(:root?).returns true
- assert !Rails::ScriptRailsLoader.in_rails_application?
- end
-
-end \ No newline at end of file
diff --git a/railties/test/test_info_test.rb b/railties/test/test_info_test.rb
new file mode 100644
index 0000000000..b9c3a9c0c7
--- /dev/null
+++ b/railties/test/test_info_test.rb
@@ -0,0 +1,60 @@
+require 'abstract_unit'
+require 'rails/test_unit/sub_test_task'
+
+module Rails
+ class TestInfoTest < ActiveSupport::TestCase
+ def test_test_files
+ info = new_test_info ['test']
+ assert_predicate info.files, :empty?
+ assert_nil info.opts
+ assert_equal ['test'], info.tasks
+ end
+
+ def test_with_file
+ info = new_test_info ['test', __FILE__]
+ assert_equal [__FILE__], info.files
+ assert_nil info.opts
+ assert_equal ['test'], info.tasks
+ end
+
+ def test_with_opts
+ info = new_test_info ['test', __FILE__, '/foo/']
+ assert_equal [__FILE__], info.files
+ assert_equal '-n /foo/', info.opts
+ assert_equal ['test'], info.tasks
+ end
+
+ def test_with_model_shorthand
+ info = new_test_info ['test', 'models/foo', '/foo/']
+
+ def info.test_file?(file)
+ file == "test/models/foo_test.rb" || super
+ end
+
+ assert_equal ['test/models/foo_test.rb'], info.files
+ assert_equal '-n /foo/', info.opts
+ assert_equal ['test'], info.tasks
+ end
+
+ def test_with_model_path
+ info = new_test_info ['test', 'app/models/foo.rb', '/foo/']
+
+ def info.test_file?(file)
+ file == "test/models/foo_test.rb" || super
+ end
+
+ assert_equal ['test/models/foo_test.rb'], info.files
+ assert_equal '-n /foo/', info.opts
+ assert_equal ['test'], info.tasks
+ end
+
+ private
+ def new_test_info(tasks)
+ Class.new(TestTask::TestInfo) {
+ def task_defined?(task)
+ task == "test"
+ end
+ }.new tasks
+ end
+ end
+end
diff --git a/tasks/release.rb b/tasks/release.rb
index 650b381e0f..66da67bfd0 100644
--- a/tasks/release.rb
+++ b/tasks/release.rb
@@ -1,4 +1,4 @@
-FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionmailer railties )
+FRAMEWORKS = %w( activesupport activemodel activerecord actionpack actionview actionmailer railties )
root = File.expand_path('../../', __FILE__)
version = File.read("#{root}/RAILS_VERSION").strip
@@ -23,20 +23,25 @@ directory "pkg"
file = Dir[glob].first
ruby = File.read(file)
- major, minor, tiny, pre = version.split('.')
- pre = pre ? pre.inspect : "nil"
+ if framework == "rails" || framework == "railties"
+ major, minor, tiny, pre = version.split('.')
+ pre = pre ? pre.inspect : "nil"
- ruby.gsub!(/^(\s*)MAJOR = .*?$/, "\\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 = .*?$/, "\\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 = .*?$/, "\\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 = .*?$/, "\\1PRE = #{pre}")
- raise "Could not insert PRE 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
File.open(file, 'w') { |f| f.write ruby }
end
diff --git a/tools/profile b/tools/profile
index d4cfa13f0a..fbea67492b 100755
--- a/tools/profile
+++ b/tools/profile
@@ -1,13 +1,9 @@
#!/usr/bin/env ruby
# Example:
# tools/profile activesupport/lib/active_support.rb
-abort 'Use REE so you can profile memory and object allocation' unless GC.respond_to?(:enable_stats)
-
ENV['NO_RELOAD'] ||= '1'
ENV['RAILS_ENV'] ||= 'development'
-GC.enable_stats
-Gem.source_index
require 'benchmark'
module RequireProfiler
@@ -23,23 +19,18 @@ module RequireProfiler
def profile(file)
stats << [file, depth]
self.depth += 1
- heap_before, objects_before = GC.allocated_size, ObjectSpace.allocated_objects
result = nil
elapsed = Benchmark.realtime { result = yield }
- heap_after, objects_after = GC.allocated_size, ObjectSpace.allocated_objects
self.depth -= 1
stats.pop if stats.last.first == file
- stats << [file, depth, elapsed, heap_after - heap_before, objects_after - objects_before] if result
+ stats << [file, depth, elapsed] if result
result
end
end
end
GC.start
-before = GC.allocated_size
-before_gctime, before_gcruns = GC.time, GC.collections
before_rss = `ps -o rss= -p #{Process.pid}`.to_i
-before_live_objects = ObjectSpace.live_objects
path = ARGV.shift
if mode = ARGV.shift
@@ -53,12 +44,8 @@ end
elapsed = Benchmark.realtime { require path }
results = RubyProf.stop if mode
-after_gctime, after_gcruns = GC.time, GC.collections
GC.start
-after_live_objects = ObjectSpace.live_objects
after_rss = `ps -o rss= -p #{Process.pid}`.to_i
-after = GC.allocated_size
-usage = (after - before) / 1024.0
if mode
if printer = ARGV.shift
@@ -74,11 +61,11 @@ if mode
end
end
-RequireProfiler.stats.each do |file, depth, sec, bytes, objects|
+RequireProfiler.stats.each do |file, depth, sec|
if sec
- puts "%10.2f KB %10d obj %8.1f ms %s%s" % [bytes / 1024.0, objects, sec * 1000, ' ' * depth, file]
+ puts "%8.1f ms %s%s" % [sec * 1000, ' ' * depth, file]
else
- puts "#{' ' * (42 + depth)}#{file}"
+ puts "#{' ' * (13 + depth)}#{file}"
end
end
-puts "%10.2f KB %10d obj %8.1f ms %d KB RSS %8.1f ms GC time %d GC runs" % [usage, after_live_objects - before_live_objects, elapsed * 1000, after_rss - before_rss, (after_gctime - before_gctime) / 1000.0, after_gcruns - before_gcruns]
+puts "%8.1f ms %d KB RSS" % [elapsed * 1000, after_rss - before_rss]
diff --git a/version.rb b/version.rb
index ec878f9dcf..5a6d8d0983 100644
--- a/version.rb
+++ b/version.rb
@@ -1,10 +1,10 @@
module Rails
- module VERSION #:nodoc:
+ module VERSION
MAJOR = 4
- MINOR = 0
+ MINOR = 1
TINY = 0
PRE = "beta"
- STRING = [MAJOR, MINOR, TINY, PRE].compact.join('.')
+ STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
end
end